0/10 已展开

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-ID20260804151324.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 语境里执行:

  1. 构造候选集:复用 per-CPU 的 select_rq_mask,把 nohz.idle_cpus_mask 与 housekeeping 噪声集求交,得到 ilb_cpus
  2. 遍历候选集
    • busy housekeeping CPU:说明这个核已废,若 fallback 已设(之前见过空闲 sibling),把它的 SMT 兄弟直接剔除。
    • idle housekeeping CPU:检查 is_core_idle();整核空闲立即返回;否则把首个空闲 sibling 记成 fallback,并把整个核心的兄弟剔除。
  3. 退出循环:若遍历结束都没遇到整核空闲,返回 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,commit 293f9611ae73564febc553935830074f0f300694,作者 Andrea Righi。

一句话总结

find_new_ilb() 现在优先挑一个整核都闲的 housekeeping CPU 跑 NOHZ idle load balancer,找不到再退回首个空闲 CPU,并在遍历中剪掉同核兄弟,避免在 NVIDIA Olympus/Vera 这类对兄弟唤醒敏感的核上被 10 Ki 周期的 qualification 间隔反复拖累性能。