上一章《高效注意力》说:瓶颈不在「算」,而在「搬」——Decode 也一样,为生成 1 个 token 要把全部权重读一遍。
那能不能一次前向多产出几个 token?
投机解码用一个很巧的办法做到了,而且输出分布和原来一模一样。
一次 Decode 前向要做什么:把全部权重从显存读进计算单元,做完矩阵乘法,吐出一个 token。
互动 · 一次前向到底忙不忙
Prefill 阶段(读 prompt,也就是你输入的那段提示词):一次处理几百上千个位置,
矩阵足够大,GPU
Decode 阶段(吐 token):一次只处理 1 个位置,但还是要读一遍全部权重——
读了几十 GB,只做了极少的乘加,算力利用率可能只有个位数百分比,这叫 memory-bound(带宽受限)。
卡住 Decode 的不是算得快不快,而是显存搬得快不快。
互动 · 一次 Decode 的时间,都花在哪
一张图看清 Prefill 和 Decode 差在哪
像一个每次只运一块砖的卡车。卡车的载重能力(算力)完全不是问题,
问题是跑一趟要烧的油(读取全量权重)永远不变。
那最直接的想法就是:能不能一趟多运几块砖?
「一趟多运几块砖」这个想法卡在一个地方:你没法确定下一块砖在哪。
所以在讲怎么运之前,先解决一个更前置的疑问——凭什么猜得准?
下面是一句普通的中文,切成 10 个 token。逐个位置问模型:「下一个词是什么?」 把模型在每个位置给出的最高概率画出来。
互动 · 逐个位置的预测置信度(真过一遍 softmax)
把上面那张图从上往下扫一遍——10 个位置里有 6 个的置信度在 0.83 以上:
「的」(0.92)、「是」(0.95)、「于」(0.89)、「北京」(0.89)、「年」(0.92)这些位置,
模型几乎没有第二种答案,它们被语法和常识锁死了。
真正没底的位置只有两个:句首(0.36)和那个年份(最高一项只有 0.20,1949 和 1978 几乎打平)。
所以「让小模型去猜」这句话才成立:它要猜的不是难的位置,而是那些本来就没有悬念的位置。
互动 · 分布越平,熵越大:拖一拖「分布平坦度」
互动 · 「每个位置都好猜」为什么不等于「整串都能猜对」
这像一条只有一两个岔路口的山路——剩下八九个弯都是死弯,闭着眼也开得过去。
于是真正的问题不是「怎么把山路修直」,而是:既然大部分弯不需要思考,
能不能先派个便宜的人去开?
既然大部分位置本来就只有一个合理答案,「猜」这件事就有戏了。 但要真的省下时间,还得先过一道坎:你猜的这一串,大模型要怎么验?
这里有个看起来很矛盾的地方:大模型天生就只能一次吐一个 token—— 因为第 2 个 token 依赖第 1 个,你没法并行猜。
但换个角度:「验证」不需要串行。如果你已经拿到了一串候选 token, 大模型可以一次前向同时给这串里每个位置打分——这是并行的。
一张图看完一轮:猜 → 一次并行验证 → 接前缀 → 白拿一个
判断一条猜测留不留,规则很朴素:以概率 min(1, p/q) 接受
(p 是大模型给这个 token 的概率,q 是 draft 给的)。
直觉是:只有当 draft 比大模型还自信(q 比 p 大)时才打折,其余照单全收。
被拒的那一位,从残差分布(draft 漏掉的那部分概率,归一化之后)里重采一个补上——
为什么这样拼出来的分布和原来一模一样,到第 M 节证明。
整章的机制就在上面那张四步图里:小模型猜 k 个 → 大模型一次前向并行验证 →
接住前缀、在第一个被拒处重采。后面几节只是把它的账算清楚。
最妙的是:组合出来的最终分布严格等于大模型自己的分布 p——它不是近似,只是快。
(别把大模型想成「审稿人」:审稿人会按自己的意思改稿;这里的验证是一条数学规则,
保证最终输出的分布和大模型自己写的一模一样。前提是 draft 和 target 用同一套采样设置,破坏它的是实现细节,见第 7 节「四条必须知道的限制」。)
下面用一个小词表(6 个候选词,大模型的答案集中在第一个词上)真实地跑投机采样。 注意最下面那一行指标——它会检查采样出来的分布是不是真的等于大模型的分布。
互动 · 真实的投机采样 + 分布正确性检验
它里面有两个都叫 TV 的数,说的是两件事:
一个是 TV(采样出来的分布, p)——「跑出来的分布」和大模型真实分布差多远,应该非常小;
另一个是 TV(p, q)——大模型和 draft 本身差多远,它决定了接受率(1 − TV(p, q) = 理论接受率)。
(总变差距离 TV:两个分布逐项差的一半之和,0 表示一模一样。)
把 draft 质量拉到最差(q 接近均匀分布)再跑一次:接受率会掉,但第一个 TV 仍然很小。
接受率影响的是速度,不影响正确性。
互动 · 「分布没变」这件事,跑得越多越看得清
直觉上你会想:多猜几个不是更好吗?不是。因为每多猜一个,就要多付一份小模型的成本, 而越靠后的猜测越可能被拒绝——一旦第 2 个就错了,后面猜的全白费。
互动 · 找最优的 k
接受率高(draft 好)时:最优 k 可以很大,常见设置下加速比 3~4 倍;
本页这个成本模型在 α=0.99 时读数能到 5 倍(那是很理想的情况)。
接受率低(draft 差)时:最优 k 缩到 1~2,加速比可能只有 1.1 倍,甚至低于 1(反而更慢)。
所以投机解码不是"开了就快"——它高度依赖你的 draft 模型和目标任务是否匹配。
第 3 节留下了一句最重的话:接受规则不是随便定的——用上它之后,输出分布严格等于大模型自己的分布。 这句话值得单独画一张图。
下面这张图只做一件事:把「大模型的概率 p」一格一格拆开,看它是由哪两段拼起来的。
猜得准不准,只影响这两段各有多高,不影响它们叠起来正好等于 p。
(这一节换一张更平的词表:大模型最高的一项只有 0.52——所以就算用同一套混合规则(draft 偏差都取 0.35),接受率也会和第 4 节不一样。)
一张图说清:猜错的部分去哪了
绿色那一段 = min(p, q):draft 猜到的部分,直接接受。
粉色那一段 = max(0, p − q):draft 漏掉的部分,拒绝之后从残差分布里重新采一次补回来——
这件事就叫
绿色原样留下,粉色被整体放大再补上,两段叠起来一格不差地等于 p。
所以这不是近似——是把同一个分布换了条路生成。
微型图解 · 公式里那个 αk+1 是从哪来的
把上面微型图解的 α 拖到 0.9,再把 k 从 3 拖到 12:柱子越加越多,
但新加进来的那根越来越矮——这就是第 5 节那条曲线先升后降的原因。
反过来把 α 拖到 0.3:第二根柱子就几乎没了,每轮平均只产出 1.4 个 token——多猜的全白花。
| 方法 | 猜的来源 | 特点 |
|---|---|---|
| Speculative Decoding 原始版 |
另一个小模型(同族或蒸馏出来的) | 思路最清晰,理论保证严格。代价:要多加载一个小模型 |
| Self-Speculative | 不用额外模型——只用大模型的前几层当 draft | 零额外显存。因为浅层的特征本来就够猜个大概 |
| Medusa | 在模型上挂几个额外的解码头,各自预测不同位置的未来 token | 不改变主干,只训很轻的头。可以用树状结构一次猜多个分支(一次把多个候选一起猜,再挑通过的那条) |
| Lookahead Decoding | 不用任何额外网络,用Jacobi 迭代在已生成的轨迹里找可用的 token(Jacobi 迭代:把相互依赖的几步拆成同时猜、反复修正) | 完全不需要训练。但加速幅度通常不如专门训过的方法 |
| EAGLE | 在特征层(模型内部的中间表示,而不是最后的词概率)做自回归猜(用已生成的词接着猜下一个) | 目前公开报告加速比最高的方法之一。代价是要额外训练一个 draft 头 |
| 连续批处理 Continuous Batching |
(不是投机,但同属 |
不逐条请求等完,而是每个 step 动态重组 batch。提升的是吞吐,不是单条延迟 |
投机解码提升的是「单条请求的生成速度」(延迟)。
连续批处理提升的是「同时服务多少请求」(吞吐)。
两者可以叠加,而且经常一起用。但它们的"快"说的是两件不同的事——
看你是在优化你的等待时间,还是在优化服务器能扛多少用户。
一张图看完六条路线:要「猜」的那一份从哪来
| 限制 | 说明 | 什么时候真的会痛 → 换什么 |
|---|---|---|
| ① 要多一份 |
draft 模型本身要占显存,本来就被 |
显存本来就紧 → 选 Self-Speculative / Medusa / EAGLE,它们不加载第二个完整模型 |
| ② 接受率低时反而更慢 | 每轮固定付出 k 次小模型 + 1 次大模型的成本;大部分猜测被拒时,比老老实实一个个生成还慢 | draft 和任务不匹配、α 很低 → 换更贴合的 draft,或把 k 调小 |
| ③ 分布等价有前提 | 严格等价对任意温度都成立,前提是 draft 和 target 用同一套采样设置 (同样的 temperature / top-p / top-k) | 只对 draft 做 top-p 截断,或用了 typical acceptance(非精确的接受准则,拿等价换接受率)→ 要可复现就别这么配 |
| ④ 大 batch 时收益消失 | 大 batch 下 GPU 算力本就被喂饱,不再是 memory-bound,投机解码不但没收益,还白白多算 k 倍 draft | 高并发、高吞吐场景 → 优先做批处理,别开投机解码 |
投机解码是「延迟优化」,不是「吞吐优化」。
它拿空闲的算力去换更少的显存访问次数。一旦算力不再空闲(大 batch),这个交易就不成立了。
看到"提速 3 倍"的宣传时,先问一句:这是在什么 batch size 下测的?
互动 · 批一大,这份「闲置算力」就没了
回到第 5 节那张「找最优的 k」互动。先把「接受率 α」这个滑块一路拖到 0.1 附近 (它下面那张曲线图会跟着重画)。看右下角「最优加速比」那个读数——它会掉到 1 以下。
那个数字跌破 1 的瞬间,你看到的就是这件事的价钱: 你拿「闲置的算力」去换「更少的显存访问次数」,而这个兑换比例由接受率决定—— 接受率低的时候,你换回来的带宽还不够付出去的那些 draft 计算。 如果你刚才没有想指着某个东西说这句话,那这一节对你就是没用的——回到第 3 节那张四步流程图上再走一遍。
这一章是纯工程章,回答集中在「参数账本」和「跑在什么上」两条暗线上,外加一条它默认成立的假设;其余几条暗线本章没有特别的话要说。
| 暗线 | 这一章的回答 |
|---|---|
| C 参数账本 | 以 7B 模型(bf16)为例:权重 = 7×109 × 2 字节 = 14 GB。
Decode 每生成 1 个 token,就要把这 14 GB 读一遍。 A100 的显存带宽约 2 TB/s → 理论上限 = 14 GB ÷ 2 TB/s ≈ 7 ms/token (约 143 tok/s,单请求)——这就是 Decode 的物理天花板。 算力侧:一次 7B 前向约 2 × 7×109 = 1.4×1010 FLOP, A100 的 bf16 算力约 312 TFLOPS,所以算力只需要 0.045 ms。 7 ms 和 0.045 ms 差了 156 倍——这就是「算力闲置」的来源, 也是投机解码能拿去交换的那块闲置产能。 draft 的头也一起算:用同系列的小模型(比如 1B),权重 2 GB, 成本比 c 大约就是 2 ÷ 14 ≈ 0.14——和互动里那个默认值 0.12 是同一个数量级。 |
| D 跑在什么上 | 显存带宽受限,不是算力受限。最贵的动作是
「把全部权重从显存搬进计算单元」——它每生成一个 token 就要发生一次,
多少算力都省不掉它。 算术强度(每读一字节能做多少次乘加)在 Decode 阶段低到个位数, 远在硬件的「脊点」(算力与带宽的平衡点)左边,所以要靠带宽而不是算力去理解它。 完整的算术强度推导见 《硬件与算力账本》。 |
| E 它假设了什么 | 假设 draft 和 target 在「容易的位置」上足够一致。
否则接受率塌掉,方案不但不快还更慢——这个假设可以用一个数字(α)检验,
但 α 只有跑了才知道,论文里不会写你这个任务上的 α。 更深的假设在语言本身:它假设自然语言里大部分位置的熵很低—— 「的」「是」「北京」这些位置本来就没有第二种答案(第 2 节那个观测)。 如果文字真的是逐字高熵的,这个方案从根上就不成立。 |
一张图看清代价:接受率 α 决定加速比的天花板
它接住了上一章的什么:《高效注意力》说 「瓶颈不在算,而在搬」。这一章把那个观测变成了一个可执行的交易—— 空转的算力原来是可以花掉的。
它给同阶段后面几章留了什么:投机解码解决的是单条请求的延迟。 如果瓶颈换成了「同时服务多少人」,它就不对症了—— 《服务化与吞吐》 接的是那一半; 而如果一张卡根本放不下整个模型,那要回到本阶段前面的 《分布式训练与集群》。
让小模型一次猜 k 个、大模型一次前向并行验证,用 min(1, p/q) 接受、残差重采,
输出分布严格不变——投机解码不是近似,只是把「搬权重的等待」换成了并行计算。
但它是延迟优化不是吞吐优化:收益全看接受率 α(低了反而更慢),大 batch 下算力不再闲置,好处直接消失——
问「提速几倍」之前先问「在什么 batch 下测的」。
上面讲的都是「够用」的版本。想往下挖,这里有三个入口—— 它们不是必修内容,是给想再往前走一步的读者准备的。