0/8 已展开

LLM 分析

sched/cache:仅扫描任务实际访问过的 CPU

系列概况

  • 标题[PATCH v5 linux 0/2] Cache aware scheduling: Reduce the overhead of task_cache_work
  • 作者:Luo Gengkun
  • 版本:v5
  • 规模:2 个补丁;1/2 正式优化,2/2 明确标注 DO NOT APPLY 的测试补丁
  • 修改文件include/linux/sched.hinclude/trace/events/sched.hkernel/sched/fair.ckernel/sched/features.h
  • 代码统计:正式补丁约 +30/-9,调试补丁约 +40/-5
  • Message-ID20260709130053.2749834-1-luogengkun2@huawei.com
  • 完整性:0/2、1/2、2/2 与 5 封回复均在输入中;cover letter 末尾和 4 封回复正文被截断,评审争议的最终结论无法从当前材料确认

该系列是已进入 mainline 的 cache-aware scheduling 之上的性能修复,不是在重新引入整套缓存感知调度。

补丁目的

cache-aware scheduling 通过 task_cache_work() 统计同一 mm 在各 CPU/LLC 的活动,用来选 pref_llc。原实现可能遍历整机的 CPU,即使某个进程从未在绝大部分 CPU 上运行过。384 CPU 的 AMD 测试机单次扫描经常扫满 384 个 CPU。

本系列在每个 mm 上增加 visited_cpus

  1. 任务在 CPU 上获得运行时间时记录该 CPU。
  2. task_cache_work() 只扫描允许范围、且该 mm 实际访问过的 CPU。
  3. 长时间未访问的 CPU 从集合中淘汰。
  4. 保留 get_scan_cpumasks(),避免突破 NUMA balancing 扫描约束。

AMD 测试中,扫描数量从 384 降至最多约 16,task_cache_work() 的 cycles 占比由约 0.82%~1.12% 降至 0.02%

旧流程的问题

get_scan_cpumasks(cpus, p);

for_each_relevant_llc(sd) {
        for_each_cpu(cpu, sched_domain_span(sd)) {
                occ = fraction_mm_sched(cpu, mm);
                update_preferred_llc(occ);
        }
}

主要无效工作:扫描进程从未运行过的 CPU、扫描曾经运行但已不再活跃的 CPU、为每个候选 CPU 取状态并计算占用比例。成本随机器 CPU 数量放大,而不是随进程真实 CPU 足迹放大。Redis 多实例中这部分额外工作直接反映为 p99 延迟回退。

Old task_cache_work
        |
        v
Allowed CPU mask
        |
        v
Scan almost every CPU in each LLC
        |
        +--> CPU never used by mm
        +--> CPU used long ago
        `--> CPU currently relevant

新流程

任务运行时,account_mm_sched() 刷新时间并把 CPU 加进集合:

pcpu_sched->epoch_timeout = rq->cpu_epoch;

if (!cpumask_test_cpu(cpu_of(rq), &mm->sc_stat.visited_cpus))
        cpumask_set_cpu(cpu_of(rq), &mm->sc_stat.visited_cpus);

task_cache_work() 把允许范围与访问集合做交集后扫描:

get_scan_cpumasks(cpus, p);
cpumask_and(cpus, cpus, &mm->sc_stat.visited_cpus);

for_each_relevant_llc(sd) {
        for_each_cpu_and(cpu, sched_domain_span(sd), cpus)
                inspect_cpu(cpu);
}

fraction_mm_sched() 检查访问是否过期,差值超过 llc_epoch_affinity_timeout 就清掉对应 bit 并跳过:

if (rq->cpu_epoch - pcpu_sched->epoch_timeout >
    llc_epoch_affinity_timeout) {
        cpumask_clear_cpu(cpu, &mm->sc_stat.visited_cpus);
        return 0;
}

关系示意:

NUMA-allowed CPUs ----+
                      +--> AND --> LLC CPUs --> scan
visited_cpus ---------+
                          |
                          `--> expire stale CPU bits

关键点是先保留 get_scan_cpumasks() 给出的 NUMA/调度约束,再叠加访问集合和 LLC domain 的过滤,而不是用 visited_cpus 取代原约束。

Patch 概览

1/2:正式优化

  • sched_cache_time::epoch_timeout:记录此 mm 最近在该 CPU 活动时的 epoch。
  • sched_cache_stat::visited_cpus:记录此 mm 实际访问过的 CPU。
  • mm_init_sched() 初始化 epoch_timeout 并清空 visited_cpus
  • account_mm_sched() 刷新访问时间并设置 CPU bit。
  • fraction_mm_sched() 淘汰过期 CPU。
  • task_cache_work() 遍历 sched_domain_span(sd) ∩ 原扫描范围 ∩ visited_cpus

2/2:仅用于测试,不能合入

  • 增加 SC_VISIT sched feature,用作开关。
  • 增加 sched_cache_scan trace event,输出 comm、pid、扫描 CPU 数量。
  • 为 cover letter 中 384 -> 1~16 的 trace 数据提供观测手段。

关键实现

扫描成本从机器规模收缩为任务足迹:

cost ~= number_of_cpus_in_scanned_domains
        --> number_of_recently_visited_cpus

Valkey/Redis 20 万/30 万/40 万 RPS:未优化前 p99 相对 baseline 恶化约 47.3%/38.4%/27.4%;应用本系列后差距收窄到 1.5%/2.0%/1.7%;task_cache_work() 的 cycles 比例都降到约 0.02%。这说明本系列是在消除 cache-aware scheduling 自身的周期性扫描成本,不改变其 LLC 选择目标。

评审发现的 epoch 停滞风险:

CPU1: account_mm_sched()
       set CPU1 in visited_cpus
       epoch_timeout = CPU1.cpu_epoch
       CPU1 becomes idle; cpu_epoch 冻结
CPU2: task_cache_work()
       delta = cpu_epoch - epoch_timeout == 0
       CPU1 bit 不被清除
later: __update_mm_sched() 更新 epoch,已晚

该问题不会让活跃 CPU 被错误淘汰,反而会让已空闲的 CPU 长期留在 visited_cpus,使优化逐渐退化为额外扫描。评审者建议在过期判断前调用 __update_mm_sched(),但后续邮件在关键位置被截断,无法确认作者和维护者最终是否接受。

类比

把整机 CPU 想象成酒店 384 个房间,task_cache_work() 是每天统计某位客人在哪些楼层活动的管理员。

旧方法是每天打开 384 个房间逐一查,即使客人只去过其中 16 个。新方法给客人维护一张“最近到访房间清单”,管理员先按酒店规则确定允许检查的楼层,再只检查清单上落在这些楼层的房间。epoch_timeout 就是清单上的“最后到访日期”。

评审争议在于:如果某个房间的时钟在空置后停止走动,管理员会误以为访问记录仍然很新,迟迟不把房间从清单删除。

Highlight:风险与注意点

  • epoch 停滞影响淘汰:空闲 CPU 的 rq->cpu_epoch 不再推进,差值可能为零,visited_cpus 中的陈旧 bit 无法清除。
  • 当前材料没有评审结论:4 封相关回复在关键位置被截断,不能据此宣称竞态已解决。
  • v5 必须保留 NUMA 约束visited_cpus 只是额外过滤器,不能替代 get_scan_cpumasks(),否则可能违反 NUMA_BALANCING 边界。
  • Hackbench 并非全部改善:启用 visited 扫描后仍有约 -1%~-7% 的回退项;不能等同于“所有性能项目均无回退”。
  • 测试平台覆盖有限:核心数据来自一台 AMD 服务器,还需覆盖不同 LLC/NUMA 拓扑、CPU hotplug、长时间 idle、cpuset/affinity 变化。
  • 并发状态需要继续验证:写入 bit 与清除 bit 的锁和生命周期应通过 KCSAN/lockdep 继续检查。
  • 调试补丁不可合入:第二次 cpumask 分配失败时的资源清理值得检查,但不应与正式补丁混为一谈。
  • 描述与可见 diff 存在细节差异:调试补丁说明提到“两项 sched features”,但可见新增只有 SC_VISIT,可能是正文被截断。

版本变化

  • v2:在设置/清除 visited_cpus 前增加预检查,减少 cache-to-cache 开销;以 static key 优化 timeout 路径。
  • v3:移除 static key 并默认启用;复用 llc_epoch_affinity_timeout;把 epoch 差值计算移入 fraction_mm_sched();曾重置 work->next 防止同进程多线程并发。
  • v4:基于 master 重放;引入独立 epoch_timeout 避免周期性 fraction_mm_sched() 刷新原 epoch 后无法淘汰;调整 nr_running 统计顺序;删除多余 work->next 重置;加入扫描计数调试补丁。
  • v5:恢复 get_scan_cpumasks(),避免违反 NUMA_BALANCING 约束;使用 for_each_cpu_and() 把扫描进一步限制在 LLC domain 与候选 cpumask 的交集内。

系列与已合入 mainline 的 cache-aware scheduling 基础功能配套:前者提供按 mm 统计 CPU/LLC 活动并选 pref_llc 的机制,本系列只优化其 task_cache_work() 扫描路径。

一句话总结

本系列通过维护每个 mm 最近访问的 CPU 集合,把 task_cache_work() 从近似全机扫描缩减为任务实际 CPU 足迹扫描,但基于 rq->cpu_epoch 淘汰空闲 CPU 的可靠性仍是线程中尚未确认解决的核心问题。