生成 4 个 Token 为何不一定要跑 4 次大模型?Speculative Decoding 的提议、验证与接受
从自回归解码的串行瓶颈出发,手算 draft–verify 接受过程,推导精确采样修正,追踪张量形状,并给出可检查实现、指标与失败边界。
上一篇用 Chunked Prefill(分块预填充)限制长 prompt 对 decode 的阻塞,但生成仍有一条硬约束:第 个 token 要把第 个 token 当作输入。普通自回归解码生成 4 个 token,目标大模型就要串行执行 4 次。
Speculative Decoding(投机解码)引入一个便宜的 draft model(草稿模型)连续猜若干 token,再让 target model(目标模型)用一次前向并行检查这些位置。猜对得多,就用一次昂贵前向推进多个 token;猜错时通过接受—拒绝修正,仍保持目标模型的采样分布。
01 瓶颈为什么是串行轮数,而不只是 FLOPs?#
设当前前缀为 。普通解码依次执行:
第二个分布必须等第一个采样结果产生。即便 GPU 在单轮 decode 中没有吃满,也不能直接把未知的未来 token 放进同一批。
普通 decode: Target -> 1 token -> Target -> 1 token -> Target -> 1 token
投机 decode: Draft -> a,b,c -> Target 一次验证 -> 接受若干 tokentext目标不是减少所有计算:系统额外运行了 draft。真正要换取的是更少的 target 串行调用,以及更高的单次验证算术强度。
02 Draft 与 Target 各自产生什么?#
给定 batch size 、草稿长度 、词表大小 :
- draft 自回归提出 个 token,
draft_ids为[N,γ]; - 同时保存每一步的草稿分布 ,
draft_probs为[N,γ,V]; - target 把原前缀与草稿 token 一起前向,得到验证分布 以及一个 bonus 位置,
target_probs为[N,γ+1,V]; - verifier(验证器)逐位置决定接受、拒绝与替换。
flowchart LR
C[已确认前缀 x1:t] --> D[Draft 串行提出<br/>y1,y2,y3]
C --> T[Target 一次前向]
D --> T
T --> V[逐位置验证]
V -->|接受前缀| A[提交 y1..yk]
V -->|首次拒绝| R[采样修正 token<br/>丢弃其后草稿]
V -->|全部接受| B[再提交 1 个 bonus token]mermaid“一次验证”并不代表 verifier 可以乱序接受。位置 2 只有在位置 1 已接受后才仍处于 target 评估过的那个前缀上;首次拒绝之后,后面的草稿条件链已经失效。
03 Greedy 模式先看最直观版本#
当 temperature 为 0,可把流程理解为:
draft 提议: [A, B, C, D]
target 对各位置: [A, B, X, ...]
提交: [A, B, X]text前两个 token 与 target 的 argmax 相同,可以接受;第三个不同,就提交 target 的 X 并丢弃 C,D。一次 target 前向推进了 3 个 token,而不是 1 个。
若四个草稿全部匹配,还可从 target 的第 5 个位置提交 bonus token,因此一次验证最多推进 个 token。
04 精确采样为何按概率比接受?#
在某一步,draft 从 抽到候选 ,target 希望最终服从 。接受概率是
若 ,draft 对 没有“提议过量”,直接接受;若 ,只以 接受,削掉多出来的概率质量。
拒绝后不能直接再从 采样,否则被拒候选的质量会重复计算。修正分布为
接受分支贡献 ,拒绝分支再补上 ;两者相加恰好回到 。这就是“无损”指向的数学含义:在相同采样变换与数值实现假设下,输出分布不因 draft 改变。
05 用两个词手算一次接受与修正#
词表只有 A/B。第二个位置的分布为:
| token | draft | target | |
|---|---|---|---|
| A | 0.2 | 0.6 | 3.0 |
| B | 0.8 | 0.4 | 0.5 |
若 draft 提议 B,接受概率为 0.5。抽到均匀随机数 时拒绝。残差为:
归一化后必然选择 A。于是该轮在首次拒绝处提交 A,后续草稿全部作废。
从总体概率看:B 被提议的概率 0.8,乘接受率 0.5,最终质量为 0.4;其余 0.6 全由修正分支给 A,正好得到 target 的 (0.6,0.4)。
06 一轮算法的透明伪代码#
given confirmed prefix x
draft_tokens, q[1:γ] = draft_autoregressively(x, γ)
p[1:γ+1] = target_verify(x + draft_tokens)
accepted = []
for i in 1..γ:
y = draft_tokens[i]
α = min(1, p[i,y] / q[i,y])
if uniform(0,1) < α:
accepted.append(y)
else:
replacement ~ normalize(max(p[i] - q[i], 0))
return accepted + [replacement]
bonus ~ p[γ+1]
return accepted + [bonus]text实际实现还要在同一位置应用完全一致的 temperature、top-k、top-p、禁用 token、重复惩罚等 logits transforms。若 和 经过不同处理,精确性证明不再成立。
07 一个可单元测试的 PyTorch 验证器#
下面只实现“一条序列的一轮验证”。输入是已归一化概率,不依赖模型封装。
import torch
def verify_one(
draft_ids: torch.Tensor, # [G]
draft_probs: torch.Tensor, # [G,V]
target_probs: torch.Tensor, # [G+1,V]
uniforms: torch.Tensor, # [G],测试时显式注入
) -> torch.Tensor:
G, V = draft_probs.shape
assert draft_ids.shape == (G,)
assert target_probs.shape == (G + 1, V)
out: list[torch.Tensor] = []
for i in range(G):
token = draft_ids[i]
q = draft_probs[i, token]
p = target_probs[i, token]
alpha = torch.minimum(torch.ones_like(p), p / q.clamp_min(1e-12))
if uniforms[i] < alpha:
out.append(token)
continue
residual = (target_probs[i] - draft_probs[i]).clamp_min(0)
residual = residual / residual.sum().clamp_min(1e-12)
replacement = torch.multinomial(residual, num_samples=1)[0]
out.append(replacement)
return torch.stack(out)
bonus = torch.multinomial(target_probs[G], num_samples=1)[0]
out.append(bonus)
return torch.stack(out)python可测试例输入:draft_ids=[B],draft_probs=[[0.2,0.8]],target_probs=[[0.6,0.4],[0.3,0.7]],uniforms=[0.7]。输出应为 [A]。再用大量随机样本做频率检验,确认结果逼近 (0.6,0.4)。
生产代码还需处理 logits 为 -inf、零概率、浮点舍入、批内不同拒绝位置与 EOS;教学代码的 clamp_min 只防除零,不能替代这些策略。
08 Target 为什么能并行验证未来位置?#
draft 已经把候选 y1..yγ 填进输入,target 可像 prefill 一样对这段做 causal forward。位置 的 hidden state 只看确认前缀和 y1..y{i-1},正好给出验证 yi 所需的条件分布。
对每层,若历史 KV 为 [N,Hkv,L,Dh],验证块新产生 KV 为 [N,Hkv,γ,Dh];验证 logits 为 [N,γ+1,V]。最后一维词表通常不必全部长期保存,kernel 可融合采样或只保留所需统计量。
被拒位置之后的候选 KV 必须回滚或不提交:它们依赖已被替换的错误前缀。常见做法是先暂存 speculative slots,再按每条请求的接受长度更新有效 context length。
09 速度取决于接受长度,不只取决于 Draft 大小#
令一次验证接受的草稿 token 数为 ,本轮推进量为:
其中 +1 是首次拒绝的修正 token,或全部接受后的 bonus token。平均每次 target 调用推进 ,但墙钟收益还要扣掉 draft、验证更长序列、采样与状态管理的成本。
近似可写为:
它不是可直接代入的硬件公式,却说明三个杠杆:draft 要便宜、与 target 足够一致、验证 个位置不能比单 token target 慢太多。
10 Draft 长度为什么不是越大越好?#
若每个位置条件接受率粗略为 ,到第 个候选仍全部接受的概率约为 。低一致性时,长草稿的大部分尾部都会被计算后丢弃。
| 情况 | 增大 的可能结果 |
|---|---|
| 高、低并发、target memory-bound | 一次推进更多 token,ITL 下降 |
| 低 | 被拒后的尾部浪费快速增加 |
| 高并发 | 验证块抢占 batch token 预算,吞吐可能下降 |
| draft 很大 | draft 本身接近一次 target 成本,收益消失 |
因此需要按 prompt 类型、采样参数、batch size 与负载动态测量,不能只在单请求 greedy 上宣布加速比。
11 当前 vLLM 中怎样落地与核实?#
当前官方文档把投机方法分为 draft model、EAGLE、Multi-Token Prediction(多 token 预测,MTP)、n-gram/suffix 等。配置入口为 --speculative-config,具体字段与模型兼容性会随版本演进。
vllm serve your-org/target-model \
--speculative-config \
'{"method":"draft_model","model":"your-org/draft-model","num_speculative_tokens":4}'bash上线前必须用所安装版本的 vllm serve --help 与官方版本页核对方法名;不要从旧文章复制参数。vLLM 文档也明确提醒:投机解码主要针对中低 QPS、decode 偏显存带宽受限的负载,并不保证所有 prompt 与采样设置都降低 ITL。
12 应记录哪些指标?#
至少同时记录:
- proposal length:每轮实际提议数;
- accepted tokens 与 acceptance rate;
- mean accepted length / target step;
- draft、target verify、sampler 各自耗时;
- TTFT、ITL/TPOT、E2E 的 p50/p95/p99;
- output tokens/s 与 requests/s;
- batch size、prompt/output 长度、采样参数和拒绝位置分布。
只报 acceptance rate 会误导:接受率 80% 但 draft 占了一半时间,可能仍更慢;只报单请求 ITL 也会掩盖高并发吞吐退化。
13 正确性怎样验证?#
确定性模式可做逐 token 对账:关闭投机与开启投机,在相同模型、tokenizer、greedy 设置下输出 token IDs 必须一致。
随机模式不能要求相同 seed 后逐 token 完全一致,因为随机数消耗顺序可能不同。应做分布检验:构造极小词表或可枚举模型,运行大量样本,比较首 token 与短序列频率、总变差距离或卡方统计,并覆盖第一位拒绝、中间拒绝、全部接受、EOS 和零概率边界。
还要断言:
committed_length = accepted_draft_count + 1
rejected_suffix KV 不可见
每个请求独立维护 RNG 与接受位置
停止条件只检查已提交 tokentext14 常见错误与最短调试路径#
| 症状 | 常见原因 | 最短检查 |
|---|---|---|
| Greedy 输出不同 | position、KV 回滚或 bonus 对齐错 | 在 γ=2 下逐位置比较 target logits |
| 随机输出分布漂移 | 拒绝后仍从原 采样 | 枚举二词表,检查残差分布 |
| 接受率高但更慢 | draft/验证开销超过串行轮数收益 | 分段计时并与相同 batch 基线比较 |
| EOS 后仍提交 token | 对草稿尾部一次性应用停止规则 | 只对已提交前缀逐 token 检查 |
| batch 内串线 | 每条请求拒绝位置与 KV 长度未独立 | 构造一条全接受、一条首位拒绝的 batch |
| top-p 下无法复现 | 使用了不同 logits transforms | 记录过滤后的支持集和概率和 |
15 它与相近方法的边界#
Chunked prefill 是把一个长输入拆到多轮,保护 decode 延迟;speculative decoding 是让一次 target decode 尝试推进多个输出。Prefix Caching(前缀缓存)复用其他请求已计算的 prompt KV,只减少重复 prefill。
EAGLE、MTP 与独立小模型都可充当 proposer(提议器),差别在如何低成本预测未来 token,不改变“提出—验证—提交”的核心契约。Medusa 等多头方案可形成候选树;候选越多,target 验证与调度形状也越复杂。
16 失败场景#
- 代码生成、罕见领域或高温采样使 draft 与 target 分歧大;
- target 很小,额外调度和 sampler 开销不可忽略;
- 高 QPS 已通过大 continuous batch 吃满 GPU,验证块反而挤压吞吐;
- draft 占用额外权重显存,使 KV 容量下降并增加抢占;
- tokenizer、词表或 logits processor 不兼容,无法定义逐 token 概率比;
- 多租户服务没有隔离草稿状态、RNG 与 metrics。
17 今天真正需要记住什么?#
- 投机解码减少的是昂贵 target 的串行调用次数,不保证减少总 FLOPs。
- target 可以并行验证草稿块,但只能提交首次拒绝之前的连续前缀。
- 随机采样必须用 接受率和 残差修正,才能保持目标分布。
- 性能由接受长度、draft 成本、验证成本和并发负载共同决定,必须同时看延迟、吞吐与正确性。
18 思考题与小练习#
- 词表为
A/B/C,,。draft 提议A时接受率是多少?若拒绝,写出归一化残差分布。 - 令 ,一轮在第 3 个候选拒绝。哪些 token 与 KV slots 可以提交,下一轮 context length 增加多少?
- 为
verify_one增加 EOS 处理和确定性 RNG;用 100,000 次抽样验证输出频率逼近 target 分布。
相关工作#
- Leviathan et al., Fast Inference from Transformers via Speculative Decoding ↗,给出保持目标分布的经典 speculative sampling 算法。
- Chen et al., Accelerating Large Language Model Decoding with Speculative Sampling ↗,独立系统化研究草稿—验证加速。
- Cai et al., Medusa: Simple LLM Inference Acceleration Framework with Multiple Decoding Heads ↗,用多解码头构造候选树。
- Li et al., EAGLE: Speculative Sampling Requires Rethinking Feature Uncertainty ↗,从特征层改进草稿质量。
- vLLM, Speculative Decoding 官方文档 ↗,列出当前支持方法、配置入口与适用负载。
19 下一篇预告#
投机解码减少一条请求的 target decode 轮数,但大量请求若共享同一段 system prompt 或长文档,仍会反复做相同 prefill。下一篇将拆解 Automatic Prefix Caching:怎样用链式块哈希找到最长可复用前缀,又如何避免错误共享与多租户侧信道。