sched discussion
[PATCH v4] sched/fair: Prefer fully idle cores for NOHZ balancing
LLM 分析
sched/fair:NOHZ 平衡优先选择完全空闲的物理核
系列概况
- 标题:[PATCH v4] sched/fair: Prefer fully idle cores for NOHZ balancing
- 作者:Andrea Righi arighi@nvidia.com
- 版本:v4(单 patch,经历 v1→v2→v3→v4)
- 规模:1 file changed, 44 insertions(+), 11 deletions(-)
- 修改文件:
kernel/sched/fair.c(仅find_new_ilb()) - Message-ID:
20260804151324.918020-1-arighi@nvidia.com - 完整性:首封给出完整 diff、commit message 与版本变化日志;后续 9 封为 maintainer/作者评审与 select_rq_mask → scratchmask 清理讨论,外加一封 tip-bot2 入树通知(commit
293f9611ae73564febc553935830074f0f300694)
补丁目的
find_new_ilb() 在 NOHZ 平衡流程里挑一个 housekeeping CPU 来跑 idle load balancer(ILB)。当前实现只看 nohz.idle_cpus_mask ∩ housekeeping_cpumask(HK_TYPE_KERNEL_NOISE) 并返回第一个 idle CPU,完全不考虑同一物理核上是否有 sibling 在跑。
大多数 SMT 系统上 ILB 短暂,误差可忽略;但在 NVIDIA Vera 的 Olympus 核上,短暂唤醒一个空闲兄弟会让它在 WFI 之后还要经历约 10 Ki 周期的 qualification 间隔才能恢复满血单线程性能;期间原本独占核的兄弟只能跑在降级性能下。反复短促唤醒会让干扰持续叠加,CPU-bound 基准从约 6.2 TFLOP/s 提升到约 9.4 TFLOP/s。
补丁目标:优先选一个整颗 SMT 核都空闲的 housekeeping CPU;找不到时保留首个 idle CPU 作为兜底,保证 NOHZ 平衡不卡死。
旧流程的问题
旧实现一次线性扫描、命中即返回:
static inline int find_new_ilb(void)
{
int this_cpu = smp_processor_id();
const struct cpumask *hk_mask;
int ilb_cpu;
hk_mask = housekeeping_cpumask(HK_TYPE_KERNEL_NOISE);
for_each_cpu_and(ilb_cpu, nohz.idle_cpus_mask, hk_mask) {
if (ilb_cpu == this_cpu)
continue;
if (idle_cpu(ilb_cpu))
return ilb_cpu;
}
return -1;
}
- 完全忽略 SMT 拓扑:thread-0 忙、thread-1 闲时它会挑中 thread-1,ILB 一唤醒就把整核拖进低性能窗口。
- 在 Vera 这种"唤醒兄弟要赔 10 Ki 周期 qualification"的设计上是真金白银的吞吐损失。
- Non-SMT 路径虽然没损失,但同样的代码每次都多扫几个 CPU。
新流程
新 find_new_ilb() 在 IRQ-off 语境里执行:
- 构造候选集:复用 per-CPU 的
select_rq_mask,把nohz.idle_cpus_mask与 housekeeping 噪声集求交,得到ilb_cpus。 - 遍历候选集:
- 遇 busy housekeeping CPU:说明这个核已废,若 fallback 已设(之前见过空闲 sibling),把它的 SMT 兄弟直接剔除。
- 遇 idle housekeeping CPU:检查
is_core_idle();整核空闲立即返回;否则把首个空闲 sibling 记成fallback,并把整个核心的兄弟剔除。
- 退出循环:若遍历结束都没遇到整核空闲,返回
fallback;非 SMT 路径上sched_smt_active()为 false,跳过兄弟剔除,等价于"取第一个 idle CPU"。
关键实现
static inline int find_new_ilb(void)
{
struct cpumask *ilb_cpus;
int ilb_cpu, fallback = -1;
lockdep_assert_irqs_disabled();
ilb_cpus = this_cpu_cpumask_var_ptr(select_rq_mask);
cpumask_and(ilb_cpus, nohz.idle_cpus_mask,
housekeeping_cpumask(HK_TYPE_KERNEL_NOISE));
for_each_cpu(ilb_cpu, ilb_cpus) {
if (!idle_cpu(ilb_cpu)) {
if (sched_smt_active() && fallback >= 0)
cpumask_andnot(ilb_cpus, ilb_cpus,
cpu_smt_mask(ilb_cpu));
continue;
}
if (sched_smt_active() && !is_core_idle(ilb_cpu)) {
if (fallback < 0)
fallback = ilb_cpu;
cpumask_andnot(ilb_cpus, ilb_cpus,
cpu_smt_mask(ilb_cpu));
continue;
}
return ilb_cpu;
}
return fallback;
}
关键设计点:
- fallback 语义:保证即便没有任何完全空闲核,ILB 仍能起动,NOHZ 平衡不会停摆。
- 剪枝顺序:先
continue再在循环出口返回,避免 wide-SMT 上重复is_core_idle()。 - IRQ-off 假设:复用
select_rq_mask不引入新分配,调用方已确保 IRQ 关闭。
类比
把 housekeeping CPU 想象成公司里临时被拉来帮忙的志愿者。原来的做法是"看见第一个闲着的人就拉去搬货",结果他本来正在帮隔壁组扶稳一块大板子,他一走,板子砸下来还得重新花 10 Ki 秒重新扶稳——隔壁组的活儿就这么被反复打断。
新做法是先找一间整个工位都没人占的房间去搬货;实在没有整间空房,才"拉最近那个闲着的人"凑合,但只拉一次。这样既不打断关键工作,也不会因为没人搬货让仓库调度完全停摆。
ASCII 流程图
find_new_ilb() flow
+-------------------------+
| disable IRQ |
| borrow select_rq_mask |
+----------+--------------+
|
v
+-------------------------+
| ilb_cpus = nohz.idle |
| & HK_KERNEL_NOISE |
+----------+--------------+
|
v
+-------+--------+ busy +-------------------+
| for_each |--------->| fallback set? |
| ilb_cpu in set | | yes -> drop sibs |
+-------+--------+ +---------+---------+
| idle |
v v
+-------+--------+ core_idle? no +---+---+
| is_core_idle() |----------------> drop |
+-------+--------+ | SMT |
| yes | sibs |
v +--+---+
+-------+--------+ |
| return ilb_cpu | |
+----------------+ v
+-------+--------+
| loop continues |
| return fallback |
+-----------------+
候选集剪枝示意(以 SMT2、核0/1 为例):
candidates: { c0, c1, c2, c3 } (nohz.idle ∩ HK)
|
| c0 idle, is_core_idle(c0)=false (brother c1 busy)
| -> fallback = c0; drop {c0, c1} from candidates
v
candidates: { c2, c3 }
|
| c2 idle, is_core_idle(c2)=true
v
return c2 (whole core 2 idle, optimal)
if all fail -> return fallback (c0)
Highlight:风险与注意点
- ILB 唤醒延迟可能上升:可能跨多个 SMT 核才命中一个整核空闲,对延迟敏感负载要复测。
- 能耗变化:唤醒整核空闲的核去跑 ILB 可能比复用空闲 sibling 更耗电,移动端需要重新评估。
- 扫核代价:
cpu_smt_mask()+cpumask_andnot()在 wide-SMT 上仍有线性开销,但已被剪枝限制。 - 评审焦点演进:三轮评审聚焦"是否需要再判兄弟"和"this_cpu 检查冗余"两个细节;v4 Shrikanth Hegde 给 Reviewed-by。
- 混淆项澄清:
nohz_full=不能替代本补丁,它要显式分区并保留 housekeeping CPU,isolated CPU 上还引入 context tracking 开销;Hegde 已实测对照数据。 - 后续议题:
select_rq_mask重命名/访问抽象与 per-CPU scratch mask 池化(sched_scratchmask_irq+guard_sched_scratchmask_irq0())的清理被维护者拆出来单独 follow-up,避免拖累本 patch。
版本变化
- v1 → v2:根据 Prateek Nayak 反馈,避免在 wide-SMT 上对"部分忙"核再做
is_core_idle()重复检查,引入cpumask_andnot(..., cpu_smt_mask(...))剪枝。 - v2 → v3:根据 Mete Durlu 反馈,找到 fallback 之后再遇到 busy CPU 时才剔除它的兄弟,避免过早剪掉后续可能命中完全空闲核的情况。
- v3 → v4:根据 Prateek Nayak / Vincent Guittot 反馈,移除冗余的
this_cpu检查(find_new_ilb()的调用方已保证不会挑到当前 CPU)。 - 入树:tip-bot2 在 2026-08-08 通告该补丁合入
tip: sched/core,commit293f9611ae73564febc553935830074f0f300694,作者 Andrea Righi。
一句话总结
find_new_ilb() 现在优先挑一个整核都闲的 housekeeping CPU 跑 NOHZ idle load balancer,找不到再退回首个空闲 CPU,并在遍历中剪掉同核兄弟,避免在 NVIDIA Olympus/Vera 这类对兄弟唤醒敏感的核上被 10 Ki 周期的 qualification 间隔反复拖累性能。