rvv_dirty_local 单策略:每核唯一定位驱动相位同步推进:为同一 llama-server
的向量 worker 分配互不重复的 CPU 和 per-team/per-CPU vector DSQ,使其稳定地在
各自核上推进、不被迁移或被其它 team 顶替;同一 server 的这组 worker 因此以相近
节奏到达 graph barrier(组内相位 lockstep),显著降低 barrier 自旋/futex 等待。
100ms vector slice 与 500ms local 窗口是这一绑定的配套,不是策略本质。
自动 phase-aware(组间仲裁):PREFILL/DECODE 是 team 级相位,由同一 server
的 worker 共同经历,并非 worker 各自进入。调度器读 ABI v4 phase hint,仅在多个
server 的 team 于同一核相撞时仲裁:PREFILL/MIXED 优先消费、preferred wakeup
定向抢占、prefill 安全片;无有效 hint 时回退默认轮转。无需策略/模式开关。
make -C "$BR_OUT" libbpf
export STAGING_DIR="$BR_OUT/host/riscv64-buildroot-linux-gnu/sysroot"
test -f "$STAGING_DIR/usr/lib/libbpf.a"
test -f "$STAGING_DIR/usr/lib/libelf.a"
test -f "$BR_OUT/build/linux-custom/vmlinux"
INFERSCHED:面向 RISC-V 的 LLM 推理调度器
本项目面向 Milk-V Jupiter / SpacemiT K1 八核 RISC-V 平台,研究 Linux
sched_ext对 llama.cpp CPU 推理工作负载的调度优化。项目使用 eBPF 调度器识别 执行 RVV 向量计算的 worker,并结合 CPU 分配、vector DSQ、长时间片以及 llama.cpp 提供的 prefill/decode 阶段提示,改善多线程和多模型竞争时的调度行为。仓库提供一套可复现的实验环境,包括 K1 Linux 6.6 的 sched_ext 补丁、llama.cpp 用户态提示补丁、RVV-aware eBPF 调度器、低开销观测工具和 benchmark。
核心能力
rvv_dirty_local单策略:每核唯一定位驱动相位同步推进:为同一 llama-server 的向量 worker 分配互不重复的 CPU 和 per-team/per-CPU vector DSQ,使其稳定地在 各自核上推进、不被迁移或被其它 team 顶替;同一 server 的这组 worker 因此以相近 节奏到达 graph barrier(组内相位 lockstep),显著降低 barrier 自旋/futex 等待。 100ms vector slice 与 500ms local 窗口是这一绑定的配套,不是策略本质。PREFILL/DECODE是 team 级相位,由同一 server 的 worker 共同经历,并非 worker 各自进入。调度器读 ABI v4 phase hint,仅在多个 server 的 team 于同一核相撞时仲裁:PREFILL/MIXED 优先消费、preferred wakeup 定向抢占、prefill 安全片;无有效 hint 时回退默认轮转。无需策略/模式开关。PREFILL、DECODE、MIXED和IDLE阶段。src/x86_scx_llm_pin用 ABI v4 hint 替代 RISC-Vsstatus.VS分类,并把 2×4 worker 固定 spread 到 4C/8T 的 0-3/4-7。tests/benchmark提供 CFS、dynamic、scx-llm-pin、 cfs-affinity 策略,覆盖 dual-async、dual-sync、multi 三种场景,并支持本地 x86 运行。项目结构
src/kernel_patches/src/llama_cpp_patches/src/scx_llm_pin/rvv_dirty_local+ 自动 phase-awaresrc/x86_scx_llm_pin/src/rvv_sched_cost/src/dynamic_threads/src/x86_dynamic_threads/tests/benchmark/tests/config.sh工作方式
scx_llm_pinloader 通过 libbpf 加载 eBPF 程序、初始化并 pinconfig/workersmaps,最后把 struct_ops 附加到 Linux sched_ext。patched llama-server 使用BPF_OBJ_GET + mmap写入 OpenMP worker 身份和推理 phase;rvv_lease_ctl使用 BPF map lookup/update syscall 读取运行状态或在线调整 slice、vector window 和 dirty 阈值(rvv_lease_ctl status/set/reset)。内核调度核心调用 eBPF 的
select_cpu、enqueue、dispatch、running和stoppingcallbacks。eBPF 调度器结合 BPF maps 与task_struct中的 RISC-V Vector 状态完成 RVV worker 识别、CPU 分配和 DSQ 选择,再通过 sched_ext kfunc 把任务交回 Linux 调度核心执行。Phase-Aware Dirty Lease
当前策略的核心是相位同步推进(phase-locked progression),而不是单纯把时间片 拉长:把同一 llama-server 的 RVV worker 每核唯一定位后,per-team/per-CPU VDSQ 让 每个 worker 稳定回到自己的 CPU,避免迁移和被其它 team 顶替;于是这组 worker 在各自 分片上以相近速率推进、在 graph barrier 上同步到达(组内 lockstep),barrier 的 “最后到达者”不再被落后者拖住,自旋/futex 等待下降。100 ms vector slice 和 500 ms local 窗口是这一绑定的配套,不是策略本质。
PREFILL/DECODE是 team 级相位,由同一 server 的 worker 共同经历;phase hint 只用于组间仲裁:当多个 server 的 team 在 同一核相撞时选择优先运行的一方。图从左到右展示 CPU、local DSQ、来源 DSQ 和任务;箭头按真实数据流从右向左。 CPU i 最终只从自己的 local DSQ 运行任务。local 为空时,sched_ext 核心先把 global DSQ 的普通任务转入 local;只有 global 没有提供任务时,BPF dispatch 才从 vector DSQ 消费一个 worker 到 local。图中
VECTOR DSQs (CPU i)内的 A/B 是源码 中的两条独立队列VDSQ(A,i)和VDSQ(B,i),不是共享 FIFO。local 窗口在
--vector-local-window-ms > 0(默认 500ms)时始终启用:每消费一次 VDSQ,该 team 的向量任务在随后窗口内改走 local DSQ,与普通任务混排。BPF 优先消费 处于PREFILL/MIXED的 team,首选队列为空时回退到另一 team;preferred worker 唤醒且同核正在运行另一 team 的 V worker 时可定向抢占。向远端 VDSQ 投递后按需kick目标 CPU。等待达到 1 秒的 runnable V worker 由 pending safety 兜底,普通 任务、helper 和内核任务仍通过 global DSQ 获得服务。以上 phase-aware 分支只在读到 有效 hint 时启用;否则全部回退到rvv_dirty_local默认。调度对比:CFS 与相位同步推进
上图用同一工作负载的时间轴对比两种调度结果。worker 的执行顺序是计算 → spin → futex,futex 之后才发生上下文切换(红虚线)。
左栏是 CFS:同一 server 的 A0/A1 会竞争同一核、被时间片反复打断,A2/A3 也被
normal等非向量任务穿插,相位错乱,直到末尾 A3 结束才到达同步点(黄虚线)。 右栏是scx-llm-pin:同一 server 的 A0–A3 各占一个 CPU 且稳定不被迁移,只在normal(非向量任务)和早期自旋结束处发生少量上下文切换;A1–A3 各自在自己的分片 上以相近节奏推进,并在第一个同步点(黄虚线,贯穿 A0–A3)对齐、同步完成。这正是相位同步推进的实现路径:
select_cpu/enqueue把每核唯一定位与 per-team/per-CPU VDSQ 结合,使每个 worker 稳定回到自己的 CPU、不被其它 team 顶替, 于是同一 server 的这组 worker 以相近速率推进并在 graph barrier 上同步到达,而不是 靠拉长时间片。其直接结果是上下文切换大幅下降:板端 dual-sync 短测中本策略把切换 从 CFS 的 40 万级降到 1.3 万级(见下方“上下文切换对比”)。实验数据对比
以下正式 campaign 使用两个 llama-server 各 8 个 worker 共享 CPU0-7。四个默认 prompt 文本不同,但均构造为 128 input token;每请求固定输出 256 token。A 侧 incumbent 请求达到 token-4 后,按 1 秒间隔向 B 侧注入等量请求。
c=2/4/8 分别运行 60/30/15 个有效轮次,因此每个策略与并发组合都包含 60 个 injected TTFT 样本。TPS 为跨轮 mean +/- sample SD;TTFT mean、sample SD 和 nearest-rank p95 均直接从 60 个 injected 单请求汇总。
c=2
observe)c=4
observe)c=8
observe)observe读取并校验 worker/phase hint,但不根据 phase 改变调度;完整策略通过prefill-lease选择 preferred team,并允许满足条件的 preferred wakeup 定向抢占。 Phase-Aware Dirty Lease 相对 CFS 在 c=2/4/8 下分别提升 TPS 约 54.9%、30.7% 和 25.6%,并将 injected TTFT p95 分别降低约 38.6%、42.3% 和 42.5%。相对 Observe, Phase-Aware 的 TPS 变化不超过 0.4%,但 p95 进一步降低约 14.9%、24.1% 和 15.3%。上下文切换对比
“每核唯一定位使 worker 稳定驻留、相位同步推进”的一个直接体现是上下文切换大幅下降。 下表为
tests/benchmark的 dual-sync 短测(2×llama-server 共享 CPU0-7、每请求 128 input / 64 output、各 3 轮)在板端 RISC-V 上的上下文切换总数:本策略把上下文切换从 CFS 的 40 万级降到 1.3 万级(约 1/30),吞吐随之提升;而 “短 spin”类做法会让 worker 频繁 futex 睡眠/唤醒,切换反而最多(56.5 万)。dynamic 的切换最少,但吞吐与本策略基本持平,说明本策略的核心收益来自稳定绑核下的相位 同步推进,而非压缩切换数量本身。
x86(i5-10200H,2×4 线程、64 output、各 3 轮)趋势一致:CFS 约 84.0k,
x86-dynamic约 3.7k,x86-scx-llm-pin(spread)约 2.4k。环境准备
硬件与主机依赖
sshpass。-target bpf的 Clang。以下变量用于描述源码、输出目录和板卡连接信息:
板卡连接、工具链与运行时路径也可以直接写进
tests/config.sh(REMOTE_*、TOOLCHAIN_ROOT、LLM_PATH、DT_LLAMA_BIN等)。所有 Makefile 与 benchmark 都从该文件读取,且环境变量优先。从 Buildroot 构建完整实验环境
这一节给出从原厂 Buildroot 到可运行实验环境的关键步骤。补丁来源、许可证、完整 构建选项和回退方法分别记录在
src/kernel_patches/README.md和src/llama_cpp_patches/README.md。1. 获取 K1 Buildroot 源码
使用 Milk-V Jupiter 的
k1-bl-v2.1.ymanifest:Buildroot 配置使用
buildroot-ext/configs/spacemit_k1_v2_defconfig,内核源码位于bsp-src/linux-6.6。2. 为原厂内核添加 sched_ext
Linux 6.6 主线不包含 sched_ext;该调度类在 Linux 6.12 才正式合入主线。本项目 使用 sched-ext 项目在主线化之前维护的非主线
v6.6.4-scx2发布树作为移植源, 再适配 K1 厂商 Linux 6.6 的调度器和 cgroup 接口。内核补丁以 K1 Linux 提交
46ef2dae4ac50c7e556137e4cbbdcc024921d9e3为准确基线。两个补丁必须按编号顺序 应用:第一个补丁移植 sched-ext
v6.6.4-scx2发布树中的 sched_ext 框架;第二个补丁 完成 K1 厂商树适配,并提供可选的 RVV lazy restore 实验功能。k1_defconfig已启用CONFIG_SCHED_CLASS_EXT和 BTF。Dirty lease 调度实验不依赖 RVV lazy restore;需要评估 lazy restore 时再启用:3. 构建内核与系统镜像
配置并执行完整 Buildroot 构建:
应用新的内核补丁后,可清理内核输出并重新生成镜像:
主要构建产物:
vmlinux需要包含.BTF,用于生成 CO-RE eBPF 程序使用的vmlinux.h。部署Image时需要同时部署同一次构建生成的内核模块和 rootfs。4. 应用并构建 llama.cpp 补丁
llama.cpp 补丁以官方提交
678d43d7205cffc3a8a026cb60b1de3868cc70a2为准确基线:使用 GCC 14、RVV 和 OpenMP 构建 llama-server:
部署时需要使用同一次构建生成的
llama-server、libllama、libggml和libggml-cpu。5. 编译 sched_ext/eBPF 扩展
先在 Buildroot staging 中准备目标端 libbpf、libelf 和 zlib:
构建过程使用 host Clang 生成 BPF object,使用 host bpftool 生成 skeleton,并使用 Buildroot RISC-V 工具链链接板端 loader。
src/下的 Makefile 都从tests/config.sh读取TOOLCHAIN_ROOT(含jupiter-buildroot)与板卡连接,并内置fetch-btf:从板卡取运行内核的/sys/kernel/btf/vmlinux生成vmlinux.h。若工具链不在默认位置,先设置TOOLCHAIN_ROOT或修改tests/config.sh。编译 RVV-aware sched_ext 调度器:
编译调度与 RVV 观测工具:
产物为
src/scx_llm_pin/{scx_llm_pin,rvv_lease_ctl,rvv_team_exec}与src/rvv_sched_cost/rvv_sched_cost。更换内核后,需要重新make,以便用新内核的vmlinux重新生成vmlinux.h、BPF object 和 skeleton。x86 上的同策略调度器是自包含工程(用运行内核 BTF、host clang/libbpf/bpftool):
6. 部署并运行调度器
启动包含 sched_ext 补丁的内核后,将调度器部署到板卡:
启动 patched llama-server,并指定 worker/phase hint map:
取得 llama-server 的 TGID 后,启动调度器(单策略,phase-aware 自动生效):
检查 sched_ext 状态、调度器名称和 worker 注册:
rvv_lease_ctl status应显示完整的 worker TID、OpenMPith、CPU 映射和 phase。 调度器运行期间,内核日志中不应出现 sched_ext watchdog、stall、BUG 或 Oops。Benchmark
tests/benchmark是统一的 Python benchmark(benchmark.py),覆盖三种场景:--scenariodual-asyncdual-syncmulti策略
--strategy:cfs(默认)、scx-llm-pin(主策略)、cfs-affinity;附录 中的dynamic与两个 x86 策略。板卡连接与路径默认从tests/config.sh读取。每次运行写入
results/<timestamp>_<scenario>_<strategy>_suite/,包含summary.csv、summary.json、report.md和每轮run_metrics.json;有效性门禁 覆盖请求完整性、注入顺序、频率/温度/内存、内核日志以及(开启时)RVV trace 和 调度器事件校验。多 llama 同时到达场景用--config指定 JSON 模型列表(示例见example.json)。指标口径与结果结构见tests/benchmark/README.md。双模型 c=4 正式对比
以下两个样例复现“实验数据对比”表中的
c=4工作负载:两个服务各使用 8 个 worker 共享 CPU0-7,A/B 各承载 2 个请求,A 侧请求均输出第 4 个 token 后注入 B 侧请求,两个不同文本的 prompt 均为 128 input token,每请求固定输出 256 token, 每种策略重复 30 个有效轮次。先设置板端路径:使用 Linux 默认 CFS/EEVDF 调度器运行 30 轮:
使用 scx-llm-pin(Phase-Aware Dirty Lease,需要 ABI v4 hinted runtime)运行相同 工作负载:
两个命令都会自动执行请求完整性、注入顺序、频率、温度、内核日志和服务存活检查;
scx-llm-pin还会校验 worker 注册、phase hint、VDSQ 收支、定向抢占和 pending safety,无效 attempt 不进入汇总。每次运行结束后可在结果目录的report.md查看 Aggregate TPS、逐请求 TTFT、pooled p95 TTFT 和上下文切换。文档
src/kernel_patches/README.md:内核补丁来源、准确基线、 构建、RVV lazy restore 和回退。src/llama_cpp_patches/README.md:llama.cpp hint API、 ABI、交叉编译和无调度器运行方式。src/scx_llm_pin/README.md:主调度策略、参数、用户态控制 和故障排查。src/x86_scx_llm_pin/README.md:x86/SMT 移植与用法。src/rvv_sched_cost/README.md:RVV、调度和 futex 指标 定义。tests/benchmark/README.md:统一 benchmark 的场景、策略、 指标、有效性检查和报告生成。上游项目与许可证
本项目为混合许可证作品:默认许可证为
GPL-2.0-only,其余文件以上游许可证或 文件头部SPDX-License-Identifier标识为准。顶层声明见LICENSE, 第三方组件、版本/基线与版权人见THIRD_PARTY_NOTICES.md, 许可证正文见LICENSES/。内核补丁遵循 Linux 内核 GPL-2.0-only 许可证。llama.cpp 修改遵循上游 MIT License,共享 ABI 头文件使用
GPL-2.0 OR MIT。scx_llm_pinBPF 调度器和 loader 使用 GPL-2.0,rvv_sched_cost各文件的许可证以其 SPDX 标识为准。补丁来源、版权 和许可证边界见对应补丁目录的 README。附录:dynamic 与 x86 策略
主策略是
scx-llm-pin;benchmark 另外支持以下对照策略。dynamic(dynamic_threads 动态授权)
src/dynamic_threads(RISC-V)与src/x86_dynamic_threads(x86)按 graph 边界 动态授予 OpenMP team 宽度:patched llama-server 通过 Unix socket 与dt_controller协商,控制器用 sched_ext 把授权 worker 放到 custom DSQ。板端用法:x86_scx_llm_pin(scx-llm-pin 的 x86 移植)
src/x86_scx_llm_pin用 ABI v4 hint 替代 RISC-Vsstatus.VS分类,并把 2×4 worker 固定 spread 到 4C/8T 的 CPU0-3 / CPU4-7(每个 server-t 4 -tb 4,--team PID:4:4:0xff)。先构建:在 x86 主机本地直跑(
--local,不经 SSH;加载 sched_ext 需要 root):x86_dynamic(x86 动态授权)
src/x86_dynamic_threads/build/dt_controller是 x86 版控制器,同样本地直跑:example.json中的 model 需替换为本机可用的 GGUF;x86 场景建议每个模型threads = threads_batch = 4。