跳转至

第 14 章 投机解码的系统实现

开场问题:论文说加速 2–3 倍,为什么线上一开并发收益就没了,甚至更慢?

14.1 先算清楚这笔账

第一册第 14 章讲了原理:草稿猜 k 个,大模型一次前向并行验证,接受前面连续对的部分。系统视角要把它写成一个能判断"开了会不会更慢"的式子。

一轮投机的成本是:草稿生成 k 个 token 的开销,加上一次验证前向的开销(比普通 Decode 前向略贵,因为要同时算 k+1 个位置)。收益是这一轮产出的 token 数,等于期望接受长度。

于是三个量决定一切:期望接受长度(越高越好)、草稿成本占比(越低越好)、验证的额外开销(k 越大越贵)。任何一项失衡,加速比就掉到 1 以下。

14.2 草稿从哪来,各自的系统代价

独立小模型:用同系列的小号模型。接受率取决于两个模型的分布接近程度,但它要占显存、占算力,还要自己的 KV 管理和调度,是最重的方案。

自投机头(Medusa / Eagle / MTP):在主模型上挂额外的头。不需要第二套权重和 KV。Medusa 各头独立预测不同位置,接受率一般;Eagle 在特征层面做自回归,接受率明显更高;MTP 是预训练阶段就带的(第一册 14.4 节),质量最好且零额外训练成本。

无模型方法(n-gram / prompt lookup):直接从上下文里找重复片段当草稿。零成本,在代码补全、文档改写、RAG 这类重复度高的场景意外地有效,值得作为默认兜底。

14.3 树状候选与 KV 回滚

多个草稿头会产生分叉:位置 +1 有 2 个候选,各自的 +2 又有 2 个……把它们组织成一棵树,用一个特制的 attention mask 让每个候选只能看到自己那条路径上的祖先,就能在一次前向里同时验证多条路径,最后保留被接受的最长前缀。

树越宽接受率越高,但无效计算也越多——这又是一个要按负载调的参数。

KV 管理上,被拒绝的分支写进去的 KV 必须回收,被接受的前缀要提交为主序列的一部分。分页 KV 在这里帮了大忙:分支共享前缀块、拒绝时按块回收,都是现成的能力。

14.4 和连续批处理的冲突

这是开场问题的答案。投机解码的前提是"Decode 阶段有富余算力",而这个前提在大 batch 下不成立——连续批处理已经把 batch 堆起来,算术强度上去了,工作点接近算力受限,此时再塞投机验证的额外计算,就是纯粹的负担。

还有一个二阶问题:batch 内各请求每轮接受的 token 数不同,序列进度参差不齐,调度器按请求数凑批的假设被打破,需要按预计验证成本来组批。

所以生产系统的正确姿态是动态开关:低并发时开,并根据近期接受率自适应调整草稿长度;并发上来后自动关掉。把它当成永远打开的开关,高峰期会反噬。

14.5 它买的是延迟,不是吞吐

这条结论值得单独强调:投机解码用算力换延迟。在 GPU 有富余算力时(低并发、交互式、长思维链推理),它能显著改善 TPOT;在追求最大吞吐的离线批处理里,它几乎没有价值,甚至有害。

第一册尾声提到的长思维链模型让它的价值上升了——生成几万 token 的思考过程,TPOT 的改善会被放大很多倍。

核心问答

Q:投机解码什么时候反而更慢? 并发高(算力已经不富余)、接受率低(草稿不准)、草稿成本高(小模型太大)、或者验证 kernel 实现不好。

Q:为什么代码补全特别适合? 重复片段多,连 n-gram 这种零成本草稿都有很高的接受率。

Q:它会改变输出质量吗? 不会。接受—重采样规则保证输出分布与逐 token 生成完全一致。但要注意:这个保证依赖实现正确,上线前应该做分布一致性验证。

Q:应该报告什么指标? 只报"加速 2 倍"没有意义。要给出:接受长度、不同并发下的 TPOT 和吞吐变化、额外显存占用,以及在什么并发区间应该关闭。