系列:《0.8MB 跑通 Qwen:从零实现 ARM 零依赖纯 C 推理引擎》(30 天 × 90 篇) | 适配模型:Qwen3-VL-8B-Instruct(千问3_VL_8B_Instruct)· Qwen3-VL-2B-Instruct · Qwen3-30B-A3B | 测试设备:RK3588(4×Cortex-A76 + 4×Cortex-A55,aarch64)
系列总纲:《0.8MB 跑通 Qwen》30 天 90 篇 · 总纲(阿里云社区)
上一篇:11-3《缓存的一生:内存、容量与淘汰》 | 下一篇:12-2《L3 快照格式与 mmap 落盘》
真机实测通过:本文实验已在 RK3588 板端实测完成(2026-09;方法学与原始记录见仓库 docs 与《实验脚本》目录)
一句话导读:跨进程恢复:推理引擎的进程内缓存再快、进程一死全没,真实服务必遇更新与重启——本篇讲
--disk-kv把 KV 快照落盘、新进程扫描建索引、重启后按前缀找回复用,回答"服务重启了会话为什么不能断"。关键词:手搓 Qwen 推理引擎、千问大模型推理、Qwen3-VL、零依赖纯 C、跨进程恢复、磁盘 KV、KV cache、前缀缓存
导语:进程内缓存再快,进程一死全没,而真实服务必遇更新与重启。本篇讲 --disk-kv 怎么把 KV 快照落盘、新进程启动扫描建索引、重启后按前缀找回并只补算增量尾巴,从而回答"服务重启了会话为什么不能断",也指向仓库报告里那个 13.1×。
11-3 结尾说了句扎心的实话:进程内缓存再快,进程一死全没。真实服务一定会碰到"更新/重启/断电"。今天解决它:--disk-kv 把 KV 快照落盘,重启后按前缀找回——仓库基准报告里的 13.1× 跨进程恢复就是这条路径。
1. 知识点:为什么"进程内缓存"救不了重启
11-3 的缓存一生里,KV 区是进程内存的一部分:
- 进程退出 → 内存归还 OS → 算好的 KV 烟消云散;
- 重启后客户端重发同样的历史 → 引擎只能从零全量 prefill——多轮长会话直接回到"最贵的那一段"。
解决思路直白得像把大象装进冰箱:把 KV 从内存"复制"到磁盘。存下三样东西:
- token 序列(这一段的身份,跨进程的"缓存键"——11-2 的 LCP 在磁盘上同样适用);
- KV 行(K、V 张量,跨进程的"缓存状态");
- 模型几何(层数、头数、维度——防止用错模型的快照去恢复,几何不符直接跳过)。
重启后,新进程扫描目录建索引,把请求的 token 序列与每个快照做 LCP(还是 11-2 那套 kv_lcp),最长且 ≥16 的命中就把 KV 读回内存,然后只 prefill 增量尾巴。用户在客户端无感知——他照常重发全历史,引擎在内部把"全量重算"偷换成"读盘 + 算尾巴"。
两个诚实边界(先摆出来):
- F32 快照很大:28 层 × 8 KV 头 × 128 维 × 4B ×(K+V)= 229 KB/token——8K 会话 ≈ 1.87 GB。它买的是位级一致(恢复 == 重算),贵但绝不引入误差;
- 落盘有成本:turn1 响应后要同步写盘(8K 会话 +16s wall),这个成本换"重启不丢长会话"。
2. 对应代码:save/load 与"启动扫描建索引"
写盘(vllm_safetensors.c 第 12357 行 st_kv_disk_save):头注释(第 12312–12335 行)把文件布局写得明明白白——64B 头(VLKV magic + 版本 + 几何 + token 数 + FNV-1a 哈希)+ token ids + 逐层逐块的 K/V F32 行。每层按 kv_bs 块写:先 K 块后 V 块(第 12389–12400 行),尾块只写有效行。
serve 侧触发(vllm_server.c 第 620–621 行):每个成功的文本请求结束后调用 save,并打印 [KV-DISK] saved %d-token KV -> %s。
启动建索引 + 命中恢复:新进程启动时 vllm_server_diskkv_scan 扫描目录(main.c 第 4465/4717 行,serve 日志 [KV-DISK] index: N checkpoint(s)),把每个快照的 token 序列读进内存;请求来时(vllm_server.c 第 1308–1317 行)用 dkv_best_lcp 找最长前缀命中,st_kv_disk_load 读回,日志打 [KV-DISK] loaded %d-token KV prefix from %s。
读回(第 12406 行 st_kv_disk_load):先校验头里每个几何字段——有一个对不上就拒载(第 12428–12430 行),这是"绝不用错模型的 KV"的硬闸;随后跳过 token ids(它们已被 serve 层 LCP 逐 token 验证过),只按文件块的边界把 K/V 行读进缓存。
容量纪律:快照上限 DISKKV_MAX_TOKENS = 8192(vllm_server.c 第 451 行,覆盖 max_seq 窗口)、目录最多 DISKKV_MAX_FILES = 8 个文件、按 mtime LRU 淘汰(第 648 行)——磁盘缓存不是无限膨胀的。
3. 改动后果:同一台板子,跨进程恢复实测
口径:RK3588 / Qwen3-VL-2B / 2026-09 /
--disk-kv//v1/completions贪婪。S1 进程全量 prefill 2K 上下文并生成回答 → 快照落盘 → 杀掉 S1 → 启动 S2 → 客户端重发"历史+追问",观察 S2 是"读盘恢复 + 增量"还是"全量重算"。
| 阶段 | 事件 | 实测 |
|---|---|---|
| S1:turn1 全量 | prefill 2044-token 上下文 | 46.37 s(请求总耗时 51.34 s),日志 [KV-DISK] saved 2056-token KV -> kv_a0db…cb8.kv |
| 落盘 | 快照写入 eMMC | 471,605,344 B(≈450 MiB,ls 实测) |
| 杀 S1 / 启 S2 | 模拟"服务重启" | S2 启动即扫描目录:[KV-DISK] index: 1 checkpoint(s) |
| S2:turn2(历史+追问) | 恢复 + 增量 | 日志 loaded 2034-token KV prefix;prefill n=31 / 1.96 s(请求总耗时 7.27 s) |
判读:
- S2 没有全量重算:日志
loaded 2034-token KV prefix from kv_a0db…证明恢复的是 S1 的 KV;[PREFILL-TIMING] n=31 total=1962.2ms证明只算了增量尾巴(本轮 31 个新 token); - prefill 加速 ≈23.6×(46.37s → 1.96s),请求总耗时 51.34s → 7.27s(≈7.1×,双方都含 decode,见 12-3 的"口径拆解");
- 输出正确:S2 在恢复的完整上下文上答出了追问——
Based on the provided information, Bob lives in Tokyo.;KV 是"算出来的状态",恢复 == 重算(F32 位级一致),所以回答与从未重启一致。
仓库基准报告的 8K 档同款实验(RK3588_性能基准报告.md §3.2):turn1 全量 TTFT 201.7s → 快照 1.87 GB(+16s 写盘)→ 重启 boot 1.0s → 恢复后首 token 15.4s(
loaded 8104-token prefix)→ 相对全量 13.1×。llama.cpp 无等价物(重启即失),诚实标注这是本引擎相对它的架构级差异。我们这轮 2K 复现的 prefill 口径加速比 ≈23.6×、与 13.1× 同向(差异来自上下文比例与口径,12-3 细拆)。
4. 学员调试任务
- A 档(板端动手):复刻第 3 节:S1 发长 prompt → 记
[KV-DISK] saved与快照文件大小;杀进程;S2 同参启动 → 重发"历史+追问" → 抄[KV-DISK] loaded与[PREFILL-TIMING] n=。用ls -la看快照字节数,对照 12-2 的公式估算是否吻合。把上下文拉长一档再跑,观察加速比变化。 - B 档(纯读源码):读
st_kv_disk_load(vllm_safetensors.c 第 12406 行起)的几何校验段,回答:① 换一个不同几何的模型加载同一个 kv 目录,会不会发生错误恢复?② 文件名是 token 序列的 FNV-1a 哈希——哈希碰撞最坏会怎样(提示:serve 层还做逐 token LCP)?
预期输出:你能演示一次"杀掉进程再回来,会话没断"的完整闭环,并报出自己的恢复加速比与快照字节数。
收尾
- 本篇源码点名:vllm_safetensors.c(
st_kv_disk_save12357、st_kv_disk_load12406、布局注释 12312–12335)、vllm_server.c(save 620、索引 560、LRU 648、DISKKV_MAX_TOKENS451、命中路径 1308–1317)、main.c(--disk-kv4798、启动扫描 4465/4717)。 - 开源仓库:Kestrel-LLM (Gitee)(AGPL-3.0-or-later 或商业许可,二选一)
- 下篇预告:快照文件里到底躺着什么?12-2 用
xxd拆一个真实的.kv文件:VLKV 头、token ids、按块排布的 K/V——顺便回答"为什么 2K 会话就要 ~0.47GB"。