系列:《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-1《decode 为什么慢——逐词、带宽、不可并行》 | 下一篇:13-3《回退与收益边界:哪些文本 spec 划算》
真机实测通过:本文实验已在 RK3588 板端实测完成(2026-09;方法学与原始记录见仓库 docs 与《实验脚本》目录)
一句话导读:草稿加验证:spec decode 的实现——推理引擎先猜 K 个候选词、再拿一次批量前向同时验证,赌对了一次循环就能吐 K+1 个词,本篇拆草稿从哪来、怎么验、赌错如何无损回退。
关键词:手搓 Qwen 推理引擎、千问大模型推理、spec decode、推测解码、批量验证、无损回放、Qwen3-VL、零依赖纯 C
导语:推测解码的思路是:与其每一步白读一遍权重,不如先猜 K 个候选词,再用一次批量前向同时验证它们。本篇拆开这份「草稿 + 验证」的实现——草稿怎么从历史 n-gram 里翻出来、怎么批量验、赌错又如何靠 KV 回滚与无损回放退回来。
13-1 说 decode 慢在"每词一次前向"。spec(speculative decode,推测解码)的思路是:先猜 K 个候选词,再拿一次批量前向同时验证它们——赌对了,一次循环就能吐 K+1 个词。今天拆这个"赌"的实现:草稿哪来、怎么验、赌错怎么办。
1. 知识点:n-gram 草稿 + 批量验证 + 无损回放
三个部件,各司其职:
- 草稿(draft):不需要额外的小模型(那要再部署一套模型)。引擎直接翻历史:把"当前上下文最后几个 token"当窗口,在更早的历史里找一模一样的片段,把那个片段后面跟过的 K 个 token 当作候选——这就是 n-gram 自推测。前提是文本在精确重复(叠句、固定格式列表、代码模板)——自由文本没有重复,草稿自然为空;
- 验证(verify):把 K 个草稿 token 拼进上下文,做一次批量 prefill(K 个位置一起前向,权重只读一遍),批量算出每个位置下一步的 top-1。如果位置 i 的 top-1 恰好等于草稿[i+1],说明模型"本来就要这么说",草稿[i+1] 被接受;
- 接受/回退:连串接受的部分一口气交给客户端;第一个不一致处断开——已接受的草稿没白算,断点处换成模型自己的真实预测。赌输的部分 KV 回滚,成本是一次批量前向 + 一次回滚。
为什么要有"无损回放"(本引擎的关键设计,vllm_server.c 第 961–978 行注释):批量验证走的融合 GEMM 与单 token decode 走的单行 GEMM 是两条数值路径,在停止边界附近会漂移(注释记录了 2026-08-31 的真实事故:repeat_same 在两条路径下产生 90 对 86 个 token)。为了 KV 与正常解码位级一致,引擎用批量验证只做"接受上界探测",然后把接受的前缀用 decode 路径逐 token 重放一遍确认。代价是:每轮 spec 的成本 = 1 次 K-token 批前向 + R 次回放前向 + 1 次 bonus 前向——它不省前向次数,省的是循环/发射的调度开销,并且用批前向换"接受长度"的信息。这意味着 spec 能不能赚,完全取决于"decode 单步多贵、批前向相对多便宜、命中率多高"(13-3 用板端数字算这笔账)。
2. 对应代码:逐段导读
草稿构造(spec_build_draft,第 786–814 行):
for (int w = 8; w >= 3; w--) {
/* 窗口最长优先:8 → 7 → … → 3 */
int lo = len - w; /* 当前上下文末尾 w 个 token */
for (int s = 0; s + w + 1 <= lo; s++) {
/* 在更早历史里找相同窗口 */
int match = 1;
for (int j = 0; j < w; j++)
if (seq[s+j] != seq[lo+j]) {
match = 0; break; }
if (!match) continue;
for (int i = 0; i < n; i++) /* 取那段后面跟过的 token */
draft[i] = seq[s + w + i];
return n; /* 只返回最长窗口的那一注 */
}
}
要点:窗口从 8 个 token 开始往下试——窗口越长,续写越可靠(固定模板是长窗口,自由文本连 3 个都凑不齐就返回 0 走普通解码)。注释把适用/不适用场景写得很直白(第 786–791 行):叠句/固定列表/代码模板全中;计数器/变化文本("第 1 点/第 2 点…")系统性给旧值、命中率为 0。
门控(第 910–912 行):--spec + 纯文本 + 贪婪(temp≤0 / top_p≥1 / min_p≤0)+ 非 L3 淘汰 + 非稀疏 + 非连续批处理——一条不满足就退化为普通 decode。
批量验证与回放(第 958 行起):st_qwen_model_prefill_batch(st, draft, K) 一次算 K 个位置的 logits(第 958 行);探测接受上界(第 970–973 行);KV 回滚到 L、恢复 logits(第 979–982 行);用 decode 路径逐 token 重放确认(第 984–993 行);接受≥1 则发射草稿+bonus(第 1028–1053 行)。收尾打印每请求统计(第 1113–1118 行):
[SPEC] tries=2 hits=5 kv_rolls=3 (62% draft hit)
hits = 接受的真实 token 数;kv_rolls = 因赌输回滚的草稿数;命中率 = hits / (tries×k)。
3. 改动后果:板端实测——开/关 spec 的吞吐与一致性
口径:RK3588 / Qwen3-VL-2B / 2026-09 / serve 贪婪(temp 0)短上下文。重复文本(输出 16 个
kestrel)与自由文本(一段解释)各一,--spec --spec-k 4开与关对照;请求文本与 max_tokens 完全一致。
| 请求 | spec 关 | spec 开 | 输出(开 vs 关) | 引擎统计 |
|---|---|---|---|---|
重复文本 kestrel×16 |
3.449 s | 3.594 s(+4.2%) | 逐字节一致(127 字符) | spec 基本未触发(输出太短) |
| 自由文本(种子→植物) | 2.021 s | 2.229 s(+10.3%) | 逐字节一致(179 字符) | [SPEC] tries=2 hits=5 kv_rolls=3 (62%) |
判读:
- 输出逐字节一致:spec 的"无损回放"承诺兑现——开不开 spec,文本一模一样。这是"数学无损"的板端证据;
- 吞吐没赚,反而略亏:两个请求 spec 都比普通 decode 慢 ~4–10%。原因见第 1 节的成本模型:本引擎 spec 每轮要额外付一次 K-token 批前向 + 回放,而 2B 的普通 decode 单步才 ~44ms(13-1 表)——批前向的"信息增益"cover 不掉它自己的开销;
- 自由文本那个请求命中率其实不低(62%,因为模型回答里自己重复了一句"the seed absorbs water and begins to grow"),可有命中 ≠ 有收益——命中省下的调度开销远小于验证的固定成本。
诚实口径:本节是"短上下文 + 2B + 本引擎现行无损回放实现"下的实测。仓库文档 §3.4 的 1.30×(文本)/1.64×(多模态)来自 x86 8B 更早的测量与更慢的单步 decode;本引擎的回放逻辑后经 2026-08-31 修正为保位级一致(vllm_server.c 第 963–969 行注释),单步 decode 越快的地方 spec 的账越难算平——13-3 给"该不该开"的判据。
4. 学员调试任务
- A 档(板端动手):复测第 3 节两对(重复文本/自由文本 × spec 开/关),抄
WALL_S与[SPEC]行,验证"输出逐字节一致、本板短上下文无正收益"。再把--spec-k调成 8 跑重复文本,看命中率与耗时怎么变。 - B 档(纯读源码):读 spec_build_draft(786–814 行),回答:① 为什么窗口从 8 往 3 试、而不是从 3 往 8?② 计数器文本("1)…2)…")为什么注定 0 命中?③ 第 799 行
s + w + 1 <= lo的 +1 在防什么?
预期输出:你能手写一遍"找窗口 → 取草稿"的伪码,并解释为什么"有 62% 命中率却仍然更慢"。
收尾
- 本篇源码点名:vllm_server.c(
spec_build_draft786–814、spec_on 门 910–912、验证 958、无损回放 961–993、发射 1028–1053、统计 1113–1118)、main.c(--spec4801、--spec-k4803)、优化配置与边界说明.md(§3.4)。 - 开源仓库:Kestrel-LLM (Gitee)(AGPL-3.0-or-later 或商业许可,二选一)
- 下篇预告:既然短上下文开 spec 还亏,那它到底什么时候赚?13-3 给出收益判据:decode 单步越贵、命中率越高、批量前向越划算——并用一张表说清"赌错要回滚多少、什么时候别赌"。