0/6 已展开

LLM 分析

Cache aware scheduling:减少 task_cache_work 的扫描开销(v7)

系列概况

  • 标题:[PATCH v7 linux 0/2] Cache aware scheduling: Reduce the overhead of task_cache_work
  • 作者:Luo Gengkun luogengkun2@huawei.com
  • 版本:v7,cover letter 日期 2026-07-20
  • 规模:2 枚 patch(1 枚功能 patch + 1 枚 "DO NOT APPLY" 调试 patch)+ cover letter
  • 修改文件:include/linux/mm_types.h、include/linux/sched.h、kernel/sched/fair.c;调试 patch 额外改 include/trace/events/sched.h、kernel/sched/features.h
  • 代码统计:功能 patch 3 files changed, +46 / -10;调试 patch 3 files changed, +43 / -5
  • Message-ID20260720122214.3977092-1-luogengkun2@huawei.com
  • 完整性:cover letter、patch 1/2、patch 2/2 完整;后续 Tim Chen 与 Luo Gengkun 的 3 封正文在 lore 抓取时被截断,但 message-id 与 thread 关系完整可索引

补丁目的

主线 cache-aware scheduling 已合入,但 task_cache_work() 在多 NUMA 系统里把 LLC 域的所有 CPU 都拉一遍,开销过大。本系列在 mm->sc_stat 上新增 visited_cpus 位图,只让 task_cache_work() 扫"近期真正执行过该 mm 的 CPU 子集",超时未访问的位由 fraction_mm_sched() 主动清掉。AMD 服务器 valkey-benchmark 400k rps(关闭 NUMA balancing):p99 latency 0.554 → 0.441 ms(≈ -20%),cycles:k 中 task_cache_work 占比 0.81% → 0.02%,trace 中 scan 规模 384 → 16。hackbench 多数组显著改善,少数组合小幅 regress,需要 v8 跟进。

旧流程的问题

  1. task_cache_work() 对 LLC 域内每一颗 CPU 调用 fraction_mm_sched() 再选 pref_llc,CPU 越多调度延迟越大。
  2. 大多数被扫到的 CPU 其实从未跑过该 mm,或距上次访问超过 llc_epoch_affinity_timeout,对 pref_llc 决策没有贡献。
  3. fraction_mm_sched() 对过期 CPU 仍做几何级数累加,浪费算力却几乎不影响结果。
  4. 没有任何位图或时间戳把"哪些 CPU 真正活跃"记下来。

新流程

  1. mm->sc_stat 新增 cpumask_var_t visited_cpus,mm_alloc 时 zalloc_cpumask_var(),mm_destroy 时 free_cpumask_var()
  2. account_mm_sched() 在更新 runtime 的同时把 cpu_of(rq) 写入 visited_cpus,并刷新 pcpu_sched->epoch_last_visit
  3. fraction_mm_sched() 入口比较 rq->cpu_epoch - epoch_last_visit,若超过 llc_epoch_affinity_timeout,主动 cpumask_clear_cpu() 并返回 0。
  4. task_cache_work()cpumask_and() 把 LLC 域与 visited_cpus 取交,再用 for_each_cpu_and() 迭代,扫描规模随活跃 CPU 收缩。
  5. 调试 patch 增加 SCHED_FEAT(SC_VISIT)trace_sched_cache_scan,量化每次扫描的 CPU 数。

关键实现

include/linux/mm_types.h 中分配/释放:

if (!zalloc_cpumask_var(&mm->sc_stat.visited_cpus, GFP_KERNEL)) {
    free_percpu(pcpu_sched);
    return -ENOMEM;
}
...
free_cpumask_var(mm->sc_stat.visited_cpus);

include/linux/sched.h 扩展结构体:

struct sched_cache_time {
    u64  runtime;
    unsigned long epoch;
    unsigned long epoch_last_visit;
};

struct sched_cache_stat {
    struct sched_cache_time __percpu *pcpu_sched;
    cpumask_var_t visited_cpus;
    ...
};

fair.cfraction_mm_sched() 加入超时分支:

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

account_mm_sched() 同步写入:

pcpu_sched->epoch_last_visit = 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() 改为只扫交集:

cpumask_and(cpus, cpus, mm->sc_stat.visited_cpus);
...
for_each_cpu_and(i, sched_domain_span(sd), mm->sc_stat.visited_cpus) {
    cur = rcu_dereference_all(cpu_rq(i)->curr);
    if (cur && !(cur->flags & (PF_EXITING | PF_KTHREAD)) && cur->mm == mm)
        nr_running++;
    occ = fraction_mm_sched(i, mm);
    if (occ == 0)
        continue;
    ...
}

调试 patch(patch 2/2)核心:新增 SCHED_FEAT(SC_VISIT)trace_sched_cache_scan,支持 echo SC_VISIT > /sys/kernel/debug/sched/features,trace 中输出 scan 数量。

         +---------------------------+
         |  task_cache_work (reader) |
         +-----------+---------------+
                     |
                     | cpumask_and(LLC_span, visited_cpus)
                     v
+-----------------------------------------------------+
|  mm->sc_stat.visited_cpus (cpumask_var_t)          |
|   bits set  : account_mm_sched() in cpu_of(rq)      |
|   bits clear: fraction_mm_sched() on timeout        |
+-----------------------------------------------------+
        ^                              ^
        | set/test                     | test/clear
        |                              |
        +--------------+---------------+
                       |
            +----------+----------+
            | account_mm_sched    |
            |  - runtime          |
            |  - epoch_last_visit |
            +---------------------+

类比

task_cache_work() 想象成学生会评选"哪些寝室最近最受同学 A 欢迎"。旧流程下,班长挨个敲 384 间寝室的门,即便是从没接待过 A 的寝室也要作答;现在每位同学都会在宿舍楼访客表(visited_cpus)上登记到访时间,过期就擦掉(epoch_last_visit + llc_epoch_affinity_timeout),班长只走访"近期真的来过"的寝室。visited_cpus 是入口登记表,epoch_last_visit 是时间戳,超时阈值相当于登记保留时长。

Highlight:风险与注意点

  • 写者/读者无锁 race(v7 新增注释承认):account_mm_sched() 写位与 task_cache_work() 读位之间没有同步,可能漏掉并发新访问的 CPU,被定性为"精度换效率",但若 race 频繁出现会持续漏扫。
  • 跨 CPU 可见性(Tim Chen 在第三封回复中指出):account_mm_sched()epoch_last_visittask_cache_work() 在远程 CPU 比较并清位,中间缺 smp_mb / smp_store_release 配对,可能错误清掉仍属活跃的 bit,破坏 pref_llc 选择 —— v8 必须给出内存序配套。
  • 小幅 regress:hackbench "threads 1 20、2 10、2 4、4 4、4 6" 等组合相对 schedcache 出现 -2% ~ -7% regress,需要在更广负载矩阵里复测。
  • 调试 patch 不能合入:patch 2/2 标记 "DO NOT APPLY",引入新的 sched feature 与 tracepoint,仅用于量化对比,merge 时必须丢弃。
  • cpumask_and 调用形态:旧实现先把 LLC span 复制到 cpus,新代码在 task_cache_work() 里再做 cpumask_and();这意味着 cpus_read_lock() 仍必须覆盖后续扫描,确保调度域不被释放。

版本变化

  • v3:去除 static key,默认开启;复用 llc_epoch_affinity_timeout,不再单独引入 llc_epoch_visited_timeout
  • v4:rebase 到 master;引入 epoch_timeout 取代原先依赖 epoch 的方案;nr_running++ 前移到 fraction_mm_sched() 之前;清理冗余 work->next 复位;首版附 debug patch。
  • v5:恢复 get_scan_cpumasks() 以兼容 NUMA_BALANCING;改用 for_each_cpu_and() 在 LLC 域内过滤。
  • v6visited_cpus 改为 cpumask_var_t 动态分配,避免 NR_CPUS 大时每进程内存膨胀;把 LLC span 与 visited_cpus 在分发前取交,规避跨节点 LLC 拓扑漏 CPU;epoch_timeout 改名 epoch_last_visit__update_mm_sched 前移到超时判断之前。
  • v7:仅在 task_cache_work() 内补长注释,明确无锁 race 的取舍;尚未回应 Tim Chen 关于 epoch_last_visit 跨 CPU 可见性的 issue,预计 v8 处理。

一句话总结

v7 用 visited_cpustask_cache_work() 从全 CPU 扫描收敛到"近期真正执行过该 mm"的子集,AMD 服务器 Redis 场景扫描规模 384 → 16、p99 latency 改善约 20%;但写者/读者无同步以及跨 CPU 可见性 race 仍未解决,hackbench 少数组合 regress 也待 v8 评估。