系列:《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 篇 · 总纲(阿里云社区)
上一篇:13-2《草稿 + 验证:spec decode 的实现》 | 下一篇:第 14 天《批处理与调度》
源码精读篇:本文为源码/方法论精读,无独立实测;文中数字均引述仓库 docs 的板端实测记录
一句话导读:回退与收益边界:推理引擎里哪些场景 spec 才划算——把收益公式写出来,用数字说清"decode 单步多贵、命中率多高、批量验证多便宜"三条变量如何决定输赢,并附一次诚实的回退量化。
关键词:手搓 Qwen 推理引擎、千问大模型推理、spec decode、收益边界、命中率、KV 回滚、Qwen3-VL、零依赖纯 C
导语:命中率高达 62%,spec 却仍然更慢——这正是本篇要解释的反直觉。把「单步 decode 多贵、批量验证多便宜、命中率多高」写成一条收益公式,就能看清推测解码在什么场景才划算:何时该赌、赌错要赔多少,以及为什么默认把它关掉。
13-2 的板端结论有点反直觉:命中率 62% 的开销反而更慢。今天把 spec 的收益公式写出来,用数字说清"什么时候该赌、什么时候别赌"——包括一次诚实的回退量化。
1. 知识点:spec 的收益公式
一次 spec 轮次的账可以写成(K=草稿长,R=被接受数,T=单步 decode 耗时,V=一次 K-token 批量验证耗时):
普通 decode 产 R+1 个词耗时: (R+1) × T
spec 产 R+1 个词耗时: V + R×T(回放)+ 发射/调度开销
赚的条件: V + 开销 < T
三个变量决定胜负:
- T(单步 decode 越贵越好):上下文越长、模型越大、权重越宽,T 越大,V 的相对占比越小——spec 的主场是长上下文/大模型的慢 decode;
- V(批量验证越便宜越好):K-token 批量 prefill 权重只读一遍,理论上 ≈ 一次 decode 的带宽——K 越大摊得越薄,但 K 太大会让"赌输回滚"的浪费也变大;
- R/K(命中率):只有精确重复文本才高;自由文本 R≈0 时,spec 纯亏一次 V + 回滚。
回退(kv_rolls)是纯浪费:赌 K 个、只中 R 个,就有 K−R 个草稿的 KV 被写进去又回滚掉——白算的。13-2 那轮 free 请求 tries=2 hits=5 kv_rolls=3:赌了 8 个草稿位、中了 5 个、回滚 3 个。
2. 对应代码:边界在哪
- 只有贪婪纯文本路径:
temperature<=0 / top_p>=1 / min_p<=0(vllm_server.c 第 910–912 行)——采样路径(温度>0)下"草稿+验证"的定义不成立; - 与 L3 / 稀疏 / 连续批处理互斥:
!g_l3_evict && !g_sparse_attn && batch_max<2——它们共享同一份推理状态,spec 的回滚语义会打架; - 草稿来源受限:只对精确重复生效。编号递增类("第 N 点")草稿系统性给旧值、实测 0% 命中(注释原话,vllm_server.c 第 786–791 行);
- 收益依赖 T 的绝对值:仓库文档(优化配置与边界说明.md §3.4)自己写得很诚实——"2B 本地 decode 仅 25ms/token,verify 开销 ~28ms 吃掉了大部分增益;RK3588(341ms/token)上理论增益 2-3×"。我们板端实测把这件事钉得更实:本板 2B 的短上下文单步 decode 只有 ~44ms(13-1 表,q4 dual),比文档假设的 341ms 快一个量级,于是 spec 的净效应是负的(13-2 实测 +4%~+10%)。
3. 改动后果:板端实测的"别开"证据 + 文档的"能开"旁证
本板(RK3588 / Qwen3-VL-2B / 短上下文 / 2026-09):
| 请求 | spec 关 | spec 开 | 净效应 | 输出 |
|---|---|---|---|---|
重复文本 kestrel×16 |
3.449 s | 3.594 s | +4.2%(亏) | 逐字节一致 |
| 自由文本 | 2.021 s | 2.229 s | +10.3%(亏) | 逐字节一致 |
同一引擎,什么条件下才转正(文档口径,非本板实测):x86 8B 更早测量文本草稿命中 ~75%、tpot 25.3→19.5ms(1.30×);多模态命中 ~92%、28.8→17.6ms(1.64×)。对比两行:文档场景的单步 decode 在 20–30ms 级、命中 75%+ 才赚 ~1.3×;我们板端单步 44ms 且输出短、命中覆盖低,spec 的固定开销就压不住了。
把账算平需要的条件(给读者当 checklist):
- 单步 decode ≥ ~100ms(长上下文 / 更大模型 / 更宽 KV);
- 输出是精确重复型(模板、叠句、代码、清单);
- 输出足够长(几十个 token 起步),让"命中省下的"有机会累积;
- 请求是贪婪、纯文本、无 L3/稀疏/batch 叠加。
四条全中才值得开 --spec;否则默认关(引擎默认就是关的,--spec 显式开启)。
诚实标注:本板没跑"长上下文+重复文本"的 spec 正向案例——要把单步 decode 拉到 100ms+,需要 8K 上下文(Day 10 的 155ms/词),而 8K prefill 要 ~2.5 分钟/次、加 spec 多轮重放的总成本更高,本系列在 2B 上选择如实报告"无收益区",把正向收益留给更大模型档位(文档 8B 数据可作旁证,不做板端背书)。
4. 学员调试任务
- A 档(板端动手):把第 3 节的两个 prompt 换成"输出 100 个 kestrel"(长重复)与"解释…且结尾必须重复一句话"(带自重复的自由文本),各跑 spec 开/关,观察:① 命中率是否上升;② 净效应是否从负转平/转正;③ 输出是否仍逐字节一致。用更长上下文(先喂 2K 前缀)再试一次,验证"T 越大越容易赚"。
- B 档(纯读源码):读 spec 轮次统计(vllm_server.c 第 923、935、1012–1013、1022、1113–1118 行),回答:①
kv_rolls在"全拒"与"部分接受"时各加多少?② 为什么hits只在real_acc>=1时累加?③ 若 K=4 但每轮 R=3,写一下tries/hits/kv_rolls三者的代数关系。
预期输出:你能用收益公式 V + 回滚 < T 向别人解释"为什么有 62% 命中率还更慢",并给自己手头的模型/场景给出开不开 spec 的判断。
收尾
- 本篇源码点名:vllm_server.c(spec_on 门 910–912、统计 923/1113–1118、草稿注释 786–791)、优化配置与边界说明.md(§3.4 边界与收益)。
- 开源仓库:Kestrel-LLM (Gitee)(AGPL-3.0-or-later 或商业许可,二选一)
- 下篇预告:单请求能省的都省了——可真实服务同时来了 8 个请求呢?Day 14 上连续批处理(
--batch-max):让一批请求共用一次权重读取,还顺手回答"为什么批量下 spec/前缀复用得让位"。