sched/fair: Skip sched_balance_running cmpxchg when balance is not due
PATCH v3 1/1 sched/fair: Skip sched_balance_running cmpxchg when balance is not due
The NUMA sched domain sets the SD_SERIALIZE flag by default, allowing only one NUMA load balancing operation to run system-wide at a time.
Currently, each sched group leader directly under NUMA domain attempts to acquire the global sched_balance_running flag via cmpxchg() before checking whether load balancing is due or whether it is the designated load balancer for that NUMA domain. On systems with a large number of cores, this causes significant cache contention on the shared sched_balance_running flag.
This patch reduces unnecessary cmpxchg() operations by first checking that the balancer is the designated leader for a NUMA domain from should_we_balance(), and the balance interval has expired before trying to acquire sched_balance_running to load balance a NUMA domain.
On a 2-socket Granite Rapids system with sub-NUMA clustering enabled, running an OLTP workload, 7.8% of total CPU cycles were previously spent in sched_balance_domain() contending on sched_balance_running before this change.
: 104 static __always_inline int arch_atomic_cmpxchg(atomic_t *v, int old, int new)
: 105 {
: 106 return arch_cmpxchg(&v->counter, old, new);
0.00 : ffffffff81326e6c: xor %eax,%eax
0.00 : ffffffff81326e6e: mov $0x1,%ecx
0.00 : ffffffff81326e73: lock cmpxchg %ecx,0x2394195(%rip) # ffffffff836bb010 <sched_balance_running>
: 110 sched_balance_domains():
: 12234 if (atomic_cmpxchg_acquire(&sched_balance_running, 0, 1))
99.39 : ffffffff81326e7b: test %eax,%eax
0.00 : ffffffff81326e7d: jne ffffffff81326e99 <sched_balance_domains+0x209>
: 12238 if (time_after_eq(jiffies, sd->last_balance + interval)) {
0.00 : ffffffff81326e7f: mov 0x14e2b3a(%rip),%rax # ffffffff828099c0 <jiffies_64>
0.00 : ffffffff81326e86: sub 0x48(%r14),%rax
0.00 : ffffffff81326e8a: cmp %rdx,%raxAfter applying this fix, sched_balance_domain() is gone from the profile and there is a 5% throughput improvement.
---
v3:
1. Move check balance time to after should_we_balance()
link to v2: https://lore.kernel.org/lkml/248b775fc9030989c829d4061f6f85ae33dabe45.1761682932.git.tim.c.chen@linux.intel.com/
v2:
1. Rearrange the patch to get rid of an indent level per Peter's suggestion.
2. Updated the data from new run by OLTP team.
--- kernel/sched/fair.c | 56 ++++++++++++++++++++++++--------------------- 1 file changed, 30 insertions(+), 26 deletions(-)
kernel/sched/fair.c +30 -26
@@ -11732,6 +11732,21 @@ static void update_lb_imbalance_stat(struct lb_env *env, struct sched_domain *sd } } +/* +* This flag serializes load-balancing passes over large domains +* (above the NODE topology level) - only one load-balancing instance +* may run at a time, to reduce overhead on very large systems with +* lots of CPUs and large NUMA distances. +* +* - Note that load-balancing passes triggered while another one +* is executing are skipped and not re-tried. +* +* - Also note that this does not serialize rebalance_domains() +* execution, as non-SD_SERIALIZE domains will still be +* load-balanced in parallel. +*/ +static atomic_t sched_balance_running =ATOMIC_INIT (0 ); +/* * Check this_cpu to ensure it is balanced within domain. Attempt to move * tasks if there is an imbalance.
@@ -11757,6 +11772,7 @@ static int sched_balance_rq(int this_cpu, struct rq *this_rq, .fbq_type = all, .tasks = LIST_HEAD_INIT(env.tasks), }; +int need_unlock = false;cpumask_and (cpus,sched_domain_span (sd), cpu_active_mask);
@@ -11768,6 +11784,13 @@ static int sched_balance_rq(int this_cpu, struct rq *this_rq, goto out_balanced; } +if (idle !=CPU_NEWLY_IDLE && (sd->flags &SD_SERIALIZE )) { +if (atomic_cmpxchg_acquire (&sched_balance_running,0 ,1 )) { + goto out_balanced; + } + need_unlock = true; + } + group =sched_balance_find_src_group (&env);if (!group) {schedstat_inc (sd->lb_nobusyg[idle]);
@@ -11892,6 +11915,9 @@ static int sched_balance_rq(int this_cpu, struct rq *this_rq,if (!cpumask_subset (cpus, env.dst_grpmask)) { env.loop =0 ; env.loop_break =SCHED_NR_MIGRATE_BREAK ; +if (need_unlock) +atomic_set_release (&sched_balance_running,0 ); + goto redo; } goto out_all_pinned;
@@ -12008,6 +12034,9 @@ static int sched_balance_rq(int this_cpu, struct rq *this_rq, sd->balance_interval < sd->max_interval) sd->balance_interval *= 2; out: +if (need_unlock) +atomic_set_release (&sched_balance_running,0 ); +return ld_moved; }
@@ -12132,21 +12161,6 @@ static int active_load_balance_cpu_stop(void *data)return 0 ; } -/* -* This flag serializes load-balancing passes over large domains -* (above the NODE topology level) - only one load-balancing instance -* may run at a time, to reduce overhead on very large systems with -* lots of CPUs and large NUMA distances. -* -* - Note that load-balancing passes triggered while another one -* is executing are skipped and not re-tried. -* -* - Also note that this does not serialize rebalance_domains() -* execution, as non-SD_SERIALIZE domains will still be -* load-balanced in parallel. -*/ -static atomic_t sched_balance_running =ATOMIC_INIT (0 ); -/* * Scale the max sched_balance_rq interval with the number of CPUs in the system. * This trades load-balance latency on larger machines for less cross talk.
@@ -12202,7 +12216,7 @@ static void sched_balance_domains(struct rq *rq, enum cpu_idle_type idle)/* Earliest time when we have to do rebalance again */ unsigned long next_balance = jiffies +60 *HZ ;int update_next_balance =0 ; -int need_serialize, need_decay =0 ; +int need_decay =0 ;u64 max_cost =0 ;rcu_read_lock ();
@@ -12226,13 +12240,6 @@ static void sched_balance_domains(struct rq *rq, enum cpu_idle_type idle) } interval =get_sd_balance_interval (sd, busy); - - need_serialize = sd->flags &SD_SERIALIZE ; -if (need_serialize) { -if (atomic_cmpxchg_acquire (&sched_balance_running,0 ,1 )) - goto out; - } -if (time_after_eq (jiffies, sd->last_balance + interval)) {if (sched_balance_rq (cpu, rq, sd, idle, &continue_balancing)) {/*
@@ -12246,9 +12253,6 @@ static void sched_balance_domains(struct rq *rq, enum cpu_idle_type idle) sd->last_balance = jiffies; interval =get_sd_balance_interval (sd, busy); } -if (need_serialize) -atomic_set_release (&sched_balance_running,0 ); -out:if (time_after (next_balance, sd->last_balance + interval)) { next_balance = sd->last_balance + interval; update_next_balance =1 ;
Signature · 1 line
2.32.0
Hello Tim,
Quoted 23 lines · Tim Chen
kernel/sched/fair.c +8
-0@@ -11757,6 +11772,7 @@ static int sched_balance_rq(int this_cpu, struct rq *this_rq, .fbq_type = all, .tasks = LIST_HEAD_INIT(env.tasks), }; +int need_unlock = false;cpumask_and (cpus,sched_domain_span (sd), cpu_active_mask);@@ -11768,6 +11784,13 @@ static int sched_balance_rq(int this_cpu, struct rq *this_rq, goto out_balanced; } +if (idle !=CPU_NEWLY_IDLE && (sd->flags &SD_SERIALIZE )) { +if (atomic_cmpxchg_acquire (&sched_balance_running,0 ,1 )) { + goto out_balanced; + } + need_unlock = true; + } + group =sched_balance_find_src_group (&env);if (!group) {schedstat_inc (sd->lb_nobusyg[idle]);
kernel/sched/fair.c static int sched_balance_rq(int this_cpu, struct rq *this_rq, · line 11919
Quoted 5 lines
kernel/sched/fair.c +2
-0@@ -11892,6 +11915,9 @@ static int sched_balance_rq(int this_cpu, struct rq *this_rq,if (!cpumask_subset (cpus, env.dst_grpmask)) { env.loop =0 ; env.loop_break =SCHED_NR_MIGRATE_BREAK ; +if (need_unlock) +atomic_set_release (&sched_balance_running,0 );
I believe we should reset "need_unlock" to false here since "redo" can fail the atomic_cmpxchg_acquire() while still having "need_unlock" set to "true" and the "out_balanced" path will then perform the atomic_set_release() when another CPU is in middle of a busy / idle balance on a SD_SERIALIZE domain.
We can also initialize the "need_unlock" to false just after the redo label too - whichever you prefer.
nit. "need_unlock" can just be a bool instead of an int.
Apart from that, feel free to include:
Quoted 14 lines
kernel/sched/fair.c +4
-0+ goto redo; } goto out_all_pinned;@@ -12008,6 +12034,9 @@ static int sched_balance_rq(int this_cpu, struct rq *this_rq, sd->balance_interval < sd->max_interval) sd->balance_interval *= 2; out: +if (need_unlock) +atomic_set_release (&sched_balance_running,0 ); +return ld_moved; }
Signature · 2 lines
Thanks and Regards, Prateek
Quoted 9 lines · K Prateek Nayak
Hello Tim,
Quoted 6 lines · Tim Chen
kernel/sched/fair.c +1
-0@@ -11757,6 +11772,7 @@ static int sched_balance_rq(int this_cpu, struct rq *this_rq, .fbq_type = all, .tasks = LIST_HEAD_INIT(env.tasks), }; +int need_unlock = false;
Quoted 7 lines
kernel/sched/fair.c static int sched_balance_rq(int this_cpu, struct rq *this_rq, · line 11787
Quoted 7 lines
kernel/sched/fair.c +1
-0cpumask_and (cpus,sched_domain_span (sd), cpu_active_mask);@@ -11768,6 +11784,13 @@ static int sched_balance_rq(int this_cpu, struct rq *this_rq, goto out_balanced; } +if (idle !=CPU_NEWLY_IDLE && (sd->flags &SD_SERIALIZE )) {
Can you also try removing "idle != CPU_NEWLY_IDLE" and see the workload behavior? If workloads don't observe regression, it might be worth serializing it too.
Quoted 13 lines
Quoted 13 lines
kernel/sched/fair.c +6
-0+if (atomic_cmpxchg_acquire (&sched_balance_running,0 ,1 )) { + goto out_balanced; + } + need_unlock = true; + } + group =sched_balance_find_src_group (&env);if (!group) {schedstat_inc (sd->lb_nobusyg[idle]);@@ -11892,6 +11915,9 @@ static int sched_balance_rq(int this_cpu, struct rq *this_rq,if (!cpumask_subset (cpus, env.dst_grpmask)) { env.loop =0 ; env.loop_break =SCHED_NR_MIGRATE_BREAK ;
Quoted 8 lines
Review static int sched_balance_rq(int this_cpu, struct rq *this_rq, · line 11919
Quoted 2 lines
(quoted diff) +2
-0+ if (need_unlock) + atomic_set_release(&sched_balance_running, 0);I believe we should reset "
need_unlock" to false here since "redo" can fail theatomic_cmpxchg_acquire()while still having "need_unlock" set to "true" and the "out_balanced" path will then perform theatomic_set_release()when another CPU is in middle of a busy / idle balance on aSD_SERIALIZEdomain.
Yes. Setting need_unlock = false looks better.
Quoted 25 lines
We can also initialize the "
need_unlock" to false just after the redo label too - whichever you prefer.nit. "
need_unlock" can just be a bool instead of an int.Apart from that, feel free to include:
Reviewed-by K Prateek Nayak <[email protected]>Quoted 14 lines
kernel/sched/fair.c +4
-0+ goto redo; } goto out_all_pinned;@@ -12008,6 +12034,9 @@ static int sched_balance_rq(int this_cpu, struct rq *this_rq, sd->balance_interval < sd->max_interval) sd->balance_interval *= 2; out: +if (need_unlock) +atomic_set_release (&sched_balance_running,0 ); +return ld_moved; }
Quoted 13 lines · Shrikanth Hegde
Quoted 11 lines · K Prateek Nayak
Hello Tim,
Quoted 8 lines · Tim Chen
kernel/sched/fair.c +1
-0@@ -11757,6 +11772,7 @@ static int sched_balance_rq(int this_cpu, struct rq *this_rq, .fbq_type = all, .tasks = LIST_HEAD_INIT(env.tasks), }; +int need_unlock = false;cpumask_and (cpus,sched_domain_span (sd), cpu_active_mask);
Quoted 8 lines
Quoted 5 lines
kernel/sched/fair.c static int sched_balance_rq(int this_cpu, struct rq *this_rq, · line 11787
Quoted 5 lines
kernel/sched/fair.c +1
-0@@ -11768,6 +11784,13 @@ static int sched_balance_rq(int this_cpu, struct rq *this_rq, goto out_balanced; } +if (idle !=CPU_NEWLY_IDLE && (sd->flags &SD_SERIALIZE )) {Can you also try removing "idle !=
CPU_NEWLY_IDLE" and see the workload behavior? If workloads don't observe regression, it might be worth serializing it too.
Let me ask my colleague running OLTP to give it a try.
Tim
K Prateek Nayak
reviewerHello Shrikanth,
Quoted 7 lines · Shrikanth Hegde
Quoted 4 lines
Quoted 4 lines
@@ -11768,6 +11784,13 @@ static int sched_balance_rq(int
this_cpu, struct rq*this_rq, gotoout_balanced;
}
+ if (idle !=CPU_NEWLY_IDLE&& (sd->flags&SD_SERIALIZE)) {Can you also try removing "idle !=
CPU_NEWLY_IDLE" and see the workload behavior? If workloads don't observe regression, it might be worth serializing it too.
P.S. In one of my previous testing, I had tested only serialized for !env.idle (__CPU_NOT_IDLE) and I didn't spot any difference in my benchmark runs compared to always serializing.
I believe the "max_newidle_lb_cost" along with the plethora of need_resched() checks we have help bail out of newidle balance if there is a wakeup on the same CPU.
Idle balance too was okay with a greater number of search. If the first CPU of group fails to pull any task and remains idle, all the other idle CPUs simply bail out at should_we_balance() which is probably why there was no difference in the set of benchmarks I tested.
Serializing all shouldn't make it any worse that what we have now so I don't mind either.
Signature · 2 lines
Thanks and Regards, Prateek
Quoted 19 lines · K Prateek Nayak
Hello Shrikanth,
Quoted 7 lines · Shrikanth Hegde
Quoted 4 lines
Quoted 4 lines
@@ -11768,6 +11784,13 @@ static int sched_balance_rq(int
this_cpu, struct rq*this_rq, gotoout_balanced;
}
+ if (idle !=CPU_NEWLY_IDLE&& (sd->flags&SD_SERIALIZE)) {Can you also try removing "idle !=
CPU_NEWLY_IDLE" and see the workload behavior? If workloads don't observe regression, it might be worth serializing it too.
P.S. In one of my previous testing, I had tested only serialized for !env.idle (__CPU_NOT_IDLE) and I didn't spot any difference in my benchmark runs compared to always serializing.I believe the "
max_newidle_lb_cost" along with the plethora ofneed_resched()checks we have help bail out of newidle balance if there is a wakeup on the same CPU.
Quoted 8 lines
Idle balance too was okay with a greater number of search. If the first CPU of group fails to pull any task and remains idle, all the other idle CPUs simply bail out at
should_we_balance()which is probably why there was no difference in the set of benchmarks I tested.Serializing all shouldn't make it any worse that what we have now so I don't mind either.
Serializing the CPU_NEWLY_IDLE case does not make things worse from our testing. I will be posting an updated patch shortly.
Tim
Quoted 29 lines · K Prateek Nayak
Hello Tim,
Quoted 26 lines · Tim Chen
kernel/sched/fair.c +8
-0@@ -11757,6 +11772,7 @@ static int sched_balance_rq(int this_cpu, struct rq *this_rq, .fbq_type = all, .tasks = LIST_HEAD_INIT(env.tasks), }; +int need_unlock = false;cpumask_and (cpus,sched_domain_span (sd), cpu_active_mask);@@ -11768,6 +11784,13 @@ static int sched_balance_rq(int this_cpu, struct rq *this_rq, goto out_balanced; } +if (idle !=CPU_NEWLY_IDLE && (sd->flags &SD_SERIALIZE )) { +if (atomic_cmpxchg_acquire (&sched_balance_running,0 ,1 )) { + goto out_balanced; + } + need_unlock = true; + } + group =sched_balance_find_src_group (&env);if (!group) {schedstat_inc (sd->lb_nobusyg[idle]);@@ -11892,6 +11915,9 @@ static int sched_balance_rq(int this_cpu, struct rq *this_rq,if (!cpumask_subset (cpus, env.dst_grpmask)) { env.loop =0 ; env.loop_break =SCHED_NR_MIGRATE_BREAK ;
Quoted 8 lines
Review static int sched_balance_rq(int this_cpu, struct rq *this_rq, · line 11919
Quoted 2 lines
(quoted diff) +2
-0+ if (need_unlock) + atomic_set_release(&sched_balance_running, 0);I believe we should reset "
need_unlock" to false here since "redo" can fail theatomic_cmpxchg_acquire()while still having "need_unlock" set to "true" and the "out_balanced" path will then perform theatomic_set_release()when another CPU is in middle of a busy / idle balance on aSD_SERIALIZEdomain.
Makes sense.
Quoted 5 lines
We can also initialize the "
need_unlock" to false just after the redo label too - whichever you prefer.nit. "
need_unlock" can just be a bool instead of an int.
Sure.
Tim
Quoted 19 lines
Apart from that, feel free to include:
Reviewed-by K Prateek Nayak <[email protected]>Quoted 14 lines
kernel/sched/fair.c +4
-0+ goto redo; } goto out_all_pinned;@@ -12008,6 +12034,9 @@ static int sched_balance_rq(int this_cpu, struct rq *this_rq, sd->balance_interval < sd->max_interval) sd->balance_interval *= 2; out: +if (need_unlock) +atomic_set_release (&sched_balance_running,0 ); +return ld_moved; }