Skip to contentSkip to conversation

sched/fair: Skip sched_balance_running cmpxchg when balance is not due

PATCH RESEND Superseded Tim Chen 5 participants 19 messages Oct 2, 2025 – Oct 22, 2025 Scheduler

v1 of 4 Superseded Latest is v4 →

lore
Share & tools
lore

Tim Chen

author
#0 lore raw

PATCH RESEND 1/1 sched/fair: Skip sched_balance_running cmpxchg when balance is not due

Repost comments:

There have been past discussions about avoiding serialization in load balancing, but no objections were raised to this patch itself during its last posting:
https://lore.kernel.org/lkml/[email protected]/

Vincent and Chen Yu have already provided their Reviewed-by tags.

We recently encountered this issue again on a 2-socket, 240-core Clearwater Forest server running SPECjbb. In this case, 14% of CPU cycles were wasted on unnecessary acquisitions of sched_balance_running. This reinforces the need for the change, and we hope it can be merged.

Tim

---

During load balancing, balancing at the LLC level and above must be serialized. The scheduler currently checks the atomic sched_balance_running flag before verifying whether a balance is actually due. This causes high contention, as multiple CPUs may attempt to acquire the flag concurrently.

On a 2-socket Granite Rapids system with sub-NUMA clustering enabled and running OLTP workloads, 7.6% of CPU cycles were spent on cmpxchg operations for sched_balance_running. In most cases, the attempt aborts immediately after acquisition because the load balance time is not yet due.

Fix this by checking whether a balance is due *before* trying to acquire sched_balance_running. This avoids many wasted acquisitions and reduces the cmpxchg overhead in sched_balance_domain() from 7.6% to 0.05%. As a result, OLTP throughput improves by 11%.

---
 kernel/sched/fair.c | 16 ++++++++--------
 1 file changed, 8 insertions(+), 8 deletions(-)
kernel/sched/fair.c +8 -8
index 8ce56a8d507f..bedd785c4a39 100644
@@ -12126,13 +12126,13 @@ 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)) {
+			need_serialize = sd->flags & SD_SERIALIZE;
+			if (need_serialize) {
+				if (atomic_cmpxchg_acquire(&sched_balance_running, 0, 1))
+					goto out;
+			}
+
 			if (sched_balance_rq(cpu, rq, sd, idle, &continue_balancing)) {
 				/*
 				 * The LBF_DST_PINNED logic could have changed
@@ -12144,9 +12144,9 @@ 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);
 		}
-		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;
Signature · 1 line
2.32.0
Reviewed-by on v1 1/1
#1 lore raw
Quoted 36 lines · Tim Chen

Repost comments:

There have been past discussions about avoiding serialization in load balancing, but no objections were raised to this patch itself during its last posting:
https://lore.kernel.org/lkml/[email protected]/

Vincent and Chen Yu have already provided their Reviewed-by tags.

We recently encountered this issue again on a 2-socket, 240-core Clearwater Forest server running SPECjbb. In this case, 14% of CPU cycles were wasted on unnecessary acquisitions of sched_balance_running. This reinforces the need for the change, and we hope it can be merged.

Tim

---

During load balancing, balancing at the LLC level and above must be serialized. The scheduler currently checks the atomic sched_balance_running flag before verifying whether a balance is actually due. This causes high contention, as multiple CPUs may attempt to acquire the flag concurrently.

On a 2-socket Granite Rapids system with sub-NUMA clustering enabled and running OLTP workloads, 7.6% of CPU cycles were spent on cmpxchg operations for sched_balance_running. In most cases, the attempt aborts immediately after acquisition because the load balance time is not yet due.

Fix this by checking whether a balance is due *before* trying to acquire sched_balance_running. This avoids many wasted acquisitions and reduces the cmpxchg overhead in sched_balance_domain() from 7.6% to 0.05%. As a result, OLTP throughput improves by 11%.

Quoted 4 lines

---

Hi Tim.

Fine by me. unnecessary atomic operations do hurt on large systems. The further optimization that i pointed out can come in later i guess. That would help only further. this should be good to begin with.

With that.

author
#2 lore raw
Quoted 43 lines · Shrikanth Hegde
Quoted 36 lines · Tim Chen

Repost comments:

There have been past discussions about avoiding serialization in load balancing, but no objections were raised to this patch itself during its last posting:
https://lore.kernel.org/lkml/[email protected]/

Vincent and Chen Yu have already provided their Reviewed-by tags.

We recently encountered this issue again on a 2-socket, 240-core Clearwater Forest server running SPECjbb. In this case, 14% of CPU cycles were wasted on unnecessary acquisitions of sched_balance_running. This reinforces the need for the change, and we hope it can be merged.

Tim

---

During load balancing, balancing at the LLC level and above must be serialized. The scheduler currently checks the atomic sched_balance_running flag before verifying whether a balance is actually due. This causes high contention, as multiple CPUs may attempt to acquire the flag concurrently.

On a 2-socket Granite Rapids system with sub-NUMA clustering enabled and running OLTP workloads, 7.6% of CPU cycles were spent on cmpxchg operations for sched_balance_running. In most cases, the attempt aborts immediately after acquisition because the load balance time is not yet due.

Fix this by checking whether a balance is due *before* trying to acquire sched_balance_running. This avoids many wasted acquisitions and reduces the cmpxchg overhead in sched_balance_domain() from 7.6% to 0.05%. As a result, OLTP throughput improves by 11%.

Quoted 4 lines

---

Quoted 5 lines

Hi Tim.

Fine by me. unnecessary atomic operations do hurt on large systems. The further optimization that i pointed out can come in later i guess. That would help only further. this should be good to begin with.

Thanks for your review and your past comments. We'll look into further optimization if we find that this became a hot path again. For now this change seemed to be good enough.

Tim

Quoted 4 lines

With that.

maintainer
#3 lore raw
In reply to Tim Chen
Quoted 2 lines · Tim Chen

During load balancing, balancing at the LLC level and above must be serialized.

I would argue the wording here, there is no *must*, they *are*. Per the current rules SD_NUMA and up get SD_SERIALIZE.

This is a *very* old thing, done by Christoph Lameter back when he was at SGI. I'm not sure this default is still valid or not. Someone would have to investigate. An alternative would be moving it into node_reclaim_distance or somesuch.

Quoted 4 lines

The scheduler currently checks the atomic sched_balance_running flag before verifying whether a balance is actually due. This causes high contention, as multiple CPUs may attempt to acquire the flag concurrently.

Right.

Quoted 5 lines

On a 2-socket Granite Rapids system with sub-NUMA clustering enabled and running OLTP workloads, 7.6% of CPU cycles were spent on cmpxchg operations for sched_balance_running. In most cases, the attempt aborts immediately after acquisition because the load balance time is not yet due.

So I'm not sure I understand the situation, @continue_balancing should limit this concurrency to however many groups are on this domain -- your granite thing with SNC on would have something like 6 groups?

Quoted 4 lines

Fix this by checking whether a balance is due *before* trying to acquire sched_balance_running. This avoids many wasted acquisitions and reduces the cmpxchg overhead in sched_balance_domain() from 7.6% to 0.05%. As a result, OLTP throughput improves by 11%.

Yeah, I see no harm flipping this, but the Changelog needs help.

Quoted 33 lines
---
 kernel/sched/fair.c | 16 ++++++++--------
 1 file changed, 8 insertions(+), 8 deletions(-)
kernel/sched/fair.c +6 -6
index 8ce56a8d507f..bedd785c4a39 100644
@@ -12126,13 +12126,13 @@ 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)) {
+			need_serialize = sd->flags & SD_SERIALIZE;
+			if (need_serialize) {
+				if (atomic_cmpxchg_acquire(&sched_balance_running, 0, 1))
+					goto out;
+			}
+
 			if (sched_balance_rq(cpu, rq, sd, idle, &continue_balancing)) {
 				/*
 				 * The LBF_DST_PINNED logic could have changed
@@ -12144,9 +12144,9 @@ static void sched_balance_domains(struct rq *rq, enum cpu_idle_type idle)
 			}

kernel/sched/fair.c static void sched_balance_domains(struct rq *rq, enum cpu_idle_type idle) · line 12152

Quoted 10 lines
kernel/sched/fair.c +2 -2
 			sd->last_balance = jiffies;
 			interval = get_sd_balance_interval(sd, busy);
+			if (need_serialize)
+				atomic_set_release(&sched_balance_running, 0);
 		}
-		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;

Instead of making the indenting worse, could we make it better?

---

kernel/sched/fair.c +13 -12
index e743d9d0576c..6318834ff42a 100644
@@ -12215,6 +12215,8 @@ static void sched_balance_domains(struct rq *rq, enum cpu_idle_type idle)
 		}
 
 		interval = get_sd_balance_interval(sd, busy);
+		if (time_before(jiffies, sd->last_balance + interval))
+			goto out;
 
 		need_serialize = sd->flags & SD_SERIALIZE;
 		if (need_serialize) {
@@ -12222,19 +12224,18 @@ static void sched_balance_domains(struct rq *rq, enum cpu_idle_type idle)
 				goto out;
 		}
 
-		if (time_after_eq(jiffies, sd->last_balance + interval)) {
-			if (sched_balance_rq(cpu, rq, sd, idle, &continue_balancing)) {
-				/*
-				 * The LBF_DST_PINNED logic could have changed
-				 * env->dst_cpu, so we can't know our idle
-				 * state even if we migrated tasks. Update it.
-				 */
-				idle = idle_cpu(cpu);
-				busy = !idle && !sched_idle_cpu(cpu);
-			}
-			sd->last_balance = jiffies;
-			interval = get_sd_balance_interval(sd, busy);
+		if (sched_balance_rq(cpu, rq, sd, idle, &continue_balancing)) {
+			/*
+			 * The LBF_DST_PINNED logic could have changed
+			 * env->dst_cpu, so we can't know our idle
+			 * state even if we migrated tasks. Update it.
+			 */
+			idle = idle_cpu(cpu);
+			busy = !idle && !sched_idle_cpu(cpu);
 		}
+		sd->last_balance = jiffies;
+		interval = get_sd_balance_interval(sd, busy);
+
 		if (need_serialize)
 			atomic_set_release(&sched_balance_running, 0);
 out:
#4 lore raw
Quoted 5 lines · Peter Zijlstra
Quoted 2 lines · Tim Chen

During load balancing, balancing at the LLC level and above must be serialized.

Quoted 8 lines

I would argue the wording here, there is no *must*, they *are*. Per the current rules SD_NUMA and up get SD_SERIALIZE.

This is a *very* old thing, done by Christoph Lameter back when he was at SGI. I'm not sure this default is still valid or not. Someone would have to investigate. An alternative would be moving it into node_reclaim_distance or somesuch.

Do you mean the following:

kernel/sched/topology.c +0 -1
index 444bdfdab731..436c899d8da2 100644
@@ -1697,11 +1697,16 @@ sd_init(struct sched_domain_topology_level *tl,
                sd->cache_nice_tries = 2;
 
                sd->flags &= ~SD_PREFER_SIBLING;
-               sd->flags |= SD_SERIALIZE;
                if (sched_domains_numa_distance[tl->numa_level] > 
node_reclaim_distance) {
                        sd->flags &= ~(SD_BALANCE_EXEC |
                                       SD_BALANCE_FORK |
                                       SD_WAKE_AFFINE);
+                       /*
+                        * Nodes that are far away need to be serialized to
+                        * reduce the overhead of long-distance task migration
+                        * caused by load balancing.
+                        */
+                       sd->flags |= SD_SERIALIZE;
                }

We can launch some tests to see if removing SD_SERIALIZE would bring any impact.

Quoted 10 lines
Quoted 5 lines

On a 2-socket Granite Rapids system with sub-NUMA clustering enabled and running OLTP workloads, 7.6% of CPU cycles were spent on cmpxchg operations for sched_balance_running. In most cases, the attempt aborts immediately after acquisition because the load balance time is not yet due.

So I'm not sure I understand the situation, @continue_balancing should limit this concurrency to however many groups are on this domain -- your granite thing with SNC on would have something like 6 groups?

My understanding is that, continue_balancing is set to false after atomic_cmpxhg(sched_balance_running), so if sched_balance_domains() is invoked by many CPUs in parallel, the atomic operation still compete?

thanks,
Chenyu

#5 lore raw
Quoted 49 lines · Yu C
Quoted 5 lines · Peter Zijlstra
Quoted 2 lines · Tim Chen

During load balancing, balancing at the LLC level and above must be serialized.

Quoted 8 lines

I would argue the wording here, there is no *must*, they *are*. Per the current rules SD_NUMA and up get SD_SERIALIZE.

This is a *very* old thing, done by Christoph Lameter back when he was at SGI. I'm not sure this default is still valid or not. Someone would have to investigate. An alternative would be moving it into node_reclaim_distance or somesuch.

Do you mean the following:

kernel/sched/topology.c +3 -1
index 444bdfdab731..436c899d8da2 100644
@@ -1697,11 +1697,16 @@ sd_init(struct sched_domain_topology_level *tl,
                 sd->cache_nice_tries = 2;
 
                 sd->flags &= ~SD_PREFER_SIBLING;
-               sd->flags |= SD_SERIALIZE;
                 if (sched_domains_numa_distance[tl->numa_level] > 
 node_reclaim_distance) {
                         sd->flags &= ~(SD_BALANCE_EXEC |
                                        SD_BALANCE_FORK |
                                        SD_WAKE_AFFINE);
+                       /*
+                        * Nodes that are far away need to be serialized to
+                        * reduce the overhead of long-distance task 

migration
+                        * caused by load balancing. +                        */
+                       sd->flags |= SD_SERIALIZE;
                }

We can launch some tests to see if removing SD_SERIALIZE would bring any impact.

Quoted 6 lines
Quoted 5 lines

On a 2-socket Granite Rapids system with sub-NUMA clustering enabled and running OLTP workloads, 7.6% of CPU cycles were spent on cmpxchg operations for sched_balance_running. In most cases, the attempt aborts immediately after acquisition because the load balance time is not yet due.

Quoted 9 lines
Quoted 4 lines

So I'm not sure I understand the situation, @continue_balancing should limit this concurrency to however many groups are on this domain -- your granite thing with SNC on would have something like 6 groups?

My understanding is that, continue_balancing is set to false after atomic_cmpxhg(sched_balance_running), so if sched_balance_domains() is invoked by many CPUs in parallel, the atomic operation still compete?

From what i could remember,

This mostly always happens at SMT after which continue_balancing = 0. Since multiple CPUs end up calling it (specially on busy system) it causes a lot cacheline bouncing. and ends up showing in perf profile.

#6 lore raw
Quoted 62 lines · Shrikanth Hegde
Quoted 49 lines · Yu C
Quoted 5 lines · Peter Zijlstra
Quoted 2 lines · Tim Chen

During load balancing, balancing at the LLC level and above must be serialized.

Quoted 8 lines

I would argue the wording here, there is no *must*, they *are*. Per the current rules SD_NUMA and up get SD_SERIALIZE.

This is a *very* old thing, done by Christoph Lameter back when he was at SGI. I'm not sure this default is still valid or not. Someone would have to investigate. An alternative would be moving it into node_reclaim_distance or somesuch.

Do you mean the following:

kernel/sched/topology.c +0 -1
index 444bdfdab731..436c899d8da2 100644
@@ -1697,11 +1697,16 @@ sd_init(struct sched_domain_topology_level *tl,
                  sd->cache_nice_tries = 2;
 
                  sd->flags &= ~SD_PREFER_SIBLING;
-               sd->flags |= SD_SERIALIZE;
                  if (sched_domains_numa_distance[tl->numa_level] > node_reclaim_distance) {

                         sd->flags &= ~(SD_BALANCE_EXEC | SD_BALANCE_FORK | SD_WAKE_AFFINE); +                       /*
+                        * Nodes that are far away need to be serialized to
+                        * reduce the overhead of long-distance task migration
+                        * caused by load balancing.
+                        */
+                       sd->flags |= SD_SERIALIZE;
                 }

We can launch some tests to see if removing SD_SERIALIZE would bring any impact.

Quoted 6 lines
Quoted 5 lines

On a 2-socket Granite Rapids system with sub-NUMA clustering enabled and running OLTP workloads, 7.6% of CPU cycles were spent on cmpxchg operations for sched_balance_running. In most cases, the attempt aborts immediately after acquisition because the load balance time is not yet due.

Quoted 9 lines
Quoted 4 lines

So I'm not sure I understand the situation, @continue_balancing should limit this concurrency to however many groups are on this domain -- your granite thing with SNC on would have something like 6 groups?

My understanding is that, continue_balancing is set to false after atomic_cmpxhg(sched_balance_running), so if sched_balance_domains() is invoked by many CPUs in parallel, the atomic operation still compete?

Quoted 6 lines

From what i could remember,

This mostly always happens at SMT after which continue_balancing = 0. Since multiple CPUs end up calling it (specially on busy system) it causes a lot cacheline bouncing. and ends up showing in perf profile.

I see, when reaching NUMA domain, the continue_balancing should already be set to 0. Thanks for pointing it out.

thanks,
Chenyu

maintainer
#7 lore raw
In reply to Chen, Yu C
Quoted 40 lines · Yu C
Quoted 5 lines · Peter Zijlstra
Quoted 2 lines · Tim Chen

During load balancing, balancing at the LLC level and above must be serialized.

Quoted 8 lines

I would argue the wording here, there is no *must*, they *are*. Per the current rules SD_NUMA and up get SD_SERIALIZE.

This is a *very* old thing, done by Christoph Lameter back when he was at SGI. I'm not sure this default is still valid or not. Someone would have to investigate. An alternative would be moving it into node_reclaim_distance or somesuch.

Do you mean the following:

kernel/sched/topology.c +3 -1
index 444bdfdab731..436c899d8da2 100644
@@ -1697,11 +1697,16 @@ sd_init(struct sched_domain_topology_level *tl,
                sd->cache_nice_tries = 2;
 
                sd->flags &= ~SD_PREFER_SIBLING;
-               sd->flags |= SD_SERIALIZE;
                if (sched_domains_numa_distance[tl->numa_level] >
 node_reclaim_distance) {
                        sd->flags &= ~(SD_BALANCE_EXEC |
                                       SD_BALANCE_FORK |
                                       SD_WAKE_AFFINE);
+                       /*
+                        * Nodes that are far away need to be serialized to
+                        * reduce the overhead of long-distance task

migration

kernel/sched/topology.c +3 -0
+                        * caused by load balancing.
+                        */
+                       sd->flags |= SD_SERIALIZE;
                }
Quoted 2 lines

We can launch some tests to see if removing SD_SERIALIZE would bring any impact.

Yeah, something like that. But lets first get this other thing sorted. I agree that the SD_SERIALIZE thing is in the wrong place.

author
#8 lore raw
In reply to Peter Zijlstra
Quoted 26 lines · Peter Zijlstra
Quoted 2 lines · Tim Chen

During load balancing, balancing at the LLC level and above must be serialized.

I would argue the wording here, there is no *must*, they *are*. Per the current rules SD_NUMA and up get SD_SERIALIZE.

This is a *very* old thing, done by Christoph Lameter back when he was at SGI. I'm not sure this default is still valid or not. Someone would have to investigate. An alternative would be moving it into node_reclaim_distance or somesuch.

Quoted 4 lines

The scheduler currently checks the atomic sched_balance_running flag before verifying whether a balance is actually due. This causes high contention, as multiple CPUs may attempt to acquire the flag concurrently.

Right.

Quoted 5 lines

On a 2-socket Granite Rapids system with sub-NUMA clustering enabled and running OLTP workloads, 7.6% of CPU cycles were spent on cmpxchg operations for sched_balance_running. In most cases, the attempt aborts immediately after acquisition because the load balance time is not yet due.

Quoted 3 lines

So I'm not sure I understand the situation, @continue_balancing should limit this concurrency to however many groups are on this domain -- your granite thing with SNC on would have something like 6 groups?

That's a good point. But I think the contention is worse than 6 CPUs.

The hierarchy would be

SMT
NUMA-level1
NUMA-level2
NUMA-level3
NUMA-level4

There would be multiple CPUs in that are first in the SMT group with continue_balancing=1 going up in the hierachy and attempting the cmpxchg in the first NUMA domain level, before calling should_we_balance() and finding that they are not the first in the NUMA domain and set continue_balancing=0 and abort. Those CPUS are in same L3.
But at the same time, there could be CPUs in other sockets cmpxchg on sched_balance_running.

In our experiment, we have not applied
NUMA hierarchy fix [1]. The system can have up to 4 NUMA domains, with 3 NUMA levels spanning different sockets.
That means there can be up to three concurrent cmpxchg attempts per level from CPUs in separate sockets. So that would make things worse.

[1] https://lore.kernel.org/lkml/[email protected]/

Quoted 45 lines
Quoted 5 lines

Fix this by checking whether a balance is due *before* trying to acquire sched_balance_running. This avoids many wasted acquisitions and reduces the cmpxchg overhead in sched_balance_domain() from 7.6% to 0.05%. As a result, OLTP throughput improves by 11%.

Yeah, I see no harm flipping this, but the Changelog needs help.

Quoted 37 lines
---
 kernel/sched/fair.c | 16 ++++++++--------
 1 file changed, 8 insertions(+), 8 deletions(-)
kernel/sched/fair.c +8 -6
index 8ce56a8d507f..bedd785c4a39 100644
@@ -12126,13 +12126,13 @@ 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)) {
+			need_serialize = sd->flags & SD_SERIALIZE;
+			if (need_serialize) {
+				if (atomic_cmpxchg_acquire(&sched_balance_running, 0, 1))
+					goto out;
+			}
+
 			if (sched_balance_rq(cpu, rq, sd, idle, &continue_balancing)) {
 				/*
 				 * The LBF_DST_PINNED logic could have changed
@@ -12144,9 +12144,9 @@ 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);
Quoted 8 lines

kernel/sched/fair.c static void sched_balance_domains(struct rq *rq, enum cpu_idle_type idle) · line 12152

Quoted 6 lines
kernel/sched/fair.c +0 -2
 		}
-		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;

Instead of making the indenting worse, could we make it better?

Yes, this is much better. Thanks.

Tim

Quoted 46 lines

---

kernel/sched/fair.c +13 -12
index e743d9d0576c..6318834ff42a 100644
@@ -12215,6 +12215,8 @@ static void sched_balance_domains(struct rq *rq, enum cpu_idle_type idle)
 		}
 
 		interval = get_sd_balance_interval(sd, busy);
+		if (time_before(jiffies, sd->last_balance + interval))
+			goto out;
 
 		need_serialize = sd->flags & SD_SERIALIZE;
 		if (need_serialize) {
@@ -12222,19 +12224,18 @@ static void sched_balance_domains(struct rq *rq, enum cpu_idle_type idle)
 				goto out;
 		}
 
-		if (time_after_eq(jiffies, sd->last_balance + interval)) {
-			if (sched_balance_rq(cpu, rq, sd, idle, &continue_balancing)) {
-				/*
-				 * The LBF_DST_PINNED logic could have changed
-				 * env->dst_cpu, so we can't know our idle
-				 * state even if we migrated tasks. Update it.
-				 */
-				idle = idle_cpu(cpu);
-				busy = !idle && !sched_idle_cpu(cpu);
-			}
-			sd->last_balance = jiffies;
-			interval = get_sd_balance_interval(sd, busy);
+		if (sched_balance_rq(cpu, rq, sd, idle, &continue_balancing)) {
+			/*
+			 * The LBF_DST_PINNED logic could have changed
+			 * env->dst_cpu, so we can't know our idle
+			 * state even if we migrated tasks. Update it.
+			 */
+			idle = idle_cpu(cpu);
+			busy = !idle && !sched_idle_cpu(cpu);
 		}
+		sd->last_balance = jiffies;
+		interval = get_sd_balance_interval(sd, busy);
+
 		if (need_serialize)
 			atomic_set_release(&sched_balance_running, 0);
 out:
maintainer
#9 lore raw
Quoted 7 lines · Tim Chen
Quoted 3 lines

So I'm not sure I understand the situation, @continue_balancing should limit this concurrency to however many groups are on this domain -- your granite thing with SNC on would have something like 6 groups?

That's a good point. But I think the contention is worse than 6 CPUs.

Quoted 7 lines

The hierarchy would be

SMT
NUMA-level1
NUMA-level2
NUMA-level3
NUMA-level4

Aren't you missing the LLC/NODE domain here? We should have at least one !SD_NUMA domain above SMT.

Quoted 8 lines

There would be multiple CPUs in that are first in the SMT group with continue_balancing=1 going up in the hierachy and attempting the cmpxchg in the first NUMA domain level, before calling should_we_balance() and finding that they are not the first in the NUMA domain and set continue_balancing=0 and abort. Those CPUS are in same L3.
But at the same time, there could be CPUs in other sockets cmpxchg on sched_balance_running.

Right, Yu Chen said something like that as well, should_we_balance() is too late.

Should we instead move the whole serialize thing inside sched_balance_rq() like so:

---

kernel/sched/fair.c +28 -26
index bc0b7ce8a65d..e9f719ba17e1 100644
@@ -11722,6 +11722,22 @@ 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.
@@ -11747,6 +11763,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);
 
@@ -11758,6 +11775,12 @@ 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]);
@@ -11998,6 +12021,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;
 }
 
@@ -12122,21 +12148,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.
@@ -12192,7 +12203,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();
@@ -12216,13 +12227,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)) {
 				/*
@@ -12236,9 +12240,7 @@ 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;
#10 lore raw
Quoted 72 lines · Peter Zijlstra
Quoted 7 lines · Tim Chen
Quoted 3 lines

So I'm not sure I understand the situation, @continue_balancing should limit this concurrency to however many groups are on this domain -- your granite thing with SNC on would have something like 6 groups?

That's a good point. But I think the contention is worse than 6 CPUs.

Quoted 7 lines

The hierarchy would be

SMT
NUMA-level1
NUMA-level2
NUMA-level3
NUMA-level4

Aren't you missing the LLC/NODE domain here? We should have at least one !SD_NUMA domain above SMT.

Quoted 8 lines

There would be multiple CPUs in that are first in the SMT group with continue_balancing=1 going up in the hierachy and attempting the cmpxchg in the first NUMA domain level, before calling should_we_balance() and finding that they are not the first in the NUMA domain and set continue_balancing=0 and abort. Those CPUS are in same L3.
But at the same time, there could be CPUs in other sockets cmpxchg on sched_balance_running.

Right, Yu Chen said something like that as well, should_we_balance() is too late.

Should we instead move the whole serialize thing inside sched_balance_rq() like so:

---

kernel/sched/fair.c +17 -0
index bc0b7ce8a65d..e9f719ba17e1 100644
@@ -11722,6 +11722,22 @@ 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.
@@ -11747,6 +11763,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);
 

kernel/sched/fair.c static int sched_balance_rq(int this_cpu, struct rq *this_rq, · line 11780

Quoted 6 lines
kernel/sched/fair.c +3 -0
@@ -11758,6 +11775,12 @@ 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;

Maybe goto out instead of out_balanced ?

Quoted 72 lines
kernel/sched/fair.c +8 -26
+		need_unlock = true;
+	}
+
  	group = sched_balance_find_src_group(&env);
  	if (!group) {
  		schedstat_inc(sd->lb_nobusyg[idle]);
@@ -11998,6 +12021,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;
  }
 
@@ -12122,21 +12148,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.
@@ -12192,7 +12203,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();
@@ -12216,13 +12227,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)) {
  				/*
@@ -12236,9 +12240,7 @@ 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;
maintainer
#11 lore raw
Quoted 8 lines · Shrikanth Hegde

kernel/sched/fair.c static int sched_balance_rq(int this_cpu, struct rq *this_rq, · line 11779

Quoted 6 lines
kernel/sched/fair.c +3 -0
@@ -11758,6 +11775,12 @@ 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;

Maybe goto out instead of out_balanced ?

That would be inconsistent with the !should_we_balance() goto out_balanced right above this, no?

Quoted 69 lines
Quoted 68 lines
kernel/sched/fair.c +8 -26
+		need_unlock = true;
+	}
+
  	group = sched_balance_find_src_group(&env);
  	if (!group) {
  		schedstat_inc(sd->lb_nobusyg[idle]);
@@ -11998,6 +12021,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;
  }
@@ -12122,21 +12148,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.
@@ -12192,7 +12203,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();
@@ -12216,13 +12227,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)) {
  				/*
@@ -12236,9 +12240,7 @@ 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;
#12 lore raw
Quoted 11 lines · Peter Zijlstra
Quoted 8 lines · Shrikanth Hegde

kernel/sched/fair.c static int sched_balance_rq(int this_cpu, struct rq *this_rq, · line 11779

Quoted 6 lines
kernel/sched/fair.c +3 -0
@@ -11758,6 +11775,12 @@ 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;

Maybe goto out instead of out_balanced ?

Quoted 2 lines

That would be inconsistent with the !should_we_balance() goto out_balanced right above this, no?

Yes. But whats the reason for saying out_balanced for !should_we_balance?

Load balance wasn't even attempted there right? Could this be updating it wrongly? At-least comments around out_all_pinned doesn't make sense if we came here via !swb

        schedstat_inc(sd->lb_balanced[idle]);
        sd->nr_balance_failed = 0;
Quoted 70 lines
Quoted 70 lines
Quoted 69 lines
kernel/sched/fair.c +8 -26
+		need_unlock = true;
+	}
+
   	group = sched_balance_find_src_group(&env);
   	if (!group) {
   		schedstat_inc(sd->lb_nobusyg[idle]);
@@ -11998,6 +12021,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;
   }
@@ -12122,21 +12148,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.
@@ -12192,7 +12203,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();
@@ -12216,13 +12227,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)) {
   				/*
@@ -12236,9 +12240,7 @@ 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;
#13 lore raw
In reply to Peter Zijlstra
Quoted 11 lines · Peter Zijlstra
Quoted 8 lines · Shrikanth Hegde

Review static int sched_balance_rq(int this_cpu, struct rq *this_rq, · line 11779

Quoted 6 lines
(quoted diff) +3 -0
@@ -11758,6 +11775,12 @@ 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;

Maybe goto out instead of out_balanced ?

Quoted 3 lines

That would be inconsistent with the !should_we_balance() goto out_balanced right above this, no?

Hi Peter.

Did similar probe points numbers compared to this. Even the patch is quite similar to what was suggested there a while ago.
https://lore.kernel.org/all/[email protected]/

480 CPUs system with 6 NUMA nodes. (different system than last time)

tl;dr

- Number of time sched_balance_running is taken is way less after the swb check. (which is great)
- Number of time it fails to set is very low after swb. (So out_balanced vs out may not make a significant difference.)
- Patch is at the end. It is this patch + redo stuff + (ref_variable_stuff(ignore))

--- detailed log----

++++++++++++ probe points +++++++++++++++
(added a ref("crap") so i could put a probe where i want )

0 static void sched_balance_domains(struct rq *rq, enum cpu_idle_type idle)
...

     20                 max_cost += sd->max_newidle_lb_cost;
         
                        /*
                         * Stop the load balance at this level. There is another
                         * CPU in our sched group which is doing load balancing more
                         * actively.
                         */
                        if (!continue_balancing) {
                                if (need_decay)
                                        continue;
                                break;
                        }
     33                 if (sd->flags & SD_SERIALIZE)
     34                         ref = ref + 5;

<sched_balance_rq@/home/shrikanth/sched_tip/kernel/sched/fair.c:0>
0 static int sched_balance_rq(int this_cpu, struct rq *this_rq,
struct sched_domain *sd, enum cpu_idle_type idle, int *continue_balancing)
...

                int need_unlock = false;
         
                cpumask_and(cpus, sched_domain_span(sd), cpu_active_mask);
         
     25         schedstat_inc(sd->lb_count[idle]);
...
     34         if (idle != CPU_NEWLY_IDLE && (sd->flags & SD_SERIALIZE)) {
     35                 if (atomic_cmpxchg_acquire(&sched_balance_running, 0, 1)) {
     36                         ref = ref+1;
     37                         goto out_balanced;
                        }
     39                 ref = ref + 2;
     40                 need_unlock = true;
...
                                        env.loop_break = SCHED_NR_MIGRATE_BREAK;
    167                                 if (need_unlock) {
    168                                         ref = ref+3;
    169                                         atomic_set_release(&sched_balance_running, 0);
                                        }
                                        goto redo;
...
         out:
    287         if (need_unlock) {
    288                 ref = ref +4;
    289                 atomic_set_release(&sched_balance_running, 0);
                }
         
                return ld_moved;

probe:sched_balance_domains_L34 (on sched_balance_domains:34@kernel/sched/fair.c) probe:sched_balance_rq_L168 (on sched_balance_rq:168@kernel/sched/fair.c) probe:sched_balance_rq_L21 (on sched_balance_rq+312@kernel/sched/fair.c) probe:sched_balance_rq_L288 (on sched_balance_rq+312@kernel/sched/fair.c) probe:sched_balance_rq_L35 (on sched_balance_rq+312@kernel/sched/fair.c) probe:sched_balance_rq_L36 (on sched_balance_rq+312@kernel/sched/fair.c) probe:sched_balance_rq_L39 (on sched_balance_rq+312@kernel/sched/fair.c)

+++++++++++ Data on various load points ++++++++++++++++++++++++ --- idle ---
perf stat -a -e probe:* sleep 10

             6,123      probe:sched_balance_domains_L34
            10,378      probe:sched_balance_rq_L21
                79      probe:sched_balance_rq_L35
                17      probe:sched_balance_rq_L36
                62      probe:sched_balance_rq_L39
                 0      probe:sched_balance_rq_L168
                62      probe:sched_balance_rq_L288

--- 25% load ---
perf stat -a -e probe:* stress-ng --cpu=480 -l 25 -t 10

           510,551      probe:sched_balance_domains_L34
           303,892      probe:sched_balance_rq_L21
               442      probe:sched_balance_rq_L35
                 3      probe:sched_balance_rq_L36
               439      probe:sched_balance_rq_L39
                 0      probe:sched_balance_rq_L168
               439      probe:sched_balance_rq_L288

--- 50% load ---

           248,969      probe:sched_balance_domains_L34
           187,864      probe:sched_balance_rq_L21
               926      probe:sched_balance_rq_L35
                 6      probe:sched_balance_rq_L36
               920      probe:sched_balance_rq_L39
                 0      probe:sched_balance_rq_L168
               920      probe:sched_balance_rq_L288

--- 75% load ---

           110,294      probe:sched_balance_domains_L34
            71,568      probe:sched_balance_rq_L21
               861      probe:sched_balance_rq_L35
                 6      probe:sched_balance_rq_L36
               855      probe:sched_balance_rq_L39
                 0      probe:sched_balance_rq_L168
               855      probe:sched_balance_rq_L288

--- 100% load ---

            85,960      probe:sched_balance_domains_L34
            48,169      probe:sched_balance_rq_L21
                71      probe:sched_balance_rq_L35
                 4      probe:sched_balance_rq_L36
                67      probe:sched_balance_rq_L39
                 0      probe:sched_balance_rq_L168
                67      probe:sched_balance_rq_L288

++++++++++++++++++ patch ++++++++++++++++++++++++++++++++++++ (ignore ref crap)

kernel/sched/fair.c +44 -26
index cee1793e8277..832104705500 100644
@@ -11722,10 +11722,29 @@ 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.
  */
+
+int ref = 0;
+
 static int sched_balance_rq(int this_cpu, struct rq *this_rq,
 			struct sched_domain *sd, enum cpu_idle_type idle,
 			int *continue_balancing)
@@ -11747,10 +11766,12 @@ 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);
 
 	schedstat_inc(sd->lb_count[idle]);
+	ref = 1;
 
 redo:
 	if (!should_we_balance(&env)) {
@@ -11758,6 +11779,15 @@ 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)) {
+			ref = ref+1;
+			goto out_balanced;
+		}
+		ref = ref + 2;
+		need_unlock = true;
+	}
+
 	group = sched_balance_find_src_group(&env);
 	if (!group) {
 		schedstat_inc(sd->lb_nobusyg[idle]);
@@ -11882,6 +11912,10 @@ 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) {
+					ref = ref+3;
+					atomic_set_release(&sched_balance_running, 0);
+				}
 				goto redo;
 			}
 			goto out_all_pinned;
@@ -11998,6 +12032,11 @@ 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) {
+		ref = ref +4;
+		atomic_set_release(&sched_balance_running, 0);
+	}
+
 	return ld_moved;
 }
 
@@ -12122,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.
@@ -12192,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();
@@ -12215,14 +12239,10 @@ static void sched_balance_domains(struct rq *rq, enum cpu_idle_type idle)
 			break;
 		}
 
-		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 (sd->flags & SD_SERIALIZE)
+			ref = ref + 5;
 
+		interval = get_sd_balance_interval(sd, busy);
 		if (time_after_eq(jiffies, sd->last_balance + interval)) {
 			if (sched_balance_rq(cpu, rq, sd, idle, &continue_balancing)) {
 				/*
@@ -12236,9 +12256,7 @@ 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;
#14 lore raw
Quoted 18 lines · Shrikanth Hegde
Quoted 12 lines · Peter Zijlstra
Quoted 9 lines · Shrikanth Hegde

kernel/sched/fair.c static int sched_balance_rq(int this_cpu, · line 11780

Quoted 7 lines
kernel/sched/fair.c +3 -0
@@ -11758,6 +11775,12 @@ 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;

Maybe goto out instead of out_balanced ?

Quoted 3 lines

That would be inconsistent with the !should_we_balance() goto out_balanced right above this, no?

Quoted 8 lines

Hi Peter.

Did similar probe points numbers compared to this. Even the patch is quite similar to what was suggested there a while ago.
https://lore.kernel.org/all/41e11090-a100-48a7-a0dd- [email protected]/

480 CPUs system with 6 NUMA nodes. (different system than last time)

Hi Peter, Tim.

How are we proceeding here? Should i send the below patch with changelog or is it being worked out by either of you.

It is really beneficial for large systems to avoid un-neccessary cache bouncing.

Quoted 11 lines

tl;dr

- Number of time sched_balance_running is taken is way less after the swb check. (which is great)
- Number of time it fails to set is very low after swb. (So out_balanced vs out may not make a
significant difference.)
- Patch is at the end. It is this patch + redo stuff + (ref_variable_stuff(ignore))

#15 lore raw
In reply to Peter Zijlstra
Quoted 55 lines · Peter Zijlstra

On Mon, Oct 13, 2025 at 02:54:19PM -0700, Tim Chen wrote:

Right, Yu Chen said something like that as well, should_we_balance() is too late.

Should we instead move the whole serialize thing inside sched_balance_rq() like so:

kernel/sched/fair.c +1 -23
@@ -12122,21 +12148,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.
@@ -12192,7 +12203,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();
@@ -12216,13 +12227,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)) {
 				/*

kernel/sched/fair.c static void sched_balance_domains(struct rq *rq, enum cpu_idle_type idle) · line 12246

Quoted 10 lines
kernel/sched/fair.c +1 -3
@@ -12236,9 +12240,7 @@ 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;

I think this is better since previously the one CPU which was not suppose to do the balancing may increment the atomic variable. If the CPU, that was suppose to do the balance now tries it may fail since the variable was not yet decremented.

Signature · 2 lines
Thanks and Regards
Srikar Dronamraju
maintainer
#16 lore raw
Quoted 64 lines · Srikar Dronamraju
Quoted 62 lines · Peter Zijlstra

On Mon, Oct 13, 2025 at 02:54:19PM -0700, Tim Chen wrote:

Right, Yu Chen said something like that as well, should_we_balance() is too late.

Should we instead move the whole serialize thing inside sched_balance_rq() like so:

kernel/sched/fair.c +2 -26
@@ -12122,21 +12148,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.
@@ -12192,7 +12203,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();
@@ -12216,13 +12227,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)) {
 				/*
@@ -12236,9 +12240,7 @@ 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:
+
Quoted 8 lines

kernel/sched/fair.c static void sched_balance_domains(struct rq *rq, enum cpu_idle_type idle) · line 12246

Quoted 3 lines
kernel/sched/fair.c +0 -0
 		if (time_after(next_balance, sd->last_balance + interval)) {
 			next_balance = sd->last_balance + interval;
 			update_next_balance = 1;

I think this is better since previously the one CPU which was not suppose to do the balancing may increment the atomic variable. If the CPU, that was suppose to do the balance now tries it may fail since the variable was not yet decremented.

Right, it would do that acquire and then still have at least 2 ways to not actually balance, which is a waste.

#17 lore raw
In reply to Peter Zijlstra
Quoted 109 lines · Peter Zijlstra

Should we instead move the whole serialize thing inside sched_balance_rq() like so:

---

kernel/sched/fair.c +27 -23
index bc0b7ce8a65d..e9f719ba17e1 100644
@@ -11722,6 +11722,22 @@ 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.
@@ -11747,6 +11763,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);
 
@@ -11758,6 +11775,12 @@ 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]);
@@ -11998,6 +12021,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;
  }
 
@@ -12122,21 +12148,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.
@@ -12192,7 +12203,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();
@@ -12216,13 +12227,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)) {
  				/*

kernel/sched/fair.c static void sched_balance_domains(struct rq *rq, enum cpu_idle_type idle) · line 12246

Quoted 10 lines
kernel/sched/fair.c +1 -3
@@ -12236,9 +12240,7 @@ 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;

One thing is missing, need to reset to 0 for redo logic to work.

@@ -11882,6 +11905,8 @@ 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;
author
#18 lore raw
In reply to Peter Zijlstra
Quoted 11 lines · Peter Zijlstra
Quoted 9 lines · Tim Chen
Quoted 3 lines

So I'm not sure I understand the situation, @continue_balancing should limit this concurrency to however many groups are on this domain -- your granite thing with SNC on would have something like 6 groups?

That's a good point. But I think the contention is worse than 6 CPUs.

The hierarchy would be

Quoted 8 lines
Quoted 5 lines

SMT
NUMA-level1
NUMA-level2
NUMA-level3
NUMA-level4

Aren't you missing the LLC/NODE domain here? We should have at least one !SD_NUMA domain above SMT.

Yeah, I should have said the MC level which contains SMT groups

SMT
MC
NUMA-level1
...

Actual dmesg log:

[    7.977893] CPU0 attaching sched-domain(s):
[    7.977897]  domain-0: span=0,192 level=SMT
[    7.977902]   groups: 0:{ span=0 cap=972 }, 192:{ span=192 cap=1022 }[    7.977907]   domain-1: span=0-31,192-223 level=MC[    7.977909]    groups: 0:{ span=0,192 cap=1994 }, 1:{ span=1,193

cap=2048 }, 2:{ span=2,194 cap=2047 }, 3:{ span=3,195 cap=2047 }, 4:{ span=4,196 cap=2048 }, 5:{ span=5,197 cap=2046 }, 6:{ span=6,198 cap=2047 }, 7:{ span=7,199 cap=2048 }, 8:{ span=8,200 cap=2047 }, 9:{ span=9,201 cap=2046 }, 10:{ span=10,202 cap=2046 }, 11:{ span=11,203 cap=2046 }, 12:{ span=12,204 cap=2044 }, 13:{ span=13,205 cap=2048 }, 14:{ span=14,206 cap=2046 }, 15:{ span=15,207 cap=2043 }, 16:{ span=16,208 cap=2046 }, 17:{ span=17,209 cap=2048 }, 18:{ span=18,210 cap=2046 }, 19:{ span=19,211 cap=2045 }, 20:{ span=20,212 cap=2046 }, 21:{ span=21,213 cap=2044 }, 22:{ span=22,214 cap=2045 }, 23:{ span=23,215 cap=2046 }, 24:{ span=24,216 cap=2045 }, 25:{ span=25,217 cap=2044 }, 26:{ span=26,218 cap=2046 }, 27:{ span=27,219 cap=2045 }, 28:{ span=28,220 cap=2045 }, 29:{ span=29,221 cap=2046 }, 30:{ span=30,222 cap=2045 }, 31:{ span=31,223 cap=2045 }

[    7.977956]    domain-2: span=0-63,192-255 level=NUMA
[    7.977958]     groups: 0:{ span=0-31,192-223 cap=65418 }, 32:{ span=32-63,224-255 cap=65453 }
[    7.977962]     domain-3: span=0-95,192-287 level=NUMA
[    7.977963]      groups: 0:{ span=0-63,192-255 mask=0-31,192-223 cap=130871 }, 64:{ span=32-95,224-287 mask=64-95,256-287 cap=130929 }
[    7.977968]      domain-4: span=0-127,192-319 level=NUMA
[    7.977970]       groups: 0:{ span=0-95,192-287 cap=196314 }, 96:{ span=96-127,288-319 cap=65439 }
[    7.977974]       domain-5: span=0-127,160-319,352-383 level=NUMA
[    7.977975]        groups: 0:{ span=0-127,192-319 mask=0-31,192-223 cap=261753 }, 160:{ span=160-191,352-383 cap=65430 }
[    7.977980]        domain-6: span=0-383 level=NUMA 
Quoted 10 lines
Quoted 9 lines

There would be multiple CPUs in that are first in the SMT group with continue_balancing=1 going up in the hierachy and attempting the cmpxchg in the first NUMA domain level, before calling should_we_balance() and finding that they are not the first in the NUMA domain and set continue_balancing=0 and abort. Those CPUS are in same L3.
But at the same time, there could be CPUs in other sockets cmpxchg on sched_balance_running.

Quoted 5 lines

Right, Yu Chen said something like that as well, should_we_balance() is too late.

Should we instead move the whole serialize thing inside sched_balance_rq() like so:

I think that makes sense. Probably a separate patch.

Tim

Quoted 116 lines

---

kernel/sched/fair.c +28 -26
index bc0b7ce8a65d..e9f719ba17e1 100644
@@ -11722,6 +11722,22 @@ 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.
@@ -11747,6 +11763,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);
 
@@ -11758,6 +11775,12 @@ 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]);
@@ -11998,6 +12021,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;
 }
 
@@ -12122,21 +12148,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.
@@ -12192,7 +12203,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();
@@ -12216,13 +12227,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)) {
 				/*
@@ -12236,9 +12240,7 @@ 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;