然而,RVV 的引入带来了值得单独研究的操作系统调度挑战。x86 AVX-512 与 ARM SVE 等架构同样存在扩展状态上下文管理问题,但 RVV 的变长寄存器(VLEN 由实现定义,最大可达数千 bit)与 Vector Length Agnostic(VLA)设计使其系统代价与管理复杂度显著不同。在RISC-V 架构中,每个 hart 拥有 32 个向量寄存器(v0–v31),单次完整 V 状态保存需要512 字节至 1024 字节以上的内存写入。Linux 通过 VS 状态位配合惰性上下文管理策略(lazy save / restore)降低不必要的状态切换开销,但在多向量密集型线程并发运行时,当向量状态为 dirty 的线程被抢占,仍会触发 V 寄存器组的完整保存,在 LLM 推理这类高强度持续向量计算场景下积累出不可忽视的调度开销。
LLM 推理是典型的高强度持续向量计算场景:prefill 阶段执行大规模矩阵乘(GEMM),decode阶段执行矩阵向量乘(GEMV),两个阶段均需长时间持续占用向量寄存器。在推理服务器同时处理多个并发请求时,Linux 默认调度策略(CFS)对向量密集型推理线程与普通线程一视同仁,导致推理线程在计算关键路径上被频繁抢占,积累的 V 状态切换开销造成整体吞吐下降与尾延迟(P99 TTFT)恶化。
目前,学术界和工业界均缺乏专门针对 RISC-V V 状态感知的 LLM 推理调度策略,这是一个兼具理论价值与工程意义的开放问题。本赛题以 RISC-V Vector 扩展为实验平台,其核心关注点——具有高代价扩展状态的处理器在 AI 推理负载下的 OS 调度协同——同样适用于 x86 AMX/AVX-512、ARM SVE2 等架构,参赛成果具备向其他平台迁移的理论基础。
a. 结合 CPU affinity 与 cgroup cpuset 策略,将推理服务的工作线程池统一绑定至指定核心集合(llama.cpp 可通过 --cpu-mask / --cpu-range 配合 -t / -tb 参数进行线程与 CPU 绑定控制),降低线程迁移与抢占导致的 V 状态保存/恢复频率(入门级方案,同等有效);
b. 基于 sched_ext(Linux 6.12+ 的 BPF 可扩展调度框架)实现 V 状态感知调度器,或基于 sched_setattr / affinity / cpuset 等现有接口对持有 Dirty V 状态的推理线程进行调度提示与放置优化,减少同一时间窗口内的 V 状态切换次数(进阶方案);
Linux 内核调度器负责在多个任务之间分配 CPU 时间,是操作系统的核心子系统之一。Linux 维护了一套调度器类体系,主要包括停止(Stop)、实时(RT)、公平(CFS/EEVDF)和空闲(Idle)等调度类,其中公平调度类是多数通用场景下的默认选择。该体系在服务器和桌面等通用负载下具有较好的稳健性,但调度策略主要固化在内核代码中,实验新的调度算法通常需要修改内核源码、重新编译并部署内核,迭代和维护成本较高。
flowchart LR
A[Windows x86_64 / WSL2 RISC-V 交叉编译] -->|"llama-server / llama-cli"| B[BananaPi BPI-F3 llama-server]
A -->|"vstate_profiler.ko"| D["内核 V 状态探针"]
A -->|"vstate_observer"| G["libbpf observer"]
A -->|"调度器二进制"| S["sched_ext 调度器产物"]
A -->|"x86-avx2 llama-server + 对照脚本"| H["x86_avx2 AVX2 / XSAVE 实验模块"]
R["rvv_llm_lab/run_exp.sh 统一 run_id 编排"] --> M0
R --> M1
R --> M2
subgraph M["调度模式(三选一,每轮只启用一种)"]
direction TB
M0["Baseline CFS 默认调度"]
M1["scx_vstate V-hot 长时间片 / vtime / 原 CPU 直派"]
M2["dynamic_sched_ext phase_hints / Prefill-Decode 权重 / sticky-adaptive"]
end
S --> M1
S --> M2
M0 -->|"本轮调度环境"| B
M1 -->|"本轮调度环境"| B
M2 -->|"本轮调度环境"| B
B -.->|"仅 dynamic 模式写入 phase_hints"| M2
E["异步 loadgen 2/4/8 并发"] -->|"/v1/completions"| B
B -->|"TGID、sched_switch、迁核"| D
B -->|"sched_switch、迁核辅助观测"| G
D -->|"debugfs stats/csv"| F["结果合并与统计分析"]
G -->|"observer.json"| F
B -->|"流式 token、TTFT、latency"| F
H -->|"LLM 指标、上下文切换、xstate 字节"| F
出口探针读取当前单调时钟,与入口保存的 start_ns 相减得到本次 V 状态保存耗时。统计写入 this_cpu_ptr(&cpu_stats) 指向的 per-CPU 结构,避免多 CPU 并发更新时发生全局锁竞争。恢复路径采用同样的入口/出口结构;若目标内核的恢复热路径不适合探测,分析脚本将结果标记为 module_save_only_lower_bound,表示当前 V 状态时间为保存侧下界。
elapsed_ns() 描述本轮采样窗口持续时间;若读取发生在实验停止前,则用当前时间临时计算已运行时长。cpu_capacity_ratio() 将 V 状态保存/恢复总耗时除以“实验窗口时间 × 在线 CPU 数”,得到 vstate_cpu_capacity_ppm。相比单独报告总毫秒数,该指标更适合比较不同并发和不同运行时长的实验,因为它表示 V 状态管理消耗了整机 CPU 时间容量中的百万分之几。
sched_ext/vstate/ 中的 scx_vstate 是一个基于 Linux sched_ext 的 RISC-V V 状态感知调度器。它以内核在任务停止运行时读取到的 RISC-V VS=Dirty 作为向量热点信号:当任务切出时仍为 Dirty,表明该线程刚使用过 RVV,后续再次被短时间片打断时更可能产生 V 状态保存压力。调度器据此将任务标记为短期 V-hot,并在后续 CPU 选择、时间片和虚拟运行时间计费中给予有限倾斜。
该调度器的基本流程是:任务被唤醒时首先执行 select_cpu(),优先将 V-hot 任务放回上次运行的空闲 CPU;若无法本地直派,则在 enqueue() 中按虚拟运行时间插入共享 VSTATE_DSQ;CPU 本地队列为空时由 dispatch() 从共享队列取出任务;任务开始运行时由 running() 记录运行 CPU 和起始时间;任务停止运行时由 stopping() 读取 VS 状态、更新 V-hot 历史并完成 vtime 计费。该过程仅改变任务选择、时间片和公平计费,不修改 Linux 原有的 V 状态保存/恢复语义。
select_cpu() 是任务被唤醒时的第一道放置决策。对于 V-hot 任务,调度器优先检查上次运行 CPU 是否仍在任务亲和性集合内且处于 idle 状态;若满足条件,则将任务插入该 CPU 的本地 DSQ,使线程继续在原 CPU 上运行。该策略主要降低迁核带来的 cache/TLB 扰动,并减少 V-hot 线程在共享队列中被其他任务反复插队的概率。若原 CPU 不空闲,则回退到 sched_ext 默认选核逻辑,以避免过度粘性导致并行 worker 在同一 CPU 上排队。
#define VSTATE_PHASE_HINT_ABI_VERSION 2
#define VSTATE_PHASE_MAP_PATH "/sys/fs/bpf/scx_vstate/phase_hints"
enum vstate_phase {
VSTATE_PHASE_NONE,
VSTATE_PHASE_IDLE,
VSTATE_PHASE_PREFILL,
VSTATE_PHASE_DECODE,
VSTATE_PHASE_MIXED,
VSTATE_PHASE_NR,
};
struct vstate_phase_hint {
unsigned int abi_version;
unsigned int phase;
unsigned long long updated_ns;
unsigned int prefill_tokens;
unsigned int decode_tokens;
unsigned int batch_tokens;
unsigned int reserved;
};
_Static_assert(sizeof(struct vstate_phase_hint) == 32, "phase hint ABI changed");
if (partial)
skel->struct_ops.vstate_ops->flags |= SCX_OPS_SWITCH_PARTIAL;
link = SCX_OPS_ATTACH(skel, vstate_ops, scx_vstate);
if (partial) {
for (unsigned int i = 0; i < nr_targets; i++)
set_process_sched_ext(targets[i]);
}
for (unsigned int i = 0; i < nr_targets; i++)
add_target_tgid(skel, targets[i]);
while (!exit_req && !UEI_EXITED(skel, uei)) {
print_stats(skel);
sleep(10);
}
板端运行时使用:
cd /home/kiara/bananaOS/dynamic_sched_ext
make -j"$(nproc)"
sudo ./build/scx_vstate \
--partial \
--sticky-adaptive \
--phase-policy proportional \
-p "$(pidof llama-server)"
add_opt(common_arg(
{"-t", "--threads"}, "N",
"number of CPU threads to use during generation",
[](common_params & params, int value) {
params.cpuparams.n_threads = value;
}
));
add_opt(common_arg(
{"-tb", "--threads-batch"}, "N",
"number of threads to use during batch and prompt processing",
[](common_params & params, int value) {
params.cpuparams_batch.n_threads = value;
}
));
add_opt(common_arg(
{"-np", "--parallel"}, "N",
"number of server slots",
[](common_params & params, int value) {
params.n_parallel = value;
}
));
两种调度器都有效降低了 V 状态保存次数和保存时间。在 2、4、8 并发下,V-hot 和 V-hot User-Kernel Cooperative Scheduling 相比 Baseline 都减少了 save count、save time 和 saves/s,说明 V 状态感知调度确实降低了 Dirty V 线程被频繁切出的概率。
动态PD阶段提示调度在高并发下优势更明显
8 并发下,协同调度 save count 降低约 34.3%,save time 降低约 32.7%,TPS 提升约 12.4%,Mean TTFT 降低约 12.5%,P95 TTFT 降低约 10.9%。这说明阶段提示在高并发下能更好地区分 Prefill/Decode 工作特征,调度收益更稳定。
本项目在完成 V 状态开销量化和 sched_ext V 状态感知调度器的基础上,进一步完成了赛题可选完成项 c 和 d:其一,设计并实现 llama.cpp 到 BPF 调度器的用户态-内核阶段提示接口,使推理框架能够向调度器声明 Prefill/Decode/Mixed 计算阶段;其二,新增 x86 AVX2 横向对照实验模块,用定量数据分析 RVV VLA 设计在 V 状态切换问题上的特殊性。以下创新点按“真实状态观测—调度策略实现—可选项扩展—实验闭环验证”的逻辑展开。
5.1 基于架构 Dirty 状态的真实 V-hot 反馈
常见的 CPU affinity 方案只能静态限制线程运行位置,基于进程名或 CPU 利用率的策略也无法判断任务切出时是否需要保存向量寄存器。本项目在 sched_ext 的 stopping() 时点读取 RISC-V pt_regs.status.VS,将 VS=Dirty 转换为调度器可消费的 V-hot 信号。该信号来自体系结构状态,而非工作负载名称或外部经验规则,因此能够更准确地描述“本次切出是否可能产生 V 状态保存压力”。
bananaOS:面向 RISC-V Vector 的大语言模型并发推理调度优化
项目简介
bananaOS 在真实 RISC-V Vector(RVV)平台上研究大语言模型并发推理时的向量寄存器状态保存开销。项目以 llama.cpp 的
llama-server为负载,使用内核探针测量 V 状态保存次数与耗时,通过 Linuxsched_ext识别近期使用 RVV 的 V-hot 线程,并结合 Prefill/Decode 阶段提示调整时间片、虚拟运行时间和 CPU 放置。实验框架将内核观测、调度日志、TPS 和首 Token 延迟按同一run_id汇总。应用场景:搭载 RVV 1.0 的 RISC-V 设备在 CPU 上同时处理多路量化模型推理请求;也可用于研究向量密集型任务的调度与上下文切换开销。项目的主要验证平台为 BananaPi BPI-F3(SpacemiT K1)。
主要功能:
vstate_profiler内核模块与 eBPF observer 采集 V 状态保存、调度切换和迁核数据。scx_vstate根据VS=Dirty历史识别 V-hot 线程,配置时间片、vtime 权重与 CPU 局部性;服务端还可通过 BPF Map 声明 Prefill、Decode、Mixed 阶段。详细设计见技术报告和项目创新说明;包含完整参数、故障排查与复现实验矩阵的操作手册见完整构建与运行说明。
目录结构
src/llama.cpp/src/sched_ext/vstate/scx_vstate调度器及加载器,阶段提示 ABI v3src/dynamic_sched_ext/、src/long_time_sched_ext/src/os_module/src/rvv-llm-lab/src/rvv_llm_lab/src/x86_avx2/tests/rvv_llm_lab/result/docs/、presentation/以下命令均从项目根目录执行。迁移仓库后建议重新配置 CMake 构建目录;模型路径、内核树和工具链路径通过变量设置,不依赖某个用户的主目录。
系统环境与依赖要求
6.18.37-current-spacemit,启用sched_ext、BPF、BTF、kprobe 和 debugfsvmlinux,板端 BTF,RISC-V 64 位 GNU 交叉工具链aiohttp在 Debian/Ubuntu/Armbian 上可准备常用构建依赖:
板端先确认内核能力:
内核模块和调度器需要与实际运行的内核匹配。
KERNEL_SRC应指向对应内核源码树,且该树包含带符号的vmlinux;VMLINUX_BTF应指向从板端/sys/kernel/btf/vmlinux取得的 BTF 文件。两者用途不同。构建方法
1. 在 Linux/WSL2 构建 RISC-V 产物
先设置本机路径,再执行项目提供的构建命令:
生成的主要文件是
src/llama.cpp/build-bpi-rvv/bin/llama-server、src/os_module/vstate_profiler.ko和src/sched_ext/vstate/build/scx_vstate。调度器的deps目标下载并解包 RISC-V 静态库;已有目标架构依赖时,也可通过TARGET_DEPS_SYSROOT指定 sysroot。构建细节及板端原生构建 llama.cpp 的方法见完整构建与运行说明。2. 在 RISC-V 开发板构建观测与压测工具
先将项目源码复制到开发板,再在板端的项目根目录执行:
产物为
src/rvv-llm-lab/bin/vstate_observer、src/rvv-llm-lab/ebpf/vstate_observer.bpf.o和src/rvv-llm-lab/bin/rvv_vlenb。可执行src/rvv-llm-lab/bin/rvv_vlenb检查向量寄存器字节数;K1 的预期值为vlenb=32。安装与板端准备
将完整项目目录复制到开发板的任意位置,并把交叉构建产物放回板端项目中的相同相对路径。如果直接在板端构建对应组件,这一步无需复制该组件的产物。例如,在构建主机的项目根目录、且板端已有项目源码时,可这样同步三个交叉构建产物(替换板端地址与目录):
另行准备已获使用许可的 GGUF 模型,设置其实际路径;模型文件不包含在仓库中。
在板端项目根目录挂载所需文件系统并加载模块:
run_exp.sh会控制已加载模块的采样窗口,并按轮启动/停止受管调度器;模块本身由上述insmod命令加载。正式对照实验还需将各 CPU policy 的 governor 固定为performance:运行方法
在开发板项目根目录设置公共参数。
MODEL应指向板端实际存在的 GGUF 文件,LLAMA_SERVER和SCX_BIN指向板端构建产物:实验脚本会启动
llama-server,等待/health就绪,启动 observer 和可选调度器,发送流式推理请求,然后保存结果并清理本轮进程。三种运行模式:脚本默认启用
vstate_profiler采样,运行前需完成上面的模块加载。每轮输出位于src/rvv-llm-lab/results/<run_id>/,跨轮摘要追加到src/rvv-llm-lab/results/summary.csv。需要不同模型、线程、并发或路径时,覆盖相应环境变量即可。测试与验证方法
llama-server与scx_vstate为 RISC-V 可执行文件,并确认模块的 vermagic 与板端uname -r一致。REQUESTS=2 MAX_TOKENS=16运行 Baseline,再运行协同调度组;检查loadgen_result.json的error_count、observer.json、vstate_profiler_stats_raw/debugfs_stats.txt和scx_vstate.log。CONCURRENCY与PARALLEL同设为 2、4、8,每种策略和并发组合独立运行至少 3 次。对 TPS、平均 TTFT、P95 TTFT、V 状态保存次数和保存耗时计算均值及样本标准差。例如,检查一次冒烟结果:
仓库中的既有实验记录提供每轮配置、请求输出、内核观测与汇总 CSV;具体的多轮统计和排查命令见完整构建与运行说明。
以下是技术报告
面向 RISC-V Vector 扩展的大语言模型推理并发调度优化技术报告
1-6赛题题目:面向 RISC-V Vector 扩展的大语言模型推理并发调度优化(高校赛题)
参赛小组:组里都飞升了
目录:
voice_parser.py)query_builder.py)speech_service.py)engine/rendering/)engine/camera/)engine/plugins/)app/data/)webworker/)moonbit-injuryrisk)sched_ext技术scx_vstate调度器模块实现sched_ext中时间片、vtime 与 CPU 局部性的组合策略1、赛题背景及赛题要求
1.1 赛题背景
RISC-V Vector 扩展(RVV 1.0)已被纳入 RVA23 Profile 强制规范(2024 年 10 月批准),正在成为 RISC-V 处理器的标准配置。2025 年初,llama.cpp 等主流大语言模型(LLM)推理框架完成了 RVV 1.0 适配,使得在 RISC-V 设备上运行 LLM 成为现实。
然而,RVV 的引入带来了值得单独研究的操作系统调度挑战。x86 AVX-512 与 ARM SVE 等架构同样存在扩展状态上下文管理问题,但 RVV 的变长寄存器(VLEN 由实现定义,最大可达数千 bit)与 Vector Length Agnostic(VLA)设计使其系统代价与管理复杂度显著不同。在RISC-V 架构中,每个 hart 拥有 32 个向量寄存器(v0–v31),单次完整 V 状态保存需要512 字节至 1024 字节以上的内存写入。Linux 通过 VS 状态位配合惰性上下文管理策略(lazy save / restore)降低不必要的状态切换开销,但在多向量密集型线程并发运行时,当向量状态为 dirty 的线程被抢占,仍会触发 V 寄存器组的完整保存,在 LLM 推理这类高强度持续向量计算场景下积累出不可忽视的调度开销。
LLM 推理是典型的高强度持续向量计算场景:prefill 阶段执行大规模矩阵乘(GEMM),decode阶段执行矩阵向量乘(GEMV),两个阶段均需长时间持续占用向量寄存器。在推理服务器同时处理多个并发请求时,Linux 默认调度策略(CFS)对向量密集型推理线程与普通线程一视同仁,导致推理线程在计算关键路径上被频繁抢占,积累的 V 状态切换开销造成整体吞吐下降与尾延迟(P99 TTFT)恶化。
目前,学术界和工业界均缺乏专门针对 RISC-V V 状态感知的 LLM 推理调度策略,这是一个兼具理论价值与工程意义的开放问题。本赛题以 RISC-V Vector 扩展为实验平台,其核心关注点——具有高代价扩展状态的处理器在 AI 推理负载下的 OS 调度协同——同样适用于 x86 AMX/AVX-512、ARM SVE2 等架构,参赛成果具备向其他平台迁移的理论基础。
1.2 赛题要求
1、基于 openEuler、openKylin、OpenHarmony 等至少一个国内主流开源操作系统开发,鼓励在更多Linux发行版上编译、运行和测试。
2、问题量化(必须完成) 在 RISC-V RVV 1.0 环境下,可采用内核 tracepoint、kprobe、eBPF 程序、perf/PMU 或其他等价、可复现的系统观测手段,对 V 状态保存/恢复路径插桩(如挂载内核函数
riscv_vstate_save),量化多并发 LLM 推理场景下 V 状态上下文切换的实际开销,建立其与并发请求数、模型规模之间的量化关系,分析该开销对推理吞吐与延迟的影响。若实验平台 PMU 支持相关硬件事件,可作为辅助验证手段,但非必要。3、调度优化实现(必须完成,以下策略为推荐参考路径,允许使用其他等价的OS 调度优化机制,至少完成一种)
a. 结合 CPU affinity 与 cgroup cpuset 策略,将推理服务的工作线程池统一绑定至指定核心集合(llama.cpp 可通过
--cpu-mask/--cpu-range配合-t/-tb参数进行线程与 CPU 绑定控制),降低线程迁移与抢占导致的 V 状态保存/恢复频率(入门级方案,同等有效);b. 基于
sched_ext(Linux 6.12+ 的 BPF 可扩展调度框架)实现 V 状态感知调度器,或基于sched_setattr/ affinity / cpuset 等现有接口对持有 Dirty V 状态的推理线程进行调度提示与放置优化,减少同一时间窗口内的 V 状态切换次数(进阶方案);4、性能评测(必须完成)
以 llama.cpp server 模式为基准,使用不超过 3B 参数量的量化模型(如 Llama-3.2-1B/3B
Q4_K_M),在 2、4、8 并发请求场景下,采用统一基线配置(相同模型与量化版本、相同--threads参数、相同输入长度分布、CPU 频率策略固定为performancegovernor),每组配置至少重复 3 次,报告均值与标准差,对比优化前后的推理吞吐(TPS)、平均 TTFT 与 P95 TTFT(毫秒)。性能评测数据须来自真实 RVV 1.0 硬件。5、可选完成项
探索用户态-内核调度提示接口设计(如通过 sched_setattr 扩展 flag 或自定义 ioctl),允许推理框架向调度器声明计算阶段,不限定接口形式;
与 x86(AVX2/AVX-512)平台进行横向对比,定量分析 RVV VLA 设计在V 状态切换问题上的特殊性;
向 llama.cpp 或 Linux 内核上游社区提交相关补丁或 RFC,附上链接。
2、项目设计
2.1 技术落地可行性方案
2.1.1 eBPF 技术
eBPF(extended Berkeley Packet Filter)是 Linux 内核提供的安全可编程执行机制,允许用户在受验证和受限制的特权上下文中运行沙盒化程序。用户态程序可以将小型 eBPF 程序动态加载到内核中,并在系统调用、函数入口/出口、内核跟踪点、网络数据包到达等事件触发时执行。eBPF 源于经典 BPF(Berkeley Packet Filter),其应用范围已从网络包过滤扩展到可观测性、安全、性能分析和内核策略扩展等领域。
在传统内核扩展方式中,开发者通常需要编写内核模块,或修改内核源码后重新编译和部署内核。前者具有较高权限,一旦实现错误可能影响系统稳定性;后者维护成本高,迭代周期长。eBPF 提供了另一种工程路径:在不修改内核源码、不重新构建内核的前提下,通过受验证程序向内核注入有限逻辑。所有 eBPF 程序在加载前均需通过内核验证器检查,包括循环边界、非法指令和内存访问安全等约束;加载后,eBPF 字节码可由 JIT 编译器转换为目标平台原生机器指令,从而兼顾安全边界和执行效率。
eBPF 程序的开发与运行流程包括:使用受限 C 子集编写内核侧逻辑,通过 LLVM 编译为 eBPF 字节码,再通过
bpf()系统调用加载到内核。加载阶段由验证器进行安全审查,通过验证后由 JIT 编译器生成原生机器码。eBPF 程序与用户态程序之间通常通过 eBPF Map 交换数据;Map 是内核中持久存在的键值对数据结构,可由 eBPF 程序读写,也可由用户态程序通过系统调用访问。 本项目使用固定在 bpffs 中的 eBPF Map(
/sys/fs/bpf/scx_vstate/phase_hints)作为 llama.cpp 与 sched_ext 调度器之间的共享键值接口。用户态在推理阶段变化时,将计算线程 TID、PREFILL/DECODE 阶段、Token 数量及更新时间写入 Map;运行在内核态的调度器根据当前任务查询提示,动态调整时间片、调度优先级和 CPU 粘性。 提示超过 TTL 后自动失效,调度器退回基于实际 RVV 活跃度的 V-hot 策略,避免错误提示长期影响调度。该方案无需修改系统调用接口或重新编译内核,具有开销低、接口灵活、用户态与调度策略解耦等优点。
2.1.2 Linux 调度与
sched_ext技术 Linux 内核调度器负责在多个任务之间分配 CPU 时间,是操作系统的核心子系统之一。Linux 维护了一套调度器类体系,主要包括停止(Stop)、实时(RT)、公平(CFS/EEVDF)和空闲(Idle)等调度类,其中公平调度类是多数通用场景下的默认选择。该体系在服务器和桌面等通用负载下具有较好的稳健性,但调度策略主要固化在内核代码中,实验新的调度算法通常需要修改内核源码、重新编译并部署内核,迭代和维护成本较高。
sched_ext(Extensible Scheduler Class)是 Linux 6.12 引入的 BPF 可扩展调度框架。其核心特征是将调度策略从固定内核实现中抽离出来,由一组 BPF 程序在运行时定义。sched_ext导出任务选核、入队、分发、运行和停止等调度接口,使开发者能够在不修改内核源码、不重启系统的情况下加载和卸载实验性调度器。sched_ext在内核中作为独立调度类存在;当 BPF 调度器未加载或退出时,系统可以回退到默认公平调度器。1、框架定义基础事件驱动:sched_ext 的工作流程由调度事件驱动。当内核调度器核心发生任务唤醒、CPU 空闲等事件时,会调用
sched_ext_ops中对应的 BPF 回调函数,使用户定义的调度策略参与任务选择。主要回调函数包括:select_cpu**:任务被唤醒时调用,用于选择目标 CPU,是实现负载均衡和局部性策略的重要入口。enqueue/dequeue**:任务加入或移出调度队列时调用,可用于维护自定义队列和优先级结构。dispatch**:CPU 需要可运行任务时调用,负责从调度器队列中选择任务并分发到 CPU 本地 DSQ。tick**:调度器时钟 tick 到来时调用,可用于实现基于时间片的控制或检测任务运行时间。runnable/running/stopping**:分别在任务变为可运行、开始运行、停止运行时调用,用于跟踪任务状态。通过实现这些回调,调度器可以控制任务排队位置、运行时机和运行时间等关键调度行为。
2、调度队列设计:
sched_ext_ops定义调度逻辑,任务队列由 Dispatch Queues(DSQ)承载。DSQ 将任务排队与具体 CPU 执行解耦,使 BPF 调度器能够根据策略将任务分发到不同 CPU。sched_ext 提供以下队列类型:SCX_DSQ_GLOBAL):系统全局 FIFO 队列,所有 CPU 均可从中获取任务,适合作为基础共享队列模型。SCX_DSQ_LOCAL):每个 CPU 对应一个本地 FIFO 队列,CPU 从本地 DSQ 取出任务执行;dispatch回调负责将任务从其他队列移动到本地 DSQ。scx_bpf_create_dsq()创建自定义 DSQ,用于实现优先级队列、虚拟时间队列或负载分类队列等策略。3、看门狗兜底:sched_ext 将系统完整性置于调度策略之上。BPF 调度器需要满足验证器约束,并接受运行时看门狗保护,主要机制包括:
SysRq-S时,sched_ext 会中止当前 BPF 调度器,并将任务切回内核默认公平调度器。SCX_OPS_SWITCH_PARTIAL标志,sched_ext 可仅调度显式标记为SCHED_EXT策略的任务,其他普通任务(SCHED_NORMAL等)继续由默认公平调度类管理,适合在实验环境中控制影响范围。2.1.3 llama.cpp 框架应用方案
llama.cpp 是基于 C/C++ 实现的开源大语言模型推理框架,设计目标是在资源受限设备上提供轻量化 CPU 推理能力。框架以 GGUF 量化模型文件作为主要模型载体,支持通过命令行工具和服务化组件完成推理调用。
llama-cli适合本地交互式推理,llama-server提供与 OpenAI 兼容的 HTTP API,便于将推理能力接入现有应用系统。服务模式支持流式输出,实验框架可以据此记录首个非空输出片段的到达时间,并计算 TTFT。 llama.cpp 的部署依赖较少,不依赖 PyTorch、TensorFlow 等完整深度学习运行时,编译后可生成相对独立的二进制程序。模型量化是其降低内存和存储需求的重要机制:通过将模型权重从 32 位浮点数压缩至 4 位或 8 位整数,可以显著降低模型文件体积和运行时内存占用。框架同时支持多核并行计算,并针对 x86 AVX2/AVX-512、ARM NEON、RISC-V RVV 等 SIMD/向量扩展提供后端优化路径,适合作为本项目的 RISC-V LLM 推理负载。
在 RISC-V 架构适配方面,llama.cpp 及其底层计算后端 ggml 已提供 RVV 相关支持。2025 年 3 月,RISC-V 128 位向量扩展支持补丁合入 ggml 主分支,框架正式具备 RVV 后端能力。在此基础上,社区围绕 RVV 1.0 扩展持续优化矩阵向量乘法等关键算子。公开结果显示,在 64 核 RISC-V 平台上,Prompt 处理吞吐量可从基准版本的 14.01 tokens/s 提升至 35.53 tokens/s,生成阶段吞吐量可从 8.74 tokens/s 提升至 17.33 tokens/s;在特定 RISC-V AI 核心上,Prompt 处理速度报告值可达到 111 tokens/s。此外,框架 CPU 后端针对 RISC-V 提供
GGML_RV_ZFH、GGML_RV_ZVFH等编译选项,用于控制半精度浮点和向量相关特性的启用状态。这些适配工作说明 RISC-V 平台已具备承载大语言模型推理任务的工程基础。 llama.cpp 内置了多项性能统计接口。通过
llama_perf接口或llama_perf_print函数,可以获取当前推理上下文的计时与计数数据,包括模型加载耗时t_load_ms、提示词处理耗时t_p_eval_ms、token 生成耗时t_eval_ms、已处理提示词 token 数n_p_eval、已生成 token 数n_eval以及计算图复用次数n_reused。基于这些原始数据,可以计算 prompt 处理速度和生成速度。在更底层的性能分析中,还可以结合 Linuxperf等工具采集缓存访问、指令执行和浮点/向量相关硬件事件,用于定位特定硬件平台上的计算热点和性能瓶颈。2.1.4 x86_64 到 RISC-V 的交叉编译技术
x86 到 RISC-V 的交叉编译是 RISC-V 应用开发中常用的工程方法。GCC 和 LLVM 的 RISC-V 后端已较为稳定,并被主流 Linux 发行版收录为官方工具链组件。对于 llama.cpp、Linux 内核模块和 BPF 用户态加载器等包含较多 C/C++ 源文件的项目,交叉编译可以显著缩短构建时间,避免在算力有限的开发板上进行长时间原生编译。本项目采用 x86_64 WSL2 主机构建、RISC-V 板端部署的流程,其优点如下:
-march参数和依赖路径,减少板端环境差异对构建结果的影响。 因此,本项目将交叉编译作为默认构建方式,用于保证 llama.cpp、内核模块、BPF skeleton 和用户态加载器在同一工具链约束下生成可部署产物。
2.1.5 RISC-V 多核平台上的 LLM 推理优化技术(V-Seek)与 PerfXLM 等 RISC-V 推理工作
V-Seek 研究了在 64 核 Sophon SG2042 RISC-V 平台上运行 llama.cpp 的性能优化,主要包括三个方向:
论文结果表明,优化向量内核能够改善数据局部性和计算吞吐;Clang 19 在其测试中优于 GCC 13.2;关闭不合适的 NUMA balancing 并采用内存交错可以减少线程和内存页迁移。论文在 Llama 7B、DeepSeek R1 Distill Llama 8B 和 Qwen 14B 等模型上报告了吞吐提升。
PerfXLM 是较早公开展示的 RISC-V CPU 大模型推理引擎工作,其公开材料介绍了 RISC-V 平台上的 LLM 推理引擎设计、计算内核和性能优化方法。此外,已有研究还从专用向量处理器、低比特量化和软硬件协同等方向提高 RISC-V 上的神经网络或语言模型推理性能。这些工作主要关注计算内核效率提升,包括提高向量单元利用率、减少权重访存、改进量化点积以及设计专用指令或加速器。
2.2 问题量化方案
2.2.1 RVV 与 Linux V 状态管理技术
RISC-V Vector Extension(RVV)采用向量长度无关(Vector Length Agnostic,VLA)的编程模型。软件通过
vsetvl/vsetvli根据硬件实现的向量寄存器长度VLEN和本次计算的数据类型设置有效向量长度vl,同一套向量代码因而可以运行在不同VLEN的处理器上。RVV 架构包含 32 个向量寄存器v0~v31,并包含vl、vtype、vstart、vcsr等控制状态。VLA 提高了程序的可移植性,但操作系统在任务切换时必须为每个使用过 RVV 的任务维护与实际VLEN对应的寄存器映像。
sstatus.VS是 RISC-V 特权架构中用于描述 Vector 上下文状态的字段。Linux 目标内核使用Off、Initial、Clean和Dirty四种编码:Off表示当前上下文不允许直接使用 Vector;Initial表示 Vector 状态已初始化;Clean表示内存映像与寄存器状态一致;Dirty表示任务执行 Vector 指令后,寄存器内容可能已经改变,需要在相应的上下文切换路径上保存。需要强调的是,Dirty是体系结构提供的状态提示,并不直接给出保存耗时;具体是否调用保存函数以及何时恢复,取决于目标 Linux 内核的实现。 在本项目使用的
6.18.37-current-spacemit内核中,__switch_to_vector()调用riscv_v_vstate_save()检查切出任务的VS状态。仅当状态为Dirty时,内核才执行__riscv_v_vstate_save(),随后将状态标记为Clean;对于即将运行的任务,内核设置延迟恢复标志,并在返回用户态前执行恢复。该机制避免了对未使用 Vector 或寄存器映像未变化任务的无条件保存,可以视为基于VS状态的条件保存与延迟恢复机制。该机制仍无法消除一种情况:若一个持续执行 RVV 指令的可运行线程被调度器切出,其状态通常已经变为Dirty,此次保存属于保证任务语义正确所必需的操作。因此,本项目不修改 Linux V 状态保存/恢复的正确性逻辑,而是在调度层减少由策略导致的、可避免的 V-hot 线程切出。 对于 SpacemiT K1 的
VLEN=256实现,仅 32 个向量寄存器组的一次完整传输量为:SV−reg=32×8VLEN=32×8256=1024 Byte
该式描述向量寄存器组的主要数据量,不包含
vl、vtype、vstart、vcsr等控制状态的读写,也不等同于缓存或内存总线上的实际流量。真实时间还受 CPU 频率、缓存命中、内存层次、探针开销和同时运行任务影响,因此必须在真机上直接测量,不宜仅根据 1024 Byte 推算延迟。2.2.2 kprobe、kretprobe、tracepoint 与 eBPF 观测技术
Linux 提供多类动态观测机制。调度 tracepoint(如
sched_switch和sched_migrate_task)接口相对稳定,适合统计任务切换与迁核行为,但一次sched_switch并不必然伴随 V 状态保存。kprobe 可以在允许探测的内核指令位置执行处理函数,kretprobe 在函数进入和返回时分别记录时间,适合测量具体保存函数的调用次数及持续时间;其限制是内联函数可能没有独立全局符号,局部同名符号可能指向不同调用路径,.noinstr.text等黑名单区域也不允许动态探测。eBPF 配合 libbpf 可以安全地加载受验证的内核观测程序、使用 per-CPU map 低开销汇总数据,并将附着状态输出到用户态,但它同样受目标内核 kprobe 可用性约束。基于上述差异,本项目采用“内核模块主测量 + eBPF 辅助观测”的两级方案,避免将调度切换次数直接等同于 V 状态保存次数:
vstate_profiler.ko从与开发板内核完全一致的vmlinux中解析调度路径的局部__riscv_v_vstate_save目标,使用 kretprobe 统计调用次数与纳秒耗时,并通过target_tgid只选择目标llama-server进程。vstate_observer.bpf.o尝试附着 V 状态 save/restore kprobe,同时始终使用调度 tracepoint 统计目标进程切换和迁核;若函数探针不可用,JSON 结果记录附着失败并退化为调度行为观测。run_id对齐。kprobe/kretprobe 会向被测路径引入固定观测开销,因此绝对纳秒数不应直接解释为无探针条件下的纯硬件开销。实验设计要求基线组与优化组加载相同版本的 profiler、使用相同过滤方式,并以相对变化作为主要证据;若未来需要报告绝对周期,应增加空探针标定或使用内核内建静态计时点进行校正。
2.2.3 量化指标
主要字段及其数学含义如下表所示:
save_countrestore_countsave_total_nsrestore_total_nscompleted_requestselapsed_nsvstate_ns_per_requestvstate_cpu_capacity_ppm设第 i 次保存耗时为 tisave,第 j 次可探测恢复耗时为 tjrestore,在线 CPU 数为 NCPU,则:
Tsave=i=1∑Nsavetisave,Trestore=j=1∑Nrestoretjrestore
TV,obs=Tsave+Trestore,TV/req=NreqTV,obs
Rcapacity=Twall×NCPUTV,obs
2.3 调度器设计方案
2.3.1 V 状态切换问题的本质
RISC-V Vector Extension 为每个 hart 提供 32 个向量寄存器
v0–v31,寄存器宽度由硬件的 VLEN 决定。在本实验使用的 SpacemiT K1 平台上,VLEN=256 bit,因此完整保存一组向量寄存器需要写入: 恢复时还需要读取同等规模的数据,并处理
vl、vtype、vstart、vcsr等向量控制状态。因此,向量线程切换不仅包含普通通用寄存器上下文,还会产生额外的内存访问和内核执行时间。 LLM 推理中的 Prefill 和 Decode 阶段均会持续使用 RVV 指令。线程执行向量指令后,
sstatus.VS会变为 Dirty。若该线程此时被抢占、阻塞或迁移,内核必须保存其向量状态;线程再次运行时,还需要恢复相应状态。单次开销虽然有限,但在高并发推理中会随大量调度切换反复发生,并影响推理吞吐、首 Token 延迟(TTFT)和尾延迟。2.3.2 与 Linux 公平调度机制的关系
Linux 普通任务由公平调度类管理,传统上通常称为 CFS。现代内核在公平调度类中进一步采用 EEVDF 进行任务选择,其核心目标仍然是根据任务权重、虚拟运行时间和可运行状态公平分配 CPU 时间。
默认调度器不感知某个线程是否持有高代价的 Dirty V 状态,也不了解该线程正在执行 Prefill 还是 Decode。因此,LLM 计算线程与普通 CPU 密集型线程在通用调度目标下缺少区别化处理,可能出现:
公平调度的优化目标是通用公平性,而非最小化扩展寄存器状态的切换成本。因此,高并发 RVV 推理需要在公平调度基础上增加对向量状态和推理阶段的感知。
本项目通过 sched_ext 实现可扩展调度策略,不直接修改 Linux 公平调度器代码。调度器退出或加载失败时,任务可以回到 Linux 默认公平调度类,从而降低实验性调度策略对系统稳定性的影响。
2.3.3 与 Linux lazy save/restore 的关系
Linux 利用 RISC-V
VS状态位管理向量上下文。对于未使用向量指令,或向量状态未发生变化的线程,内核可以避免无意义的完整保存,从而降低普通负载下的上下文切换成本,这类机制可概括为 lazy save/restore。 然而,lazy 管理只能判断“本次是否有必要保存”,无法解决保存触发频率高的根因。LLM 计算线程通常长时间连续执行 RVV 指令,其 V 状态通常处于 Dirty。在这种负载下,关键路径上的抢占仍可能触发完整保存,lazy 机制能够省略的操作有限。因此,本项目不替代或修改 Linux 的 lazy save/restore,而是从调度层减少触发保存的次数:
两者处于不同层次并能够协同工作:lazy 机制负责保证上下文管理的正确性并避免无效保存,V 状态感知调度器负责降低必要保存发生的频率。
2.3.4 V-hot 状态感知调度
调度器通过任务的
VS=Dirty状态判断其是否真实使用过 RVV,并记录最近一次发现 Dirty 状态的时间。若任务在设定的衰减窗口内仍具有向量活动,则将其识别为 V-hot 任务。对于 V-hot 任务,调度器采用以下策略:
V-hot 判断来自内核能够观察到的真实任务状态,不依赖用户态声明。因此,即使 llama.cpp 未提供推理阶段信息,调度器仍能执行基本的向量感知优化。
2.3.5 用户态—内核态阶段协同
仅依靠 V-hot 可以判断线程近期是否使用过向量寄存器,但无法区分其正在执行 Prefill 还是 Decode。为此,项目通过固定在 bpffs 中的 eBPF Map 建立用户态—内核态提示接口。
llama.cpp 在计算 batch 开始前写入:
调度器将阶段提示与 V-hot 检测结合,不直接信任用户态提示。只有近期被检测为 Dirty 的向量任务才应用阶段策略,避免 HTTP、日志和控制线程因属于同一个 llama-server 进程而被错误提升。
不同阶段采用不同的调度倾向:
提示超过 TTL、字段校验失败或 Map 不可用时,调度器自动退回 V-hot 策略;sched_ext 调度器退出后,任务则回到 Linux 默认公平调度类。由此形成“主动提示—V-hot 回退—默认调度回退”的多级保护机制。
2.3.6 整体优化逻辑
Linux 默认调度下的问题路径可以概括为:
本项目的优化路径为:
综上,本方案保留 Linux lazy save/restore 的正确性保障,以 sched_ext 补充默认公平调度对高代价向量状态缺乏感知的问题,再通过 eBPF Map 引入 llama.cpp 的推理阶段语义,形成内核状态观测与用户态负载提示相结合的 V 状态感知调度机制。
3、项目实现
3.1 系统总体架构
系统由交叉编译环境的内核(kernel_build)、V 状态观测内核模块(os_module)、llama框架(llama.cpp)、模型板端调用推理服务和实验分析(rvv_llm_lab)、 v-hot调度器(sched_ext)、动态PD阶段提示调度器(dynamic_sched_ext)、和 x86 AVX2 横向对照(x86_avx2)七部分组成。各部分由
run_exp.sh使用统一的run_id串联,降低人工转录造成的数据错配风险;x86 对照实验复用同一套 loadgen 口径,补充 XSAVE/xstate 与调度切换数据,用于分析 RVV VLA 状态管理的特殊性。flowchart LR A[Windows x86_64 / WSL2RISC-V 交叉编译] -->|"llama-server / llama-cli"| B[BananaPi BPI-F3
llama-server] A -->|"vstate_profiler.ko"| D["内核 V 状态探针"] A -->|"vstate_observer"| G["libbpf observer"] A -->|"调度器二进制"| S["sched_ext 调度器产物"] A -->|"x86-avx2 llama-server + 对照脚本"| H["x86_avx2
AVX2 / XSAVE 实验模块"] R["rvv_llm_lab/run_exp.sh
统一 run_id 编排"] --> M0 R --> M1 R --> M2 subgraph M["调度模式(三选一,每轮只启用一种)"] direction TB M0["Baseline
CFS 默认调度"] M1["scx_vstate
V-hot 长时间片 / vtime / 原 CPU 直派"] M2["dynamic_sched_ext
phase_hints / Prefill-Decode 权重 / sticky-adaptive"] end S --> M1 S --> M2 M0 -->|"本轮调度环境"| B M1 -->|"本轮调度环境"| B M2 -->|"本轮调度环境"| B B -.->|"仅 dynamic 模式写入 phase_hints"| M2 E["异步 loadgen
2/4/8 并发"] -->|"/v1/completions"| B B -->|"TGID、sched_switch、迁核"| D B -->|"sched_switch、迁核辅助观测"| G D -->|"debugfs stats/csv"| F["结果合并与统计分析"] G -->|"observer.json"| F B -->|"流式 token、TTFT、latency"| F H -->|"LLM 指标、上下文切换、xstate 字节"| F
代码按职责划分为:
1、
os_module/测量目标内核保存路径;2、
sched_ext/vstate/实现静态 V-hot 感知调度策略和加载器;3、
dynamic_sched_ext/实现带 Prefill/Decode 阶段提示的动态调度器;4、
rvv_llm_lab/实现 llama.cpp 构建、eBPF 辅助观测、并发压测、结果合并及绘图;5、
x86_avx2/实现 x86 AVX2 版本 llama.cpp 对照实验、XSAVE 状态探测和批量结果汇总。 该分层设计使 profiler、静态调度器和动态调度器可以独立启停,能够组织
baseline、profiler、scx_vstate、dynamic_sched_ext和scx_profiler等实验组合,并支持对 V-hot 判断、阶段提示、时间片和 CPU 局部性策略进行消融测试。3.2 V 状态开销量化模块实现
1、总体框架
V 状态开销量化模块的目标是将“调度过程中是否实际保存 RVV 寄存器”这一内核行为转换为可统计指标。普通
sched_switch仅能表明线程发生了切换,无法判断该线程是否持有 Dirty V 状态;因此本模块将 kretprobe 挂载至__riscv_v_vstate_save()和对应 restore 热路径,在函数入口记录时间戳,在返回时计算一次保存或恢复的纳秒级耗时,并按目标llama-server的 TGID 过滤样本。该模块以 debugfs 暴露
control、stats和csv三个接口。实验开始时由脚本写入control: start清空旧数据并打开采样窗口,压测结束后写入control: stop固定时间范围,再读取stats/csv与 loadgen 输出合并。该流程确保 V 状态保存次数、保存耗时、TTFT、TPS 和请求数来自同一轮实验窗口,避免不同时间段手工观测造成数据错配。2、地址解析函数
SpacemiT 6.18 内核中的底层保存函数可能以内联或局部符号形式存在,不宜仅依赖公开导出的符号名。初始化阶段首先通过与目标内核一致的
vmlinux和预先确认的 anchor/delta 解析出 save/restore 实际地址,随后将 kretprobe 注册到该地址。模块同时检查CONFIG_RISCV_ISA_V,避免在未启用 RVV 的内核上产生误导性数据。初始化成功后,模块创建 debugfs 目录并打印解析到的保存地址、恢复地址和 VLEN。报告中的
vlen_bits=256来源于该处读取的riscv_v_vsize,后续用于计算每次保存对应的理论 V 寄存器状态字节数。3、入口探针函数
入口探针仅执行轻量过滤和时间戳记录。
enabled保证探针只在正式实验窗口内计数,task_selected(current)用目标 TGID 排除系统后台线程,start_ns保存在 kretprobe instance 的私有数据中。若当前任务不属于目标进程,函数返回 1 并跳过本次返回探针,从而降低观测开销并避免污染统计。4、出口探针函数
出口探针读取当前单调时钟,与入口保存的
start_ns相减得到本次 V 状态保存耗时。统计写入this_cpu_ptr(&cpu_stats)指向的 per-CPU 结构,避免多 CPU 并发更新时发生全局锁竞争。恢复路径采用同样的入口/出口结构;若目标内核的恢复热路径不适合探测,分析脚本将结果标记为module_save_only_lower_bound,表示当前 V 状态时间为保存侧下界。5、V 状态保存次数累加
metric_add()将单次延迟同时累加到总次数、总耗时、最小/最大耗时和直方图中。直方图桶使用fls64(ns)按 2 的幂分布,用于观察保存路径是否存在偶发长尾;均值由total_ns / count得到,用于报告save_avg_ns。这些统计保持在 per-CPU 数据中,读取stats/csv时再统一合并。6、采样窗口与 CPU 时间容量计算
elapsed_ns()描述本轮采样窗口持续时间;若读取发生在实验停止前,则用当前时间临时计算已运行时长。cpu_capacity_ratio()将 V 状态保存/恢复总耗时除以“实验窗口时间 × 在线 CPU 数”,得到vstate_cpu_capacity_ppm。相比单独报告总毫秒数,该指标更适合比较不同并发和不同运行时长的实验,因为它表示 V 状态管理消耗了整机 CPU 时间容量中的百万分之几。3.3
scx_vstate调度器模块实现1、总体框架
sched_ext/vstate/中的scx_vstate是一个基于 Linuxsched_ext的 RISC-V V 状态感知调度器。它以内核在任务停止运行时读取到的 RISC-VVS=Dirty作为向量热点信号:当任务切出时仍为 Dirty,表明该线程刚使用过 RVV,后续再次被短时间片打断时更可能产生 V 状态保存压力。调度器据此将任务标记为短期 V-hot,并在后续 CPU 选择、时间片和虚拟运行时间计费中给予有限倾斜。该调度器的基本流程是:任务被唤醒时首先执行
select_cpu(),优先将 V-hot 任务放回上次运行的空闲 CPU;若无法本地直派,则在enqueue()中按虚拟运行时间插入共享VSTATE_DSQ;CPU 本地队列为空时由dispatch()从共享队列取出任务;任务开始运行时由running()记录运行 CPU 和起始时间;任务停止运行时由stopping()读取VS状态、更新 V-hot 历史并完成 vtime 计费。该过程仅改变任务选择、时间片和公平计费,不修改 Linux 原有的 V 状态保存/恢复语义。2、V-hot 任务判定
V-hot 判断由目标任务声明和 Dirty 历史共同完成。loader 将目标 TGID 写入 BPF map,使刚被接管的推理线程在初始阶段进入调度器管理;任务进入实际运行后,
stopping()中观测到的VS=Dirty更新该任务的last_dirty_ns。当当前时间与last_dirty_ns的差值小于hot_decay_ns时,任务被视为 V-hot。默认hot_decay_ns约为 200 ms,用于覆盖 llama.cpp worker 在同步、等待 batch 和再次唤醒之间的短暂停顿,避免刚使用过 RVV 的线程立刻被当作普通任务轮转。3、CPU 选择与本地 DSQ 直派
select_cpu()是任务被唤醒时的第一道放置决策。对于 V-hot 任务,调度器优先检查上次运行 CPU 是否仍在任务亲和性集合内且处于 idle 状态;若满足条件,则将任务插入该 CPU 的本地 DSQ,使线程继续在原 CPU 上运行。该策略主要降低迁核带来的 cache/TLB 扰动,并减少 V-hot 线程在共享队列中被其他任务反复插队的概率。若原 CPU 不空闲,则回退到 sched_ext 默认选核逻辑,以避免过度粘性导致并行 worker 在同一 CPU 上排队。4、共享 DSQ 入队与 vtime 排序
未在
select_cpu()中插入本地 DSQ 的任务由enqueue()处理。入队流程包括三项操作:根据 V-hot 状态选择时间片,读取p->scx.dsq_vtime作为排序键,并将任务插入共享VSTATE_DSQ等待后续分发。enqueue()负责将未能本地直派的任务放入共享VSTATE_DSQ。V-hot 任务使用较长的vector_slice_ns,普通任务使用normal_slice_ns;二者仍按照p->scx.dsq_vtime排序,并非将 V-hot 任务固定放置在队首。代码中的 sleep credit 限幅用于避免长时间睡眠任务醒来后携带过多虚拟时间优势,降低饥饿风险。5、共享 DSQ 分发
当某个 CPU 的本地 DSQ 没有可运行任务时,sched_ext 调用
dispatch(),从共享VSTATE_DSQ取出排序最靠前的任务,并移动到当前 CPU 的本地 DSQ。该回调的实现保持简洁:策略复杂度主要集中在 V-hot 标记、时间片长度和 vtime 计费上,分发阶段仅执行队列搬运,以减少 BPF 调度器热路径中的额外判断。6、运行状态记录
running()在任务实际开始占用 CPU 时更新全局vtime_now、任务上次运行时间和上次 CPU。若本次 CPU 与记录中的last_cpu不同,则累加迁核计数;若该任务仍处于 V-hot 窗口,则累加 V-hot 运行次数。后续分析可将这些调度器内部计数与 profiler 的保存次数放在同一 run ID 下联合分析,用于评估调度过程是否减少迁核和 Dirty 切出。7、停止运行与 vtime 计费
stopping()是调度器获得真实 V 状态反馈的关键回调。任务即将离开 CPU 时,RISC-V 架构状态仍可从task_pt_regs()中读取;若VS=Dirty,调度器记录last_dirty_ns,并根据runnable区分可运行抢占和主动阻塞。前者更接近调度策略可优化的保存压力,后者多来自任务主动等待 I/O、锁或同步屏障。同一回调同时完成 vtime 计费。任务本轮实际运行时间
used按权重折算为charge,V-hot 任务再除以有限的vector_boost,使其在共享队列中获得一定连续运行倾向。由于 boost 仅影响本次计费,且队列仍按 vtime 排序,普通任务仍可获得 CPU 时间,调度器在优化 RVV 保存压力和维持公平性之间保持平衡。3.4 可选完成项c.基于用户态-内核态接口的主动提示调度器模块实现
dynamic_sched_ext/实现面向 llama.cpp server 推理负载的动态 Prefill/Decode(PD)分离调度器。该模块由四类代码共同构成:scx_vstate.h定义 llama-server 与 BPF 调度器共享的阶段提示 ABI;llama.cpp 的tools/server/server-context.cpp和server-vstate.cpp在llama_decode()执行窗口内发布当前 batch 的阶段信息;scx_vstate.c负责加载 sched_ext struct_ops、pin BPF map、写入调度参数并在 partial 模式下接管目标线程;scx_vstate.bpf.c在内核调度回调中读取 V 状态和阶段提示,完成 CPU 选择、DSQ 入队、运行统计与 vtime 计费。该调度器保持 Linux 原有 RVV 上下文保存/恢复语义,仅在 sched_ext 允许的调度决策点上,根据真实推理阶段和 Dirty V 历史调整运行窗口与公平计费。1、总体框架
动态 PD 分离的阶段来源为 llama.cpp 服务端的 batch 构造路径。Prefill 阶段对应 prompt token 被加入 batch 的过程,Decode 阶段对应采样 token 或 speculative draft token 被加入 batch 的过程;当同一个
llama_batchview 同时包含两类 token 时,用户态写入MIXED,并同时携带 prefill/decode token 计数。BPF 调度器在后续时间片和 vtime 权重计算中使用这些计数,从而使 Mixed 阶段的调度参数与当前 batch 组成保持一致。2、共享 ABI 与 BPF map
调度器和 llama-server 通过
dynamic_sched_ext/scx_vstate.h共享 ABI。头文件固定了phase_hintsmap 的 pin 路径、ABI 版本、阶段枚举和 value 布局,并使用_Static_assert约束结构体大小,避免用户态与 BPF 侧因结构布局变化产生不兼容:BPF 程序定义了调度器运行所需的 map:
task_ctx保存任务级 Dirty 历史、上次运行 CPU 和运行起点;target_tgids保存 loader 声明的目标进程;phase_hints接收 llama-server 写入的阶段提示;workers用于 GGML worker 稳定放置实验;stats采用 per-CPU array 统计调度事件。调度热路径主要依赖task_ctx、phase_hints、workers和stats;partial 模式下的线程接管由 loader 调用sched_setscheduler(SCHED_EXT)完成。loader 在加载 BPF 程序后将
phase_hintspin 到固定路径。llama-server 随后通过bpf(BPF_OBJ_GET)获取 map fd,并以 TGID 为 key 更新阶段提示:3、llama-server 阶段提示生成路径
llama.cpp 的
tools/server/CMakeLists.txt将server-vstate.cpp和server-vstate.h加入server-context静态库,使阶段提示逻辑随llama-server一同编译:在
server_batch中,add()接口增加server_vstate_phase phase参数,batch 内部为每个 token 保存阶段属性。get_vstate_workload()直接遍历即将传入llama_decode()的 token view,统计其中 prefill 与 decode token 数,并据此生成prefill、decode或mixed阶段:Prefill token 在 prompt batch 填充处写入:
Decode token 在采样 token 和 speculative draft token 入 batch 时写入:
llama_decode()的阶段提示生命周期由 RAII scope 管理。scope 构造时写入当前 batch 阶段,析构时写回IDLE,从而将提示有效期限制在本次 decode 调用窗口内:server-vstate.cpp中的phase_map_client::set()通过 Linuxbpf()syscall 更新 map。该接口默认关闭,仅在LLAMA_VSTATE_PHASE_HINTS=1时启用;map key 使用getpid(),即 llama-server 进程 TGID:4、BPF 侧阶段提示校验与 V-hot 判定
task_phase()从phase_hintsmap 按 TGID 查找提示,并逐项校验 ABI 版本、阶段范围、时间戳、TTL 和 token 数一致性。若任一条件不满足,函数返回VSTATE_PHASE_NONE,该提示不进入后续调度参数计算:阶段提示本身不构成 V-hot 判据。
task_is_vhot()要求任务已经具备近期 Dirty 历史;阶段提示仅用于在 V-hot 任务内部进一步区分 Prefill、Decode 或 Mixed 阶段。因此,同一 TGID 下的 HTTP 线程、控制线程等非向量计算线程不会因进程级阶段提示而被视为 V-hot:Dirty 信号来自
stopping()阶段对pt_regs.status的读取。该读取点位于任务停止运行的 sched_ext 回调中,仍可通过bpf_task_pt_regs()获取有效的用户态架构状态;调度器据此判断 RISC-VVS字段是否为 Dirty:5、阶段信息到调度参数的映射
阶段信息进入调度决策的主要入口为
task_slice()和task_boost()。task_slice()决定本次入队时间片,task_boost()决定任务停止运行后的 vtime 增长速度。普通任务使用normal_slice_ns,V-hot 且无有效阶段提示的任务使用vector_slice_ns;Prefill 使用prefill_slice_ns,Decode 使用decode_slice_ns;Mixed 阶段在 proportional 模式下按两类 token 数加权:vtime boost 同样按阶段计算。Prefill 使用
prefill_boost,Decode 使用decode_boost,Mixed 以 token 占比进行四舍五入加权:在该实现中,动态 PD 分离具体表现为时间片与 vtime 计费参数的分阶段计算。Prefill 阶段获得更长的连续运行窗口,Decode 阶段保留适度优先级但限制单次时间片长度,Mixed 阶段根据当前 batch 中 prefill/decode token 比例在两类参数之间加权。所有任务仍通过 DSQ vtime 排序,调度器未绕过 sched_ext 的公平计费边界。
6、调度生命周期:选核、入队、分发、运行、停止
select_cpu()处理任务唤醒后的 CPU 选择。该函数先调用task_phase()、task_is_vhot()和task_slice(),得到当前任务的阶段、V-hot 状态和本次时间片;若任务注册了 worker hint,则尝试稳定映射到对应 CPU;否则 V-hot 任务优先返回last_cpu,并在目标 CPU 空闲时直接插入本地 DSQ:若任务未能在
select_cpu()中本地直派,enqueue()将其放入共享VSTATE_DSQ。该阶段首先限制 sleep credit,避免长时间睡眠任务在唤醒后获得过大的 vtime 优势;随后根据 sticky 策略决定是否回到上次 CPU。--sticky-adaptive的实现逻辑是:当 home CPU 本地队列已有等待任务时,放弃硬粘性并回退到共享 DSQ,避免并行 worker 因单个 CPU 局部排队而影响整批计算:dispatch()的职责限于在本地队列为空时,从共享 DSQ 搬运 vtime 最靠前的任务到当前 CPU 本地 DSQ:running()记录本次开始运行时间、迁核情况和阶段命中次数。Mixed 阶段额外统计 prefill 主导、decode 主导或相对均衡的 batch 组成,用于后续分析动态 batch 的阶段分布:stopping()完成任务停止运行后的状态更新:首先读取VS=Dirty并更新 V-hot 历史;随后统计本次阶段运行时间;最终调用task_boost()更新p->scx.dsq_vtime。boost 越大,vtime 增长越慢,任务在共享 DSQ 中越容易保持靠前位置;所有任务仍按 vtime 排序,因此该策略属于 sched_ext 公平计费框架内的优先级倾斜:7、loader 参数下发与目标线程接管
scx_vstate.c的 loader 将命令行参数写入 BPF.rodata,随后加载并 attach struct_ops。--prefill-ms、--decode-ms、--decay-ms、--phase-policy、--sticky-adaptive等参数最终成为 BPF 程序中的只读配置:partial 模式下,loader 设置
SCX_OPS_SWITCH_PARTIAL,并枚举/proc/<tgid>/task,将目标进程的每个线程切换到SCHED_EXT:目标 TGID 写入
target_tgidsmap 作为本轮调度目标声明;partial 模式下的线程切换动作由set_process_sched_ext()完成。loader 同时每 10 秒读取statsper-CPU array 并输出累计统计:板端运行时使用:
与实验脚本配合时,
run_exp.sh同时负责调度器启动和 llama-server 环境变量注入:8、可观测性与正确性边界
调度器提供阶段命中、阶段运行时间和 sticky 策略行为的可观测字段。
scx_vstate.h中的VSTAT_PREFILL_RUNS、VSTAT_DECODE_RUNS、VSTAT_MIXED_RUNS、VSTAT_PREFILL_RUNTIME_NS、VSTAT_DECODE_RUNTIME_NS、VSTAT_MIXED_RUNTIME_NS、VSTAT_STICKY_LOCAL、VSTAT_ADAPTIVE_QUEUE_FALLBACK等统计项由 BPF 程序累加,并由 loader 输出到调度器日志。因此,阶段提示命中情况、Mixed 阶段的 token 主导类型、sticky-adaptive 的回退次数等信息均可通过scheduler.log观测和核对。正确性边界方面,
dynamic_sched_ext不修改 Linux 的 RVV 保存/恢复函数,也不跳过必要的寄存器保存。调度器只在sched_ext允许的任务选择、DSQ 入队、时间片和 vtime 计费上调整策略;当 Dirty V 线程发生实际切出时,内核仍按原路径保存 V 状态。若 BPF 程序异常退出,sched_ext 通过vstate_exit()记录退出信息并回退;用户态 loader 卸载 link 后,系统恢复到普通调度状态。3.5 llama.cpp 框架应用模块实现
本项目以 llama.cpp 的
llama-server作为大模型推理服务,同一份 RISC-V RVV 二进制同时支持两套调度方案:静态scx_vstate方案不依赖应用侧提示,调度器仅依据内核在任务切出时观测到的VS=Dirty判断 V-hot 线程;动态dynamic_sched_ext方案在服务端 batch 执行路径加入轻量级阶段提示接口,在llama_decode()前后声明当前 batch 属于 Prefill、Decode 还是 Mixed 阶段。模型加载、token 采样、GGML 计算图和 RVV 量化算子仍沿用 llama.cpp 原有执行路径,新增逻辑仅提供可选调度提示,因此 Baseline、静态调度和动态调度可以使用同一模型、同一线程数和同一压测负载进行对照。1、面向 BPI-F3 的 RVV 交叉构建
rvv_llm_lab/scripts/build_llama_bpi.sh统一指定 SpacemiT 交叉工具链、目标 ISA 和 llama.cpp 构建组件。目标 ISA 与板端 X60 内核暴露的扩展保持一致,启用标准 RVV、标量与向量半精度扩展;同时关闭本项目不使用的 GPU、OpenMP、测试和 Web UI 等组件,减少部署依赖。-fno-tree-vectorize与-fno-tree-loop-vectorize用于避免编译器在非关键通用代码中自动引入额外向量化,使主要 RVV 行为集中于 GGML 已实现的向量内核,便于分析推理负载的 V 状态开销。llama.cpp 的
ggml/src/ggml-cpu/CMakeLists.txt根据上述选项组合 RISC-V 的-march,并将量化与 repack 实现加入 CPU 后端。因此,项目最终使用 GGML 原生 RISC-V 量化计算路径,并非在板端模拟向量指令。2、GGUF 模型加载与推理服务实例化
rvv_llm_lab/bin/run_server.sh将模型路径、生成线程数、批处理线程数、并行 slot 数和上下文长度转换为 llama.cpp 命令行参数。正式实验固定THREADS=4、THREADS_BATCH=4和CTX_SIZE=1024,仅随实验档位改变PARALLEL,从而使 Baseline、静态scx_vstate与动态dynamic_sched_ext组的框架参数一致。exec使脚本进程直接替换为llama-server,实验编排器记录到的 PID 即服务进程 TGID,可直接传递给 V 状态观测模块和两类 sched_ext 调度器。llama.cpp 内部将
--threads用于逐 token 生成阶段,将--threads-batch用于 batch 和 prompt 处理,并通过--parallel建立并行服务 slot。三者分别控制 decode 工作线程、prefill 工作线程和可同时驻留的请求序列,是本项目建立统一并发基线的核心参数。3、OpenAI 兼容接口与流式输出
服务端在 HTTP 层注册健康检查和 OpenAI 兼容补全接口。实验脚本轮询
/health,在确认模型加载完成后启动 observer 和 loadgen,防止模型加载时间进入 TTFT 统计;压测请求统一发送到/v1/completions。请求进入服务端后被分配到一个 slot,slot 状态依次经历
STARTED、PROCESSING_PROMPT、DONE_PROMPT和GENERATING。每得到一个有效 token,process_token()将文本追加到该 slot 的生成结果;当请求设置stream=true时立即发送局部响应。loadgen 因而能够以收到第一个非空 SSE 文本片段的时刻作为首 token 到达时刻。4、llama-server 主动阶段提示接入
动态 PD 分离调度器需要获得当前
llama_decode()正在执行 Prefill、Decode 还是混合 batch。本项目在 llama-server 侧加入server-vstate.h和server-vstate.cpp两个轻量文件,通过 Linuxbpf()syscall 打开调度器 pin 到 bpffs 的 BPF map:该接口默认关闭,仅在设置
LLAMA_VSTATE_PHASE_HINTS=1时写入;LLAMA_VSTATE_PHASE_MAP可覆盖默认 map 路径。因此,Baseline 和静态scx_vstate实验可以运行同一份 llama-server 二进制但不发送提示,动态调度实验通过环境变量打开提示通道。接入位置集中在
tools/server/:构建系统将server-vstate.cpp和server-vstate.h加入server-context静态库,server-context.cpp引入该头文件;server_batch::token增加阶段字段,server_batch::add()在加入 prompt token 时写prefill,在加入采样 token 或 draft token 时写decode。因此,阶段判断来自 llama-server 已有的 batch 构造语义,无需在调度器中根据线程名或 CPU 利用率推断推理阶段。阶段提示的 value 与 BPF 调度器共享同一 ABI。用户态写入
abi_version、phase、updated_ns、prefill_tokens、decode_tokens和batch_tokens,调度器侧再做版本、时间戳和 token 一致性校验。在 server batch 管理中,每个进入 batch 的 token 均带有阶段标记:prompt/prefill token 标记为
prefill,采样后继续生成的 token 和 speculative draft token 标记为decode。在调用llama_decode(ctx_tgt, batch_view)前,get_vstate_workload()统计当前 batch view 中两类 token 数,并由 RAII scope 写入阶段提示;llama_decode()返回后 scope 析构,将进程阶段状态恢复为IDLE。当前 batch 仅含 prompt token 时,提示为
PREFILL;仅含生成 token 时,提示为DECODE;两类 token 同时存在时提示为MIXED,并同时写入 prefill/decode token 数。调度器据此对 Mixed 阶段进行比例加权,避免将连续批处理中的混合 batch 固定归为单一阶段。基于上述实现,llama.cpp 服务模块同时具备可控制线程数、并发 slot、输入输出规模和阶段提示的能力。其计算线程既是
vstate_profiler的观测目标,也是静态scx_vstate与动态dynamic_sched_ext的调度目标;当阶段提示开启时,它同时作为 Prefill/Decode 动态调度提示的来源。3.6 大模型应用与压测模块实现
大模型应用与压测模块位于
rvv_llm_lab/,以scripts/run_exp.sh为入口,将 llama-server 生命周期、sched_ext 调度器、阶段提示、固定并发负载、V 状态观测和结果汇总串联为一个 run ID。脚本支持 Baseline、SCX_SCHEDULER=vstate静态scx_vstate、SCX_SCHEDULER=dynamic动态dynamic_sched_ext三类运行模式,并可按需打开 llama-server 阶段提示。1、固定输入负载与异步并发控制
benchmark/loadgen.py在未指定 prompt 文件时调用make_fixed_prompt()生成确定性的固定词数文本。所有请求复用完全相同的字符串,并使用temperature=0和相同的max_tokens,从而减少输入分布和采样随机性对调度实验的干扰。需要说明的是,本实验固定的是 HTTP 输入字符串和词数,而不是 tokenizer 处理后的精确 token 数;程序同时将字符数和词数写入结果,保证各组可核查。每个请求构造相同的 OpenAI Completion JSON。
asyncio.Semaphore限制同时进行的请求数,asyncio.gather()等待本轮所有请求完成;当REQUESTS大于CONCURRENCY时,请求按并发窗口分批执行。连接设置为force_close=True,避免长时间流式响应结束后复用已被服务端关闭的 HTTP/1.1 连接而产生非服务端计算失败。2、SSE 解析与 TTFT、Latency 采集
每个请求在获得 semaphore 后记录单调时钟
start_ns,避免排队等待并发名额的时间被误计入服务端延迟。程序按行解析 SSE 数据,兼容/v1/completions的choices[0].text和 chat 风格的delta.content;收到第一个非空文本片段时仅记录一次first_token_ns,流结束时记录end_ns。单个请求失败不影响其他协程执行,而是写入error字段;整轮结束后若存在错误则返回非零状态,防止残缺数据进入正式结果。SSE 事件可能包含一个或多个模型 token,因此
output_tokens在当前实现中表示“非空流式文本事件数”。本项目的 Baseline 和优化组使用同一版本 server、同一解析器和同一输入输出配置,该值可作为相对吞吐指标;正式解释绝对 token/s 时,应与 llama.cpp 服务端官方 token 计数交叉验证。3、整轮性能指标计算
loadgen 对所有成功请求计算 TTFT 均值、P95 TTFT 和平均总延迟。P95 使用相邻次序统计量线性插值;吞吐量以所有成功请求的输出事件总数除以
asyncio.gather()外层记录的整轮墙钟时间,能够覆盖并发请求彼此重叠的真实服务区间。4、一键实验生命周期编排
scripts/run_exp.sh每次执行仅产生一轮独立实验。脚本首先保存内核版本、模型、线程数、并发数、调度器模式和 phase hint 配置,然后以环境变量形式启动 llama-server。服务健康检查通过后,脚本将真实SERVER_PID传给当前选定调度器的-p参数:静态方案加载sched_ext/vstate/build/scx_vstate,动态方案加载dynamic_sched_ext/build/scx_vstate并在开启 hints 时消费 phase map。由此,两种调度器均可在 partial 模式下仅接管本轮推理服务线程;随后启动 observer、profiler 和 loadgen。压测完成后,脚本按相反顺序停止 profiler、observer、scheduler 和 server,并归档所有日志。脚本用
ENABLE_SCX、SCX_SCHEDULER、ENABLE_PHASE_HINTS和ENABLE_VSTATE_PROFILER组合实验模式。SCX_SCHEDULER=vstate或scx_vstate选择sched_ext/vstate/build/scx_vstate,用于验证纯内核 Dirty V-hot 反馈策略;SCX_SCHEDULER=dynamic选择dynamic_sched_ext/build/scx_vstate,默认参数为--partial --sticky-adaptive --phase-policy proportional;ENABLE_PHASE_HINTS=1同时向 llama-server 注入LLAMA_VSTATE_PHASE_HINTS=1,形成“应用声明阶段、调度器消费提示”的闭环。在手动管理模式下,可设置SCX_MANAGE=0,此时脚本仅记录外部调度器状态。5、结果合并与重复实验校验
analysis/analyze.py以run_id为主键,将loadgen_result.json、observer.json、profiler debugfs 快照和run_meta.json合并为一行 CSV。V 状态数据按“内核模块、内核 debugfs、eBPF 辅助值”的顺序选择可用数据源,并在同一记录中给出 TPS、TTFT、save/restore 次数、总耗时、迁核次数及实验模式,避免人工抄录造成错配。正式评测可分别执行 2、4、8 并发的 Baseline、静态
scx_vstate和动态dynamic_sched_ext配置。静态优化组使用ENABLE_SCX=1、SCX_SCHEDULER=vstate,无需ENABLE_PHASE_HINTS;动态优化组使用ENABLE_SCX=1、SCX_SCHEDULER=dynamic和ENABLE_PHASE_HINTS=1。每个组合至少生成 3 个独立 run ID。plot_results.py面向 Baseline 与静态scx_vstate的成对对比;多调度器批量实验则由scripts/run_exp.sh -s none,vstate,dynamic和scripts/summarize_runs.py输出按 scheduler 分组的均值、标准差。上述流程形成“统一配置启动—固定并发负载—同步采集应用与内核指标—独立 run ID 留档—重复性统计”的闭环,使大模型性能变化可以与同一轮的 V 状态保存、调度切换和迁核行为对应分析。
3.7 可选完成项d.与 x86(AVX2/AVX-512)平台进行横向对比
本项目在
x86_avx2/中实现了 x86_64 / WSL2 / AVX2 对照实验。该实验复用rvv_llm_lab/benchmark/loadgen.py,保持模型、并发、请求数、THREADS=4、THREADS_BATCH=4、CTX_SIZE=1024和MAX_TOKENS=64等口径一致;同时额外采集/proc线程上下文切换、perf stat可用性和 CPUID leaf 0xD 暴露的 XSAVE/xstate 状态大小。由于 WSL2 对 PMU、kprobe 和部分 perf 事件有限制,本节 x86 数据仅作为机制对照和趋势对照,不作为裸机性能结论。本机 x86 对照平台为 Intel Core i9-13900HX,
xstate_info.json显示XCR0=0x7,启用了 x87、SSE 和 AVX 状态;当前可用 XSAVE area 为 832 B,其中 AVX2 对应的AVX_YMM_Hi128组件为 256 B。因此,该批实验是 AVX2 实测对照,未启用 AVX-512。在启用 AVX-512 的 x86 平台上,opmask、ZMM_hi256 和 Hi16_ZMM 等状态组件会进一步扩大 XSAVE area,但其大小仍由 CPUID/XCR0 固定给出。x86_avx2/experiments/x86-avx2/20260722T081031Z-x86-avx2/group_summary.csv中每个并发档位重复 3 次,汇总结果如下:x86 对照结果显示,随着并发从 2 增加到 8,进程上下文切换均值从约 1111 次增加到约 7067 次,切换/请求从 138.83 增加到 883.42,约为 6.36 倍;由于 AVX2 的 XSAVE area 固定为 832 B,估算 xstate 数据搬运量也按相同比例从约 112.8 KiB/请求增加到约 717.8 KiB/请求。因此,在固定长度 SIMD 架构上,扩展状态压力主要由“发生多少次切换”决定,单次状态大小在同一平台上是稳定常数。
RVV 的特殊性在于 VLA 编程模型使同一份用户态二进制与固定向量长度解耦,但操作系统保存对象并非抽象的 VLA 语义,而是硬件真实
VLEN对应的寄存器映像。对于本项目的 SpacemiT K1,vlen_bits=256,一次完整 V 寄存器保存的理论数据量为:因此,在相同 Dirty 切出次数下,K1 的 RVV V 寄存器保存字节数约为当前 AVX2 启用 XSAVE area 的
1024 / 832 = 1.23倍;如果只比较 AVX2 额外的 YMM 高 128 位组件,则为1024 / 256 = 4倍。此外,RVV 该项开销随VLEN线性增长:VLEN=512时仅 V 寄存器为 2048 B,约为当前 AVX2 XSAVE area 的 2.46 倍;VLEN=1024时为 4096 B,约为 4.92 倍。应用层仍可使用同一套 VLA 代码,但调度器和内核上下文切换路径面对的实际保存成本随硬件实现而变化。综上,RVV VLA 的调度问题具有两层放大效应:第一层与 x86 AVX2 相同,并发升高会增加上下文切换频率;第二层是 RVV 特有的,单次 Dirty V 状态保存成本同时与硬件
VLEN绑定。同样是 256 bit 向量计算,AVX2 的状态大小由 ISA 和 XCR0 固定,而 RVV 的 OS 保存规模按实现定义的VLEN变化。对于 LLM 推理这种长时间保持 vector dirty 的负载,减少可运行 V-hot 线程的短时间片抢占和无效迁核,比单纯限制 CPU affinity 更有必要。因此,本项目在 RISC-V 真机上实现scx_vstate与动态阶段提示调度器。4、实验
4.1 模型量化实验
模型规模:
从图中可以得出模型量化的结论如下:
在相同并发下,模型规模越大,V 状态保存次数越高
Qwen2.5 3B 的 V-State save count 明显高于 1.5B 和 0.5B,说明更大模型会带来更持续、更密集的 RVV 计算,从而增加 Dirty V 线程被切出时的保存压力。
并发数增加会显著放大 V 状态保存压力
0.5B、1.5B、3B 三条曲线都随并发 2→4→8 上升,说明并发请求越多,推理线程之间的调度竞争越强,V 状态保存次数也越多。
TTFT 对模型规模和并发都非常敏感,且 3B 模型尾延迟增长最明显
平均 TTFT 和 P95 TTFT 都随模型规模、并发数上升而上升;其中 3B 在 8 并发下 P95 TTFT 接近 90s,说明大模型在当前板端资源下已经进入明显排队和调度竞争状态。
0.5B 与 1.5B 的 TTFT 差距相对小于 3B 与前两者的差距
这说明在 BPI-F3 上,小模型和中等模型仍处于相对可控范围,而 3B 模型会显著放大计算负载、V 状态开销和尾延迟。
4.2 调度器实验环境
正式性能测试在 BananaPi BPI-F3 真机上完成,处理器为 SpacemiT K1八核 RISC-V,支持 RVV 1.0,
VLEN=256;操作系统为 Armbian,内核版本为6.18.37-current-spacemit,CPU 频率策略固定为performance governor。 测试模型统一采用 Qwen2.5-0.5B-Instruct Q4_K_M,llama.cpp 设置。THREADS=8
THREADS_BATCH=8、CTX_SIZE=4096,输入长度固定为 64 词,最大输出长度为 64 Token。每轮发送 16 个内容不同但长度一致的请求,并发数和llama-server 的 parallel 参数分别设为 2、4、8。 Baseline 关闭 sched_ext 与阶段提示,使用 Linux 默认调度策略;优化组启用V-hot 用户态—内核态协同调度,使用 proportional 阶段策略和 adaptive CPU粘性,Prefill 配置为
50 ms/3×,Decode 配置为30 ms/2×。 每种并发和策略均独立运行 3 轮。以下数据均采用“均值 ± 样本标准差”表示,标准差使用样本标准差计算,即分母为
n-1。4.3 调度器实验基线与实验指标
默认模型为
Qwen2.5-0.5B-Instruct Q4_K_M。当前实验记录中的文件大小约为 490 MiB;文件类型为量化 GGUF 的磁盘模式,模型参数量不超过 3B,符合赛题限制。实验方面,Baseline 组不加载调度器默认采用CFS,优化组分别加载scx_vstate与dynamic调度器,三组均使用相同版本的 profiler 和 observer。正式评测环境为 BananaPi BPI-F3、SpacemiT K1、Armbian(bianbu)、6.18.37-current-spacemit。以下是调度器实验的相关指标说明:
4.4 调度器实验结果
使用
vstate_profiler可加载内核模块统计 RISC-V Vector 状态保存路径的调用次数与执行耗时,并结合调度 tracepoint 统计 llama-server 线程的调度切换次数。 相较统一 Baseline,协同调度在 2、4、8 并发下分别取得以下改善:
其中,TPS 的正数表示吞吐提升,其余指标的负数表示开销或延迟下降。结果表明,优化方案在全部并发档位均取得可重复的收益。随着并发压力增大,吞吐提升由9.63% 增至 12.44%,P95 TTFT 降幅由 6.42% 增至 10.87%,V 状态保存总耗时降幅由 27.57% 增至32.73%,说明该方案在高并发场景下具有更加明显的优化效果。
2并发
4并发
8并发
4.5 调度器实验结果分析
根据实验结果得出下面几个结论:
调度器有效
两种调度器都有效降低了 V 状态保存次数和保存时间。在 2、4、8 并发下,V-hot 和 V-hot User-Kernel Cooperative Scheduling 相比 Baseline 都减少了 save count、save time 和 saves/s,说明 V 状态感知调度确实降低了 Dirty V 线程被频繁切出的概率。
动态PD阶段提示调度在高并发下优势更明显
8 并发下,协同调度 save count 降低约 34.3%,save time 降低约 32.7%,TPS 提升约 12.4%,Mean TTFT 降低约 12.5%,P95 TTFT 降低约 10.9%。这说明阶段提示在高并发下能更好地区分 Prefill/Decode 工作特征,调度收益更稳定。
调度器不仅降低内核 V 状态开销,也能转化为应用层性能收益
三种并发下,优化组 TPS 均高于 Baseline,Mean TTFT 和 P95 TTFT 基本低于 Baseline,说明减少 V 状态保存压力并不是孤立的内核指标改善,而是能反馈到大模型服务吞吐和首 token 延迟上。
4.6数据来源与复现要求
本项目统计数据来自:
为保证结果可比较,重复实验需要保持以下条件不变:
performance governor;5、特色创新点
本项目在完成 V 状态开销量化和
sched_extV 状态感知调度器的基础上,进一步完成了赛题可选完成项 c 和 d:其一,设计并实现 llama.cpp 到 BPF 调度器的用户态-内核阶段提示接口,使推理框架能够向调度器声明 Prefill/Decode/Mixed 计算阶段;其二,新增 x86 AVX2 横向对照实验模块,用定量数据分析 RVV VLA 设计在 V 状态切换问题上的特殊性。以下创新点按“真实状态观测—调度策略实现—可选项扩展—实验闭环验证”的逻辑展开。5.1 基于架构 Dirty 状态的真实 V-hot 反馈
常见的 CPU affinity 方案只能静态限制线程运行位置,基于进程名或 CPU 利用率的策略也无法判断任务切出时是否需要保存向量寄存器。本项目在
sched_ext的stopping()时点读取 RISC-Vpt_regs.status.VS,将VS=Dirty转换为调度器可消费的 V-hot 信号。该信号来自体系结构状态,而非工作负载名称或外部经验规则,因此能够更准确地描述“本次切出是否可能产生 V 状态保存压力”。5.2
sched_ext中时间片、vtime 与 CPU 局部性的组合策略静态
scx_vstate调度器没有将“向量任务优先”实现为不可控的高优先级策略,而是组合三类机制:长时间片用于减少 Dirty runnable 线程的短周期抢占;原 CPU 空闲直派用于保持 cache/TLB 局部性;有界 vtime boost 用于提供有限优先级,同时通过共享 DSQ、原任务权重和睡眠积分上限维持公平性。三类机制分别作用于抢占频率、任务放置和服务份额,且参数可以独立调节,便于后续通过boost=1、关闭 sticky 等配置进行消融分析。5.3 可选完成项 c:用户态-内核阶段提示接口
针对“允许推理框架向调度器声明计算阶段”的可选要求,本项目没有停留在接口设想层面,而是在
dynamic_sched_ext/scx_vstate.h中定义了固定 ABI、阶段枚举和phase_hintsmap 路径。llama-server 通过LLAMA_VSTATE_PHASE_HINTS=1显式开启提示通道,并使用 Linuxbpf()syscall 更新 pin 到 bpffs 的 BPF map。该接口包含 ABI 版本、阶段、更新时间戳、prefill/decode token 数和 batch token 数;BPF 侧对版本、TTL、阶段范围和 token 一致性进行校验,避免无效或过期提示进入调度决策。5.4 可选完成项 c:动态 PD 提示调度器实现
动态
dynamic_sched_ext调度器将可选完成项 c 的阶段提示进一步落实为调度策略。llama.cpp 在 batch 构造路径中为 prompt token 标记 Prefill,为采样 token 和 speculative draft token 标记 Decode,并在llama_decode()执行窗口内通过 RAII scope 写入阶段提示。BPF 调度器在task_slice()和task_boost()中消费该提示:Prefill、Decode 和 Mixed 阶段分别使用不同时间片与 vtime 权重,Mixed 阶段按照当前 batch 中 prefill/decode token 比例进行加权。由此,调度器不依赖线程名或 CPU 利用率推断阶段,而是使用推理框架内部真实 batch 语义进行动态调度。5.5 可选完成项 d:x86 AVX2 横向对照与 RVV VLA 定量分析
针对“与 x86(AVX2/AVX-512)平台进行横向对比”的可选要求,本项目新增
x86_avx2/实验模块,复用同一套 llama.cpp server 压测口径,并采集/proc上下文切换、CPUID leaf 0xD、XCR0 与 XSAVE/xstate 状态大小。实测平台启用 AVX2,XSAVE area 为 832 B,其中 AVX2 的AVX_YMM_Hi128组件为 256 B;在 2、4、8 并发下,x86 对照数据给出了上下文切换/请求与估算 xstate 数据量随并发增长的趋势。结合 SpacemiT K1 的VLEN=256,一次 RVV V 寄存器保存理论数据量为 1024 B,约为当前 AVX2 XSAVE area 的 1.23 倍;同时该数据量随VLEN线性增长。这一对照定量说明:x86 AVX2 的扩展状态大小在同一平台上基本固定,而 RVV 的 OS 保存规模与实现定义的VLEN绑定,体现了 VLA 设计在调度与上下文切换问题上的特殊性。5.6 面向真实硬件评测的一体化实验闭环
项目覆盖 x86_64 WSL2 到 RISC-V 的 llama.cpp、内核模块、BPF skeleton、libbpf 加载器和两类 sched_ext 调度器交叉构建,并将服务健康检查、固定输入并发压测、内核观测、调度器日志、环境快照、均值/标准差计算和绘图统一到 run ID 目录。该闭环同时支撑 Baseline、静态
scx_vstate、动态dynamic_sched_ext、profiler 和 x86 AVX2 对照实验,能够追溯每个结果对应的模型、内核、线程、调度器和阶段提示状态,降低系统软件实验中常见的配置漂移和手工记录误差。6、项目地址及成员分工
项目地址:KiaraStark/bananaOS
成员分工:
成员1:系统观测与开销量化负责人:负责 RVV V-state 保存/恢复开销的观测方案设计与实现,包括内核埋点、debugfs/profiler 数据采集、实验日志整理,以及 V-state total ms、save/restore count 等指标统计。
成员2:调度优化与内核侧实现负责人:负责 V-hot 负载感知调度策略设计与实现,包括 sched_ext/scx_vstate 调度器接入、V-hot 线程识别、CPU 亲和性优化、时间片参数调优,以及不同并发场景下的策略对比。
成员3:LLM 推理实验与结果分析负责人:负责 llama.cpp 服务端部署、模型与并发实验配置、自动化测试脚本维护,以及 TPS、mean TTFT、P95 TTFT 等性能指标的统计分析、图表绘制和报告撰写。
7、参考