0.8MB 跑通 Qwen|第 13-2 篇:草稿 + 验证——推理引擎里 spec decode 的实现

简介: 本系列《0.8MB跑通Qwen》聚焦ARM端零依赖纯C推理引擎,实测RK3588上运行Qwen3-VL多模态模型;详解speculative decode实现——基于n-gram草稿、批量验证与无损回放,确保输出位级一致,兼顾 correctness 与工程可控性。(239字)

系列:《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 草稿 + 批量验证 + 无损回放

三个部件,各司其职:

  1. 草稿(draft):不需要额外的小模型(那要再部署一套模型)。引擎直接翻历史:把"当前上下文最后几个 token"当窗口,在更早的历史里找一模一样的片段,把那个片段后面跟过的 K 个 token 当作候选——这就是 n-gram 自推测。前提是文本在精确重复(叠句、固定格式列表、代码模板)——自由文本没有重复,草稿自然为空;
  2. 验证(verify):把 K 个草稿 token 拼进上下文,做一次批量 prefill(K 个位置一起前向,权重只读一遍),批量算出每个位置下一步的 top-1。如果位置 i 的 top-1 恰好等于草稿[i+1],说明模型"本来就要这么说",草稿[i+1] 被接受;
  3. 接受/回退:连串接受的部分一口气交给客户端;第一个不一致处断开——已接受的草稿没白算,断点处换成模型自己的真实预测。赌输的部分 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%)

判读:

  1. 输出逐字节一致:spec 的"无损回放"承诺兑现——开不开 spec,文本一模一样。这是"数学无损"的板端证据;
  2. 吞吐没赚,反而略亏:两个请求 spec 都比普通 decode 慢 ~4–10%。原因见第 1 节的成本模型:本引擎 spec 每轮要额外付一次 K-token 批前向 + 回放,而 2B 的普通 decode 单步才 ~44ms(13-1 表)——批前向的"信息增益"cover 不掉它自己的开销;
  3. 自由文本那个请求命中率其实不低(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_draft 786–814、spec_on 门 910–912、验证 958、无损回放 961–993、发射 1028–1053、统计 1113–1118)、main.c(--spec 4801、--spec-k 4803)、优化配置与边界说明.md(§3.4)。
  • 开源仓库:Kestrel-LLM (Gitee)(AGPL-3.0-or-later 或商业许可,二选一)
  • 下篇预告:既然短上下文开 spec 还亏,那它到底什么时候赚?13-3 给出收益判据:decode 单步越贵、命中率越高、批量前向越划算——并用一张表说清"赌错要回滚多少、什么时候别赌"。
相关文章
|
1天前
|
缓存 人工智能 索引
0.8MB 跑通 Qwen|第 12-1 篇:LLM 推理的跨进程恢复——服务重启了,会话不能断
本系列《0.8MB跑通Qwen》聚焦ARM端零依赖纯C推理引擎,实现在RK3588上部署Qwen3-VL等多尺寸模型。本文详解`--disk-kv`机制:将KV Cache落盘持久化,支持进程重启后按前缀精准恢复,实现“会话不断连”,2K上下文prefill加速达23.6×,真正打通边缘AI服务可用性最后一环。(239字)
|
2天前
|
缓存 算法 前端开发
翻译服务的批处理设计:装箱、断点续跑与上下文保持
翻译服务批处理三件套:6000 字符装箱(交互/离线双队列隔离)、每批落盘断点续跑(重跑携带上下文前缀)、三级缓存(LRU+术语表版本键控+前缀缓存),缓存节省约 15%。
32 1
|
1天前
|
人工智能 开发者
阿里云Token Plan个人版:四个版本区别对比、费用价格、Credits计费用量及问题解答FAQ
阿里云Token Plan个人版含Lite/ Essential/Standard/Pro四档:Lite(¥39,1.15万Credits)适合轻量使用;Essential(¥79)用量翻倍;Standard(¥139,推荐)起赠Harness权益;Pro(¥499)适配高并发与海量调用。权益、额度、并发数逐级提升,详见官网。
|
2天前
|
JSON Java Linux
0.8MB 跑通 Qwen|第 3-3 篇:推理引擎的零依赖自研 util——"不引第三方也能活"的边界感
本文实测验证“零第三方依赖”的务实边界:OS已支持的(如Linux原生UTF-8路径)仅薄封装为宏;OS缺失且轻量关键的功能(FNV-1a校验、小端memcpy、平台工具宏)才自研。RK3588真机验证(2026-09),代码简洁、可移植、无冗余。
0.8MB 跑通 Qwen|第 3-3 篇:推理引擎的零依赖自研 util——"不引第三方也能活"的边界感
|
1天前
|
NoSQL C语言 C++
0.8MB 跑通 Qwen|第 15-1 篇:推理引擎入口架构——参数 → 分发(serve/自检)→ 退出
本系列《0.8MB跑通Qwen》聚焦ARM端零依赖纯C推理引擎,实测适配Qwen3-VL-2B/8B及30B-A3B模型,在RK3588板端完成真机验证。本文详解`main()`入口如何通过命令行参数分发至serve、离线转换或自检三种命运,并用GDB抓取真实调用链,厘清初始化与退出逻辑。(239字)
|
1天前
|
缓存 算法 API
0.8MB 跑通 Qwen|第 11-3 篇:推理引擎里 KV 缓存的一生——内存、容量与淘汰
本系列《0.8MB跑通Qwen》聚焦ARM端零依赖纯C推理引擎开发,实测RK3588平台流畅运行Qwen3-VL-2B/8B等多版本。本文详解KV缓存“出生—续用/覆盖—死亡”三段式生命周期,揭示预分配、逻辑覆盖、进程级销毁本质,破除“命中即加速、淘汰即释放”认知误区。(239字)
|
1天前
|
机器学习/深度学习 区块链 C++
0.8MB 跑通 Qwen|第 10-1 篇:大模型推理的长上下文为什么必须稀疏——O(n²) 的注意力 vs O(n·k) 的"只看该看的"
本系列手搓0.8MB纯C推理引擎,零依赖跑通Qwen3-VL多模型。本文聚焦长上下文瓶颈:RK3588实测显示,注意力耗时占比从18%飙升至71%。提出块稀疏方案——将O(n²)注意力压至O(n·k),实现每词成本与上下文长度无关,为端侧大模型长文本推理提供关键路径。(239字)
|
1天前
|
缓存 C语言 内存技术
0.8MB 跑通 Qwen|第 8-2 篇:在线 softmax——推理引擎的注意力为什么不能先算完 e^x 再除
本系列手搓0.8MB零依赖纯C推理引擎,适配Qwen3-VL多模型,在RK3588(aarch64)真机实测。本文详解softmax溢出陷阱:fp32下exp&gt;88.7即inf,剖析朴素/稳定/在线三版实现,揭示“减max”数学等价性与在线rescale的流式优势。(239字)
|
3天前
|
移动开发 编译器 Linux
0.8MB 跑通 Qwen|第 2-2 篇:推理引擎的平台层——一个头文件守住全部平台契约(vllm_platform.h)
本文实测于RK3588(2026-09),提出“平台层契约”设计:将NEON、mmap、绑核等平台依赖统一收口至`vllm_platform.h`,通过`ST_HAVE_NEON`等宏提供唯一真相,配合`#error`门闩实现错误前移——有守卫仅报1行错,无守卫则引发46处误导性编译失败。
0.8MB 跑通 Qwen|第 2-2 篇:推理引擎的平台层——一个头文件守住全部平台契约(vllm_platform.h)
|
1天前
|
人工智能 运维 前端开发
接入多家模型供应商之后,我总结的 7 条铁律
AI 应用接了第二家模型供应商之后,事情就不再是"多加一个接口"那么简单——不是调不通,是钱会算错。这篇讲我们定下来的 7 条规则:幂等键、任务落库时机、备用线路的切换边界、退款、凭据隔离、付款来源固化、密钥不进浏览器。

热门文章

最新文章