sched discussion
[PATCH v7 linux 0/2] Cache aware scheduling: Reduce the overhead of task_cache_work
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-ID:20260720122214.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 跟进。
旧流程的问题
task_cache_work()对 LLC 域内每一颗 CPU 调用fraction_mm_sched()再选pref_llc,CPU 越多调度延迟越大。- 大多数被扫到的 CPU 其实从未跑过该 mm,或距上次访问超过
llc_epoch_affinity_timeout,对pref_llc决策没有贡献。 fraction_mm_sched()对过期 CPU 仍做几何级数累加,浪费算力却几乎不影响结果。- 没有任何位图或时间戳把"哪些 CPU 真正活跃"记下来。
新流程
mm->sc_stat新增cpumask_var_t visited_cpus,mm_alloc 时zalloc_cpumask_var(),mm_destroy 时free_cpumask_var()。account_mm_sched()在更新runtime的同时把cpu_of(rq)写入visited_cpus,并刷新pcpu_sched->epoch_last_visit。fraction_mm_sched()入口比较rq->cpu_epoch - epoch_last_visit,若超过llc_epoch_affinity_timeout,主动cpumask_clear_cpu()并返回 0。task_cache_work()用cpumask_and()把 LLC 域与visited_cpus取交,再用for_each_cpu_and()迭代,扫描规模随活跃 CPU 收缩。- 调试 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.c 中 fraction_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_visit,task_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 域内过滤。 - v6:
visited_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_cpus 把 task_cache_work() 从全 CPU 扫描收敛到"近期真正执行过该 mm"的子集,AMD 服务器 Redis 场景扫描规模 384 → 16、p99 latency 改善约 20%;但写者/读者无同步以及跨 CPU 可见性 race 仍未解决,hackbench 少数组合 regress 也待 v8 评估。