从0训练LLM · 现代大模型体系 阅读 + 实验 ≈ 45 分钟 页内实验 ×5
C13 推理:生成为什么慢、怎么变快 第 3 课

一次前向验好几个 token,凭什么输出分布一点都不变?

投机解码:为什么一个字都不改
batch 1 时目标模型读一遍权重只算 1 个 token,算力几乎全闲着;让一个小草稿先猜几个,目标模型一次前向就能把它们全验完。难的是采样:相同才收,我们的模型只收 0.73%;被拒后从目标分布重采,分布被拉偏 0.166。正确做法是按 min(1, p/q) 接受、拒绝时从残差 norm(max(0, p − q)) 重采——输出分布与目标模型逐字相同,20 万次抽样与直接采样的差只剩抽样噪声。
❓

本课一问:batch 1 的时候,目标模型读一遍权重只算 1 个 token。如果先让一个小草稿猜几个,目标模型一次前向把它们全验完,就能一次出好几个 token。可目标模型是按概率采样的——怎么保证这样出来的字,和目标模型自己一个个采出来的,分布一模一样?

这一课先不碰:采样分布本身怎么来(温度、截断,见岔路课);草稿模型怎么训练(MTP 作为训练目标在 C11 岔路);服务端的 batch(第 2 课)。

从已有出发

从已有出发 · 一次请求在服务端走过的路 点任一格看锚点 Qwen3.8-27B 上的真实量
decode 循环:每个 token 走一圈
旁路:

第 1 课的账:锚点 batch 1 时,生成 1 个 token 的计算下界约 0.052 ms,读权重的下界约 15.30 ms。如果一次前向同时算 5 个位置,计算下界涨到 0.26 ms,仍远小于读权重的 15.30 ms——多验几个 token 几乎不多花时间。贯穿例句那样的请求,写一个字和验五个字,耗时几乎一样。

问题只剩一个:草稿猜的字,哪些该收?

🔁

这一课为什么换例子。要演示「收哪些」,需要两个真模型在同一个前缀上各自给出完整的概率分布,而且词表相同。锚点在浏览器里跑不动,贯穿例句也不在我们小模型的训练分布里。所以换成我们项目里三个同语料、同词表、同训练步数的模型:目标 ref_baseline(3,688,704 参数),草稿 c1_dim64(332,352,约 1/11)与 c1_dim128(1,066,112,约 1/3.5)。全部分布、轨迹与记账由 gen_c13_precomputed.py 离线真跑得到。

先自己设计:什么时候算「猜中」?

旧版课程的演示用的是贪心:草稿猜的 token 等于目标模型的 argmax 就收。温度 0 时这完全正确——我们在 4 个提示、2 个草稿、k = 2 和 4 上各跑了一遍,投机输出与目标自己贪心解码逐 token 相同 16 / 16。可一旦按概率采样,「目标会选哪个」本身就是随机的,没有 argmax 可比。

最自然的推广:草稿按 q 采一个,目标按 p 也采一个,相同才收,不同就用目标采的那个。输出总是目标采的,分布当然对。问题在别处:

  1. ① 朴素方案
  2. ② 它在哪失败
  3. ③ 改进方案
朴素方案草稿、目标各采一个,相同才收
朴素做法:草稿和目标各采一个,相同才收

改进:接受概率 min(1, p/q),拒绝时从残差重采

草稿按 q 采到 x 之后:如果目标给 x 的概率不比草稿低(p(x) ≥ q(x)),一定收;如果草稿高估了它(q(x) > p(x)),只以 p(x)/q(x) 的概率收。被拒时,不是从 p 重采,而是从「p 比 q 多出来的那部分」重采:

为什么输出分布恰好是 p(Leviathan et al. 2022 附录 A.1)
= + · = min(p, q) + = p(x)
👆 公式里带下划线的每一项都可以点——点哪项,讲哪项。
换成「被拒后从 p 重采」:P(x) = min(p, q) + (1 − α)·p(x) ≠ p(x)——共有的那部分被数了两次。组件里的「先预测」就是这一问。

这是一个恒等式,对任意 p 与 q 成立 。离线大样本核对(python,完整 2,048 维,提示「孔明曰:「」,200,000 次):正确做法的输出与 p 的总变差距离 0.0218,直接从 p 采同样次数的抽样噪声是 0.0215——两者一样大;「被拒后从 p 重采」是 0.1666(理论值 0.165906)。

在上面「改进方案」一侧的组件里,点「跑一轮」逐项看一次判定,再用三个按钮先预测、再跑 10,000 次:

🎯

试四件事:① 点「跑一轮」几次:落在绿 + 蓝那些 token(q ≤ p)上一定收;落在带红色的 token 上按 p/q 的比例收。② 先回答「被拒以后该从哪个分布重采」,再看 10,000 次的黑竖线是不是落在虚线(p)上:选「目标分布 p」时,「孔明」这种目标很看好、草稿却很少猜的 token 明显偏低。③ 换成大草稿:α 从 56.7% 变成 56.5%,换个前缀(「卻說曹操」)差别更大。④ 点「×100,000」,把总变差距离和离线的 0.0218 对照。

目标模型一次前向怎么同时给出 k + 1 个分布?把草稿猜的 k 个 token 接在后面整段喂进去,每个位置的输出就是「假设前面都对,下一个 token 的分布」——这就是一次 prefill:

一次目标前向验 k 个草稿 token(锚点词表真实维度;可切小规模)
P = softmax(ftarget([x1 … xk]) ÷ T)
输入输出中间结果 渐变 = 分数(连续值)编号槽 = 索引斜线 = 掩码(留白 = 不读)
边长按对数比例缩放,块边上的数字是真实值;同一维度符号的边在全图等长。
维度含义 真实规模 小规模
k1 k + 1:要验的位置数 5 5
k 草稿每轮猜的 token 数 4 4
V 词表大小(锚点 vocab_size) 248,320 8
分数(P、Q)、索引(x)、掩码(accept)是三种不同的对象:P、Q 连续,x 是编号,accept 只有收 / 不收。

一轮能出几个 token:回放与记账

下面是一次真跑的完整轨迹:目标 ref_baseline、草稿 c1_dim64、k = 4、温度 1,提示「孔明曰:「」。每一轮的草稿 token、它在两个模型下的概率、接受概率和这次用到的随机数都在:

真模型回放 · 一次真实的投机采样(我们的 3.7M 目标 + 0.33M 草稿)
回放一次真实的投机采样(我们的模型,k = 4)
🎯

试三件事(保留旧版的三条结论):① 一轮一轮点,看「每次目标前向产出」怎么变:这一次 15 轮出了 33 个 token;② 找一轮第一个就被拒的:这一轮照样产出 1 个 token(残差重采的那个)——最坏也就是逐 token 生成,不会更少;③ 找一轮 4 个全收的:再白拿 1 个,一轮 5 个,这是 k = 4 的上限。所以每轮产出 = 接受数 + 1,落在 1 到 k + 1 之间。

每轮的期望产出有一个闭式:若每个位置的接受率都是 α、互相独立,一轮产出 (1 − αk+1) ÷ (1 − α) 个 token 。真实文本里每个位置的 α 并不一样(验证集 64 个位置上从 30% 到 96%),所以要拿真跑来对:

真模型实测 · 12 组投机采样的记账(4 提示 × 3 种子 × 96 个新 token)vs 公式
一轮能产出几个 token:公式与真跑
🎯

试三件事:① 表里「每次目标前向产出(真跑)」和「公式」两列差多少?(k = 4 时小草稿 1.96 对 1.95,大草稿 2.40 对 2.40)② 大草稿每个 token 的接受率更高,可它更贵:看最后一列,小草稿 k = 3 时理论加速最高(1.50×),大草稿 k = 8 时只有 0.75×——比不投机还慢;③ 把 α 拖到 0.8、c 拖到 0:k = 1 时正好 1.8 个——这就是下面 DeepSeek-V3 的情形。

论文与报告里的真实数字(都是提出方自报,只在各自的模型与硬件上成立):

论文与报告里的真实数字

现代:让模型自己当草稿

独立训练、独立部署一个草稿模型很麻烦,还要和目标对得上。于是出现了「自带草稿」的做法:

🔍 线性注意力(GDN / KDA)在投机解码里的麻烦

全注意力层验证被拒时,把多写的 KV 丢掉就行;线性注意力层的状态是原地更新的,验证时已经把几个草稿 token 写进去了,一旦被拒没法简单回退。每个草稿位置都存一份状态快照能解决,可状态本身就大(锚点每请求 151 MB,即 144 MiB),流量会成倍增加。Kimi K3 的做法是只缓存草稿 token 的投影输入,被接受的前缀在芯片上重建状态(报告 §5.4.2)。这是混合架构为投机解码付的一笔工程账。

⚠ 常见误解

⚠

「投机解码是近似,质量会降一点。」按 min(1, p/q) 接受、残差重采,输出分布与目标模型严格相同(恒等式,上面的 20 万次抽样只剩抽样噪声)。它改的是「什么时候算」,不是「算出什么」。

⚠

「草稿越大、越准越好。」接受率高了,草稿自己的前向也贵了。Leviathan 表 2:T5-small 草稿 α = 0.75 → 3.4 倍,T5-large α = 0.82 → 只有 1.7 倍;我们的大草稿 k = 8 时理论加速 0.75×,比不投机还慢。

⚠

「被拒以后,从目标分布重采一个就行。」那会把两个分布共有的部分数两次:总变差距离 0.1666,远大于抽样噪声。必须从残差 max(0, p − q) 重采。

⚠

「batch 很大时,投机解码照样白赚。」它白赚的前提是第 1 课的「带宽受限、算力闲着」。batch 大到接近算力受限区时,多验 k 个位置就是实打实多算 k 倍,收益会缩水甚至变负;EAGLE-3 在 SGLang、batch 64 时的吞吐提升只有 1.38 倍(论文自报)。

在演进地图上的位置

推理加速的演进:每一代都被上一代的某个真数字逼出来(点名字去那一课;完整版在章首页)
  1. 每一步把整段重新喂进模型
  2. KV cache
  3. 批处理:一次解码多个请求
  4. 连续批处理(迭代级调度)
  5. PagedAttention:KV 分块按需分配
  6. 投机解码:草稿猜、目标一次验
  7. 让模型自己当草稿:MTP 与专用草稿器
  8. 量化:每个权重少读几位
  9. 按 TTFT / TPOT / goodput 调度:分块 prefill、P/D 分离
  10. 采样:温度 → top-k → top-p → min-p
  11. 少存 KV:GQA / MLA / 线性注意力
第五代投机解码:草稿猜、目标一次验Leviathan 2022;Chen 2023你在这里
解决了按 min(1, p/q) 接受、残差重采,输出分布不变;我们的模型 α = 0.567,k = 4 时每次目标前向产出 1.96 个 token(真跑)。
卡在草稿要单独训练、单独部署,还要和目标对得上:接受率太低时反而更慢(第 3 课)。
数字口径:锚点 Qwen3.8-27B 的字节与时间是 H100 SXM 上的屋顶线上界(不是实测),由 anchor-model.json 现算;调度、投机、量化、采样的数字来自我们项目的 3 个小模型(章内 precomputed.json,脚本 gen_c13_precomputed.py,2026-09-26);vLLM、DeepSeek-V3 的数字出自本地 PDF,Orca、DistServe 出自论文原文,均为提出方自报。

证据三问

问本课的回答
① 它解决什么问题?batch 小时算力闲着:让草稿先猜、目标一次验完,单个用户每次目标前向能拿到多于 1 个 token
② 数学上为什么?输出分布等于 p 是恒等式 ;期望产出公式在「各位置接受率独立同分布」下可证
③ 实验上呢?加速倍数只能实测,强烈依赖草稿、任务、温度与 batch:T5-XXL 1.4–3.4 倍、Chinchilla 70B 2–2.5 倍、DeepSeek-V3 1.8 倍

一句话

📌

草稿猜、目标一次验完;按 min(1, p/q) 接受、拒绝时从残差重采,输出分布与目标模型逐字相同,省的只是目标模型的前向次数。

🚦

先停在这里:记住「绿 + 蓝 = p」那张叠放图,就能去第 4 课。
再深一层:读 Leviathan 论文的附录 A.1 与定理 3.8(草稿代价 c 进来之后,最优的 k 怎么选)。

下一个缺口

投机解码没有减少任何一次前向要读的字节,它只是让每次读完能多出几个 token。另一条路正好相反:每次前向少读一点。锚点每个权重现在是 2 字节——非要 16 位吗? → 第 4 课:把权重从 16 位压到 4 位,为什么能变快,又会丢什么?

你可能会问

这一课还没有收到真实提问;你的提问会记到 05-问答记录/C13-L3.md。