build sched domains tracking cpusets
PATCH 1/1 build sched domains tracking cpusets
Moderate-to-large single system image systems are often carved up into dynamic cpusets, and applications are individually assigned to specific cpusets. The current CPU scheduler's find_busiest_cpu() load-balancing doesn't work well in this environment, as the busiest CPU may be busy with tasks that cannot migrate out of their constrained cpuset (because of the task->cpus_allowed), and this effectively stymies any passive load-balancing attempted by a CPU not in this busiest CPU's cpuset. This disabling of load-balancing has been observed by several SGI customers and can be a serious performance problem.
A lesser observed issue is inefficient load-balancing within (for example) a cpuset that spans one of those 16-node sched domains that are built for large NUMA systems. The next broader sched domain is the all-CPUs domain, which load-balances across the 16-node sched domain boundaries relatively infrequently.
This patch introduces the notion of dynamic sched domains (and sched groups) that track the creation and deletion of cpusets. Up to eight of these dynamic sched domains can exist per CPU, which seems to handle the observed use of cpusets by one popular job manager. Essentially, every new cpuset causes the creation of a new sched domain for the CPUs of that cpuset, and a deletion of the cpuset causes the destruction of that dynamic sched domain. New sched domains are inserted into each CPU's list as appropriate. The limit of 8/CPU (vs. unlimited) not only caps the upperbound of kmalloc memory being assigned to sched domain and sched group structs (thereby avoiding a potential denial-of-service attack by a runaway user creating cpusets), but it also caps the number of sched domains being searched by various algorithms in the scheduler.
A "feature" of this implementation is that these dynamic sched domains build up until that limit of 8/CPU is reached, at which point no additional sched domains are created; or until a cpu-exclusive cpuset gets declared, at which point all the dynamically created sched domains in the affected CPUs get destroyed. A more sophisticated algorithm in kernel/cpuset.c could redeclare the dynamic sched domains after a cpu-exclusive cpuset gets declared, but that is outside the scope of this simple patch's simple change to kernel/cpuset.c.
include/linux/sched.h +4 -0
@@ -558,6 +558,7 @@ enum idle_type#define SD_WAKE_AFFINE 32/* Wake task to waking CPU */ #define SD_WAKE_BALANCE 64/* Perform balancing at task wakeup */ #define SD_SHARE_CPUPOWER 128/* Domain members share cpu power */ +#define SD_TRACKS_CPUSET 4096/* Spam matches non-exclusive cpuset */ struct sched_group {struct sched_group *next;/* Must be a circular list */
@@ -630,6 +631,9 @@ struct sched_domain {extern void partition_sched_domains (cpumask_t *partition1,cpumask_t *partition2); +extern void add_sched_domain (const cpumask_t *cpu_map); +extern void destroy_sched_domain (const cpumask_t *cpu_map); +/* * Maximum cache size the migration-costs auto-tuning code will * search from:
kernel/sched.c +362 -101
546 lines not on this page · raw message
kernel/cpuset.c +8 -2
@@ -828,8 +828,12 @@ static int update_cpumask(struct cpusetmutex_lock (&callback_mutex); cs->cpus_allowed = trialcs.cpus_allowed;mutex_unlock (&callback_mutex); -if (is_cpu_exclusive (cs) && !cpus_unchanged) -update_cpu_domains (cs); +if (!cpus_unchanged) { +if (is_cpu_exclusive (cs)) +update_cpu_domains (cs); +else +add_sched_domain (&cs->cpus_allowed); + }return 0 ; }
@@ -1934,6 +1938,8 @@ static int cpuset_rmdir(struct inode *unset_bit (CS_REMOVED , &cs->flags);if (is_cpu_exclusive (cs))update_cpu_domains (cs); +else +destroy_sched_domain (&cs->cpus_allowed);list_del (&cs->sibling);/* delete my sibling from parent->children */ spin_lock (&cs->dentry->d_lock); d =dget (cs->dentry);
Hi John,
cool patch. I have a few comments.
Quoted 32 lines · [email protected] wrote:
This patch introduces the notion of dynamic sched domains (and sched groups) that track the creation and deletion of cpusets. Up to eight of these dynamic sched domains can exist per CPU, which seems to handle the observed use of cpusets by one popular job manager. Essentially, every new cpuset causes the creation of a new sched domain for the CPUs of that cpuset, and a deletion of the cpuset causes the destruction of that dynamic sched domain. New sched domains are inserted into each CPU's list as appropriate. The limit of 8/CPU (vs. unlimited) not only caps the upperbound of kmalloc memory being assigned to sched domain and sched group structs (thereby avoiding a potential denial-of-service attack by a runaway user creating cpusets), but it also caps the number of sched domains being searched by various algorithms in the scheduler.
A "feature" of this implementation is that these dynamic sched domains build up until that limit of 8/CPU is reached, at which point no additional sched domains are created; or until a cpu-exclusive cpuset gets declared, at which point all the dynamically created sched domains in the affected CPUs get destroyed. A more sophisticated algorithm in
kernel/cpuset.ccould redeclare the dynamic sched domains after a cpu-exclusive cpuset gets declared, but that is outside the scope of this simple patch's simple change tokernel/cpuset.c.Signed-off-by John Hawkes <[email protected]>Index:
linux/include/linux/sched.h
===================================================================
include/linux/sched.h enum idle_type · line 561
Quoted 4 lines
include/linux/sched.h +1
-0@@ -558,6 +558,7 @@ enum idle_type#define SD_WAKE_AFFINE 32/* Wake task to waking CPU */ #define SD_WAKE_BALANCE 64/* Perform balancing at task wakeup */ #define SD_SHARE_CPUPOWER 128/* Domain members share cpu power */ +#define SD_TRACKS_CPUSET 4096/* Spam matches non-exclusive cpuset */
I'd just use the next bit, and we should convert these to hex as well I guess... I don't know what I was thinking ;)
include/linux/sched.h struct sched_domain { · line 635
Quoted 9 lines
include/linux/sched.h +2
-0struct sched_group {struct sched_group *next;/* Must be a circular list */ @@ -630,6 +631,9 @@ struct sched_domain {extern void partition_sched_domains (cpumask_t *partition1,cpumask_t *partition2); +extern void add_sched_domain (const cpumask_t *cpu_map); +extern void destroy_sched_domain (const cpumask_t *cpu_map);
Not a big deal, but I'd probably be happier with a single hook from the cpusets code, which does the right thing depending on whether it is exclusive or not.
Hard to know which way around the layering should go, but I think that it is simply a hint to the balancer code to do something nice, and as such we should leave it completely to the scheduler.
include/linux/sched.h struct sched_domain { · line 636
Quoted 1 line
include/linux/sched.h +1
-0+#ifdef CONFIG_CPUSETS
CPUSETS only? Or do we want to try to help sched_setaffinity users as well?
include/linux/sched.h struct sched_domain {
Quoted 6 lines
include/linux/sched.h +6
-0+ +struct sched_domain_bundle { +struct sched_domain sd_cpuset; +struct sched_group **sg_cpuset_nodes; +int use_count; +};
Can you get away without using the bundle? Or does it make things easier? You could add a new use_count to struct sched_domain if you'd like.... does it make the setup/teardown too difficult?
include/linux/sched.h
Quoted 4 lines
include/linux/sched.h +4
-0+#define SCHED_DOMAIN_CPUSET_MAX 8/* power of 2 */ +static DEFINE_PER_CPU(long, sd_cpusets_used) = { 1UL <<SCHED_DOMAIN_CPUSET_MAX}; +static DEFINE_PER_CPU(struct sched_domain_bundle[SCHED_DOMAIN_CPUSET_MAX], + sd_cpusets_bundle);
Do we need a limit of 8? If you're worried about memory, I guess there are lots of ways to DOS the system... if you're worried about scheduler balancing overhead, we could do a seperate traversal of these guys at a reduced interval (I guess that still leaves some places needing help, though)
Quoted 1 line
+static int
find_existing_sched_domain(int cpu, const cpumask_t *cpu_map)
Probably some way to distinguish these guys as operating on your "bundle" stack would make it a bit clearer?
Quoted 20 lines
include/linux/sched.h +20
-0+{ +int sd_idx; + +for (sd_idx =0 ; sd_idx <SCHED_DOMAIN_CPUSET_MAX ; sd_idx++) { +struct sched_domain_bundle *sd_cpuset_bundle; +struct sched_domain *sd; + +if (!test_bit (sd_idx, &per_cpu (sd_cpusets_used, cpu))) +continue ; + sd_cpuset_bundle = &per_cpu (sd_cpusets_bundle[sd_idx], cpu); + sd = &sd_cpuset_bundle->sd_cpuset; +if (cpus_equal (*cpu_map, sd->span)) +return sd_idx; + } + +return SCHED_DOMAIN_CPUSET_MAX ; +} + +void add_sched_domain (const cpumask_t *cpu_map) +{
[...]
include/linux/sched.h
Quoted 6 lines
include/linux/sched.h +6
-0+/* tweak sched_domain params based upon domain size */ + *sd =SD_NODE_INIT ; + sd->flags |=SD_TRACKS_CPUSET ; + sd->max_interval =8 *(min (new_sd_span,32 )); + sd->span = *cpu_map; +cpu_set (cpu, new_sd_cpu_map);
Can we just have a new SD_xxx_INIT for these?
Quoted 21 lines
Index:
linux/kernel/cpuset.c
===================================================================kernel/cpuset.c +6
-2@@ -828,8 +828,12 @@ static int update_cpumask(struct cpusetmutex_lock (&callback_mutex); cs->cpus_allowed = trialcs.cpus_allowed;mutex_unlock (&callback_mutex); -if (is_cpu_exclusive (cs) && !cpus_unchanged) -update_cpu_domains (cs); +if (!cpus_unchanged) { +if (is_cpu_exclusive (cs)) +update_cpu_domains (cs); +else +add_sched_domain (&cs->cpus_allowed); + }return 0 ; }
kernel/cpuset.c static int cpuset_rmdir(struct inode *un · line 1945
Quoted 8 lines
kernel/cpuset.c +2
-0@@ -1934,6 +1938,8 @@ static int cpuset_rmdir(struct inode *unset_bit (CS_REMOVED , &cs->flags);if (is_cpu_exclusive (cs))update_cpu_domains (cs); +else +destroy_sched_domain (&cs->cpus_allowed);list_del (&cs->sibling);/* delete my sibling from parent->children */ spin_lock (&cs->dentry->d_lock); d =dget (cs->dentry);
So you're just doing the inside. What about the complement? That way you'd get a nice symmetric allocation on all cpus for each cpuset... but that probably would require making the limit greater than 8.
And it would be probably required for good sched_setaffinity balancing too.
Signature · 2 lines
SUSE Labs, Novell Inc. Send instant messages to your online friends http://au.messenger.yahoo.com
From: "Nick Piggin" <[email protected]>
...
Quoted 8 lines
include/linux/sched.h enum idle_type · line 561
Quoted 5 lines
include/linux/sched.h +1
-0@@ -558,6 +558,7 @@ enum idle_type#define SD_WAKE_AFFINE 32/* Wake task to waking CPU */ #define SD_WAKE_BALANCE 64/* Perform balancing at task wakeup */ #define SD_SHARE_CPUPOWER 128/* Domain members share cpu power */ +#define SD_TRACKS_CPUSET 4096/* Spam matches non-exclusive cpuset */ I'd just use the next bit, and we should convert these to hex as well I guess... I don't know what I was thinking ;)
Okay.
Quoted 12 lines
include/linux/sched.h struct sched_domain { · line 634
Quoted 7 lines
include/linux/sched.h +1
-0struct sched_group {struct sched_group *next;/* Must be a circular list */ @@ -630,6 +631,9 @@ struct sched_domain {extern void partition_sched_domains (cpumask_t *partition1,cpumask_t *partition2); +extern void add_sched_domain(const cpumask_t *cpu_map); +extern void destroy_sched_domain (const cpumask_t *cpu_map);Not a big deal, but I'd probably be happier with a single hook from the cpusets code, which does the right thing depending on whether it is exclusive or not.
Quoted 3 lines
Hard to know which way around the layering should go, but I think that it is simply a hint to the balancer code to do something nice, and as such we should leave it completely to the scheduler.
Simplicity is preferable, of course.
The "isolation" semantics differs between the two cases. None of the CPUs of a cpu-exclusive cpuset are present in the sched domain spans of the remaining non-exclusive CPUs, whereas the CPUs of a newly declared non-exclusive cpuset are still present in the sched domain spans of the other non-exclusive CPUs. The idea of this patch is that creating a non-exclusive cpuset is more of a hint to the scheduler (by way of the sched domains) that the CPUs of a cpuset ought to load-balance internally, but not go all the way to having this new non-exclusive cpuset be totally isolated from the rest of the herd.
One problem with isolation is that declaring a cpu-exclusive cpuset doesn't automatically migrate away all the tasks that currently reside in one of the now-isolated CPUs. If we always carve up CPUs of cpusets into isolated sched domains, then I fear we may impede general load-balancing of tasks which haven't been strictly constrained to a cpuset and have just gotten caught up in the newly imposed hard fencing.
Quoted 4 lines
Review struct sched_domain { · line 635
Quoted 1 line
(quoted diff) +1
-0+#ifdef CONFIG_CPUSETSCPUSETS only? Or do we want to try to help
sched_setaffinityusers as well?
Again, I was trying to keep things simple and trigger all this off the creation and deletion of cpusets, which I believe tend to be managed in a more disciplined way than a casual use of "runon" or "taskset." It's easy to assign a process to a range of CPUs using sched_setaffinity, but when does this new dynamic sched domain get destroyed?
Quoted 9 lines
kernel/sched.c next_sg: · line 5801
Quoted 5 lines
kernel/sched.c +5
-0+struct sched_domain_bundle { +struct sched_domain sd_cpuset; +struct sched_group **sg_cpuset_nodes; +int use_count; +};Can you get away without using the bundle? Or does it make things easier? You could add a new
use_countto structsched_domainif you'd like.... does it make the setup/teardown too difficult?
The use_count is a ploy to reduce the demand for SCHED_DOMAIN_CPUSET_MAX slots. I have been told that one popular job manager will create some duplicate cpusets.
The bundle is really there to simplify discovering the sched group lists.
Quoted 11 lines
Quoted 6 lines
(quoted diff) +2
-0+#define SCHED_DOMAIN_CPUSET_MAX 8 /* power of 2 */ +static DEFINE_PER_CPU(long, sd_cpusets_used) = { 1UL<<
SCHED_DOMAIN_CPUSET_MAX};
+static DEFINE_PER_CPU(struct
sched_domain_bundle[SCHED_DOMAIN_CPUSET_MAX],
+sd_cpusets_bundle);Do we need a limit of 8? If you're worried about memory, I guess there are lots of ways to DOS the system... if you're worried about scheduler balancing overhead, we could do a seperate traversal of these guys at a reduced interval (I guess that still leaves some places needing help, though)
It's more of a concern about runaway memory allocations. I don't understand what you mean by a "separate traversal." It seems to me that we want these new sched domains to be fully merged into the single per-runqueue list of sched domains to search. Are you proposing a second runqueue list for these dynamic sched domains? That gets kind of ugly for all those places in the scheduler that want to search the hierarchical sched domain list.
Quoted 4 lines
Quoted 1 line
+static int
find_existing_sched_domain(int cpu, const cpumask_t *cpu_map)Probably some way to distinguish these guys as operating on your "bundle" stack would make it a bit clearer?
Okay.
Quoted 8 lines
Quoted 6 lines
+ /* tweak
sched_domainparams based upon domain size */
+ *sd =SD_NODE_INIT;
+sd->flags|=SD_TRACKS_CPUSET;
+sd->max_interval= 8*(min(new_sd_span, 32));
+sd->span=*cpu_map;
+cpu_set(cpu, new_sd_cpu_map);Can we just have a new
SD_xxx_INITfor these?
Okay.
Quoted 25 lines
Quoted 25 lines
Index:
linux/kernel/cpuset.c
===================================================================kernel/cpuset.c 2006-07-05 16:01:14.892039725 -0700 renamed from kernel/cpuset.c 2006-07-05 15:51:38.873939805 -0700 +6
-2@@ -828,8 +828,12 @@ static int update_cpumask(struct cpuset mutex_lock(&callback_mutex); cs->cpus_allowed = trialcs.cpus_allowed; mutex_unlock(&callback_mutex); - if (is_cpu_exclusive(cs) && !cpus_unchanged) - update_cpu_domains(cs); + if (!cpus_unchanged) { + if (is_cpu_exclusive(cs)) + update_cpu_domains(cs); + else + add_sched_domain(&cs->cpus_allowed); + } return 0; } @@ -1934,6 +1938,8 @@ static int cpuset_rmdir(struct inode *un
set_bit(CS_REMOVED, &cs->flags);
if (is_cpu_exclusive(cs))
update_cpu_domains(cs);
+ else
+destroy_sched_domain(&cs->cpus_allowed);list_del(&cs->sibling); /* delete my sibling fromparent->children*/
Quoted 8 lines
Quoted 3 lines
spin_lock(&cs->dentry->d_lock);
d =dget(cs->dentry);So you're just doing the inside. What about the complement? That way you'd get a nice symmetric allocation on all cpus for each cpuset... but that probably would require making the limit greater than 8.
Doing the compliment makes things just like cpu-exclusive cpusets, right? And the problem is even more complicated since it's commonly the case that cpusets are layered, e.g., first creating a cpuset for cpus 0-31, then carving up those 32 CPUs into smaller subsets. What is then the semantics for the sched domains for all the variations of CPUs within some cpuset and outside of that cpuset?
Even worse, it's legal to create overlapping cpusets, and what is the semantics of isolated sched domains then?
Quoted 2 lines
And it would be probably required for good
sched_setaffinitybalancing too.
True, but I do have concerns (as I noted above). Besides, I believe that most cases of sched_setaffinity() involve a single target CPU. "Cpusets" tends to involve sets of multiple CPUs. At least that's my intuition.
John Hawkes