* [PATCH v1 0/8] x86, sched, mm: Speed up CPU hotplug on large systems
@ 2026-10-15 15:33 Pasha Tatashin
2026-10-15 15:33 ` [PATCH v1 1/8] x86/realmode: Remove unnecessary WBINVD from AP startup trampoline Pasha Tatashin
` (8 more replies)
0 siblings, 9 replies; 21+ messages in thread
From: Pasha Tatashin @ 2026-10-15 15:33 UTC (permalink / raw)
To: kees, rppt, juri.lelli, nik.borisov, tglx, vschneid,
kprateek.nayak, vincent.guittot, bp, thorsten.blum, cgroups,
tony.luck, david, mingo, akpm, linux-mm, seanjc, ljs,
zhangguopeng, dietmar.eggemann, mhocko, vinicius.gomes, tj,
vbabka, linux-kernel, peterz, surenb, hpa, x86, pasha.tatashin,
dmatlack, tim.c.chen, kexec, pratyush, dave.hansen, rostedt,
gmonaco, jgross, mgorman, graf, linux-edac, liam, ridong.chen,
longman, bsegall
For live update we require a fast CPU online and offline capability.
However, sequentially offlining and onlining all application processors
on modern high-core-count x86 servers exposes a series of hardware and
algorithmic bottlenecks across arch/x86, the scheduler topology builder,
deadline bandwidth accounting, and vmstat.
This series addresses these bottlenecks, reducing the time to offline and
online all AP CPUs by 95.2% (21.0x faster) on a 2-socket, 480-CPU Intel
Xeon 6985P-C and by 59.8% (2.5x faster) on a 2-socket, 512-CPU AMD EPYC
9B45.
Benchmark workload (mean of 10 runs per step):
# echo 1 > /sys/kernel/rcu_expedited
# for i in {1..N}; do echo 0 > /sys/devices/system/cpu/cpu$i/online; done
# for i in {1..N}; do echo 1 > /sys/devices/system/cpu/cpu$i/online; done
# echo 0 > /sys/kernel/rcu_expedited
2-socket, 480-CPU Intel Xeon 6985P-C (N=479):
Offline: 40.40 s -> 3.09 s (-92.4%, 13.1x faster)
Online: 65.83 s -> 1.98 s (-97.0%, 33.2x faster)
Total: 106.23 s -> 5.07 s (-95.2%, 21.0x faster)
2-socket, 512-CPU AMD EPYC 9B45 (N=511):
Offline: 4.53 s -> 2.30 s (-49.2%, 2.0x faster)
Online: 3.88 s -> 1.08 s (-72.2%, 3.6x faster)
Total: 8.41 s -> 3.38 s (-59.8%, 2.5x faster)
(On AMD EPYC 9B45, each 8-core CCD has a 32 MB L3 where WBINVD takes
< 0.5 ms, offline uses acpi_idle rather than mwait_play_dead(), and
Intel CMCI is unused, so Patches 1-3 do not affect AMD, which enters
Patch 4 at 4.53 s offline / 3.92 s online / 8.45 s total. Without
rcu_expedited=1, final 10-run mean total time with all 8 patches
applied is 12.04 s on Intel and 15.99 s on AMD.)
1. x86 WBINVD, CMCI rediscovery, and MTRR rendezvous (Patches 1-4):
On processors with large shared last-level caches (such as the 504 MB
L3 per socket on Xeon 6985P-C), CPU hotplug is initially dominated by
full coherence-domain cache flushes (WBINVD), each taking ~85-115 ms
per CPU while stalling active cores:
- Patch 1 removes the obsolete WBINVD from trampoline_start, which was
added in the 1990s for Sequent NUMA-Q hardware (removed in commit
b5660ba76b41 ("x86, platforms: Remove NUMAQ")). Per the Intel SDM
and AMD APM, INIT preserves cache contents and leaves CR0.CD and
CR0.NW unchanged. Removing it cuts 479-CPU online time on Intel from
65.83 s to 11.14 s (-83.1%) and total time from 106.23 s to 51.60 s
(-51.4%).
- Patch 2 skips the redundant WBINVD before MWAIT in mwait_play_dead()
during runtime CPU offline (retaining it when cpuhp_tasks_frozen is
set for system sleep). Architectural MWAIT C-states maintain cache
coherency while the package remains powered and flush core-private
L1/L2 caches to the LLC on C-state entry, just like normal idle in
intel_idle. Skipping it for runtime offline cuts 479-CPU offline
time on Intel from 40.46 s to 11.42 s (-71.8%) and total time from
51.60 s to 22.50 s (-56.4%).
- Patch 3 bounds cmci_rediscover() on CPU teardown. Currently,
cmci_rediscover() broadcasts an IPI via on_each_cpu() to all online
CPUs, serializing them on cmci_discover_lock while reading up to 32
MCA MSRs. Because any disabled MCA bank is shared at most with an
SMT sibling, L2 cluster peer, or die/package peer, probing a single
online topology peer via smp_call_function_single() (or returning
immediately when no shared banks were orphaned) reduces 479-CPU
offline time on Intel from 11.42 s to 9.63 s (-15.7%) and total time
from 22.50 s to 20.74 s (-7.8%).
- Patch 4 skips the stop_machine_from_inactive_cpu() all-CPU
rendezvous (and the AP's cache disable/flush and MTRR reprogramming
in cache_cpu_init()) in cache_ap_online() when the AP's hardware
MTRR MSRs already match mtrr_state (which cache_aps_init() already
programmed during initial boot and warm reset preserves). This cuts
online time on Intel from 11.11 s to 8.69 s (-21.8%; total 20.74 s
-> 18.54 s, -10.6%) and on AMD from 3.92 s to 3.15 s (-19.6%; total
8.45 s -> 7.66 s, -9.3%).
2. Scheduler domain rebuilds, deadline accounting, and vmstat (Patches 5-8):
Once the x86 hardware bottlenecks are removed, CPU hotplug on both
Intel and AMD is dominated by O(N^2) and O(N^3) work inside
partition_sched_domains_locked(), build_sched_domains(), and
refresh_zone_stat_thresholds():
- Patch 5 avoids redundant O(N^3) group weight, SMT core-mask, and
asym_prefer_cpu recomputations in init_sched_groups_capacity() by
caching those fields on struct sched_group_capacity, reuses
already-initialized sgc instances in init_overlap_sched_group(), and
allocates sdd->sg, sdd->sgc, and d->sds lazily on demand. This cuts
total hotplug time on Intel from 18.54 s to 9.20 s (-50.4%) and on
AMD from 7.66 s to 5.29 s (-30.9%).
- Patch 6 eliminates three O(N^2) dl_server bandwidth walks per
hotplug step. Because fair_server is enabled on every runqueue by
default, rq_attach_root() performs O(N^2) remote extra_bw updates
when attaching def_root_domain during teardown and again when
attaching the new root_domain during build, followed by a nested
O(N^2) loop in dl_clear_root_domain(). Dropping the redundant
__dl_server_attach_root() calls in rq_attach_root() and converting
dl_clear_root_domain() to two linear O(N) passes cuts
dl_clear_root_domain() at ~500 CPUs from 1.8-5.5 ms to < 65 us,
reducing total hotplug time by 30.5% on Intel (9.20 s -> 6.39 s)
and 18.7% on AMD (5.29 s -> 4.30 s).
- Patch 7 eliminates per-CPU bitmap scans over already-covered groups
in build_sched_groups(), reuses temporary per-CPU domain pointer
arrays across rebuilds instead of calling alloc_percpu() and
free_percpu() on every hotplug step, reuses precomputed domain/child
span weights, and hoists LLC size calculations. This cuts total
hotplug time by 15.0% on Intel (6.39 s -> 5.43 s) and 19.5% on AMD
(4.30 s -> 3.46 s).
- Patch 8 avoids unconditionally zeroing and rewriting every online
CPU's per-CPU stat_threshold in refresh_zone_stat_thresholds() on
every hotplug step (since calculate_normal_threshold() only changes
at powers of two and is clamped once num_online_cpus() >= 64),
reducing refresh_zone_stat_thresholds() by ~88-91% on Intel and ~51%
on AMD, and bringing total hotplug time down to 5.07 s on Intel
(-6.6%) and 3.38 s on AMD (-2.3%).
Pasha Tatashin (8):
x86/realmode: Remove unnecessary WBINVD from AP startup trampoline
x86/smp: Remove unnecessary WBINVD from mwait_play_dead()
x86/mce/intel: Bound cmci_rediscover() to a single topology peer
x86/cacheinfo, mtrr: Skip stop_machine rendezvous in cache_ap_online()
when MTRRs match
sched/topology: Avoid redundant O(N^3) group capacity walks and unused
allocations in build_sched_domains()
sched/deadline, topology: Avoid O(N^2) dl_server bandwidth walks
during root_domain rebuilds
sched/topology: Avoid redundant percpu allocations and bitmap walks in
build_sched_domains()
mm/vmstat: Avoid redundant per-CPU stat_threshold writes in
refresh_zone_stat_thresholds()
arch/x86/include/asm/mtrr.h | 2 +
arch/x86/kernel/cpu/cacheinfo.c | 7 +
arch/x86/kernel/cpu/mce/intel.c | 57 ++++-
arch/x86/kernel/cpu/mtrr/generic.c | 49 ++++
arch/x86/kernel/smpboot.c | 16 +-
arch/x86/realmode/rm/trampoline_32.S | 2 -
arch/x86/realmode/rm/trampoline_64.S | 1 -
include/linux/cpuset.h | 6 +-
include/linux/sched/deadline.h | 1 -
kernel/sched/deadline.c | 89 ++++---
kernel/sched/sched.h | 10 +-
kernel/sched/topology.c | 339 ++++++++++++++++-----------
mm/vmstat.c | 58 ++---
13 files changed, 422 insertions(+), 215 deletions(-)
base-commit: cee9395acd8043be0644b25c34bfa86623f2b935
--
2.56.0.385.gd3acb90ef8-goog
^ permalink raw reply [flat|nested] 21+ messages in thread
* [PATCH v1 1/8] x86/realmode: Remove unnecessary WBINVD from AP startup trampoline
2026-10-15 15:33 [PATCH v1 0/8] x86, sched, mm: Speed up CPU hotplug on large systems Pasha Tatashin
@ 2026-10-15 15:33 ` Pasha Tatashin
2026-10-15 15:41 ` sashiko-bot
2026-10-15 15:33 ` [PATCH v1 2/8] x86/smp: Remove unnecessary WBINVD from mwait_play_dead() Pasha Tatashin
` (7 subsequent siblings)
8 siblings, 1 reply; 21+ messages in thread
From: Pasha Tatashin @ 2026-10-15 15:33 UTC (permalink / raw)
To: kees, rppt, juri.lelli, nik.borisov, tglx, vschneid,
kprateek.nayak, vincent.guittot, bp, thorsten.blum, cgroups,
tony.luck, david, mingo, akpm, linux-mm, seanjc, ljs,
zhangguopeng, dietmar.eggemann, mhocko, vinicius.gomes, tj,
vbabka, linux-kernel, peterz, surenb, hpa, x86, pasha.tatashin,
dmatlack, tim.c.chen, kexec, pratyush, dave.hansen, rostedt,
gmonaco, jgross, mgorman, graf, linux-edac, liam, ridong.chen,
longman, bsegall
trampoline_start executes WBINVD in 16-bit real mode before switching to
protected mode. This WBINVD was originally added in the 1990s for
Sequent NUMA-Q hardware (as noted by the comment in trampoline_32.S:
"Needed for NUMA-Q should be harmless for others"), whose cross-quad
caches came up non-coherent prior to OS initialization. Sequent NUMA-Q
support (CONFIG_X86_NUMAQ) was removed in commit b5660ba76b41
("x86, platforms: Remove NUMAQ").
Per the Intel SDM (Vol. 3A, Table 10-1, note 2) and AMD APM, the INIT
IPI used to wake secondary CPUs preserves cache contents and leaves
CR0.CD and CR0.NW unchanged (0, as already enabled by firmware or the
kernel). Neither sev_es_trampoline_start nor trampoline_start64
executes WBINVD.
On modern multi-core processors with large shared last-level caches
(such as 504 MB shared L3 per socket on Intel Xeon 6985P-C), WBINVD is
not harmless: each execution flushes and invalidates the entire
coherence domain's caches and takes ~85-115 ms per CPU while disturbing
active workloads on online CPUs.
Remove the obsolete WBINVD from trampoline_start.
2-socket, 480-CPU Intel Xeon 6985P-C (N=479, mean of 10 runs):
Offline: 40.40 s -> 40.46 s
Online: 65.83 s -> 11.14 s (-83.1%)
Total: 106.23 s -> 51.60 s (-51.4%)
Signed-off-by: Pasha Tatashin <pasha.tatashin@soleen.com>
---
arch/x86/realmode/rm/trampoline_32.S | 2 --
arch/x86/realmode/rm/trampoline_64.S | 1 -
2 files changed, 3 deletions(-)
diff --git a/arch/x86/realmode/rm/trampoline_32.S b/arch/x86/realmode/rm/trampoline_32.S
index 3fad907a179f1..14e56ea954160 100644
--- a/arch/x86/realmode/rm/trampoline_32.S
+++ b/arch/x86/realmode/rm/trampoline_32.S
@@ -30,8 +30,6 @@
.balign PAGE_SIZE
SYM_CODE_START(trampoline_start)
- wbinvd # Needed for NUMA-Q should be harmless for others
-
LJMPW_RM(1f)
1:
mov %cs, %ax # Code and data in the same place
diff --git a/arch/x86/realmode/rm/trampoline_64.S b/arch/x86/realmode/rm/trampoline_64.S
index 14d9c7daf90f4..e5a3f03d62217 100644
--- a/arch/x86/realmode/rm/trampoline_64.S
+++ b/arch/x86/realmode/rm/trampoline_64.S
@@ -60,7 +60,6 @@
.balign PAGE_SIZE
SYM_CODE_START(trampoline_start)
cli # We should be safe anyway
- wbinvd
LJMPW_RM(1f)
1:
--
2.56.0.385.gd3acb90ef8-goog
^ permalink raw reply related [flat|nested] 21+ messages in thread
* [PATCH v1 2/8] x86/smp: Remove unnecessary WBINVD from mwait_play_dead()
2026-10-15 15:33 [PATCH v1 0/8] x86, sched, mm: Speed up CPU hotplug on large systems Pasha Tatashin
2026-10-15 15:33 ` [PATCH v1 1/8] x86/realmode: Remove unnecessary WBINVD from AP startup trampoline Pasha Tatashin
@ 2026-10-15 15:33 ` Pasha Tatashin
2026-10-15 15:44 ` sashiko-bot
2026-10-15 15:33 ` [PATCH v1 3/8] x86/mce/intel: Bound cmci_rediscover() to a single topology peer Pasha Tatashin
` (6 subsequent siblings)
8 siblings, 1 reply; 21+ messages in thread
From: Pasha Tatashin @ 2026-10-15 15:33 UTC (permalink / raw)
To: kees, rppt, juri.lelli, nik.borisov, tglx, vschneid,
kprateek.nayak, vincent.guittot, bp, thorsten.blum, cgroups,
tony.luck, david, mingo, akpm, linux-mm, seanjc, ljs,
zhangguopeng, dietmar.eggemann, mhocko, vinicius.gomes, tj,
vbabka, linux-kernel, peterz, surenb, hpa, x86, pasha.tatashin,
dmatlack, tim.c.chen, kexec, pratyush, dave.hansen, rostedt,
gmonaco, jgross, mgorman, graf, linux-edac, liam, ridong.chen,
longman, bsegall
When a logical CPU goes offline via mwait_play_dead(), it executes
WBINVD before entering the MWAIT loop. On processors with large shared
last-level caches (such as Intel Xeon 6985P-C with 504 MB L3 per
socket), each WBINVD takes ~85-115 ms, flushes and invalidates the
entire socket's shared caches, and stalls the physical core (including
any online SMT sibling on that core) for the duration of the flush.
mwait_play_dead() places the logical processor into an architectural
MWAIT C-state—the same hardware C-states entered by intel_idle during
normal idle without executing WBINVD—where hardware maintains cache
coherency while the package remains powered and automatically flushes
core-private L1/L2 caches to the coherent LLC on C-state entry.
During system sleep (cpuhp_tasks_frozen), however, power may be removed
from a non-boot package's caches after all of its CPUs are offlined,
and the sleep-entry cache flush on the boot CPU only covers the boot
CPU's own package. Skip WBINVD in mwait_play_dead() during runtime CPU
offline and only execute it when cpuhp_tasks_frozen is set. Note that
wbinvd_on_all_cpus() callers (such as cpu_cache_invalidate_memregion())
only reach online CPUs, so a package whose CPUs are all offlined at
runtime retains its cached lines while powered.
2-socket, 480-CPU Intel Xeon 6985P-C (N=479, mean of 10 runs):
Offline: 40.46 s -> 11.42 s (-71.8%)
Online: 11.14 s -> 11.08 s (-0.5%)
Total: 51.60 s -> 22.50 s (-56.4%)
Signed-off-by: Pasha Tatashin <pasha.tatashin@soleen.com>
---
arch/x86/kernel/smpboot.c | 16 +++++++++-------
1 file changed, 9 insertions(+), 7 deletions(-)
diff --git a/arch/x86/kernel/smpboot.c b/arch/x86/kernel/smpboot.c
index ba01a9e919b78..60a781dac9991 100644
--- a/arch/x86/kernel/smpboot.c
+++ b/arch/x86/kernel/smpboot.c
@@ -1414,10 +1414,6 @@ void play_dead_common(void)
local_irq_disable();
}
-/*
- * We need to flush the caches before going to sleep, lest we have
- * dirty data in our caches when we come back up.
- */
void __noreturn mwait_play_dead(unsigned int eax_hint)
{
struct mwait_cpu_dead *md = this_cpu_ptr(&mwait_cpu_dead);
@@ -1426,15 +1422,21 @@ void __noreturn mwait_play_dead(unsigned int eax_hint)
md->status = CPUDEAD_MWAIT_WAIT;
md->control = CPUDEAD_MWAIT_WAIT;
- wbinvd();
+ /*
+ * MWAIT C-states keep the caches coherent, so runtime CPU offline
+ * does not need to flush them. System sleep, however, may remove
+ * power from this package's caches while all of its CPUs are
+ * offline, and the flush done by the boot CPU before entering the
+ * sleep state only covers the boot CPU's own package.
+ */
+ if (cpuhp_tasks_frozen)
+ wbinvd();
while (1) {
/*
* The CLFLUSH is a workaround for erratum AAI65 for
* the Xeon 7400 series. It's not clear it is actually
* needed, but it should be harmless in either case.
- * The WBINVD is insufficient due to the spurious-wakeup
- * case where we return around the loop.
*/
mb();
clflush(md);
--
2.56.0.385.gd3acb90ef8-goog
^ permalink raw reply related [flat|nested] 21+ messages in thread
* [PATCH v1 3/8] x86/mce/intel: Bound cmci_rediscover() to a single topology peer
2026-10-15 15:33 [PATCH v1 0/8] x86, sched, mm: Speed up CPU hotplug on large systems Pasha Tatashin
2026-10-15 15:33 ` [PATCH v1 1/8] x86/realmode: Remove unnecessary WBINVD from AP startup trampoline Pasha Tatashin
2026-10-15 15:33 ` [PATCH v1 2/8] x86/smp: Remove unnecessary WBINVD from mwait_play_dead() Pasha Tatashin
@ 2026-10-15 15:33 ` Pasha Tatashin
2026-10-15 15:51 ` sashiko-bot
2026-10-15 15:33 ` [PATCH v1 4/8] x86/cacheinfo, mtrr: Skip stop_machine rendezvous in cache_ap_online() when MTRRs match Pasha Tatashin
` (5 subsequent siblings)
8 siblings, 1 reply; 21+ messages in thread
From: Pasha Tatashin @ 2026-10-15 15:33 UTC (permalink / raw)
To: kees, rppt, juri.lelli, nik.borisov, tglx, vschneid,
kprateek.nayak, vincent.guittot, bp, thorsten.blum, cgroups,
tony.luck, david, mingo, akpm, linux-mm, seanjc, ljs,
zhangguopeng, dietmar.eggemann, mhocko, vinicius.gomes, tj,
vbabka, linux-kernel, peterz, surenb, hpa, x86, pasha.tatashin,
dmatlack, tim.c.chen, kexec, pratyush, dave.hansen, rostedt,
gmonaco, jgross, mgorman, graf, linux-edac, liam, ridong.chen,
longman, bsegall
When a CPU goes offline, cmci_clear() disables CMCI on any MCA banks
owned by that CPU, and mce_cpu_dead() calls cmci_rediscover(). Currently
cmci_rediscover() invokes on_each_cpu(cmci_rediscover_work_func, NULL, 1),
sending a synchronous broadcast IPI to every online CPU in the system,
all of which contend on the global cmci_discover_lock while reading up
to 32 MSR_IA32_MCx_CTL2 MSRs.
Any bank disabled by cmci_clear() is either thread-private, core-scoped
(shared between SMT threads of the same physical core), L2-cluster-scoped,
die-scoped, or package-scoped:
1. Any bank index that is already owned by the target peer in its own
mce_banks_owned bitmap is private to a narrower scope than the shared
level and will be skipped by cmci_skip_bank() on the target anyway.
2. Core- or cluster-shared MCA banks can only be claimed by an online
SMT sibling or L2 cluster peer.
3. Shared die-scoped or package-scoped uncore banks can be claimed in a
single pass by one online peer on the same die or package.
Record the bitmap of banks disabled by __cmci_disable_bank() in
cmci_orphaned_banks, select a single online topology peer
cmci_orphaned_target (SMT sibling, L2 cluster peer, die peer, or package
peer) in cmci_clear(), and mask out any bank indices already owned by
that target peer. In cmci_rediscover(), return immediately when
cmci_orphaned_banks is empty; otherwise probe only cmci_orphaned_target
via a single smp_call_function_single() call.
2-socket, 480-CPU Intel Xeon 6985P-C (N=479, mean of 10 runs):
Offline: 11.42 s -> 9.63 s (-15.7%)
Online: 11.08 s -> 11.11 s
Total: 22.50 s -> 20.74 s (-7.8%)
cmci_rediscover() (cpu479 offline):
7,720.6 us -> 0.6 us (-99.99%)
Signed-off-by: Pasha Tatashin <pasha.tatashin@soleen.com>
---
arch/x86/kernel/cpu/mce/intel.c | 57 +++++++++++++++++++++++++++++++--
1 file changed, 55 insertions(+), 2 deletions(-)
diff --git a/arch/x86/kernel/cpu/mce/intel.c b/arch/x86/kernel/cpu/mce/intel.c
index 4655223ba5606..f57718b097d9b 100644
--- a/arch/x86/kernel/cpu/mce/intel.c
+++ b/arch/x86/kernel/cpu/mce/intel.c
@@ -11,6 +11,7 @@
#include <linux/percpu.h>
#include <linux/sched.h>
#include <linux/cpumask.h>
+#include <linux/topology.h>
#include <asm/apic.h>
#include <asm/cpufeature.h>
#include <asm/cpu_device_id.h>
@@ -40,6 +41,8 @@
* owned by the offlined cpu.
*/
static DEFINE_PER_CPU(mce_banks_t, mce_banks_owned);
+static mce_banks_t cmci_orphaned_banks;
+static unsigned int cmci_orphaned_target;
/*
* cmci_discover_lock protects against parallel discovery attempts
@@ -244,6 +247,7 @@ static void cmci_claim_bank(int bank, u64 val, int bios_zero_thresh, int *bios_w
/* This CPU successfully set the enable bit. */
set_bit(bank, (void *)this_cpu_ptr(&mce_banks_owned));
+ __clear_bit(bank, cmci_orphaned_banks);
if ((val & MCI_CTL2_CMCI_THRESHOLD_MASK) == CMCI_STORM_THRESHOLD) {
pr_notice("CPU%d BANK%d CMCI inherited storm\n", smp_processor_id(), bank);
@@ -328,6 +332,7 @@ static void __cmci_disable_bank(int bank)
val &= ~MCI_CTL2_CMCI_EN;
wrmsrq(MSR_IA32_MCx_CTL2(bank), val);
__clear_bit(bank, this_cpu_ptr(mce_banks_owned));
+ __set_bit(bank, cmci_orphaned_banks);
if ((val & MCI_CTL2_CMCI_THRESHOLD_MASK) == CMCI_STORM_THRESHOLD)
cmci_storm_end(bank);
@@ -339,6 +344,8 @@ static void __cmci_disable_bank(int bank)
*/
void cmci_clear(void)
{
+ unsigned int cpu = smp_processor_id();
+ unsigned int target;
unsigned long flags;
int i;
int banks;
@@ -346,8 +353,31 @@ void cmci_clear(void)
if (!cmci_supported(&banks))
return;
raw_spin_lock_irqsave(&cmci_discover_lock, flags);
+ bitmap_zero(cmci_orphaned_banks, MAX_NR_BANKS);
+ /*
+ * A bank is shared by the CPUs of one SMT core, L2 cluster, die or
+ * package, and these scopes nest. The first online peer found from
+ * the innermost scope outwards is therefore part of every sharing
+ * scope that still has an online CPU, so it alone can claim every
+ * orphaned bank that is still claimable. If it already owns an index,
+ * the instance released here is not shared with it, hence not with
+ * any online CPU, and that index can be dropped from the rediscovery.
+ */
+ target = cpumask_any_but(topology_sibling_cpumask(cpu), cpu);
+ if (target >= nr_cpu_ids)
+ target = cpumask_any_but(topology_cluster_cpumask(cpu), cpu);
+ if (target >= nr_cpu_ids)
+ target = cpumask_any_but(topology_die_cpumask(cpu), cpu);
+ if (target >= nr_cpu_ids)
+ target = cpumask_any_but(topology_core_cpumask(cpu), cpu);
+ cmci_orphaned_target = target;
for (i = 0; i < banks; i++)
__cmci_disable_bank(i);
+ if (target < nr_cpu_ids)
+ bitmap_andnot(cmci_orphaned_banks, cmci_orphaned_banks,
+ per_cpu(mce_banks_owned, target), MAX_NR_BANKS);
+ else
+ bitmap_zero(cmci_orphaned_banks, MAX_NR_BANKS);
raw_spin_unlock_irqrestore(&cmci_discover_lock, flags);
}
@@ -360,15 +390,38 @@ static void cmci_rediscover_work_func(void *arg)
cmci_discover(banks);
}
-/* After a CPU went down cycle through all the others and rediscover */
+/*
+ * After a CPU went down, rediscover orphaned shared banks on a single
+ * topology peer (SMT sibling, L2 cluster peer, die peer, or package peer)
+ * selected in cmci_clear(). Core-private banks whose core is now completely
+ * offline cannot be claimed by any other core, so probing more than one
+ * peer is redundant.
+ */
void cmci_rediscover(void)
{
+ unsigned int target;
+ unsigned long flags;
+ bool orphaned;
int banks;
if (!cmci_supported(&banks))
return;
- on_each_cpu(cmci_rediscover_work_func, NULL, 1);
+ raw_spin_lock_irqsave(&cmci_discover_lock, flags);
+ orphaned = !bitmap_empty(cmci_orphaned_banks, MAX_NR_BANKS);
+ target = cmci_orphaned_target;
+ raw_spin_unlock_irqrestore(&cmci_discover_lock, flags);
+
+ if (!orphaned)
+ return;
+
+ if (target < nr_cpu_ids && cpu_online(target))
+ smp_call_function_single(target, cmci_rediscover_work_func,
+ NULL, 1);
+
+ raw_spin_lock_irqsave(&cmci_discover_lock, flags);
+ bitmap_zero(cmci_orphaned_banks, MAX_NR_BANKS);
+ raw_spin_unlock_irqrestore(&cmci_discover_lock, flags);
}
/*
--
2.56.0.385.gd3acb90ef8-goog
^ permalink raw reply related [flat|nested] 21+ messages in thread
* [PATCH v1 4/8] x86/cacheinfo, mtrr: Skip stop_machine rendezvous in cache_ap_online() when MTRRs match
2026-10-15 15:33 [PATCH v1 0/8] x86, sched, mm: Speed up CPU hotplug on large systems Pasha Tatashin
` (2 preceding siblings ...)
2026-10-15 15:33 ` [PATCH v1 3/8] x86/mce/intel: Bound cmci_rediscover() to a single topology peer Pasha Tatashin
@ 2026-10-15 15:33 ` Pasha Tatashin
2026-10-15 15:45 ` sashiko-bot
2026-10-15 15:33 ` [PATCH v1 5/8] sched/topology: Avoid redundant O(N^3) group capacity walks and unused allocations in build_sched_domains() Pasha Tatashin
` (4 subsequent siblings)
8 siblings, 1 reply; 21+ messages in thread
From: Pasha Tatashin @ 2026-10-15 15:33 UTC (permalink / raw)
To: kees, rppt, juri.lelli, nik.borisov, tglx, vschneid,
kprateek.nayak, vincent.guittot, bp, thorsten.blum, cgroups,
tony.luck, david, mingo, akpm, linux-mm, seanjc, ljs,
zhangguopeng, dietmar.eggemann, mhocko, vinicius.gomes, tj,
vbabka, linux-kernel, peterz, surenb, hpa, x86, pasha.tatashin,
dmatlack, tim.c.chen, kexec, pratyush, dave.hansen, rostedt,
gmonaco, jgross, mgorman, graf, linux-edac, liam, ridong.chen,
longman, bsegall
When a single CPU is brought online at runtime
(cache_aps_delayed_init == false), cache_ap_online() unconditionally
calls stop_machine_from_inactive_cpu() to rendezvous all online CPUs
while the incoming AP runs cache_cpu_init() to disable caches
(CR0.CD = 1, plus wbinvd() when X86_FEATURE_SELFSNOOP is not set),
reprogram MTRRs via mtrr_generic_set_state(), and re-enable caches.
During initial boot (cache_aps_init()) and across warm CPU offline/online
cycles, the AP's hardware MTRR MSRs have already been programmed to match
the saved mtrr_state. Stopping all online CPUs in stop_machine (and
flushing caches on the AP) when the AP's MTRRs already match mtrr_state
is unnecessary.
Add mtrr_generic_state_matches() to compare the local CPU's
MSR_MTRRdefType, variable MTRR ranges, and fixed MTRR ranges against
mtrr_state (including k8_check_syscfg_dram_mod_en() on AMD), and check it
on the AP in cache_ap_online() before invoking
stop_machine_from_inactive_cpu(). If the hardware MTRRs already match
mtrr_state, initialize PAT locally on the AP and return.
2-socket, 480-CPU Intel Xeon 6985P-C (N=479, mean of 10 runs):
Offline: 9.63 s -> 9.85 s
Online: 11.11 s -> 8.69 s (-21.8%)
Total: 20.74 s -> 18.54 s (-10.6%)
cache_ap_online() (cpu479 online):
7,704.0 us -> 4.7 us (-99.94%)
2-socket, 512-CPU AMD EPYC 9B45 (N=511, mean of 10 runs):
Offline: 4.53 s -> 4.51 s (-0.4%)
Online: 3.92 s -> 3.15 s (-19.6%)
Total: 8.45 s -> 7.66 s (-9.3%)
cache_ap_online() (cpu511 online):
1,354.6 us -> 6.4 us (-99.53%)
Signed-off-by: Pasha Tatashin <pasha.tatashin@soleen.com>
---
arch/x86/include/asm/mtrr.h | 2 ++
arch/x86/kernel/cpu/cacheinfo.c | 7 +++++
arch/x86/kernel/cpu/mtrr/generic.c | 49 ++++++++++++++++++++++++++++++
3 files changed, 58 insertions(+)
diff --git a/arch/x86/include/asm/mtrr.h b/arch/x86/include/asm/mtrr.h
index 76b95bd1a4058..0527e216808c4 100644
--- a/arch/x86/include/asm/mtrr.h
+++ b/arch/x86/include/asm/mtrr.h
@@ -61,6 +61,7 @@ extern int amd_special_default_mtrr(void);
void mtrr_disable(void);
void mtrr_enable(void);
void mtrr_generic_set_state(void);
+bool mtrr_generic_state_matches(void);
# else
static inline void guest_force_mtrr_state(struct mtrr_var_range *var,
unsigned int num_var,
@@ -106,6 +107,7 @@ static inline int mtrr_trim_uncached_memory(unsigned long end_pfn)
#define mtrr_disable() do {} while (0)
#define mtrr_enable() do {} while (0)
#define mtrr_generic_set_state() do {} while (0)
+static inline bool mtrr_generic_state_matches(void) { return true; }
# endif
#ifdef CONFIG_COMPAT
diff --git a/arch/x86/kernel/cpu/cacheinfo.c b/arch/x86/kernel/cpu/cacheinfo.c
index 13ed16527905f..64796d5b34fdc 100644
--- a/arch/x86/kernel/cpu/cacheinfo.c
+++ b/arch/x86/kernel/cpu/cacheinfo.c
@@ -770,6 +770,13 @@ static int cache_ap_online(unsigned int cpu)
if (!memory_caching_control || get_cache_aps_delayed_init())
return 0;
+ if (!(memory_caching_control & CACHE_MTRR) ||
+ mtrr_generic_state_matches()) {
+ if (memory_caching_control & CACHE_PAT)
+ pat_cpu_init();
+ return 0;
+ }
+
/*
* Ideally we should hold mtrr_mutex here to avoid MTRR entries
* changed, but this routine will be called in CPU boot time,
diff --git a/arch/x86/kernel/cpu/mtrr/generic.c b/arch/x86/kernel/cpu/mtrr/generic.c
index 67cf69f24b005..8760a7b4dc38b 100644
--- a/arch/x86/kernel/cpu/mtrr/generic.c
+++ b/arch/x86/kernel/cpu/mtrr/generic.c
@@ -981,6 +981,55 @@ void mtrr_generic_set_state(void)
}
}
+bool mtrr_generic_state_matches(void)
+{
+ struct msr val;
+ unsigned int i;
+
+ if (!cpu_feature_enabled(X86_FEATURE_MTRR))
+ return true;
+
+ rdmsrq(MSR_MTRRdefType, val.q);
+ if ((val.l & MTRR_DEF_TYPE_TYPE) != mtrr_state.def_type ||
+ ((val.l & MTRR_DEF_TYPE_ENABLE) >> MTRR_STATE_SHIFT) != mtrr_state.enabled)
+ return false;
+
+ for (i = 0; i < num_var_ranges; i++) {
+ struct mtrr_var_range *vr = &mtrr_state.var_ranges[i];
+
+ rdmsrq(MTRRphysBase_MSR(i), val.q);
+ if ((vr->base_lo & ~MTRR_PHYSBASE_RSVD) != (val.l & ~MTRR_PHYSBASE_RSVD) ||
+ (vr->base_hi & ~phys_hi_rsvd) != (val.h & ~phys_hi_rsvd))
+ return false;
+
+ rdmsrq(MTRRphysMask_MSR(i), val.q);
+ if ((vr->mask_lo & ~MTRR_PHYSMASK_RSVD) != (val.l & ~MTRR_PHYSMASK_RSVD) ||
+ (vr->mask_hi & ~phys_hi_rsvd) != (val.h & ~phys_hi_rsvd))
+ return false;
+ }
+
+ if (mtrr_state.have_fixed) {
+ u64 *saved = (u64 *)mtrr_state.fixed_ranges;
+ int block = -1, range;
+
+ /*
+ * Ensure MtrrFixDramModEn is clear so fixed MTRR reads do not
+ * expose K8 RdMem/WrMem bits and cause false mismatches.
+ */
+ k8_check_syscfg_dram_mod_en();
+
+ while (fixed_range_blocks[++block].ranges) {
+ for (range = 0; range < fixed_range_blocks[block].ranges; range++) {
+ rdmsrq(fixed_range_blocks[block].base_msr + range, val.q);
+ if (val.q != *saved++)
+ return false;
+ }
+ }
+ }
+
+ return true;
+}
+
/**
* generic_set_mtrr - set variable MTRR register on the local CPU.
*
--
2.56.0.385.gd3acb90ef8-goog
^ permalink raw reply related [flat|nested] 21+ messages in thread
* [PATCH v1 5/8] sched/topology: Avoid redundant O(N^3) group capacity walks and unused allocations in build_sched_domains()
2026-10-15 15:33 [PATCH v1 0/8] x86, sched, mm: Speed up CPU hotplug on large systems Pasha Tatashin
` (3 preceding siblings ...)
2026-10-15 15:33 ` [PATCH v1 4/8] x86/cacheinfo, mtrr: Skip stop_machine rendezvous in cache_ap_online() when MTRRs match Pasha Tatashin
@ 2026-10-15 15:33 ` Pasha Tatashin
2026-10-15 15:43 ` sashiko-bot
2026-10-15 15:33 ` [PATCH v1 6/8] sched/deadline, topology: Avoid O(N^2) dl_server bandwidth walks during root_domain rebuilds Pasha Tatashin
` (3 subsequent siblings)
8 siblings, 1 reply; 21+ messages in thread
From: Pasha Tatashin @ 2026-10-15 15:33 UTC (permalink / raw)
To: kees, rppt, juri.lelli, nik.borisov, tglx, vschneid,
kprateek.nayak, vincent.guittot, bp, thorsten.blum, cgroups,
tony.luck, david, mingo, akpm, linux-mm, seanjc, ljs,
zhangguopeng, dietmar.eggemann, mhocko, vinicius.gomes, tj,
vbabka, linux-kernel, peterz, surenb, hpa, x86, pasha.tatashin,
dmatlack, tim.c.chen, kexec, pratyush, dave.hansen, rostedt,
gmonaco, jgross, mgorman, graf, linux-edac, liam, ridong.chen,
longman, bsegall
During sequential CPU hotplug on large systems, build_sched_domains() is
invoked for every CPU offline and online step. Two bottlenecks in the
domain construction path scale poorly with CPU count:
1. On non-overlapping topology levels, all CPUs sharing a domain span
share the same circular ring of sched_group instances, and on
overlapping NUMA levels, groups with identical spans share
sched_group_capacity (sgc). However, init_sched_groups_capacity()
walks the entire group ring for every CPU in the domain and repeatedly
recomputes group_weight, cores (walking the group cpumask and clearing
cpu_smt_mask), and asym_prefer_cpu (O(N^3) bitmap operations per
hotplug step). Similarly, init_overlap_sched_group() recomputes
build_balance_mask() and cpumask_weight(sg_span) even when the shared
sgc was already initialized.
2. __sdt_alloc() and __sds_alloc() upfront-allocate a sched_group,
sched_group_capacity, and sched_domain_shared for every CPU in cpu_map
at every topology level, even though get_group() only uses the entry
for the first CPU of each child domain and d.sds is only used once per
LLC (plus at most once per SD_ASYM_CPUCAPACITY_FULL domain). All other
per-CPU allocations are immediately freed unused by
__free_domain_allocs().
Cache group_weight, cores, and asym_prefer_cpu on struct
sched_group_capacity so init_sched_groups_capacity() computes them once
per shared group capacity, reuse already-initialized sgc instances in
init_overlap_sched_group(), and allocate sdd->sg, sdd->sgc, and d->sds
lazily on demand.
2-socket, 480-CPU Intel Xeon 6985P-C (N=479, mean of 10 runs):
Offline: 9.85 s -> 5.15 s (-47.7%)
Online: 8.69 s -> 4.05 s (-53.4%)
Total: 18.54 s -> 9.20 s (-50.4%)
build_sched_domains() (cpu479 offline / online):
35.65 ms -> 10.39 ms (-70.9%) / 35.68 ms -> 9.58 ms (-73.2%)
2-socket, 512-CPU AMD EPYC 9B45 (N=511, mean of 10 runs):
Offline: 4.51 s -> 3.26 s (-27.7%)
Online: 3.15 s -> 2.03 s (-35.6%)
Total: 7.66 s -> 5.29 s (-30.9%)
build_sched_domains() (cpu511 offline / online):
11.69 ms -> 5.61 ms (-52.0%) / 12.24 ms -> 5.84 ms (-52.3%)
Signed-off-by: Pasha Tatashin <pasha.tatashin@soleen.com>
---
kernel/sched/sched.h | 9 ++
kernel/sched/topology.c | 183 ++++++++++++++++++++++++----------------
2 files changed, 117 insertions(+), 75 deletions(-)
diff --git a/kernel/sched/sched.h b/kernel/sched/sched.h
index e656c7059bf86..b76e2cd695025 100644
--- a/kernel/sched/sched.h
+++ b/kernel/sched/sched.h
@@ -2261,6 +2261,15 @@ struct sched_group_capacity {
int id;
+ /*
+ * Build-time cache shared by all groups using this sgc while
+ * build_sched_domains() runs; not maintained afterwards (e.g. by
+ * sched_update_asym_prefer_cpu()). Use the sched_group copies.
+ */
+ unsigned int group_weight;
+ unsigned int cores;
+ int asym_prefer_cpu;
+
unsigned long cpumask[]; /* Balance mask */
};
diff --git a/kernel/sched/topology.c b/kernel/sched/topology.c
index 0248227d983a7..02c38089a80c9 100644
--- a/kernel/sched/topology.c
+++ b/kernel/sched/topology.c
@@ -1259,32 +1259,49 @@ build_group_from_child_sched_domain(struct sched_domain *sd, int cpu)
return sg;
}
-static void init_overlap_sched_group(struct sched_domain *sd,
- struct sched_group *sg)
+static int init_overlap_sched_group(struct sched_domain *sd,
+ struct sched_group *sg)
{
struct cpumask *mask = sched_domains_tmpmask2;
struct sd_data *sdd = sd->private;
- struct cpumask *sg_span;
- int cpu;
+ struct cpumask *sg_span = sched_group_span(sg);
+ struct sched_group_capacity *sgc;
+ int cpu = cpumask_first(sg_span);
+ struct sched_domain *first_sd = *per_cpu_ptr(sdd->sd, cpu);
+
+ sgc = *per_cpu_ptr(sdd->sgc, cpu);
+ if (sgc && atomic_read(&sgc->ref) > 0 &&
+ first_sd && first_sd->child &&
+ cpumask_equal(sg_span, sched_domain_span(first_sd->child))) {
+ atomic_inc(&sgc->ref);
+ sg->sgc = sgc;
+ return 0;
+ }
build_balance_mask(sd, sg, mask);
cpu = cpumask_first(mask);
- sg->sgc = *per_cpu_ptr(sdd->sgc, cpu);
- if (atomic_inc_return(&sg->sgc->ref) == 1)
+ sgc = *per_cpu_ptr(sdd->sgc, cpu);
+ if (!sgc) {
+ sgc = kzalloc_node(sizeof(*sgc) + cpumask_size(),
+ GFP_KERNEL, cpu_to_node(cpu));
+ if (!sgc)
+ return -ENOMEM;
+ sgc->id = cpu;
+ *per_cpu_ptr(sdd->sgc, cpu) = sgc;
+ }
+
+ sg->sgc = sgc;
+ if (atomic_inc_return(&sg->sgc->ref) == 1) {
cpumask_copy(group_balance_mask(sg), mask);
- else
+ sg->sgc->capacity = SCHED_CAPACITY_SCALE * cpumask_weight(sg_span);
+ sg->sgc->min_capacity = SCHED_CAPACITY_SCALE;
+ sg->sgc->max_capacity = SCHED_CAPACITY_SCALE;
+ } else {
WARN_ON_ONCE(!cpumask_equal(group_balance_mask(sg), mask));
+ }
- /*
- * Initialize sgc->capacity such that even if we mess up the
- * domains and no possible iteration will get us here, we won't
- * die on a /0 trap.
- */
- sg_span = sched_group_span(sg);
- sg->sgc->capacity = SCHED_CAPACITY_SCALE * cpumask_weight(sg_span);
- sg->sgc->min_capacity = SCHED_CAPACITY_SCALE;
- sg->sgc->max_capacity = SCHED_CAPACITY_SCALE;
+ return 0;
}
static struct sched_domain *
@@ -1387,7 +1404,10 @@ build_overlap_sched_groups(struct sched_domain *sd, int cpu)
sg_span = sched_group_span(sg);
cpumask_or(covered, covered, sg_span);
- init_overlap_sched_group(sibling, sg);
+ if (init_overlap_sched_group(sibling, sg)) {
+ kfree(sg);
+ goto fail;
+ }
if (!first)
first = sg;
@@ -1489,6 +1509,25 @@ static struct sched_group *get_group(int cpu, struct sd_data *sdd)
cpu = cpumask_first(sched_domain_span(child));
sg = *per_cpu_ptr(sdd->sg, cpu);
+ if (!sg) {
+ sg = kzalloc_node(sizeof(*sg) + cpumask_size(),
+ GFP_KERNEL, cpu_to_node(cpu));
+ if (!sg)
+ return NULL;
+ sg->next = sg;
+ *per_cpu_ptr(sdd->sg, cpu) = sg;
+ }
+
+ if (!*per_cpu_ptr(sdd->sgc, cpu)) {
+ struct sched_group_capacity *sgc;
+
+ sgc = kzalloc_node(sizeof(*sgc) + cpumask_size(),
+ GFP_KERNEL, cpu_to_node(cpu));
+ if (!sgc)
+ return NULL;
+ sgc->id = cpu;
+ *per_cpu_ptr(sdd->sgc, cpu) = sgc;
+ }
sg->sgc = *per_cpu_ptr(sdd->sgc, cpu);
/* Increase refcounts for claim_allocations: */
@@ -1544,6 +1583,8 @@ build_sched_groups(struct sched_domain *sd, int cpu)
continue;
sg = get_group(i, sdd);
+ if (!sg)
+ return -ENOMEM;
cpumask_or(covered, covered, sched_group_span(sg));
@@ -1577,27 +1618,40 @@ static void init_sched_groups_capacity(int cpu, struct sched_domain *sd)
WARN_ON(!sg);
do {
- int cpu, cores = 0, max_cpu = -1;
+ int i, cores = 0, max_cpu = -1;
+
+ if (sg->group_weight)
+ goto next;
+
+ if (sg->sgc->group_weight) {
+ sg->group_weight = sg->sgc->group_weight;
+ sg->cores = sg->sgc->cores;
+ sg->asym_prefer_cpu = sg->sgc->asym_prefer_cpu;
+ goto next;
+ }
sg->group_weight = cpumask_weight(sched_group_span(sg));
cpumask_copy(mask, sched_group_span(sg));
- for_each_cpu(cpu, mask) {
+ for_each_cpu(i, mask) {
cores++;
- cpumask_andnot(mask, mask, cpu_smt_mask(cpu));
+ cpumask_andnot(mask, mask, cpu_smt_mask(i));
}
sg->cores = cores;
- if (!(sd->flags & SD_ASYM_PACKING))
- goto next;
-
- for_each_cpu(cpu, sched_group_span(sg)) {
- if (max_cpu < 0)
- max_cpu = cpu;
- else if (sched_asym_prefer(cpu, max_cpu))
- max_cpu = cpu;
+ if (sd->flags & SD_ASYM_PACKING) {
+ for_each_cpu(i, sched_group_span(sg)) {
+ if (max_cpu < 0)
+ max_cpu = i;
+ else if (sched_asym_prefer(i, max_cpu))
+ max_cpu = i;
+ }
+ sg->asym_prefer_cpu = max_cpu;
}
- sg->asym_prefer_cpu = max_cpu;
+
+ sg->sgc->group_weight = sg->group_weight;
+ sg->sgc->cores = sg->cores;
+ sg->sgc->asym_prefer_cpu = sg->asym_prefer_cpu;
next:
sg = sg->next;
@@ -1871,7 +1925,8 @@ static void claim_allocations(int cpu, struct s_data *d)
{
struct sched_domain *sd;
- if (atomic_read(&(*per_cpu_ptr(d->sds, cpu))->ref))
+ if (*per_cpu_ptr(d->sds, cpu) &&
+ atomic_read(&(*per_cpu_ptr(d->sds, cpu))->ref))
*per_cpu_ptr(d->sds, cpu) = NULL;
for (sd = *per_cpu_ptr(d->sd, cpu); sd; sd = sd->parent) {
@@ -1880,10 +1935,12 @@ static void claim_allocations(int cpu, struct s_data *d)
WARN_ON_ONCE(*per_cpu_ptr(sdd->sd, cpu) != sd);
*per_cpu_ptr(sdd->sd, cpu) = NULL;
- if (atomic_read(&(*per_cpu_ptr(sdd->sg, cpu))->ref))
+ if (*per_cpu_ptr(sdd->sg, cpu) &&
+ atomic_read(&(*per_cpu_ptr(sdd->sg, cpu))->ref))
*per_cpu_ptr(sdd->sg, cpu) = NULL;
- if (atomic_read(&(*per_cpu_ptr(sdd->sgc, cpu))->ref))
+ if (*per_cpu_ptr(sdd->sgc, cpu) &&
+ atomic_read(&(*per_cpu_ptr(sdd->sgc, cpu))->ref))
*per_cpu_ptr(sdd->sgc, cpu) = NULL;
}
}
@@ -2678,8 +2735,6 @@ static int __sdt_alloc(const struct cpumask *cpu_map)
for_each_cpu(j, cpu_map) {
struct sched_domain *sd;
- struct sched_group *sg;
- struct sched_group_capacity *sgc;
sd = kzalloc_node(sizeof(struct sched_domain) + cpumask_size(),
GFP_KERNEL, cpu_to_node(j));
@@ -2687,24 +2742,6 @@ static int __sdt_alloc(const struct cpumask *cpu_map)
return -ENOMEM;
*per_cpu_ptr(sdd->sd, j) = sd;
-
- sg = kzalloc_node(sizeof(struct sched_group) + cpumask_size(),
- GFP_KERNEL, cpu_to_node(j));
- if (!sg)
- return -ENOMEM;
-
- sg->next = sg;
-
- *per_cpu_ptr(sdd->sg, j) = sg;
-
- sgc = kzalloc_node(sizeof(struct sched_group_capacity) + cpumask_size(),
- GFP_KERNEL, cpu_to_node(j));
- if (!sgc)
- return -ENOMEM;
-
- sgc->id = j;
-
- *per_cpu_ptr(sdd->sgc, j) = sgc;
}
}
@@ -2745,23 +2782,10 @@ static void __sdt_free(const struct cpumask *cpu_map)
static int __sds_alloc(struct s_data *d, const struct cpumask *cpu_map)
{
- int j;
-
d->sds = alloc_percpu(struct sched_domain_shared *);
if (!d->sds)
return -ENOMEM;
- for_each_cpu(j, cpu_map) {
- struct sched_domain_shared *sds;
-
- sds = kzalloc_node(sizeof(struct sched_domain_shared),
- GFP_KERNEL, cpu_to_node(j));
- if (!sds)
- return -ENOMEM;
-
- *per_cpu_ptr(d->sds, j) = sds;
- }
-
return 0;
}
@@ -2932,7 +2956,7 @@ static void adjust_numa_imbalance(struct sched_domain *sd_llc)
}
}
-static void
+static int
init_sched_domain_shared(struct s_data *d, struct sched_domain *sd, int flags)
{
struct sched_domain_shared *sds = NULL;
@@ -2954,6 +2978,13 @@ init_sched_domain_shared(struct s_data *d, struct sched_domain *sd, int flags)
*/
for_each_cpu(cpu, sched_domain_span(sd)) {
sds = *per_cpu_ptr(d->sds, cpu);
+ if (!sds) {
+ sds = kzalloc_node(sizeof(*sds),
+ GFP_KERNEL, cpu_to_node(cpu));
+ if (!sds)
+ return -ENOMEM;
+ *per_cpu_ptr(d->sds, cpu) = sds;
+ }
if (!sds->alloc_flags ||
sd->span_weight == 1 ||
@@ -2978,6 +3009,8 @@ init_sched_domain_shared(struct s_data *d, struct sched_domain *sd, int flags)
*/
atomic_set(&sd->shared->nr_busy_cpus, sd->span_weight);
atomic_inc(&sd->shared->ref);
+
+ return 0;
}
/*
@@ -2993,25 +3026,24 @@ init_sched_domain_shared(struct s_data *d, struct sched_domain *sd, int flags)
* degenerate into a single-capacity group. The relevant edge cases are instead
* covered by the caveats above.
*
- * Return true if this CPU's asym path claimed sd->shared, false otherwise.
+ * Return 0 on success or if not claimed, -ENOMEM on allocation failure.
*/
-static bool claim_asym_sched_domain_shared(struct s_data *d, int cpu)
+static int claim_asym_sched_domain_shared(struct s_data *d, int cpu)
{
struct sched_domain *sd = *per_cpu_ptr(d->sd, cpu);
struct sched_domain *sd_asym;
if (!sd)
- return false;
+ return 0;
sd_asym = sd;
while (sd_asym && !(sd_asym->flags & SD_ASYM_CPUCAPACITY_FULL))
sd_asym = sd_asym->parent;
if (!sd_asym || (sd_asym->flags & SD_NUMA))
- return false;
+ return 0;
- init_sched_domain_shared(d, sd_asym, SD_ASYM_CPUCAPACITY);
- return true;
+ return init_sched_domain_shared(d, sd_asym, SD_ASYM_CPUCAPACITY);
}
static int __sched_domains_alloc_llc_id(void)
@@ -3157,15 +3189,16 @@ build_sched_domains(const struct cpumask *cpu_map, struct sched_domain_attr *att
if (!sd)
continue;
- if (has_asym)
- claim_asym_sched_domain_shared(&d, i);
+ if (has_asym && claim_asym_sched_domain_shared(&d, i))
+ goto error;
/* First, find the topmost SD_SHARE_LLC domain */
while (sd->parent && (sd->parent->flags & SD_SHARE_LLC))
sd = sd->parent;
if (sd->flags & SD_SHARE_LLC) {
- init_sched_domain_shared(&d, sd, SD_SHARE_LLC);
+ if (init_sched_domain_shared(&d, sd, SD_SHARE_LLC))
+ goto error;
/*
* In presence of higher domains, adjust the
--
2.56.0.385.gd3acb90ef8-goog
^ permalink raw reply related [flat|nested] 21+ messages in thread
* [PATCH v1 6/8] sched/deadline, topology: Avoid O(N^2) dl_server bandwidth walks during root_domain rebuilds
2026-10-15 15:33 [PATCH v1 0/8] x86, sched, mm: Speed up CPU hotplug on large systems Pasha Tatashin
` (4 preceding siblings ...)
2026-10-15 15:33 ` [PATCH v1 5/8] sched/topology: Avoid redundant O(N^3) group capacity walks and unused allocations in build_sched_domains() Pasha Tatashin
@ 2026-10-15 15:33 ` Pasha Tatashin
2026-10-15 15:57 ` sashiko-bot
2026-10-15 15:33 ` [PATCH v1 7/8] sched/topology: Avoid redundant percpu allocations and bitmap walks in build_sched_domains() Pasha Tatashin
` (2 subsequent siblings)
8 siblings, 1 reply; 21+ messages in thread
From: Pasha Tatashin @ 2026-10-15 15:33 UTC (permalink / raw)
To: kees, rppt, juri.lelli, nik.borisov, tglx, vschneid,
kprateek.nayak, vincent.guittot, bp, thorsten.blum, cgroups,
tony.luck, david, mingo, akpm, linux-mm, seanjc, ljs,
zhangguopeng, dietmar.eggemann, mhocko, vinicius.gomes, tj,
vbabka, linux-kernel, peterz, surenb, hpa, x86, pasha.tatashin,
dmatlack, tim.c.chen, kexec, pratyush, dave.hansen, rostedt,
gmonaco, jgross, mgorman, graf, linux-edac, liam, ridong.chen,
longman, bsegall
Every CPU runqueue has a fair_server (and optionally an ext_server)
deadline entity enabled by default. Whenever a root_domain is rebuilt on
CPU hotplug or cpuset changes, partition_sched_domains_locked() currently
performs three O(N^2) remote runqueue bandwidth updates across all N
active CPUs (O(N^3) across sequential hotplug of N CPUs):
1. detach_destroy_domains() calls
cpu_attach_domain(NULL, &def_root_domain, i) -> rq_attach_root() ->
__dl_server_attach_root() for each of the N CPUs. Each call recomputes
cpumask_weight_and(rd->span, cpu_active_mask) twice and calls
__dl_add() -> __dl_update(), walking all active CPUs attached to
def_root_domain so far to update rq->dl.extra_bw, only for
def_root_domain to be detached milliseconds later.
2. build_sched_domains() calls cpu_attach_domain(sd, d.rd, i) ->
rq_attach_root() -> __dl_server_attach_root() for each of the N CPUs,
performing another N*(N+1)/2 remote rq->dl.extra_bw updates using
intermediate partial span weights (1..N) that are immediately
discarded at the end of partition_sched_domains_locked().
3. dl_rebuild_rd_accounting() -> dl_clear_root_domain() resets
rd->dl_bw.total_bw = 0 and cpu_rq(i)->dl.extra_bw = max_bw, and then
calls dl_server_add_bw(rd, i) -> __dl_add() -> __dl_update() in a
nested O(N^2) loop over rd->span with interrupts disabled.
Remove the redundant __dl_server_attach_root() calls from rq_attach_root()
(since partition_sched_domains_locked() always rebuilds root_domain
accounting via dl_rebuild_rd_accounting() once domain attachment is
complete), and replace the nested O(N^2) loop in dl_clear_root_domain()
with a single O(N) accumulation pass and a single O(N) extra_bw update
pass.
Also provide a !CONFIG_CPUSETS implementation of
dl_rebuild_rd_accounting() (using a lockless dl_prio(READ_ONCE(p->prio))
pre-check before dl_add_task_root_domain(p)). Previously,
dl_rebuild_rd_accounting() was a no-op stub when CONFIG_CPUSETS=n, which
left root_domain rebuilds without SCHED_DEADLINE task bandwidth
accounting and left inconsistent per-CPU extra_bw values computed from
partial active CPU counts in rq_attach_root().
2-socket, 480-CPU Intel Xeon 6985P-C (N=479, mean of 10 runs):
Offline: 5.15 s -> 3.88 s (-24.7%)
Online: 4.05 s -> 2.51 s (-38.0%)
Total: 9.20 s -> 6.39 s (-30.5%)
dl_clear_root_domain() / dl_rebuild_rd_accounting() (cpu479):
5,537.0 us -> 58.1 us (-99.0%) / 5,176.2 us -> 61.9 us (-98.8%)
2-socket, 512-CPU AMD EPYC 9B45 (N=511, mean of 10 runs):
Offline: 3.26 s -> 2.77 s (-15.0%)
Online: 2.03 s -> 1.53 s (-24.6%)
Total: 5.29 s -> 4.30 s (-18.7%)
dl_clear_root_domain() / dl_rebuild_rd_accounting() (cpu511):
1,880.5 us -> 27.5 us (-98.5%) / 1,762.3 us -> 22.6 us (-98.7%)
Signed-off-by: Pasha Tatashin <pasha.tatashin@soleen.com>
---
include/linux/cpuset.h | 6 +--
include/linux/sched/deadline.h | 1 -
kernel/sched/deadline.c | 89 +++++++++++++++++++++-------------
kernel/sched/sched.h | 1 -
kernel/sched/topology.c | 13 -----
5 files changed, 56 insertions(+), 54 deletions(-)
diff --git a/include/linux/cpuset.h b/include/linux/cpuset.h
index 914983387e955..8baa99308f49e 100644
--- a/include/linux/cpuset.h
+++ b/include/linux/cpuset.h
@@ -129,7 +129,7 @@ static inline int cpuset_do_page_mem_spread(void)
extern bool current_cpuset_is_being_rebound(void);
-extern void dl_rebuild_rd_accounting(void);
+void dl_rebuild_rd_accounting(void);
extern void rebuild_sched_domains(void);
extern void cpuset_print_current_mems_allowed(void);
@@ -273,9 +273,7 @@ static inline bool current_cpuset_is_being_rebound(void)
return false;
}
-static inline void dl_rebuild_rd_accounting(void)
-{
-}
+void dl_rebuild_rd_accounting(void);
static inline void rebuild_sched_domains(void)
{
diff --git a/include/linux/sched/deadline.h b/include/linux/sched/deadline.h
index 273538200a445..6f03357f05c76 100644
--- a/include/linux/sched/deadline.h
+++ b/include/linux/sched/deadline.h
@@ -31,7 +31,6 @@ static inline bool dl_time_before(u64 a, u64 b)
struct root_domain;
extern void dl_add_task_root_domain(struct task_struct *p);
-extern void dl_clear_root_domain(struct root_domain *rd);
extern void dl_clear_root_domain_cpu(int cpu);
/*
* Return whether moving DL task @p to @new_mask requires moving DL
diff --git a/kernel/sched/deadline.c b/kernel/sched/deadline.c
index 857dbe3519a86..01f2f24e3b814 100644
--- a/kernel/sched/deadline.c
+++ b/kernel/sched/deadline.c
@@ -1885,24 +1885,6 @@ void sched_init_dl_servers(void)
}
}
-void __dl_server_attach_root(struct sched_dl_entity *dl_se, struct rq *rq)
-{
- u64 new_bw = dl_se->dl_bw;
- int cpu = cpu_of(rq);
- struct dl_bw *dl_b;
-
- if (!dl_se->dl_bw_attached)
- return;
-
- dl_b = dl_bw_of(cpu_of(rq));
- guard(raw_spinlock)(&dl_b->lock);
-
- if (!dl_bw_cpus(cpu))
- return;
-
- __dl_add(dl_b, new_bw, dl_bw_cpus(cpu));
-}
-
int dl_server_apply_params(struct sched_dl_entity *dl_se, u64 runtime, u64 period, bool init)
{
u64 old_bw = (init || !dl_se->dl_bw_attached) ? 0 :
@@ -3448,18 +3430,22 @@ void dl_add_task_root_domain(struct task_struct *p)
raw_spin_unlock_irqrestore(&p->pi_lock, rf.flags);
}
-static void dl_server_add_bw(struct root_domain *rd, int cpu)
+static void dl_server_sum_bw(int cpu, int cpus, u64 *total_bw, s64 *extra_bw)
{
struct sched_dl_entity *dl_se;
dl_se = &cpu_rq(cpu)->fair_server;
- if (dl_server(dl_se) && dl_se->dl_bw_attached && cpu_active(cpu))
- __dl_add(&rd->dl_bw, dl_se->dl_bw, dl_bw_cpus(cpu));
+ if (dl_server(dl_se) && dl_se->dl_bw_attached) {
+ *total_bw += dl_se->dl_bw;
+ *extra_bw -= (s32)dl_se->dl_bw / cpus;
+ }
#ifdef CONFIG_SCHED_CLASS_EXT
dl_se = &cpu_rq(cpu)->ext_server;
- if (dl_server(dl_se) && dl_se->dl_bw_attached && cpu_active(cpu))
- __dl_add(&rd->dl_bw, dl_se->dl_bw, dl_bw_cpus(cpu));
+ if (dl_server(dl_se) && dl_se->dl_bw_attached) {
+ *total_bw += dl_se->dl_bw;
+ *extra_bw -= (s32)dl_se->dl_bw / cpus;
+ }
#endif
}
@@ -3480,26 +3466,32 @@ static u64 dl_server_read_bw(int cpu)
return dl_bw;
}
-void dl_clear_root_domain(struct root_domain *rd)
+static void dl_clear_root_domain(struct root_domain *rd)
{
- int i;
+ u64 total_bw = 0;
+ s64 extra_bw = 0;
+ int i, cpus;
guard(raw_spinlock_irqsave)(&rd->dl_bw.lock);
- /*
- * Reset total_bw to zero and extra_bw to max_bw so that next
- * loop will add dl-servers contributions back properly,
- */
- rd->dl_bw.total_bw = 0;
- for_each_cpu(i, rd->span)
- cpu_rq(i)->dl.extra_bw = cpu_rq(i)->dl.max_bw;
-
/*
* dl_servers are not tasks. Since dl_add_task_root_domain ignores
* them, we need to account for them here explicitly.
*/
- for_each_cpu(i, rd->span)
- dl_server_add_bw(rd, i);
+ cpus = cpumask_weight_and(rd->span, cpu_active_mask);
+ if (cpus) {
+ for_each_cpu_and(i, rd->span, cpu_active_mask)
+ dl_server_sum_bw(i, cpus, &total_bw, &extra_bw);
+ }
+
+ rd->dl_bw.total_bw = total_bw;
+ for_each_cpu(i, rd->span) {
+ struct rq *rq = cpu_rq(i);
+
+ rq->dl.extra_bw = rq->dl.max_bw;
+ if (cpu_active(i))
+ rq->dl.extra_bw += extra_bw;
+ }
}
void dl_clear_root_domain_cpu(int cpu)
@@ -3507,6 +3499,33 @@ void dl_clear_root_domain_cpu(int cpu)
dl_clear_root_domain(cpu_rq(cpu)->rd);
}
+#ifndef CONFIG_CPUSETS
+void dl_rebuild_rd_accounting(void)
+{
+ struct task_struct *g, *p;
+ u64 cookie = ++dl_cookie;
+ int cpu;
+
+ lockdep_assert_cpus_held();
+ lockdep_assert_held(&sched_domains_mutex);
+
+ rcu_read_lock();
+ for_each_possible_cpu(cpu) {
+ if (dl_bw_visited(cpu, cookie))
+ continue;
+
+ dl_clear_root_domain_cpu(cpu);
+ }
+ for_each_process_thread(g, p) {
+ /* Racy pre-check; dl_add_task_root_domain() rechecks under pi_lock. */
+ if (!dl_prio(READ_ONCE(p->prio)))
+ continue;
+ dl_add_task_root_domain(p);
+ }
+ rcu_read_unlock();
+}
+#endif
+
static void switched_from_dl(struct rq *rq, struct task_struct *p)
{
/*
diff --git a/kernel/sched/sched.h b/kernel/sched/sched.h
index b76e2cd695025..e76a6876ac4e8 100644
--- a/kernel/sched/sched.h
+++ b/kernel/sched/sched.h
@@ -418,7 +418,6 @@ extern void sched_init_dl_servers(void);
extern void fair_server_init(struct rq *rq);
extern void ext_server_init(struct rq *rq);
-extern void __dl_server_attach_root(struct sched_dl_entity *dl_se, struct rq *rq);
extern int dl_server_apply_params(struct sched_dl_entity *dl_se,
u64 runtime, u64 period, bool init);
extern int dl_server_attach_bw(struct sched_dl_entity *dl_se);
diff --git a/kernel/sched/topology.c b/kernel/sched/topology.c
index 02c38089a80c9..fe7ecee041f5b 100644
--- a/kernel/sched/topology.c
+++ b/kernel/sched/topology.c
@@ -502,19 +502,6 @@ void rq_attach_root(struct rq *rq, struct root_domain *rd)
if (cpumask_test_cpu(rq->cpu, cpu_active_mask))
set_rq_online(rq);
- /*
- * Because the rq is not a task, dl_add_task_root_domain() did not
- * move the fair server bw to the rd if it already started.
- * Add it now.
- */
- if (rq->fair_server.dl_server)
- __dl_server_attach_root(&rq->fair_server, rq);
-
-#ifdef CONFIG_SCHED_CLASS_EXT
- if (rq->ext_server.dl_server)
- __dl_server_attach_root(&rq->ext_server, rq);
-#endif
-
rq_unlock_irqrestore(rq, &rf);
if (old_rd)
--
2.56.0.385.gd3acb90ef8-goog
^ permalink raw reply related [flat|nested] 21+ messages in thread
* [PATCH v1 7/8] sched/topology: Avoid redundant percpu allocations and bitmap walks in build_sched_domains()
2026-10-15 15:33 [PATCH v1 0/8] x86, sched, mm: Speed up CPU hotplug on large systems Pasha Tatashin
` (5 preceding siblings ...)
2026-10-15 15:33 ` [PATCH v1 6/8] sched/deadline, topology: Avoid O(N^2) dl_server bandwidth walks during root_domain rebuilds Pasha Tatashin
@ 2026-10-15 15:33 ` Pasha Tatashin
2026-10-15 15:51 ` sashiko-bot
2026-10-15 23:17 ` Tim Chen
2026-10-15 15:33 ` [PATCH v1 8/8] mm/vmstat: Avoid redundant per-CPU stat_threshold writes in refresh_zone_stat_thresholds() Pasha Tatashin
2026-10-15 21:33 ` [PATCH v1 0/8] x86, sched, mm: Speed up CPU hotplug on large systems Luck, Tony
8 siblings, 2 replies; 21+ messages in thread
From: Pasha Tatashin @ 2026-10-15 15:33 UTC (permalink / raw)
To: kees, rppt, juri.lelli, nik.borisov, tglx, vschneid,
kprateek.nayak, vincent.guittot, bp, thorsten.blum, cgroups,
tony.luck, david, mingo, akpm, linux-mm, seanjc, ljs,
zhangguopeng, dietmar.eggemann, mhocko, vinicius.gomes, tj,
vbabka, linux-kernel, peterz, surenb, hpa, x86, pasha.tatashin,
dmatlack, tim.c.chen, kexec, pratyush, dave.hansen, rostedt,
gmonaco, jgross, mgorman, graf, linux-edac, liam, ridong.chen,
longman, bsegall
During CPU hotplug, build_sched_domains() is invoked on every CPU
activation and deactivation. Profiling build_sched_domains() on a
480-CPU Intel Xeon 6985P-C and a 512-CPU AMD EPYC 9B45 shows several
sources of avoidable O(N^2) and O(N) work per step:
1. build_sched_groups() and build_overlap_sched_groups() iterate over
every bit of sched_domain_span(sd) via for_each_cpu_wrap(i, span, cpu)
and check cpumask_test_cpu(i, covered) one CPU at a time, even after
entire child groups (e.g., 120-CPU NUMA nodes) have been added to
covered. Instead, track the remaining uncovered CPUs
(cpumask_copy(uncovered, span)), clear each built group's span with
cpumask_andnot(), and advance directly to the next uncovered CPU.
2. __visit_domain_allocation_hell() and __free_domain_allocs() allocate
and free 3 * nr_levels + 2 temporary per-CPU pointer arrays via
alloc_percpu() / free_percpu() on every domain rebuild. Reuse the
per-CPU pointer arrays across domain rebuilds under
sched_domains_mutex, clearing entries in __free_domain_allocs() and
only freeing the topology-level arrays when sched_domain_topology is
replaced in sched_init_numa() / sched_reset_numa().
3. sd_init() already computes sd_weight = cpumask_weight(sd_span), so
initialize sd->span_weight directly in sd_init() and reuse
child->span_weight in get_group() instead of recomputing
cpumask_weight() across all domains and groups.
4. Reuse the first LLC sibling's computed llc_bytes in alloc_sd_llc()
and hoist get_effective_llc_bytes() out of the sibling loop in
sched_update_llc_bytes().
2-socket, 480-CPU Intel Xeon 6985P-C (N=479, mean of 10 runs):
Offline: 3.88 s -> 3.37 s (-13.1%)
Online: 2.51 s -> 2.06 s (-17.9%)
Total: 6.39 s -> 5.43 s (-15.0%)
build_sched_domains() (cpu479 offline / online):
7.91 ms -> 3.96 ms (-50.0%) / 7.51 ms -> 4.26 ms (-43.3%)
2-socket, 512-CPU AMD EPYC 9B45 (N=511, mean of 10 runs):
Offline: 2.77 s -> 2.34 s (-15.5%)
Online: 1.53 s -> 1.12 s (-26.8%)
Total: 4.30 s -> 3.46 s (-19.5%)
build_sched_domains() (cpu511 offline / online):
6.55 ms -> 3.04 ms (-53.7%) / 5.28 ms -> 3.20 ms (-39.4%)
Signed-off-by: Pasha Tatashin <pasha.tatashin@soleen.com>
---
kernel/sched/topology.c | 147 ++++++++++++++++++++++++++--------------
1 file changed, 98 insertions(+), 49 deletions(-)
diff --git a/kernel/sched/topology.c b/kernel/sched/topology.c
index fe7ecee041f5b..a9122dbff379d 100644
--- a/kernel/sched/topology.c
+++ b/kernel/sched/topology.c
@@ -903,9 +903,15 @@ static bool alloc_sd_llc(const struct cpumask *cpu_map,
top_llc = parent;
if (top_llc->flags & SD_SHARE_LLC) {
+ int first = cpumask_first(sched_domain_span(top_llc));
+
sd->llc_max = max_lid + 1;
sd->llc_counts = p;
- sd->llc_bytes = get_effective_llc_bytes(i, top_llc);
+ /* All CPUs in top_llc share the same LLC size and weight. */
+ if (first < i)
+ sd->llc_bytes = (*per_cpu_ptr(d->sd, first))->llc_bytes;
+ else
+ sd->llc_bytes = get_effective_llc_bytes(i, top_llc);
} else {
/* avoid memory leak */
kfree(p);
@@ -986,6 +992,7 @@ void sched_cache_active_set(void)
void sched_update_llc_bytes(unsigned int cpu)
{
struct sched_domain *sd, *sdp;
+ unsigned long llc_bytes;
unsigned int i;
sched_domains_mutex_lock();
@@ -1002,10 +1009,11 @@ void sched_update_llc_bytes(unsigned int cpu)
* sibling on each online event corrects this once the full
* shared_cpu_map is known.
*/
+ llc_bytes = get_effective_llc_bytes(cpu, sdp);
for_each_cpu(i, sched_domain_span(sdp)) {
sd = rcu_dereference_sched_domain(cpu_rq(i)->sd);
if (sd)
- sd->llc_bytes = get_effective_llc_bytes(i, sdp);
+ sd->llc_bytes = llc_bytes;
}
unlock:
@@ -1321,19 +1329,18 @@ build_overlap_sched_groups(struct sched_domain *sd, int cpu)
{
struct sched_group *first = NULL, *last = NULL, *sg;
const struct cpumask *span = sched_domain_span(sd);
- struct cpumask *covered = sched_domains_tmpmask;
+ struct cpumask *uncovered = sched_domains_tmpmask;
struct sd_data *sdd = sd->private;
struct sched_domain *sibling;
int i;
- cpumask_clear(covered);
+ cpumask_copy(uncovered, span);
- for_each_cpu_wrap(i, span, cpu) {
+ for (i = cpu; i < nr_cpu_ids;
+ i = cpumask_next(i, uncovered),
+ i = i < nr_cpu_ids ? i : cpumask_first(uncovered)) {
struct cpumask *sg_span;
- if (cpumask_test_cpu(i, covered))
- continue;
-
sibling = *per_cpu_ptr(sdd->sd, i);
/*
@@ -1346,8 +1353,10 @@ build_overlap_sched_groups(struct sched_domain *sd, int cpu)
* Domains should always include the CPU they're built on, so
* check that.
*/
- if (!cpumask_test_cpu(i, sched_domain_span(sibling)))
+ if (!cpumask_test_cpu(i, sched_domain_span(sibling))) {
+ __cpumask_clear_cpu(i, uncovered);
continue;
+ }
/*
* Usually we build sched_group by sibling's child sched_domain
@@ -1389,7 +1398,7 @@ build_overlap_sched_groups(struct sched_domain *sd, int cpu)
goto fail;
sg_span = sched_group_span(sg);
- cpumask_or(covered, covered, sg_span);
+ cpumask_andnot(uncovered, uncovered, sg_span);
if (init_overlap_sched_group(sibling, sg)) {
kfree(sg);
@@ -1530,12 +1539,13 @@ static struct sched_group *get_group(int cpu, struct sd_data *sdd)
cpumask_copy(sched_group_span(sg), sched_domain_span(child));
cpumask_copy(group_balance_mask(sg), sched_group_span(sg));
sg->flags = child->flags;
+ sg->sgc->capacity = SCHED_CAPACITY_SCALE * child->span_weight;
} else {
cpumask_set_cpu(cpu, sched_group_span(sg));
cpumask_set_cpu(cpu, group_balance_mask(sg));
+ sg->sgc->capacity = SCHED_CAPACITY_SCALE;
}
- sg->sgc->capacity = SCHED_CAPACITY_SCALE * cpumask_weight(sched_group_span(sg));
sg->sgc->min_capacity = SCHED_CAPACITY_SCALE;
sg->sgc->max_capacity = SCHED_CAPACITY_SCALE;
@@ -1555,25 +1565,24 @@ build_sched_groups(struct sched_domain *sd, int cpu)
struct sched_group *first = NULL, *last = NULL;
struct sd_data *sdd = sd->private;
const struct cpumask *span = sched_domain_span(sd);
- struct cpumask *covered;
+ struct cpumask *uncovered;
int i;
lockdep_assert_held(&sched_domains_mutex);
- covered = sched_domains_tmpmask;
+ uncovered = sched_domains_tmpmask;
- cpumask_clear(covered);
+ cpumask_copy(uncovered, span);
- for_each_cpu_wrap(i, span, cpu) {
+ for (i = cpu; i < nr_cpu_ids;
+ i = cpumask_next(i, uncovered),
+ i = i < nr_cpu_ids ? i : cpumask_first(uncovered)) {
struct sched_group *sg;
- if (cpumask_test_cpu(i, covered))
- continue;
-
sg = get_group(i, sdd);
if (!sg)
return -ENOMEM;
- cpumask_or(covered, covered, sched_group_span(sg));
+ cpumask_andnot(uncovered, uncovered, sched_group_span(sg));
if (!first)
first = sg;
@@ -1862,16 +1871,21 @@ static int __sdt_alloc(const struct cpumask *cpu_map);
static void __sds_free(struct s_data *d, const struct cpumask *cpu_map);
static int __sds_alloc(struct s_data *d, const struct cpumask *cpu_map);
+static struct sched_domain * __percpu *sched_domains_sd;
+
static void __free_domain_allocs(struct s_data *d, enum s_alloc what,
const struct cpumask *cpu_map)
{
+ int j;
+
switch (what) {
case sa_rootdomain:
if (!atomic_read(&d->rd->refcount))
free_rootdomain(&d->rd->rcu);
fallthrough;
case sa_sd:
- free_percpu(d->sd);
+ for_each_cpu(j, cpu_map)
+ *per_cpu_ptr(d->sd, j) = NULL;
fallthrough;
case sa_sd_shared:
__sds_free(d, cpu_map);
@@ -1893,9 +1907,12 @@ __visit_domain_allocation_hell(struct s_data *d, const struct cpumask *cpu_map)
return sa_sd_storage;
if (__sds_alloc(d, cpu_map))
return sa_sd_shared;
- d->sd = alloc_percpu(struct sched_domain *);
- if (!d->sd)
- return sa_sd_shared;
+ if (!sched_domains_sd) {
+ sched_domains_sd = alloc_percpu(struct sched_domain *);
+ if (!sched_domains_sd)
+ return sa_sd_shared;
+ }
+ d->sd = sched_domains_sd;
d->rd = alloc_rootdomain();
if (!d->rd)
return sa_sd;
@@ -2019,6 +2036,7 @@ sd_init(struct sched_domain_topology_level *tl,
.last_balance = jiffies,
.balance_interval = sd_weight,
+ .span_weight = sd_weight,
/* 50% success rate */
.newidle_call = 512,
@@ -2159,6 +2177,22 @@ void __init set_sched_topology(struct sched_domain_topology_level *tl)
}
#ifdef CONFIG_NUMA
+static void __sdt_free_percpu(void)
+{
+ struct sched_domain_topology_level *tl;
+
+ for_each_sd_topology(tl) {
+ struct sd_data *sdd = &tl->data;
+
+ free_percpu(sdd->sd);
+ sdd->sd = NULL;
+ free_percpu(sdd->sg);
+ sdd->sg = NULL;
+ free_percpu(sdd->sgc);
+ sdd->sgc = NULL;
+ }
+}
+
static int cpu_numa_flags(void)
{
return SD_NUMA;
@@ -2449,6 +2483,7 @@ void sched_init_numa(int offline_node)
/*
* Copy the default topology bits..
*/
+ __sdt_free_percpu();
for (i = 0; sched_domain_topology[i].mask; i++)
tl[i] = sched_domain_topology[i];
@@ -2507,6 +2542,7 @@ static void sched_reset_numa(void)
kfree(masks);
}
if (sched_domain_topology_saved) {
+ __sdt_free_percpu();
kfree(sched_domain_topology);
sched_domain_topology = sched_domain_topology_saved;
sched_domain_topology_saved = NULL;
@@ -2708,17 +2744,23 @@ static int __sdt_alloc(const struct cpumask *cpu_map)
for_each_sd_topology(tl) {
struct sd_data *sdd = &tl->data;
- sdd->sd = alloc_percpu(struct sched_domain *);
- if (!sdd->sd)
- return -ENOMEM;
+ if (!sdd->sd) {
+ sdd->sd = alloc_percpu(struct sched_domain *);
+ if (!sdd->sd)
+ return -ENOMEM;
+ }
- sdd->sg = alloc_percpu(struct sched_group *);
- if (!sdd->sg)
- return -ENOMEM;
+ if (!sdd->sg) {
+ sdd->sg = alloc_percpu(struct sched_group *);
+ if (!sdd->sg)
+ return -ENOMEM;
+ }
- sdd->sgc = alloc_percpu(struct sched_group_capacity *);
- if (!sdd->sgc)
- return -ENOMEM;
+ if (!sdd->sgc) {
+ sdd->sgc = alloc_percpu(struct sched_group_capacity *);
+ if (!sdd->sgc)
+ return -ENOMEM;
+ }
for_each_cpu(j, cpu_map) {
struct sched_domain *sd;
@@ -2748,30 +2790,36 @@ static void __sdt_free(const struct cpumask *cpu_map)
if (sdd->sd) {
sd = *per_cpu_ptr(sdd->sd, j);
- if (sd && (sd->flags & SD_NUMA))
- free_sched_groups(sd->groups, 0);
- kfree(*per_cpu_ptr(sdd->sd, j));
+ if (sd) {
+ if (sd->flags & SD_NUMA)
+ free_sched_groups(sd->groups, 0);
+ kfree(sd);
+ *per_cpu_ptr(sdd->sd, j) = NULL;
+ }
}
- if (sdd->sg)
+ if (sdd->sg) {
kfree(*per_cpu_ptr(sdd->sg, j));
- if (sdd->sgc)
+ *per_cpu_ptr(sdd->sg, j) = NULL;
+ }
+ if (sdd->sgc) {
kfree(*per_cpu_ptr(sdd->sgc, j));
+ *per_cpu_ptr(sdd->sgc, j) = NULL;
+ }
}
- free_percpu(sdd->sd);
- sdd->sd = NULL;
- free_percpu(sdd->sg);
- sdd->sg = NULL;
- free_percpu(sdd->sgc);
- sdd->sgc = NULL;
}
}
+static struct sched_domain_shared * __percpu *sched_domains_sds;
+
static int __sds_alloc(struct s_data *d, const struct cpumask *cpu_map)
{
- d->sds = alloc_percpu(struct sched_domain_shared *);
- if (!d->sds)
- return -ENOMEM;
+ if (!sched_domains_sds) {
+ sched_domains_sds = alloc_percpu(struct sched_domain_shared *);
+ if (!sched_domains_sds)
+ return -ENOMEM;
+ }
+ d->sds = sched_domains_sds;
return 0;
}
@@ -2783,10 +2831,11 @@ static void __sds_free(struct s_data *d, const struct cpumask *cpu_map)
if (!d->sds)
return;
- for_each_cpu(j, cpu_map)
+ for_each_cpu(j, cpu_map) {
kfree(*per_cpu_ptr(d->sds, j));
+ *per_cpu_ptr(d->sds, j) = NULL;
+ }
- free_percpu(d->sds);
d->sds = NULL;
}
@@ -2810,6 +2859,7 @@ static struct sched_domain *build_sched_domain(struct sched_domain_topology_leve
cpumask_or(sched_domain_span(sd),
sched_domain_span(sd),
sched_domain_span(child));
+ sd->span_weight = cpumask_weight(sched_domain_span(sd));
}
}
@@ -3160,7 +3210,6 @@ build_sched_domains(const struct cpumask *cpu_map, struct sched_domain_attr *att
/* Build the groups for the domains */
for_each_cpu(i, cpu_map) {
for (sd = *per_cpu_ptr(d.sd, i); sd; sd = sd->parent) {
- sd->span_weight = cpumask_weight(sched_domain_span(sd));
if (sd->flags & SD_NUMA) {
if (build_overlap_sched_groups(sd, i))
goto error;
--
2.56.0.385.gd3acb90ef8-goog
^ permalink raw reply related [flat|nested] 21+ messages in thread
* [PATCH v1 8/8] mm/vmstat: Avoid redundant per-CPU stat_threshold writes in refresh_zone_stat_thresholds()
2026-10-15 15:33 [PATCH v1 0/8] x86, sched, mm: Speed up CPU hotplug on large systems Pasha Tatashin
` (6 preceding siblings ...)
2026-10-15 15:33 ` [PATCH v1 7/8] sched/topology: Avoid redundant percpu allocations and bitmap walks in build_sched_domains() Pasha Tatashin
@ 2026-10-15 15:33 ` Pasha Tatashin
2026-10-15 15:49 ` sashiko-bot
2026-10-15 21:33 ` [PATCH v1 0/8] x86, sched, mm: Speed up CPU hotplug on large systems Luck, Tony
8 siblings, 1 reply; 21+ messages in thread
From: Pasha Tatashin @ 2026-10-15 15:33 UTC (permalink / raw)
To: kees, rppt, juri.lelli, nik.borisov, tglx, vschneid,
kprateek.nayak, vincent.guittot, bp, thorsten.blum, cgroups,
tony.luck, david, mingo, akpm, linux-mm, seanjc, ljs,
zhangguopeng, dietmar.eggemann, mhocko, vinicius.gomes, tj,
vbabka, linux-kernel, peterz, surenb, hpa, x86, pasha.tatashin,
dmatlack, tim.c.chen, kexec, pratyush, dave.hansen, rostedt,
gmonaco, jgross, mgorman, graf, linux-edac, liam, ridong.chen,
longman, bsegall
During CPU hotplug, vmstat_cpu_online() and vmstat_cpu_dead() invoke
refresh_zone_stat_thresholds() on every CPU transition. Currently,
refresh_zone_stat_thresholds() first zeroes
per_cpu_nodestats->stat_threshold across all online CPUs and all online
nodes, and then unconditionally writes per_cpu_zonestats->stat_threshold
and per_cpu_nodestats->stat_threshold inside the zone loop for every
populated zone and every online CPU.
However, calculate_normal_threshold(zone) only changes when
fls(num_online_cpus()) changes (at powers of 2) and is clamped at 125
for large normal zones (>= 16 GB) once num_online_cpus() >= 64.
Unconditionally zeroing and rewriting every CPU's per-CPU stat_threshold
on every hotplug step repeatedly dirties remote per-CPU cachelines with
identical values (and briefly exposes stat_threshold == 0 on live CPUs).
Compute pgdat_threshold across each node's populated zones first, hoist
the per_cpu_nodestats->stat_threshold update out of the zone loop, and
only write stat_threshold when the value actually changes.
2-socket, 480-CPU Intel Xeon 6985P-C (N=479, mean of 10 runs):
Offline: 3.37 s -> 3.09 s (-8.3%)
Online: 2.06 s -> 1.98 s (-3.9%)
Total: 5.43 s -> 5.07 s (-6.6%)
refresh_zone_stat_thresholds() (cpu479 offline / online):
1,158.9 us -> 135.3 us (-88.3%) / 1,468.2 us -> 128.6 us (-91.2%)
2-socket, 512-CPU AMD EPYC 9B45 (N=511, mean of 10 runs):
Offline: 2.34 s -> 2.30 s (-1.7%)
Online: 1.12 s -> 1.08 s (-3.6%)
Total: 3.46 s -> 3.38 s (-2.3%)
refresh_zone_stat_thresholds() (cpu511 offline / online):
104.6 us -> 50.7 us (-51.6%) / 109.3 us -> 53.5 us (-51.1%)
Signed-off-by: Pasha Tatashin <pasha.tatashin@soleen.com>
---
mm/vmstat.c | 58 ++++++++++++++++++++++++++++-------------------------
1 file changed, 31 insertions(+), 27 deletions(-)
diff --git a/mm/vmstat.c b/mm/vmstat.c
index cb57714539fb5..c671e9669b43b 100644
--- a/mm/vmstat.c
+++ b/mm/vmstat.c
@@ -280,41 +280,45 @@ void refresh_zone_stat_thresholds(void)
int cpu;
int threshold;
- /* Zero current pgdat thresholds */
for_each_online_pgdat(pgdat) {
- for_each_online_cpu(cpu) {
- per_cpu_ptr(pgdat->per_cpu_nodestats, cpu)->stat_threshold = 0;
- }
- }
+ int pgdat_threshold = 0;
- for_each_populated_zone(zone) {
- struct pglist_data *pgdat = zone->zone_pgdat;
- unsigned long max_drift, tolerate_drift;
+ for (int z = 0; z < MAX_NR_ZONES; z++) {
+ unsigned long max_drift, tolerate_drift;
- threshold = calculate_normal_threshold(zone);
+ zone = &pgdat->node_zones[z];
+ if (!populated_zone(zone))
+ continue;
- for_each_online_cpu(cpu) {
- int pgdat_threshold;
+ threshold = calculate_normal_threshold(zone);
+ /* Base nodestat threshold on the largest populated zone. */
+ pgdat_threshold = max(threshold, pgdat_threshold);
- per_cpu_ptr(zone->per_cpu_zonestats, cpu)->stat_threshold
- = threshold;
+ for_each_online_cpu(cpu) {
+ s8 *pz = &per_cpu_ptr(zone->per_cpu_zonestats, cpu)->stat_threshold;
- /* Base nodestat threshold on the largest populated zone. */
- pgdat_threshold = per_cpu_ptr(pgdat->per_cpu_nodestats, cpu)->stat_threshold;
- per_cpu_ptr(pgdat->per_cpu_nodestats, cpu)->stat_threshold
- = max(threshold, pgdat_threshold);
+ if (*pz != threshold)
+ *pz = threshold;
+ }
+
+ /*
+ * Only set percpu_drift_mark if there is a danger that
+ * NR_FREE_PAGES reports the low watermark is ok when in fact
+ * the min watermark could be breached by an allocation
+ */
+ tolerate_drift = low_wmark_pages(zone) - min_wmark_pages(zone);
+ max_drift = num_online_cpus() * threshold;
+ if (max_drift > tolerate_drift)
+ zone->percpu_drift_mark = high_wmark_pages(zone) +
+ max_drift;
}
- /*
- * Only set percpu_drift_mark if there is a danger that
- * NR_FREE_PAGES reports the low watermark is ok when in fact
- * the min watermark could be breached by an allocation
- */
- tolerate_drift = low_wmark_pages(zone) - min_wmark_pages(zone);
- max_drift = num_online_cpus() * threshold;
- if (max_drift > tolerate_drift)
- zone->percpu_drift_mark = high_wmark_pages(zone) +
- max_drift;
+ for_each_online_cpu(cpu) {
+ s8 *pn = &per_cpu_ptr(pgdat->per_cpu_nodestats, cpu)->stat_threshold;
+
+ if (*pn != pgdat_threshold)
+ *pn = pgdat_threshold;
+ }
}
}
--
2.56.0.385.gd3acb90ef8-goog
^ permalink raw reply related [flat|nested] 21+ messages in thread
* Re: [PATCH v1 1/8] x86/realmode: Remove unnecessary WBINVD from AP startup trampoline
2026-10-15 15:33 ` [PATCH v1 1/8] x86/realmode: Remove unnecessary WBINVD from AP startup trampoline Pasha Tatashin
@ 2026-10-15 15:41 ` sashiko-bot
0 siblings, 0 replies; 21+ messages in thread
From: sashiko-bot @ 2026-10-15 15:41 UTC (permalink / raw)
To: Pasha Tatashin, kees, rppt, juri.lelli, nik.borisov, tglx,
vschneid, kprateek.nayak, vincent.guittot, bp, thorsten.blum,
tony.luck, david, mingo, akpm, seanjc, ljs, zhangguopeng,
dietmar.eggemann, mhocko, vinicius.gomes, tj, vbabka, peterz,
surenb, hpa, x86, dmatlack, tim.c.chen, kexec, pratyush,
dave.hansen, rostedt, gmonaco, jgross, mgorman, graf, liam,
ridong.chen, longman, bsegall
> trampoline_start executes WBINVD in 16-bit real mode before switching to
> protected mode. This WBINVD was originally added in the 1990s for
> Sequent NUMA-Q hardware (as noted by the comment in trampoline_32.S:
> "Needed for NUMA-Q should be harmless for others"), whose cross-quad
> caches came up non-coherent prior to OS initialization. Sequent NUMA-Q
> support (CONFIG_X86_NUMAQ) was removed in commit b5660ba76b41
> ("x86, platforms: Remove NUMAQ").
>
> Per the Intel SDM (Vol. 3A, Table 10-1, note 2) and AMD APM, the INIT
> IPI used to wake secondary CPUs preserves cache contents and leaves
> CR0.CD and CR0.NW unchanged (0, as already enabled by firmware or the
> kernel). Neither sev_es_trampoline_start nor trampoline_start64
> executes WBINVD.
>
> On modern multi-core processors with large shared last-level caches
> (such as 504 MB shared L3 per socket on Intel Xeon 6985P-C), WBINVD is
> not harmless: each execution flushes and invalidates the entire
> coherence domain's caches and takes ~85-115 ms per CPU while disturbing
> active workloads on online CPUs.
>
> Remove the obsolete WBINVD from trampoline_start.
>
> 2-socket, 480-CPU Intel Xeon 6985P-C (N=479, mean of 10 runs):
> Offline: 40.40 s -> 40.46 s
> Online: 65.83 s -> 11.14 s (-83.1%)
> Total: 106.23 s -> 51.60 s (-51.4%)
>
> Signed-off-by: Pasha Tatashin <pasha.tatashin@soleen.com>
Sashiko has reviewed this patch and found no issues. It looks great!
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261015153331.1872641-1-pasha.tatashin@soleen.com?part=1
^ permalink raw reply [flat|nested] 21+ messages in thread
* Re: [PATCH v1 5/8] sched/topology: Avoid redundant O(N^3) group capacity walks and unused allocations in build_sched_domains()
2026-10-15 15:33 ` [PATCH v1 5/8] sched/topology: Avoid redundant O(N^3) group capacity walks and unused allocations in build_sched_domains() Pasha Tatashin
@ 2026-10-15 15:43 ` sashiko-bot
0 siblings, 0 replies; 21+ messages in thread
From: sashiko-bot @ 2026-10-15 15:43 UTC (permalink / raw)
To: Pasha Tatashin, kees, rppt, juri.lelli, nik.borisov, tglx,
vschneid, kprateek.nayak, vincent.guittot, bp, thorsten.blum,
tony.luck, david, mingo, akpm, seanjc, ljs, zhangguopeng,
dietmar.eggemann, mhocko, vinicius.gomes, tj, vbabka, peterz,
surenb, hpa, x86, dmatlack, tim.c.chen, kexec, pratyush,
dave.hansen, rostedt, gmonaco, jgross, mgorman, graf, liam,
ridong.chen, longman, bsegall
> During sequential CPU hotplug on large systems, build_sched_domains() is
> invoked for every CPU offline and online step. Two bottlenecks in the
> domain construction path scale poorly with CPU count:
>
> 1. On non-overlapping topology levels, all CPUs sharing a domain span
> share the same circular ring of sched_group instances, and on
> overlapping NUMA levels, groups with identical spans share
> sched_group_capacity (sgc). However, init_sched_groups_capacity()
> walks the entire group ring for every CPU in the domain and repeatedly
> recomputes group_weight, cores (walking the group cpumask and clearing
> cpu_smt_mask), and asym_prefer_cpu (O(N^3) bitmap operations per
> hotplug step). Similarly, init_overlap_sched_group() recomputes
> build_balance_mask() and cpumask_weight(sg_span) even when the shared
> sgc was already initialized.
>
> [ ... ]
> Total: 7.66 s -> 5.29 s (-30.9%)
> build_sched_domains() (cpu511 offline / online):
> 11.69 ms -> 5.61 ms (-52.0%) / 12.24 ms -> 5.84 ms (-52.3%)
>
> Signed-off-by: Pasha Tatashin <pasha.tatashin@soleen.com>
Sashiko has reviewed this patch and found no issues. It looks great!
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261015153331.1872641-1-pasha.tatashin@soleen.com?part=5
^ permalink raw reply [flat|nested] 21+ messages in thread
* Re: [PATCH v1 2/8] x86/smp: Remove unnecessary WBINVD from mwait_play_dead()
2026-10-15 15:33 ` [PATCH v1 2/8] x86/smp: Remove unnecessary WBINVD from mwait_play_dead() Pasha Tatashin
@ 2026-10-15 15:44 ` sashiko-bot
2026-10-15 16:26 ` Pasha Tatashin
0 siblings, 1 reply; 21+ messages in thread
From: sashiko-bot @ 2026-10-15 15:44 UTC (permalink / raw)
To: Pasha Tatashin, kees, rppt, juri.lelli, nik.borisov, tglx,
vschneid, kprateek.nayak, vincent.guittot, bp, thorsten.blum,
tony.luck, david, mingo, akpm, seanjc, ljs, zhangguopeng,
dietmar.eggemann, mhocko, vinicius.gomes, tj, vbabka, peterz,
surenb, hpa, x86, dmatlack, tim.c.chen, kexec, pratyush,
dave.hansen, rostedt, gmonaco, jgross, mgorman, graf, liam,
ridong.chen, longman, bsegall
Cc: hannes, mkoutny
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Critical] x86/smp: memory corruption during suspend if package offlined at runtime
- [High] x86/smp: offline CPUs miss wbinvd_on_all_cpus() flush causing data corruption
--
commit 2579aa1d01a44c8d96bb166fd2ce0dff4493a42a
Author: Pasha Tatashin <pasha.tatashin@soleen.com>
x86/smp: Remove unnecessary WBINVD from mwait_play_dead()
The commit aims to avoid time-consuming WBINVDs during runtime CPU offlining
by only executing it when cpuhp_tasks_frozen is set (i.e. system sleep).
> diff --git a/arch/x86/kernel/smpboot.c b/arch/x86/kernel/smpboot.c
> index ba01a9e919b78..60a781dac9991 100644
> --- a/arch/x86/kernel/smpboot.c
> +++ b/arch/x86/kernel/smpboot.c
[ ... ]
> @@ -1426,15 +1422,21 @@ void __noreturn mwait_play_dead(unsigned int eax_hint)
> md->status = CPUDEAD_MWAIT_WAIT;
> md->control = CPUDEAD_MWAIT_WAIT;
>
> - wbinvd();
> + /*
> + * MWAIT C-states keep the caches coherent, so runtime CPU offline
> + * does not need to flush them. System sleep, however, may remove
> + * power from this package's caches while all of its CPUs are
> + * offline, and the flush done by the boot CPU before entering the
> + * sleep state only covers the boot CPU's own package.
> + */
> + if (cpuhp_tasks_frozen)
> + wbinvd();
[Severity: Critical]
Could this result in memory corruption during a system suspend if an entire
package is offlined at runtime prior to the suspend?
If a package is fully offlined at runtime (so cpuhp_tasks_frozen is false),
its CPUs enter MWAIT with dirty caches. If the system later suspends,
freeze_secondary_cpus() skips CPUs that are already offline:
kernel/cpu.c:freeze_secondary_cpus() {
...
for (cpu = nr_cpu_ids - 1; cpu >= 0; cpu--) {
if (!cpu_online(cpu) || cpu == primary)
continue;
...
}
Because these CPUs are skipped, they never awaken to check cpuhp_tasks_frozen
and perform the flush. Since the sleep-entry cache flush on the boot CPU
only covers its own package, won't the dirty cache lines of the un-flushed,
offlined package be permanently lost when power is removed?
[Severity: High]
Does skipping the flush during runtime offline break the system-wide cache
flush guarantees of wbinvd_on_all_cpus()?
If a package is completely offlined at runtime and retains dirty cache lines
in its LLC, wbinvd_on_all_cpus() will miss them entirely because it only
targets online CPUs:
arch/x86/lib/cache-smp.c:wbinvd_on_all_cpus() {
on_each_cpu(__wbinvd, NULL, 1);
}
If a system-wide non-coherent operation is later triggered (such as mapping
memory as Uncacheable or persisting PMEM), the missing flush could lead to
data corruption or Machine Check Exceptions from dirty line evictions.
Furthermore, during Kexec, offline CPUs are kicked out of MWAIT:
arch/x86/kernel/smpboot.c:smp_kick_mwait_play_dead() {
...
for_each_cpu_andnot(cpu, cpu_present_mask, cpu_online_mask) {
...
WRITE_ONCE(md->control, newstate);
...
}
Will this cause the offlined CPUs to carry unflushed dirty cache lines into
the new kernel's execution context?
>
> while (1) {
> /*
[ ... ]
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261015153331.1872641-1-pasha.tatashin@soleen.com?part=2
^ permalink raw reply [flat|nested] 21+ messages in thread
* Re: [PATCH v1 4/8] x86/cacheinfo, mtrr: Skip stop_machine rendezvous in cache_ap_online() when MTRRs match
2026-10-15 15:33 ` [PATCH v1 4/8] x86/cacheinfo, mtrr: Skip stop_machine rendezvous in cache_ap_online() when MTRRs match Pasha Tatashin
@ 2026-10-15 15:45 ` sashiko-bot
0 siblings, 0 replies; 21+ messages in thread
From: sashiko-bot @ 2026-10-15 15:45 UTC (permalink / raw)
To: Pasha Tatashin, kees, rppt, juri.lelli, nik.borisov, tglx,
vschneid, kprateek.nayak, vincent.guittot, bp, thorsten.blum,
tony.luck, david, mingo, akpm, seanjc, ljs, zhangguopeng,
dietmar.eggemann, mhocko, vinicius.gomes, tj, vbabka, peterz,
surenb, hpa, x86, dmatlack, tim.c.chen, kexec, pratyush,
dave.hansen, rostedt, gmonaco, jgross, mgorman, graf, liam,
ridong.chen, longman, bsegall
> When a single CPU is brought online at runtime
> (cache_aps_delayed_init == false), cache_ap_online() unconditionally
> calls stop_machine_from_inactive_cpu() to rendezvous all online CPUs
> while the incoming AP runs cache_cpu_init() to disable caches
> (CR0.CD = 1, plus wbinvd() when X86_FEATURE_SELFSNOOP is not set),
> reprogram MTRRs via mtrr_generic_set_state(), and re-enable caches.
>
> During initial boot (cache_aps_init()) and across warm CPU offline/online
> cycles, the AP's hardware MTRR MSRs have already been programmed to match
> the saved mtrr_state. Stopping all online CPUs in stop_machine (and
> flushing caches on the AP) when the AP's MTRRs already match mtrr_state
> is unnecessary.
>
> Add mtrr_generic_state_matches() to compare the local CPU's
> MSR_MTRRdefType, variable MTRR ranges, and fixed MTRR ranges against
> [ ... ]
> Total: 8.45 s -> 7.66 s (-9.3%)
> cache_ap_online() (cpu511 online):
> 1,354.6 us -> 6.4 us (-99.53%)
>
> Signed-off-by: Pasha Tatashin <pasha.tatashin@soleen.com>
Sashiko has reviewed this patch and found no issues. It looks great!
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261015153331.1872641-1-pasha.tatashin@soleen.com?part=4
^ permalink raw reply [flat|nested] 21+ messages in thread
* Re: [PATCH v1 8/8] mm/vmstat: Avoid redundant per-CPU stat_threshold writes in refresh_zone_stat_thresholds()
2026-10-15 15:33 ` [PATCH v1 8/8] mm/vmstat: Avoid redundant per-CPU stat_threshold writes in refresh_zone_stat_thresholds() Pasha Tatashin
@ 2026-10-15 15:49 ` sashiko-bot
0 siblings, 0 replies; 21+ messages in thread
From: sashiko-bot @ 2026-10-15 15:49 UTC (permalink / raw)
To: Pasha Tatashin, kees, rppt, juri.lelli, nik.borisov, tglx,
vschneid, kprateek.nayak, vincent.guittot, bp, thorsten.blum,
tony.luck, david, mingo, akpm, seanjc, ljs, zhangguopeng,
dietmar.eggemann, mhocko, vinicius.gomes, tj, vbabka, peterz,
surenb, hpa, x86, dmatlack, tim.c.chen, kexec, pratyush,
dave.hansen, rostedt, gmonaco, jgross, mgorman, graf, liam,
ridong.chen, longman, bsegall
> During CPU hotplug, vmstat_cpu_online() and vmstat_cpu_dead() invoke
> refresh_zone_stat_thresholds() on every CPU transition. Currently,
> refresh_zone_stat_thresholds() first zeroes
> per_cpu_nodestats->stat_threshold across all online CPUs and all online
> nodes, and then unconditionally writes per_cpu_zonestats->stat_threshold
> and per_cpu_nodestats->stat_threshold inside the zone loop for every
> populated zone and every online CPU.
>
> However, calculate_normal_threshold(zone) only changes when
> fls(num_online_cpus()) changes (at powers of 2) and is clamped at 125
> for large normal zones (>= 16 GB) once num_online_cpus() >= 64.
> Unconditionally zeroing and rewriting every CPU's per-CPU stat_threshold
> on every hotplug step repeatedly dirties remote per-CPU cachelines with
> identical values (and briefly exposes stat_threshold == 0 on live CPUs).
>
> [ ... ]
> Total: 3.46 s -> 3.38 s (-2.3%)
> refresh_zone_stat_thresholds() (cpu511 offline / online):
> 104.6 us -> 50.7 us (-51.6%) / 109.3 us -> 53.5 us (-51.1%)
>
> Signed-off-by: Pasha Tatashin <pasha.tatashin@soleen.com>
Sashiko has reviewed this patch and found no issues. It looks great!
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261015153331.1872641-1-pasha.tatashin@soleen.com?part=8
^ permalink raw reply [flat|nested] 21+ messages in thread
* Re: [PATCH v1 7/8] sched/topology: Avoid redundant percpu allocations and bitmap walks in build_sched_domains()
2026-10-15 15:33 ` [PATCH v1 7/8] sched/topology: Avoid redundant percpu allocations and bitmap walks in build_sched_domains() Pasha Tatashin
@ 2026-10-15 15:51 ` sashiko-bot
2026-10-15 23:17 ` Tim Chen
1 sibling, 0 replies; 21+ messages in thread
From: sashiko-bot @ 2026-10-15 15:51 UTC (permalink / raw)
To: Pasha Tatashin, kees, rppt, juri.lelli, nik.borisov, tglx,
vschneid, kprateek.nayak, vincent.guittot, bp, thorsten.blum,
tony.luck, david, mingo, akpm, seanjc, ljs, zhangguopeng,
dietmar.eggemann, mhocko, vinicius.gomes, tj, vbabka, peterz,
surenb, hpa, x86, dmatlack, tim.c.chen, kexec, pratyush,
dave.hansen, rostedt, gmonaco, jgross, mgorman, graf, liam,
ridong.chen, longman, bsegall
> During CPU hotplug, build_sched_domains() is invoked on every CPU
> activation and deactivation. Profiling build_sched_domains() on a
> 480-CPU Intel Xeon 6985P-C and a 512-CPU AMD EPYC 9B45 shows several
> sources of avoidable O(N^2) and O(N) work per step:
>
> 1. build_sched_groups() and build_overlap_sched_groups() iterate over
> every bit of sched_domain_span(sd) via for_each_cpu_wrap(i, span, cpu)
> and check cpumask_test_cpu(i, covered) one CPU at a time, even after
> entire child groups (e.g., 120-CPU NUMA nodes) have been added to
> covered. Instead, track the remaining uncovered CPUs
> (cpumask_copy(uncovered, span)), clear each built group's span with
> cpumask_andnot(), and advance directly to the next uncovered CPU.
>
> 2. __visit_domain_allocation_hell() and __free_domain_allocs() allocate
> and free 3 * nr_levels + 2 temporary per-CPU pointer arrays via
> [ ... ]
> Total: 4.30 s -> 3.46 s (-19.5%)
> build_sched_domains() (cpu511 offline / online):
> 6.55 ms -> 3.04 ms (-53.7%) / 5.28 ms -> 3.20 ms (-39.4%)
>
> Signed-off-by: Pasha Tatashin <pasha.tatashin@soleen.com>
Sashiko has reviewed this patch and found no issues. It looks great!
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261015153331.1872641-1-pasha.tatashin@soleen.com?part=7
^ permalink raw reply [flat|nested] 21+ messages in thread
* Re: [PATCH v1 3/8] x86/mce/intel: Bound cmci_rediscover() to a single topology peer
2026-10-15 15:33 ` [PATCH v1 3/8] x86/mce/intel: Bound cmci_rediscover() to a single topology peer Pasha Tatashin
@ 2026-10-15 15:51 ` sashiko-bot
0 siblings, 0 replies; 21+ messages in thread
From: sashiko-bot @ 2026-10-15 15:51 UTC (permalink / raw)
To: Pasha Tatashin, kees, rppt, juri.lelli, nik.borisov, tglx,
vschneid, kprateek.nayak, vincent.guittot, bp, thorsten.blum,
tony.luck, david, mingo, akpm, seanjc, ljs, zhangguopeng,
dietmar.eggemann, mhocko, vinicius.gomes, tj, vbabka, peterz,
surenb, hpa, x86, dmatlack, tim.c.chen, kexec, pratyush,
dave.hansen, rostedt, gmonaco, jgross, mgorman, graf, liam,
ridong.chen, longman, bsegall
> When a CPU goes offline, cmci_clear() disables CMCI on any MCA banks
> owned by that CPU, and mce_cpu_dead() calls cmci_rediscover(). Currently
> cmci_rediscover() invokes on_each_cpu(cmci_rediscover_work_func, NULL, 1),
> sending a synchronous broadcast IPI to every online CPU in the system,
> all of which contend on the global cmci_discover_lock while reading up
> to 32 MSR_IA32_MCx_CTL2 MSRs.
>
> Any bank disabled by cmci_clear() is either thread-private, core-scoped
> (shared between SMT threads of the same physical core), L2-cluster-scoped,
> die-scoped, or package-scoped:
> 1. Any bank index that is already owned by the target peer in its own
> mce_banks_owned bitmap is private to a narrower scope than the shared
> level and will be skipped by cmci_skip_bank() on the target anyway.
> 2. Core- or cluster-shared MCA banks can only be claimed by an online
> SMT sibling or L2 cluster peer.
> [ ... ]
> Total: 22.50 s -> 20.74 s (-7.8%)
> cmci_rediscover() (cpu479 offline):
> 7,720.6 us -> 0.6 us (-99.99%)
>
> Signed-off-by: Pasha Tatashin <pasha.tatashin@soleen.com>
Sashiko has reviewed this patch and found no issues. It looks great!
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261015153331.1872641-1-pasha.tatashin@soleen.com?part=3
^ permalink raw reply [flat|nested] 21+ messages in thread
* Re: [PATCH v1 6/8] sched/deadline, topology: Avoid O(N^2) dl_server bandwidth walks during root_domain rebuilds
2026-10-15 15:33 ` [PATCH v1 6/8] sched/deadline, topology: Avoid O(N^2) dl_server bandwidth walks during root_domain rebuilds Pasha Tatashin
@ 2026-10-15 15:57 ` sashiko-bot
2026-10-15 20:43 ` Pasha Tatashin
0 siblings, 1 reply; 21+ messages in thread
From: sashiko-bot @ 2026-10-15 15:57 UTC (permalink / raw)
To: Pasha Tatashin, kees, rppt, juri.lelli, nik.borisov, tglx,
vschneid, kprateek.nayak, vincent.guittot, bp, thorsten.blum,
tony.luck, david, mingo, akpm, seanjc, ljs, zhangguopeng,
dietmar.eggemann, mhocko, vinicius.gomes, tj, vbabka, peterz,
surenb, hpa, x86, dmatlack, tim.c.chen, kexec, pratyush,
dave.hansen, rostedt, gmonaco, jgross, mgorman, graf, liam,
ridong.chen, longman, bsegall
Cc: hannes, mkoutny
Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [High] sched/deadline: Concurrent sched_setattr() during dl_rebuild_rd_accounting() corrupts bandwidth (!CONFIG_CPUSETS)
- [Medium] sched/deadline: root domain bandwidth leak for DEAD tasks on offline CPUs
- [Medium] sched/deadline: lockless u64 write to extra_bw causes data race and torn reads
--
commit 2fcbfb40fdf44ef2a7ddc6709657336b0c7fb178
Author: Pasha Tatashin <pasha.tatashin@soleen.com>
sched/deadline, topology: Avoid O(N^2) dl_server bandwidth walks during root_domain rebuilds
This commit reduces the O(N^2) complexity of dl_server bandwidth walks during
root_domain rebuilds by accumulating the total bandwidth in a single pass. It
also introduces a !CONFIG_CPUSETS implementation of dl_rebuild_rd_accounting()
to fix an issue where SCHED_DEADLINE task bandwidth was not accounted for on
rebuilds.
> diff --git a/kernel/sched/deadline.c b/kernel/sched/deadline.c
> index 857dbe3519a86..01f2f24e3b814 100644
> --- a/kernel/sched/deadline.c
> +++ b/kernel/sched/deadline.c
[ ... ]
> @@ -3480,26 +3466,32 @@ static u64 dl_server_read_bw(int cpu)
> return dl_bw;
> }
>
> -void dl_clear_root_domain(struct root_domain *rd)
> +static void dl_clear_root_domain(struct root_domain *rd)
> {
> - int i;
> + u64 total_bw = 0;
> + s64 extra_bw = 0;
> + int i, cpus;
>
> guard(raw_spinlock_irqsave)(&rd->dl_bw.lock);
>
[ ... ]
> + rd->dl_bw.total_bw = total_bw;
> + for_each_cpu(i, rd->span) {
> + struct rq *rq = cpu_rq(i);
> +
> + rq->dl.extra_bw = rq->dl.max_bw;
> + if (cpu_active(i))
> + rq->dl.extra_bw += extra_bw;
[Severity: Medium]
Can this lockless u64 write to extra_bw cause a data race and torn reads with
grub_reclaim()?
This updates rq->dl.extra_bw remotely holding only rd->dl_bw.lock. Concurrently,
grub_reclaim() reads rq->dl.extra_bw while holding only the local rq->lock.
Because extra_bw is a u64 and can be a negative delta, 32-bit architectures
process the write as two 32-bit stores. A torn read by grub_reclaim() could
observe mismatched upper and lower halves, producing an incorrect bandwidth
value that miscalculates DL task runtime.
Additionally, splitting the update into two distinct non-atomic assignments
widens the race window.
> + }
> }
>
> void dl_clear_root_domain_cpu(int cpu)
[ ... ]
> @@ -3507,6 +3499,33 @@ void dl_clear_root_domain_cpu(int cpu)
> dl_clear_root_domain(cpu_rq(cpu)->rd);
> }
>
> +#ifndef CONFIG_CPUSETS
> +void dl_rebuild_rd_accounting(void)
> +{
> + struct task_struct *g, *p;
> + u64 cookie = ++dl_cookie;
> + int cpu;
> +
> + lockdep_assert_cpus_held();
> + lockdep_assert_held(&sched_domains_mutex);
> +
> + rcu_read_lock();
> + for_each_possible_cpu(cpu) {
> + if (dl_bw_visited(cpu, cookie))
> + continue;
> +
> + dl_clear_root_domain_cpu(cpu);
> + }
> + for_each_process_thread(g, p) {
> + /* Racy pre-check; dl_add_task_root_domain() rechecks under pi_lock. */
> + if (!dl_prio(READ_ONCE(p->prio)))
> + continue;
> + dl_add_task_root_domain(p);
[Severity: High]
Does this allow a race with concurrent __sched_setscheduler()?
In a kernel without CONFIG_CPUSETS, cpuset_lock() is an empty no-op stub,
leaving __sched_setscheduler() completely unsynchronized against this loop.
If sched_setattr() transitions a task to SCHED_DEADLINE concurrently,
__sched_setscheduler() calls sched_dl_overflow() which acquires dl_b->lock
and adds the task's bandwidth. Then this iteration loop could observe the new
priority, call dl_add_task_root_domain(), and add the bandwidth a second time,
corrupting rd->dl_bw.total_bw.
Similarly, a task transitioning from SCHED_DEADLINE to SCHED_OTHER could result
in a bandwidth underflow because the rebuild clears the bandwidth, skips the
task, and later inactive_task_timer() subtracts the bandwidth.
[Severity: Medium]
Does this leak SCHED_DEADLINE bandwidth in the active root domain for DEAD tasks
on offline CPUs?
This loop blindly iterates over all tasks, including TASK_DEAD zombies, without
checking the task state. If a SCHED_DEADLINE task exits and enters TASK_DEAD
state, its bandwidth is preserved until its 0-lag inactive timer fires.
If its original CPU is hotplugged offline before the timer fires, CPU hotplug
triggers this domain rebuild. Because the task's original CPU is offline,
dl_add_task_root_domain() picks a new active CPU and adds the task's bandwidth
to the new active root domain.
When the 0-lag timer eventually fires, inactive_task_timer() subtracts the
bandwidth from task_cpu(p)'s root domain (the def_root_domain, since it was
never migrated), permanently leaking the bandwidth in the active root domain.
> + }
> + rcu_read_unlock();
> +}
> +#endif
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261015153331.1872641-1-pasha.tatashin@soleen.com?part=6
^ permalink raw reply [flat|nested] 21+ messages in thread
* Re: [PATCH v1 2/8] x86/smp: Remove unnecessary WBINVD from mwait_play_dead()
2026-10-15 15:44 ` sashiko-bot
@ 2026-10-15 16:26 ` Pasha Tatashin
0 siblings, 0 replies; 21+ messages in thread
From: Pasha Tatashin @ 2026-10-15 16:26 UTC (permalink / raw)
To: sashiko-reviews
Cc: kees, rppt, juri.lelli, nik.borisov, tglx, vschneid,
kprateek.nayak, vincent.guittot, bp, thorsten.blum, tony.luck,
david, mingo, akpm, seanjc, ljs, zhangguopeng, dietmar.eggemann,
mhocko, vinicius.gomes, tj, vbabka, peterz, surenb, hpa, x86,
dmatlack, tim.c.chen, kexec, pratyush, dave.hansen, rostedt,
gmonaco, jgross, mgorman, graf, liam, ridong.chen, longman,
bsegall, hannes, mkoutny
On Thu, Oct 15, 2026 at 11:44 AM <sashiko-bot@kernel.org> wrote:
>
> Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
> - [Critical] x86/smp: memory corruption during suspend if package offlined at runtime
> - [High] x86/smp: offline CPUs miss wbinvd_on_all_cpus() flush causing data corruption
> --
>
> commit 2579aa1d01a44c8d96bb166fd2ce0dff4493a42a
> Author: Pasha Tatashin <pasha.tatashin@soleen.com>
>
> x86/smp: Remove unnecessary WBINVD from mwait_play_dead()
>
> The commit aims to avoid time-consuming WBINVDs during runtime CPU offlining
> by only executing it when cpuhp_tasks_frozen is set (i.e. system sleep).
>
> > diff --git a/arch/x86/kernel/smpboot.c b/arch/x86/kernel/smpboot.c
> > index ba01a9e919b78..60a781dac9991 100644
> > --- a/arch/x86/kernel/smpboot.c
> > +++ b/arch/x86/kernel/smpboot.c
> [ ... ]
> > @@ -1426,15 +1422,21 @@ void __noreturn mwait_play_dead(unsigned int eax_hint)
> > md->status = CPUDEAD_MWAIT_WAIT;
> > md->control = CPUDEAD_MWAIT_WAIT;
> >
> > - wbinvd();
> > + /*
> > + * MWAIT C-states keep the caches coherent, so runtime CPU offline
> > + * does not need to flush them. System sleep, however, may remove
> > + * power from this package's caches while all of its CPUs are
> > + * offline, and the flush done by the boot CPU before entering the
> > + * sleep state only covers the boot CPU's own package.
> > + */
> > + if (cpuhp_tasks_frozen)
> > + wbinvd();
>
> [Severity: Critical]
> Could this result in memory corruption during a system suspend if an entire
> package is offlined at runtime prior to the suspend?
>
> If a package is fully offlined at runtime (so cpuhp_tasks_frozen is false),
> its CPUs enter MWAIT with dirty caches. If the system later suspends,
> freeze_secondary_cpus() skips CPUs that are already offline:
>
> kernel/cpu.c:freeze_secondary_cpus() {
> ...
> for (cpu = nr_cpu_ids - 1; cpu >= 0; cpu--) {
> if (!cpu_online(cpu) || cpu == primary)
> continue;
> ...
> }
>
> Because these CPUs are skipped, they never awaken to check cpuhp_tasks_frozen
> and perform the flush. Since the sleep-entry cache flush on the boot CPU
> only covers its own package, won't the dirty cache lines of the un-flushed,
> offlined package be permanently lost when power is removed?
I am not sure if this is actually feasible; nevertheless, I can add
wbinvd() when the the last CPU in the package (CPU that shares the
last level cache), enters mwait_play_dead(). That said, also my LLM
tells me that even:
> > + if (cpuhp_tasks_frozen)
> > + wbinvd();
that I have is not needed because:
1. Multi-socket x86 servers (where a non-boot package exists) do not
support ACPI S3 suspend-to-RAM.
2. When all cores of a package enter deep MWAIT (C6), hardware flushes
L1/L2 to the LLC on core C6 entry, and the package's power management
unit (PCU) flushes the LLC before removing LLC power in deep package
C-states (PC6+).
>
> [Severity: High]
> Does skipping the flush during runtime offline break the system-wide cache
> flush guarantees of wbinvd_on_all_cpus()?
>
> If a package is completely offlined at runtime and retains dirty cache lines
> in its LLC, wbinvd_on_all_cpus() will miss them entirely because it only
> targets online CPUs:
>
> arch/x86/lib/cache-smp.c:wbinvd_on_all_cpus() {
> on_each_cpu(__wbinvd, NULL, 1);
> }
>
> If a system-wide non-coherent operation is later triggered (such as mapping
> memory as Uncacheable or persisting PMEM), the missing flush could lead to
> data corruption or Machine Check Exceptions from dirty line evictions.
>
> Furthermore, during Kexec, offline CPUs are kicked out of MWAIT:
>
> arch/x86/kernel/smpboot.c:smp_kick_mwait_play_dead() {
> ...
> for_each_cpu_andnot(cpu, cpu_present_mask, cpu_online_mask) {
> ...
> WRITE_ONCE(md->control, newstate);
> ...
> }
>
> Will this cause the offlined CPUs to carry unflushed dirty cache lines into
> the new kernel's execution context?
Regarding carrying cachelines through kexec that Sashiko mentioned, my
LLM also says it does not make sense (and I agree with it):
1. Even before our patch, when smp_kick_mwait_play_dead() wakes an
offline CPU out of __mwait(), that CPU immediately writes to memory
(WRITE_ONCE(md->status, CPUDEAD_MWAIT_KEXEC_HLT)—dirtying its cache!)
and enters while (1) native_halt(); (smpboot.c:1461-1463) without
executing wbinvd().
2. Even more obviously, all CPUs that are online when kexec runs are
stopped via native_stop_other_cpus() → stop_this_cpu(), which simply
executes native_halt() in a loop without wbinvd().
Pasha
>
> >
> > while (1) {
> > /*
> [ ... ]
>
> --
> Sashiko AI review · https://sashiko.dev/#/patchset/20261015153331.1872641-1-pasha.tatashin@soleen.com?part=2
^ permalink raw reply [flat|nested] 21+ messages in thread
* Re: [PATCH v1 6/8] sched/deadline, topology: Avoid O(N^2) dl_server bandwidth walks during root_domain rebuilds
2026-10-15 15:57 ` sashiko-bot
@ 2026-10-15 20:43 ` Pasha Tatashin
0 siblings, 0 replies; 21+ messages in thread
From: Pasha Tatashin @ 2026-10-15 20:43 UTC (permalink / raw)
To: sashiko-bot
Cc: Pasha Tatashin, kees, rppt, juri.lelli, nik.borisov, tglx,
vschneid, kprateek.nayak, vincent.guittot, bp, thorsten.blum,
tony.luck, david, mingo, akpm, seanjc, ljs, zhangguopeng,
dietmar.eggemann, mhocko, vinicius.gomes, tj, vbabka, peterz,
surenb, hpa, x86, dmatlack, tim.c.chen, kexec, pratyush,
dave.hansen, rostedt, gmonaco, jgross, mgorman, graf, liam,
ridong.chen, longman, bsegall, hannes, mkoutny
On 10-15 15:57, sashiko-bot@kernel.org wrote:
> Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
> - [High] sched/deadline: Concurrent sched_setattr() during dl_rebuild_rd_accounting() corrupts bandwidth (!CONFIG_CPUSETS)
> - [Medium] sched/deadline: root domain bandwidth leak for DEAD tasks on offline CPUs
> - [Medium] sched/deadline: lockless u64 write to extra_bw causes data race and torn reads
> --
>
> commit 2fcbfb40fdf44ef2a7ddc6709657336b0c7fb178
> Author: Pasha Tatashin <pasha.tatashin@soleen.com>
>
> sched/deadline, topology: Avoid O(N^2) dl_server bandwidth walks during root_domain rebuilds
>
> This commit reduces the O(N^2) complexity of dl_server bandwidth walks during
> root_domain rebuilds by accumulating the total bandwidth in a single pass. It
> also introduces a !CONFIG_CPUSETS implementation of dl_rebuild_rd_accounting()
> to fix an issue where SCHED_DEADLINE task bandwidth was not accounted for on
> rebuilds.
>
> > diff --git a/kernel/sched/deadline.c b/kernel/sched/deadline.c
> > index 857dbe3519a86..01f2f24e3b814 100644
> > --- a/kernel/sched/deadline.c
> > +++ b/kernel/sched/deadline.c
> [ ... ]
> > @@ -3480,26 +3466,32 @@ static u64 dl_server_read_bw(int cpu)
> > return dl_bw;
> > }
> >
> > -void dl_clear_root_domain(struct root_domain *rd)
> > +static void dl_clear_root_domain(struct root_domain *rd)
> > {
> > - int i;
> > + u64 total_bw = 0;
> > + s64 extra_bw = 0;
> > + int i, cpus;
> >
> > guard(raw_spinlock_irqsave)(&rd->dl_bw.lock);
> >
> [ ... ]
> > + rd->dl_bw.total_bw = total_bw;
> > + for_each_cpu(i, rd->span) {
> > + struct rq *rq = cpu_rq(i);
> > +
> > + rq->dl.extra_bw = rq->dl.max_bw;
> > + if (cpu_active(i))
> > + rq->dl.extra_bw += extra_bw;
>
> [Severity: Medium]
> Can this lockless u64 write to extra_bw cause a data race and torn reads with
> grub_reclaim()?
>
> This updates rq->dl.extra_bw remotely holding only rd->dl_bw.lock. Concurrently,
> grub_reclaim() reads rq->dl.extra_bw while holding only the local rq->lock.
>
> Because extra_bw is a u64 and can be a negative delta, 32-bit architectures
> process the write as two 32-bit stores. A torn read by grub_reclaim() could
> observe mismatched upper and lower halves, producing an incorrect bandwidth
> value that miscalculates DL task runtime.
>
> Additionally, splitting the update into two distinct non-atomic assignments
> widens the race window.
>
> > + }
> > }
> >
> > void dl_clear_root_domain_cpu(int cpu)
> [ ... ]
> > @@ -3507,6 +3499,33 @@ void dl_clear_root_domain_cpu(int cpu)
> > dl_clear_root_domain(cpu_rq(cpu)->rd);
> > }
> >
> > +#ifndef CONFIG_CPUSETS
> > +void dl_rebuild_rd_accounting(void)
> > +{
> > + struct task_struct *g, *p;
> > + u64 cookie = ++dl_cookie;
> > + int cpu;
> > +
> > + lockdep_assert_cpus_held();
> > + lockdep_assert_held(&sched_domains_mutex);
> > +
> > + rcu_read_lock();
> > + for_each_possible_cpu(cpu) {
> > + if (dl_bw_visited(cpu, cookie))
> > + continue;
> > +
> > + dl_clear_root_domain_cpu(cpu);
> > + }
> > + for_each_process_thread(g, p) {
> > + /* Racy pre-check; dl_add_task_root_domain() rechecks under pi_lock. */
> > + if (!dl_prio(READ_ONCE(p->prio)))
> > + continue;
> > + dl_add_task_root_domain(p);
>
> [Severity: High]
> Does this allow a race with concurrent __sched_setscheduler()?
>
> In a kernel without CONFIG_CPUSETS, cpuset_lock() is an empty no-op stub,
> leaving __sched_setscheduler() completely unsynchronized against this loop.
>
> If sched_setattr() transitions a task to SCHED_DEADLINE concurrently,
> __sched_setscheduler() calls sched_dl_overflow() which acquires dl_b->lock
> and adds the task's bandwidth. Then this iteration loop could observe the new
> priority, call dl_add_task_root_domain(), and add the bandwidth a second time,
> corrupting rd->dl_bw.total_bw.
>
> Similarly, a task transitioning from SCHED_DEADLINE to SCHED_OTHER could result
> in a bandwidth underflow because the rebuild clears the bandwidth, skips the
> task, and later inactive_task_timer() subtracts the bandwidth.
>
> [Severity: Medium]
> Does this leak SCHED_DEADLINE bandwidth in the active root domain for DEAD tasks
> on offline CPUs?
>
> This loop blindly iterates over all tasks, including TASK_DEAD zombies, without
> checking the task state. If a SCHED_DEADLINE task exits and enters TASK_DEAD
> state, its bandwidth is preserved until its 0-lag inactive timer fires.
Both bugs, are real, and I am going to address them in the next itiration, after
waiting for feedback on the series.
^ permalink raw reply [flat|nested] 21+ messages in thread
* Re: [PATCH v1 0/8] x86, sched, mm: Speed up CPU hotplug on large systems
2026-10-15 15:33 [PATCH v1 0/8] x86, sched, mm: Speed up CPU hotplug on large systems Pasha Tatashin
` (7 preceding siblings ...)
2026-10-15 15:33 ` [PATCH v1 8/8] mm/vmstat: Avoid redundant per-CPU stat_threshold writes in refresh_zone_stat_thresholds() Pasha Tatashin
@ 2026-10-15 21:33 ` Luck, Tony
8 siblings, 0 replies; 21+ messages in thread
From: Luck, Tony @ 2026-10-15 21:33 UTC (permalink / raw)
To: Pasha Tatashin
Cc: kees, rppt, juri.lelli, nik.borisov, tglx, vschneid,
kprateek.nayak, vincent.guittot, bp, thorsten.blum, cgroups,
david, mingo, akpm, linux-mm, seanjc, ljs, zhangguopeng,
dietmar.eggemann, mhocko, vinicius.gomes, tj, vbabka,
linux-kernel, peterz, surenb, hpa, x86, dmatlack, tim.c.chen,
kexec, pratyush, dave.hansen, rostedt, gmonaco, jgross, mgorman,
graf, linux-edac, liam, ridong.chen, longman, bsegall
On Thu, Oct 15, 2026 at 11:33:23AM -0400, Pasha Tatashin wrote:
> - Patch 3 bounds cmci_rediscover() on CPU teardown. Currently,
> cmci_rediscover() broadcasts an IPI via on_each_cpu() to all online
> CPUs, serializing them on cmci_discover_lock while reading up to 32
> MCA MSRs. Because any disabled MCA bank is shared at most with an
> SMT sibling, L2 cluster peer, or die/package peer, probing a single
> online topology peer via smp_call_function_single() (or returning
> immediately when no shared banks were orphaned) reduces 479-CPU
> offline time on Intel from 11.42 s to 9.63 s (-15.7%) and total time
> from 22.50 s to 20.74 s (-7.8%).
I took this series for a spin on a timy 2-socket Icelake (36 cores, 72
threads per socket). My test was to just offfline/online the 72 logical
CPUs on socket 1, and I saw some good speedups:
offline: 4.833s -> 3.17s
online: 6.157s -> 3.796s
I noticed that your cmci_rediscover() improvements mean even better
offline performance if you work from high to low CPU numbers as the
shared banks are assigned to the lowest numbered CPU within the sharing
topology domain. So working from high to low avoids creating as many
orphan banks that then need to find new owners.
reverse order offline: 2.984s
I haven't looked into the code yet. But the general direction seems
good.
-Tony
^ permalink raw reply [flat|nested] 21+ messages in thread
* Re: [PATCH v1 7/8] sched/topology: Avoid redundant percpu allocations and bitmap walks in build_sched_domains()
2026-10-15 15:33 ` [PATCH v1 7/8] sched/topology: Avoid redundant percpu allocations and bitmap walks in build_sched_domains() Pasha Tatashin
2026-10-15 15:51 ` sashiko-bot
@ 2026-10-15 23:17 ` Tim Chen
1 sibling, 0 replies; 21+ messages in thread
From: Tim Chen @ 2026-10-15 23:17 UTC (permalink / raw)
To: Pasha Tatashin, kees, rppt, juri.lelli, nik.borisov, tglx,
vschneid, kprateek.nayak, vincent.guittot, bp, thorsten.blum,
cgroups, tony.luck, david, mingo, akpm, linux-mm, seanjc, ljs,
zhangguopeng, dietmar.eggemann, mhocko, vinicius.gomes, tj,
vbabka, linux-kernel, peterz, surenb, hpa, x86, dmatlack, kexec,
pratyush, dave.hansen, rostedt, gmonaco, jgross, mgorman, graf,
linux-edac, liam, ridong.chen, longman, bsegall
On Thu, 2026-10-15 at 11:33 -0400, Pasha Tatashin wrote:
> During CPU hotplug, build_sched_domains() is invoked on every CPU
> activation and deactivation. Profiling build_sched_domains() on a
> 480-CPU Intel Xeon 6985P-C and a 512-CPU AMD EPYC 9B45 shows several
> sources of avoidable O(N^2) and O(N) work per step:
>
> 1. build_sched_groups() and build_overlap_sched_groups() iterate over
> every bit of sched_domain_span(sd) via for_each_cpu_wrap(i, span, cpu)
> and check cpumask_test_cpu(i, covered) one CPU at a time, even after
> entire child groups (e.g., 120-CPU NUMA nodes) have been added to
> covered. Instead, track the remaining uncovered CPUs
> (cpumask_copy(uncovered, span)), clear each built group's span with
> cpumask_andnot(), and advance directly to the next uncovered CPU.
>
> 2. __visit_domain_allocation_hell() and __free_domain_allocs() allocate
> and free 3 * nr_levels + 2 temporary per-CPU pointer arrays via
> alloc_percpu() / free_percpu() on every domain rebuild. Reuse the
> per-CPU pointer arrays across domain rebuilds under
> sched_domains_mutex, clearing entries in __free_domain_allocs() and
> only freeing the topology-level arrays when sched_domain_topology is
> replaced in sched_init_numa() / sched_reset_numa().
sched_domains_mutex does not protect the free:
[snip]
> #ifdef CONFIG_NUMA
> +static void __sdt_free_percpu(void)
> +{
> + struct sched_domain_topology_level *tl;
> +
> + for_each_sd_topology(tl) {
> + struct sd_data *sdd = &tl->data;
> +
sched_init_numa() and sched_reset_numa() are called from
sched_update_numa(), which runs from sched_cpu_activate() and
sched_cpu_deactivate() without sched_domains_mutex. The free is safe
because every domain builder holds at least cpus_read_lock(), and
sched_update_numa() runs under the hotplug write lock.
Please fix the changelog and add lockdep_assert_cpus_held()
in __sdt_free_percpu(), so a future caller cannot break this without a warning.
> + free_percpu(sdd->sd);
> + sdd->sd = NULL;
> + free_percpu(sdd->sg);
> + sdd->sg = NULL;
> + free_percpu(sdd->sgc);
> + sdd->sgc = NULL;
> + }
> +}
> +
> static int cpu_numa_flags(void)
> {
> return SD_NUMA;
> @@ -2449,6 +2483,7 @@ void sched_init_numa(int offline_node)
> /*
> * Copy the default topology bits..
> */
> + __sdt_free_percpu();
> for (i = 0; sched_domain_topology[i].mask; i++)
> tl[i] = sched_domain_topology[i];
>
> @@ -2507,6 +2542,7 @@ static void sched_reset_numa(void)
> kfree(masks);
> }
> if (sched_domain_topology_saved) {
> + __sdt_free_percpu();
> kfree(sched_domain_topology);
> sched_domain_topology = sched_domain_topology_saved;
> sched_domain_topology_saved = NULL;
> @@ -2708,17 +2744,23 @@ static int __sdt_alloc(const struct cpumask *cpu_map)
> for_each_sd_topology(tl) {
> struct sd_data *sdd = &tl->data;
>
> - sdd->sd = alloc_percpu(struct sched_domain *);
> - if (!sdd->sd)
> - return -ENOMEM;
> + if (!sdd->sd) {
> + sdd->sd = alloc_percpu(struct sched_domain *);
> + if (!sdd->sd)
> + return -ENOMEM;
> + }
>
> - sdd->sg = alloc_percpu(struct sched_group *);
> - if (!sdd->sg)
> - return -ENOMEM;
> + if (!sdd->sg) {
> + sdd->sg = alloc_percpu(struct sched_group *);
> + if (!sdd->sg)
> + return -ENOMEM;
> + }
>
> - sdd->sgc = alloc_percpu(struct sched_group_capacity *);
> - if (!sdd->sgc)
> - return -ENOMEM;
> + if (!sdd->sgc) {
> + sdd->sgc = alloc_percpu(struct sched_group_capacity *);
> + if (!sdd->sgc)
> + return -ENOMEM;
> + }
>
Keeping the arrays also means every slot must be NULL when a build
starts. Patch 5 depends on that: get_group(),
init_overlap_sched_group() and init_sched_domain_shared() treat a NULL
slot as "not allocated yet", and the overlap fast path treats
"sgc->ref > 0" as "set up in this build". An old pointer would pass
these checks and be used without a warning.
The rule holds today, but nothing says so. It also depends on builds
never filling a slot outside cpu_map, because the clearing loops only
visit cpu_map. Please write this down and check it, for example in
__sdt_alloc():
> for_each_cpu(j, cpu_map) {
> struct sched_domain *sd;
Maybe add
/*
* The slot arrays are kept between builds. Every slot for a CPU
* in cpu_map must be NULL here; lazy allocation treats NULL as
* "not allocated in this build". claim_allocations() and
* __sdt_free()/__sds_free() clear the slots, but only for
* cpu_map, so a build must not fill slots outside cpu_map.
*/
for_each_cpu(j, cpu_map) {
struct sched_domain *sd;
WARN_ON_ONCE(*per_cpu_ptr(sdd->sd, j) ||
*per_cpu_ptr(sdd->sg, j) ||
*per_cpu_ptr(sdd->sgc, j));
> @@ -2748,30 +2790,36 @@ static void __sdt_free(const struct cpumask *cpu_map)
>
> if (sdd->sd) {
> sd = *per_cpu_ptr(sdd->sd, j);
> - if (sd && (sd->flags & SD_NUMA))
> - free_sched_groups(sd->groups, 0);
> - kfree(*per_cpu_ptr(sdd->sd, j));
> + if (sd) {
> + if (sd->flags & SD_NUMA)
> + free_sched_groups(sd->groups, 0);
> + kfree(sd);
> + *per_cpu_ptr(sdd->sd, j) = NULL;
> + }
> }
>
> - if (sdd->sg)
> + if (sdd->sg) {
> kfree(*per_cpu_ptr(sdd->sg, j));
> - if (sdd->sgc)
> + *per_cpu_ptr(sdd->sg, j) = NULL;
> + }
> + if (sdd->sgc) {
> kfree(*per_cpu_ptr(sdd->sgc, j));
> + *per_cpu_ptr(sdd->sgc, j) = NULL;
> + }
> }
> - free_percpu(sdd->sd);
> - sdd->sd = NULL;
> - free_percpu(sdd->sg);
> - sdd->sg = NULL;
> - free_percpu(sdd->sgc);
> - sdd->sgc = NULL;
> }
> }
>
> +static struct sched_domain_shared * __percpu *sched_domains_sds;
> +
> static int __sds_alloc(struct s_data *d, const struct cpumask *cpu_map)
> {
and maybe similar check
for_each_cpu(j, cpu_map)
WARN_ON_ONCE(*per_cpu_ptr(d->sds, j));
> - d->sds = alloc_percpu(struct sched_domain_shared *);
> - if (!d->sds)
> - return -ENOMEM;
> + if (!sched_domains_sds) {
> + sched_domains_sds = alloc_percpu(struct sched_domain_shared *);
> + if (!sched_domains_sds)
> + return -ENOMEM;
> + }
> + d->sds = sched_domains_sds;
>
> return 0;
> }
Thanks.
Tim
^ permalink raw reply [flat|nested] 21+ messages in thread
end of thread, other threads:[~2026-10-15 23:17 UTC | newest]
Thread overview: 21+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-15 15:33 [PATCH v1 0/8] x86, sched, mm: Speed up CPU hotplug on large systems Pasha Tatashin
2026-10-15 15:33 ` [PATCH v1 1/8] x86/realmode: Remove unnecessary WBINVD from AP startup trampoline Pasha Tatashin
2026-10-15 15:41 ` sashiko-bot
2026-10-15 15:33 ` [PATCH v1 2/8] x86/smp: Remove unnecessary WBINVD from mwait_play_dead() Pasha Tatashin
2026-10-15 15:44 ` sashiko-bot
2026-10-15 16:26 ` Pasha Tatashin
2026-10-15 15:33 ` [PATCH v1 3/8] x86/mce/intel: Bound cmci_rediscover() to a single topology peer Pasha Tatashin
2026-10-15 15:51 ` sashiko-bot
2026-10-15 15:33 ` [PATCH v1 4/8] x86/cacheinfo, mtrr: Skip stop_machine rendezvous in cache_ap_online() when MTRRs match Pasha Tatashin
2026-10-15 15:45 ` sashiko-bot
2026-10-15 15:33 ` [PATCH v1 5/8] sched/topology: Avoid redundant O(N^3) group capacity walks and unused allocations in build_sched_domains() Pasha Tatashin
2026-10-15 15:43 ` sashiko-bot
2026-10-15 15:33 ` [PATCH v1 6/8] sched/deadline, topology: Avoid O(N^2) dl_server bandwidth walks during root_domain rebuilds Pasha Tatashin
2026-10-15 15:57 ` sashiko-bot
2026-10-15 20:43 ` Pasha Tatashin
2026-10-15 15:33 ` [PATCH v1 7/8] sched/topology: Avoid redundant percpu allocations and bitmap walks in build_sched_domains() Pasha Tatashin
2026-10-15 15:51 ` sashiko-bot
2026-10-15 23:17 ` Tim Chen
2026-10-15 15:33 ` [PATCH v1 8/8] mm/vmstat: Avoid redundant per-CPU stat_threshold writes in refresh_zone_stat_thresholds() Pasha Tatashin
2026-10-15 15:49 ` sashiko-bot
2026-10-15 21:33 ` [PATCH v1 0/8] x86, sched, mm: Speed up CPU hotplug on large systems Luck, Tony
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.