0/1 已展开

LLM 分析

sched/core:让 fallback CPU 选择感知 NUMA 拓扑

系列概况

  • 标题:[PATCH] sched/core: Make fallback CPU selection NUMA-aware
  • 作者:Yury Norov ynorov@nvidia.com
  • 版本:单 patch,未标注 v?
  • 规模:1 个文件,25 行新增,22 行删除
  • 修改文件:kernel/sched/core.c
  • 代码统计:+25 / -22
  • Message-ID:20260905033656.311477-1-ynorov@nvidia.com
  • 完整性:邮件只包含一个 patch,diff 内嵌在正文里

补丁目的

select_fallback_rq() 在目标 CPU 不再可用时,要为当前任务重新挑一个 CPU 跑起来。理想情况下,应该选"最近的"——优先同节点、同 socket、同一 NUMA 拓扑圈。但旧实现只看一眼本地节点,剩下就退回到对 p->cpus_ptr 做按编号的线性扫描。在多 NUMA 节点系统上,这条线性路径很可能在拓扑距离上"跨过去"再"折回来",导致 fallback 选到一颗其实更远的 CPU。本 patch 改用调度器现成的 sched_numa_hop_mask 按拓扑距离逐跳扩搜,让 fallback 选择保持局部性。

旧流程的问题

nid 取自 cpu_to_node(cpu),如果有效就先在同节点 cpumask_of_node(nid) 里扫一遍;找不到就跳出去对 p->cpus_ptrfor_each_cpu 顺序扫描。在两节点系统中这没问题,因为"非本地"就只剩一颗可去的地方;但在 >=3 节点系统里,按编号扫描会撞上编号低但拓扑远的 CPU,破坏 locality,还会重复扫描同节点里已经找过的 CPU。

新流程

借助 for_each_numa_hop_mask(cpus, nid) 按 NUMA 距离一圈一圈往外扩,每跳都用 for_each_cpu_andnot(dest_cpu, cpus, prev) 只看新进入这一圈的 CPU,避免重复。prev 在每跳结束时累加当前 hop mask,所以下一跳天然不会重复扫老 CPU。最后一段 for_each_cpu_andnot(dest_cpu, p->cpus_ptr, prev) 处理拓扑正在重建时 hop mask 还没就绪的窗口,把 hop mask 没覆盖到的"残余"在线 CPU 也扫一遍。

关键实现

int nid = IS_ENABLED(CONFIG_NUMA) ? cpu_to_node(cpu) : NUMA_NO_NODE;
const struct cpumask *prev = cpu_none_mask, *cpus;

/* Look for the closest allowed, online CPU. */
rcu_read_lock();
for_each_numa_hop_mask(cpus, nid) {
    for_each_cpu_andnot(dest_cpu, cpus, prev) {
        if (is_cpu_allowed(p, dest_cpu)) {
            rcu_read_unlock();
            goto out;
        }
    }
    prev = cpus;
}

/*
 * NUMA masks may be unavailable or incomplete while the
 * topology is being rebuilt. Search CPUs not covered by them
 * before relaxing the task's affinity.
 */
for_each_cpu_andnot(dest_cpu, p->cpus_ptr, prev) {
    if (is_cpu_allowed(p, dest_cpu)) {
        rcu_read_unlock();
        goto out;
    }
}
  • IS_ENABLED(CONFIG_NUMA) 让非 NUMA 构建也能用同一段代码:此时 nid == NUMA_NO_NODE,hop mask 应退化为覆盖所有 CPU 的 mask。
  • prev = cpu_none_mask 是"空集"占位,使得第一跳就等于整张 hop mask。
  • RCU 读锁保护 hop mask 的并发访问(hop mask 可能在 topology rebuild 时被替换)。
           +----------------------------+
           |     select_fallback_rq     |
           +-------------+--------------+
                         |
              +----------v-----------+
              |  hop 0: local node   |   prev = {}
              +----------+-----------+
                         |
              +----------v-----------+
              |  hop 1: 1-hop nodes  |   prev = hop0
              +----------+-----------+
                         |
              +----------v-----------+
              |  hop 2: 2-hop nodes  |   prev = hop0|hop1
              +----------+-----------+
                         |
              +----------v-----------+
              |  residual:            |
              |  cpus_ptr \ prev      |   prev = hop0|hop1|hop2
              |  (covers hotplug gap) |
              +----------+-----------+
                         |
                    allowed CPU  -> goto out

类比

想象你在一座陌生城市找人开会:

  • 第一步:先敲自己办公室的门(同节点)。
  • 第二步:走到隔壁楼层(1-hop)。
  • 第三步:跑去对面那栋楼(2-hop)。
  • 你随身带一个小本本 (prev) 记下"已经敲过的门",每去一层新楼,只看本本里没记过的那些房间 (cpus & ~prev)。
  • 走到最后发现 hop mask 这张"通讯录"还没把今天临时搭的活动板房记进去,于是最后再扫一遍整栋大楼剩下的房间 (cpus_ptr \ prev),兜底以防漏人。

旧实现相当于:不记本本,每去新楼层都把整栋楼再敲一遍,并且越敲越远。

Highlight:风险与注意点

  • rcu_read_lock 与 rcu_read_unlock 的配对:diff 里只看到两处 rcu_read_unlock(); goto out;,其它 fallback 路径(cpuset / possible state 分支)需对照未在 diff 中展示的下文,确认所有 goto out 之前都已解锁。
  • 非 NUMA 配置 (IS_ENABLED(CONFIG_NUMA)==0) 下 nid = NUMA_NO_NODE,需要 for_each_numa_hop_mask 在 NUMA_NO_NODE 上仍能给出覆盖所有 CPU 的 mask,否则第一跳就空集,行为退化到只剩残余扫描——需结合 sched_numa.h 中 hop mask 的定义确认。
  • 拓扑重建窗口:prev 累积依赖每一跳返回的 mask 本身正确;若第一跳 mask 在 hotplug 中刚被换为不完整版本,可能跳过本应被考虑的在线 CPU;补丁末尾的残余扫描正是为此兜底,但仍建议用 hotplug stress 验证。
  • nid == -1(节点已 offline)的语义:旧代码直接走 p->cpus_ptr;新代码通过 hop mask 也能落到残余扫描路径,但要确认亲和性放宽前的 fallback 行为对 cpuset 隔离仍然等价。
  • 性能:现在每跳做一次 cpus & ~prev 的 mask 运算,对于很小的 cpuset(<8 CPU)开销可忽略;对几百 CPU + 几十 NUMA 节点的大机器,循环体内仅一次 is_cpu_allowed,命中即返回,最坏情况下仍 O(全部在线 CPU)。

一句话总结

把 fallback CPU 选择从"本地节点 + 线性 cpumask"改成"按 NUMA 距离逐跳扩搜 + 残余兜底",让多节点系统上的降级目标更近、且在拓扑重建期间仍然可靠。