投机解码原理与实现

发布时间:2026/11/5 5:34:47
投机解码原理与实现

投机解码原理与实现

前言

在基于 Transformer 的语言模型中,处理提示词和生成回答是两个不同的阶段。提示词中的字已经给定,模型可以在一次前向中处理多个位置。到了生成阶段,后一个 token 依赖前一个刚刚采样出来的 token。假设回答有 100 个 token,即使已经缓存了前文的 Key 和 Value,模型仍要依次做出这 100 个位置的选择。对于需要生成长回答的应用,这段逐步解码的时间会很明显。

投机解码(Speculative Decoding)从这里入手:先用一个计算成本较低的模型拟出几步续写,再交给原来的大模型一起验证。一次验证有可能确定多个 token,从而减少串行调用大模型的次数。本文先介绍经典的双模型方案,结合 5 个候选的例子说明概率、位置和缓存之间的对应关系,之后再讨论 EAGLE、多 token 预测和并行草稿的实现,以及这些方案分别适用于什么场景。

一、草稿模型与目标模型

经典方案中有两个角色。草稿模型(Draft)根据当前上下文逐个生成候选,并记录每一步实际采样用的概率分布。目标模型(Target)就是原本用于生成回答的大模型。它根据相同的上下文,计算这些候选在各自位置上的概率。下面假设两者使用相同的词表和 token 编码。

举例来说,已有前缀为S = [t0, t1, t2],草稿长度取 5。Draft 先根据 S 生成d3,再根据S + d3生成d4,依次得到[d3, d4, d5, d6, d7]。这 5 步仍是串行的。此时 Target 已经知道草稿每个位置填了什么,可以一起计算“如果沿着这条草稿往下读,各位置的下一个 token 会服从什么分布”。

这里可以对照普通解码。在d3尚未确定时,Target 无法知道下一步该把什么作为输入。而当整段草稿已经给定时,因果掩码允许d4所在位置读到d3和它自己,同时禁止它读到d5。这样既保留了从左到右的依赖,也允许同一层中的多个位置并行计算。拿到各位置的分数之后,再从左到右决定有多少草稿可以写入上下文。

二、Target 怎样用一次前向验证 5 个候选

设 Target 已有前缀 S 的 KV Cache,同时保留了读完t2时的下一词分布 p₃。本轮只需向模型传入 5 个新 token 的 ID。每一层复用前缀的 K、V,计算新位置的隐藏状态和 K、V。Draft 在各步得到的分布 q 留给后面的采样使用,不作为 Target 的输入。

图 1 一次验证中的位置对应关系。图中从左向右的第一列是前缀,后面五列才是本轮新增的输入。读完t2时得到 p₃,用来验证d3。读完d3时得到 p₄,用来验证d4。依次类推,最后读完d7,得到的已经是位置 8 的分布 p₈。

实际实现中,这里容易出现一位偏移。下面用伪代码表示这个缓存布局,省略批次维度,probs表示按 Target 的采样设置把 logits 转为概率:

# prefix_cache 中已有 t0、t1、t2;prefix_next 是读完 t2 时保留的 p3draft_ids=[d3,d4,d5,d6,d7]new_logits=target.forward(draft_ids,past_kv=prefix_cache).logits next_probs=probs(new_logits)# p4, p5, p6, p7, p8verify_probs=[prefix_next,*next_probs[:-1]]# p3, p4, p5, p6, p7bonus_probs=next_probs[-1]# 五个草稿全通过时使用

KV Cache 里存的是注意力的键和值,不含 p₃。有的实现只把 KV 缓存到t1,把t2连同 5 个候选一起送入本轮前向,这样也能拿到 p₃ 到 p₈。无论采用哪一种布局,用来验证d3的那一行都只能看到 S。如果直接取 5 个新位置的 logits 去验证这 5 个位置自身,就会把已经看过候选的输出用在这个候选身上。

三、从左到右验收:通过、拒绝和补位

1. 随机采样时怎样判断接受

在第 i 个候选处,记 Draft 和 Target 根据同一前缀得到的分布分别为 qᵢ 和 pᵢ。对于 Draft 实际采到的候选 dᵢ,经典投机采样采用下面的接受概率:

α i = min ⁡ ( 1 , p i ( d i ) q i ( d i ) ) . \alpha_i = \min\left(1,\frac{p_i(d_i)}{q_i(d_i)}\right).αi​=min(1,qi​(di​)pi​(di​)​).

抽取一个 0 到 1 之间的均匀随机数 u,若 u 小于 αᵢ,则接受该候选。Target 给候选的概率不低于 Draft 时,它一定被接受。反过来,假设 Draft 给d6的概率是 0.60,Target 给的是 0.30,接受概率就是 0.5。此时抽到 0.21 会通过,抽到 0.72 才会拒绝。

首次拒绝之后要补一个 token。对词表中每个 token x 计算max(pᵢ(x) − qᵢ(x), 0),再归一化,从这个分布采样修正 token。为什么要相减,可以用只有 A、B 的小词表算一遍:

token草稿概率 q目标概率 p被草稿提出且直接接受的概率
A0.600.300.30
B0.400.700.40

A 被直接接受的概率已经达到目标值 0.30,B 还差 0.30。拒绝事件的概率刚好也是 0.30,所以拒绝时应把剩下的概率全部补给 B。若在拒绝后照原样从 p 再抽,A 的最终概率将是0.30 + 0.30 × 0.30 = 0.39。这就是修正分布的作用。这里的“无损”指保持 Target 在所选采样策略下的输出分布。随机生成的两份文本可以不同。经典投机解码算法 1

2. 同一段草稿的三种结果

图 2 一次验证后,哪些 token 真正成为前文。两条路径都从同一组候选开始,下面假设尚未产生结束符,也未达到最大生成长度。

  • 第 4 个候选首次被拒。前三个d3, d4, d5已通过,d6被拒。即使已有d7的评分,也只能舍弃:计算这个评分时用的是包含d6的前文。丢弃d6, d7对应的临时 KV,从位置 6 的修正分布采出c6。本轮提交[d3, d4, d5, c6],下一轮以这个实际提交的前缀为起点,补算新 token 所需的缓存。
  • 五个候选全部通过。此时完整草稿已成为前文,可以直接从 p₈ 再采一个b8。本轮提交[d3, d4, d5, d6, d7, b8],前进了 6 个位置。
  • 第一个候选就被拒。整段草稿都舍弃,从位置 3 的修正分布采出c3。上下文只增加这一个 token,Draft 从新前缀开始下一轮。

如果使用贪心解码,判断方式可以更简单。对每个候选,取 Target 在该位置的最高分 token 与之比较,连续相同的部分保留;在首个不同处写入 Target 的选择。使用相同的平局处理方式,可以复现 Target 的贪心序列。如果服务本来采用 temperature、top-p 等随机采样,应根据实际使用的分布选择验收算法,不能一律改为比较最高分 token。

四、一次验证的开销与加速条件

还是图 2 的第一种情况。普通解码前进 4 个位置,需要 Target 连续作出 4 次选择。投机解码用一次批量验证前进了同样的 4 个位置,但还要付出 Draft 连猜 5 次、采样和处理缓存的时间。因此要比较的是生成同样数量的有效 token 花了多久。

验证块比单步解码处理更多的新位置,也会为最终丢弃的候选做计算。在小批量、受显存带宽限制的情况下,读取一遍模型权重后可以让多个位置共同使用,批量验证的实际耗时有时接近单步解码。但当前缀很长、批量较大或设备的计算资源已经充分利用时,额外位置的成本就可能很明显。理论运算量、一次调用的耗时和整段回答的时延是三个不同的量。

实际可以记录每轮平均提交的 token 数,以及草稿、验证、采样与调度各自的耗时。若每轮平均提交 4 个 token,但本轮总耗时已经超过基线 4 步,减少 Target 调用次数也没有带来加速。原论文在特定的 T5-XXL 实验中报告了约 2~3 倍加速;更换模型、硬件、请求负载或草稿长度,都要重新测量。实验设置与结果

五、候选从哪里来:独立草稿、特征预测与并行草稿

经典方案需要额外加载一个小模型。它便于给现有 Target 加上投机解码,但要留出显存,草稿模型的速度和它对当前任务的适应程度也会影响收益。其余方案可以沿着同一个问题来理解:在 Target 开始验证之前,怎样得到一段成本较低、又有希望被接受的候选?

方案如何给出候选实现时需要考虑什么
独立 Draft小模型按前缀逐步预测下一词可保持 Target 结构不变,需要额外模型;经典概率校验还需对齐 token 空间
EAGLE / EAGLE-2EAGLE 结合目标模型特征和已采样 token,用轻量模型外推后续特征。EAGLE-2 随上下文和草稿置信度调整候选树分叉可以保留多条候选,但树越大,待验证位置也越多。EAGLE 本身已经使用草稿树
Medusa在同一主干上增加预测未来不同位置的头,把候选组合成树并验证可节省独立草稿模型的加载。选择验收规则时要区分保分布采样与论文中的近似接受策略
DFlash轻量块扩散草稿器利用 Target 的上下文特征,在一次前向中并行给出一块候选草稿阶段少了逐词等待。块内候选能被连续接受多少,仍要由目标模型验证
DSpark并行主干完成主要计算,再用轻量顺序模块传递块内相邻 token 的信息系统结合预测置信度和当前服务负载,决定每个请求送验多长的前缀

这里有两种不同的“并行”。DFlash 的并行发生在拟草稿时;经典例子里 Target 的并行发生在验证时。并行草稿如果猜得不好,首先影响的是有效前缀长度。只要后续仍使用保持目标分布的验证算法,就不能由此断定最终生成的长文本质量变差。EAGLE · EAGLE-2 · Medusa · DFlash · DSpark

六、MTP 的训练结构与推理用途

多 token 预测(Multi-Token Prediction,MTP)首先是一种训练目标。通常的语言模型从当前位置学习预测下一个 token;MTP 在训练时还要求模型预测更远的位置。以共享主干、多个独立预测头的设计为例,读到位置 t 后,各个头使用同一份隐藏状态,分别预测位置 t+1、t+2 等处的 token,并按对应标签计算损失。训练数据知道这些标签,主干在位置 t 却仍只能读取截至 t 的前文。

图 3 两种 MTP 结构都学习预测后续 token,但第二个预测位置得到的输入不同。看图的上半部分,两个预测头直接读取主干表示。再看下半部分,DeepSeek-V3 的第一个 MTP 模块接收主干表示和位置 t+1 的 token 嵌入,生成新的表示,再由共享输出头预测 t+2。模块沿深度串接,保留了相邻预测步的依赖。训练时 t+1 的真实 token 来自样本;推理时要使用已经提出的候选,不能预先知道未来真值。多 token 预测论文 · DeepSeek-V3 技术报告,第 2.2 节

训练完的辅助模块可以在推理时移除,也可以用来提供投机候选。选择后一种用法,主模型仍需验证候选,首次不通过时仍要处理余下的草稿。MTP 的好处是能复用主干表示,减少单独部署完整 Draft 的需求;代价是模型需要有训练好的预测模块,推理引擎也要支持对应结构。同源的预测头究竟能带来多高的接受率、节约多少显存,取决于模型和任务,不能单凭结构作判断。

七、应用场景与测试方法

代码补全或低并发交互。这类请求往往关心首个 token 出现之后,后续内容能否更快地逐字返回。以代码补全为例,可以用同一批代码前缀,分别记录普通解码和投机解码的每输出 token 平均时延、接受前缀长度以及用户等到完整补全的时间。若草稿在当前语言和项目上经常第一个位置就失败,应缩短候选块,或重新评估草稿模型的选择。

生成较长的回答或摘要。长生成会反复经历解码阶段,值得测试多步验证。若输入很长、输出只有一两个词,时间可能主要花在处理提示词;这时应把首 token 时延和后续解码时延分开记录,否则很难看出投机解码究竟改善了哪一段。

高并发在线服务。此时除了单个请求的时延,还要比较固定硬件下的总吞吐量。保持模型、采样参数和请求集相同,分开测低负载和高负载,再比较草稿长度 1、3、5 的结果。DFlash 论文在 Qwen3.5-9B、vLLM 和 HumanEval 的特定实验中,相对自回归基线的吞吐量提升倍数从并发 1 的 4.6 倍变为并发 32 的 2.1 倍。同一方案在不同负载下收益不同。部署时,应以实际业务的并发量和输出长度选择草稿长度。DFlash 附录,表 12