系列:《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 篇 · 总纲(阿里云社区)
上一篇:12-2《磁盘 KV 快照格式——0.47 GB 从哪来》 | 下一篇:第 13 天《推测解码》
真机实测通过:本文实验已在 RK3588 板端实测完成(2026-09;方法学与原始记录见仓库 docs 与《实验脚本》目录)
一句话导读:复现 13.1×:推理引擎的跨进程恢复实测怎么做——把 S1 全量落盘、杀进程、S2 恢复的每一步命令与每一行日志摆出来,并拆清同一个实验为什么 prefill、TTFT、wall 三个口径报出三个数字。
关键词:手搓 Qwen 推理引擎、千问大模型推理、Qwen3-VL、零依赖纯 C、跨进程恢复、实测复现、性能口径、磁盘 KV
导语:仓库报告里的 8K 跨进程恢复 13.1× 怎么复现?本篇把 S1 全量落盘、杀进程、S2 恢复的每一步命令与每一行日志摆出来,并拆清同一个实验为什么在 prefill、TTFT、wall 三个口径下报出三个数字——先用你自己的板子声明口径,再报数字。
仓库基准报告写了"8K 跨进程恢复 13.1×"。今天不背书,把复现步骤与每一行日志摆出来,并且把两个口径拆清楚:为什么同一个实验,prefill 口径 ≈23.6×、而请求总耗时(wall)口径只有 ≈7.1×?因为口径不同,数字本来就不同——这是 Day 28 性能方法论的一课预演。
1. 知识点:三个口径,三个数字
跨进程恢复实验能报出至少三个加速比,别混着说:
- prefill 口径:只比"prefill 这段"——全量 46.37s vs 恢复后 1.96s → 23.6×。这是引擎真正省下的"算";
- 首 token(TTFT)口径:从请求到达算到第一个 token 吐出。恢复路径要先把 ~0.47 GB 快照从 eMMC 读进内存,TTFT 比纯 prefill 大;
- wall(整请求)口径:两端都含 decode。decode 在恢复与全量两条路径上一样贵(都要在完整上下文上逐词生成),所以 wall 加速比一定小于 prefill 加速比。
基准报告的 13.1× 用的是 TTFT 口径(202s → 15.4s,8K 档)——注意它两端都含"调度+首词",且恢复端含 1.87 GB 读盘。引用时永远带上口径,这就是"报告口径先行"的第一课。
2. 对应代码:复现的完整命令序列(板端实测模板)
环境:RK3588 / Qwen3-VL-2B / build-rk3588/vllm_kestrel。
# —— S1:启动,全量 turn1,落盘 ——
./vllm_kestrel --serve --port 18099 --model /mnt/emmc/Modl/Qwen3-VL-2B-Instruct \
--load-format vqf --auto-load --disk-kv /mnt/emmc/day12_kv \
> s1.log 2>&1 &
# 发长 prompt(12-1 的 P1)→ s1.log 应出现:
# [PREFILL-TIMING] n=2044 total=46373.1ms | GEMM=... ATTN=...
# [KV-DISK] saved 2056-token KV -> /mnt/emmc/day12_kv/kv_a0db3b0aa6b59cb8.kv
# [KV-DISK] index: 1 checkpoint(s) in /mnt/emmc/day12_kv
kill %1 # ← 杀掉 S1,模拟服务重启
# —— S2:重启,发"历史+追问" ——
./vllm_kestrel --serve --port 18099 --model /mnt/emmc/Modl/Qwen3-VL-2B-Instruct \
--load-format vqf --auto-load --disk-kv /mnt/emmc/day12_kv \
> s2.log 2>&1 &
# 发 P2(P1 + 上轮回答 + 追问)→ s2.log 应出现:
# [KV-DISK] index: 1 checkpoint(s) in /mnt/emmc/day12_kv ← 启动即建索引
# [KV-DISK] loaded 2034-token KV prefix from kv_a0db...cb8.kv ← 命中
# [PREFILL-TIMING] n=31 total=1962.2ms ← 只算增量
板端实测结果汇总(2026-09,进程串行):
| 口径 | S1(全量) | S2(恢复) | 加速比 |
|---|---|---|---|
| prefill 时间 | 46.37 s(n=2044) | 1.96 s(n=31) | ≈23.6× |
| 请求总耗时(wall,含 decode 与读盘) | 51.34 s | 7.27 s | ≈7.1× |
把 13.1× 与上表对齐:报告的 13.1× 是 8K 档 TTFT 口径(201.7s→15.4s)。8K 全量 prefill 占 TTFT 的绝对大头,恢复端 TTFT 几乎全被"1.87 GB 读盘 + 34 token 增量 + decode 首词"吃掉——所以它介于 prefill 口径与 wall 口径之间、更接近"真首 token 体验"。用你自己的板子复现时,先声明口径,再报数字。
复现要点(都是从翻车里学来的纪律):
- 同参启动:S2 的 wmode/线程/
--disk-kv目录必须与 S1 一致(几何写进快照头,不一致会拒载);- 目录要干净:S1 前先清空 kv 目录,否则旧快照可能被 LCP 命中,数字就不是"这一个会话"的;
- 串行无并发:任何后台负载都会污染计时(Day 7 的抖动纪律);
- 先 warm:第一轮请求会触发权重 mmap 触页(Day 11-3 的 RSS 实验:+929 MB),正式对照前先发一次短请求热身。
3. 改动后果:把 --disk-kv 拿掉,重启后就回到原始社会
对照实验(把 S1/S2 里的 --disk-kv 去掉,其余全同)板端实测:
- S2 启动后目录无索引(日志无
index: N checkpoint(s)); - P2 请求的 LCP 只能对上进程内的上一轮——而进程是新的,
last_ids为空 →keep=0→ 全量重算; - 实测
[PREFILL-TIMING] n=2065 total=48205.2ms(请求总耗时 52.55s),与恢复路径同请求的 n=31 / 1.96 s 形成对照。
| 配置(同一 P2 请求文本) | prefill | 请求总耗时 | [KV-DISK] |
|---|---|---|---|
带 --disk-kv(恢复) |
n=31 / 1.96 s | 7.27 s | loaded 2034-token KV prefix |
不带 --disk-kv(对照) |
n=2065 / 48.21 s | 52.55 s | 0 行(全量重算) |
prefill 口径 ≈24.6×、wall 口径 ≈7.2×,且两条路径输出逐字节一致(都是 Bob lives in Tokyo.)。这组对照把 12-1 的结论钉死:跨进程恢复的收益 100% 来自 --disk-kv,与"进程内前缀复用"无关(进程内复用只对同进程连发有效,重启即失效)。
4. 学员调试任务
- A 档(板端动手):按第 2 节模板完整复现一轮(S1 全量 → kill → S2 恢复),抄出四条日志(saved / index / loaded / PREFILL-TIMING),填出你自己的三个口径加速比,并标注用的是哪个口径。有条件的再跑一次 4K 档,看加速比随上下文比例怎么变。
- B 档(纯读源码):把 12-1/12-2 的锚点串起来,回答:① 恢复路径的 TTFT 由哪几段组成(提示:读盘、增量 prefill、decode 首词、调度)?② 为什么"目录不干净"会污染 LCP 命中?③
DISKKV_MAX_TOKENS=8192对 8K+ 会话意味着什么(提示:快照按 8192 钳制,超长会话怎么处理)?
预期输出:你能脱离文档,用四行日志向别人演示"进程杀了会话没断",并说清自己报的是 prefill / TTFT / wall 哪个口径。
收尾
- 本篇源码点名:vllm_server.c(save 620、index 560、命中 1308–1317、LRU 648)、vllm_safetensors.c(save/load)、RK3588_性能基准报告.md(§3.2)。
- 开源仓库:Kestrel-LLM (Gitee)(AGPL-3.0-or-later 或商业许可,二选一)
- 下篇预告:单请求已经很快,可 decode 逐词生成仍是"等一个、算一个"。Day 13 上推测解码(
--spec):让模型"赌"几个候选词一起验证——但 2B 本地 decode 才 ~120ms/词,verify 的账怎么算才划算?