在大模型推理部署中,Decode阶段的自回归特性决定了每个token生成都需要一次完整的前向传播,这导致推理吞吐量被GPU计算效率严重制约。即使使用了PagedAttention和Continuous Batching等优化技术,Decode阶段的token生成速度仍然受限于模型规模和显存带宽。Speculative Decoding(投机解码)通过引入一个轻量级的Draft Model(草稿模型)来”猜测”多个未来token,再由目标模型一次性验证,在不损失任何精度的前提下,将推理速度提升2-3倍。

本文将从Speculative Decoding的数学原理出发,深入解析投机采样的无损保证机制,讲解Draft Model的选型策略,并通过vLLM框架的实战配置,展示如何在实际部署中启用和调优投机解码。文章最后还会对比当前主流的投机解码变体方案,帮助你根据实际场景选择最优策略。
一、Speculative Decoding的核心思想:用小模型猜测,大模型验证
传统自回归解码的过程是串行的:目标模型(Target Model)每次生成一个token,将这个token拼接到序列末尾,再进行下一次前向传播。假设我们要生成N个token,就需要N次前向传播,每次都加载完整的模型权重到计算单元。
Speculative Decoding的核心洞察是:大模型验证多个token的成本远低于逐个生成这些token的成本。具体来说,如果用一个小模型一次性猜测K个token,大模型可以用一次前向传播同时验证这K个token(因为Attention机制天然支持处理变长序列),接受其中正确的部分,拒绝错误的部分并从第一个错误位置重新开始。这样,在理想情况下(小模型猜测全部正确),K个token只需要2次前向传播(小模型1次+大模型1次),而非K次。
1.1 工作流程详解
Speculative Decoding的一次迭代包含以下步骤:
- Draft Model自回归生成:小模型基于当前上下文,自回归地生成K个候选token,记录每一步的概率分布
- Target Model并行验证:大模型将原始序列加上K个候选token作为输入,一次前向传播得到每个位置的概率分布
- Accept/Reject决策:从左到右逐个检查候选token,使用修正的拒绝采样(Modified Rejection Sampling)决定接受还是拒绝
- 修正采样:在第一个被拒绝的位置,使用两个概率分布的差值进行重新采样,保证最终分布与目标模型完全一致
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68 import torch
import torch.nn.functional as F
def speculative_decode(target_model, draft_model, input_ids, max_new_tokens, K=4):
# 投机解码核心流程
generated = input_ids.clone()
while generated.shape[1] < input_ids.shape[1] + max_new_tokens:
# 步骤1: Draft Model 自回归生成 K 个候选 token
draft_tokens = []
draft_probs = []
current = generated.clone()
for _ in range(K):
logits = draft_model(current)
next_token_logits = logits[:, -1, :]
probs = F.softmax(next_token_logits, dim=-1)
# 从概率分布中采样
next_token = torch.multinomial(probs, num_samples=1)
draft_tokens.append(next_token)
draft_probs.append(probs)
current = torch.cat([current, next_token], dim=1)
draft_tokens = torch.cat(draft_tokens, dim=1) # [1, K]
draft_probs = torch.cat(draft_probs, dim=0) # [K, vocab_size]
# 步骤2: Target Model 并行验证
verify_input = torch.cat([generated, draft_tokens], dim=1)
target_logits = target_model(verify_input)
# 取最后 K 个位置的 logits
target_logits = target_logits[:, -K:, :] # [1, K, vocab_size]
target_probs = F.softmax(target_logits, dim=-1)
# 步骤3: Accept/Reject 决策
accepted = 0
for i in range(K):
draft_token = draft_tokens[0, i].item()
draft_p = draft_probs[i, draft_token].item()
target_p = target_probs[0, i, draft_token].item()
# 拒绝采样: 以 min(1, target_p / draft_p) 的概率接受
ratio = min(1.0, target_p / (draft_p + 1e-10))
if torch.rand(1).item() < ratio:
# 接受这个 token
accepted = i + 1
else:
# 拒绝,在位置 i 进行修正采样
residual = (target_probs[0, i] - draft_probs[i]).clamp(min=0)
residual = residual / residual.sum()
corrected_token = torch.multinomial(residual, num_samples=1)
break
else:
# 所有 K 个 token 都被接受,额外采样一个 token
last_probs = target_probs[0, -1]
extra_token = torch.multinomial(last_probs, num_samples=1)
generated = torch.cat([generated, extra_token.unsqueeze(0)], dim=1)
continue
# 拼接接受的 token 和修正采样的 token
if accepted > 0:
accepted_tokens = draft_tokens[0, :accepted].unsqueeze(0)
generated = torch.cat([generated, accepted_tokens, corrected_token], dim=1)
else:
generated = torch.cat([generated, corrected_token.unsqueeze(0)], dim=1)
return generated
二、无损保证:拒绝采样的数学原理
Speculative Decoding最精妙的地方在于,尽管使用了小模型来”猜测”token,最终生成结果的概率分布与直接使用大模型完全一致。这一保证来自于Modified Rejection Sampling(修正拒绝采样)的数学性质。
2.1 拒绝采样机制
对于Draft Model生成的每个候选token x,设小模型概率为 p(x),大模型概率为 q(x)。接受概率为:
1
2
3
4
5
6 # 接受概率公式
accept_prob(x) = min(1, q(x) / p(x))
# 如果接受: token x 被保留
# 如果拒绝: 从修正分布中重新采样
# 修正分布 = norm(max(0, q(x) - p(x)))
可以证明,最终生成token x 的概率等于 q(x):
1
2
3
4
5
6
7
8 # 最终概率推导
# P(final = x) = p(x) * min(1, q(x)/p(x))
# + P(reject) * norm(max(0, q(x) - p(x)))
#
# 展开后:
# = min(p(x), q(x)) + (1 - sum(min(p(y), q(y)))) * max(0, q(x)-p(x)) / sum(max(0, q(y)-p(y)))
# = min(p(x), q(x)) + max(0, q(x) - p(x))
# = q(x)
也就是说,无论Draft Model的质量如何,最终输出分布始终等于Target Model的分布。Draft Model的质量只影响接受率(从而影响加速比),但不会影响输出的正确性。这是Speculative Decoding相对于其他加速方案(如知识蒸馏)的最大优势——它提供了数学上的无损保证。

2.2 贪心解码的特殊情况
当使用贪心解码(Greedy Decoding)而非采样时,投机解码的验证逻辑更简单:如果Draft Model选出的token与大模型在该位置的argmax一致,则接受;否则在第一个不一致的位置用大模型的argmax替代。这种情况下,只要小模型和大模型的输出有部分重叠就能获得加速。
| 解码方式 | 验证逻辑 | 无损保证 | 适用场景 |
|---|---|---|---|
| 采样解码 | 修正拒绝采样 | 概率分布完全一致 | 创意写作、对话生成 |
| 贪心解码 | argmax比较 | 输出序列完全一致 | 代码生成、数学推理 |
| Top-K/Top-P采样 | 截断后的拒绝采样 | 截断分布一致 | 可控文本生成 |
三、Draft Model选型:加速比的关键
Speculative Decoding的加速比主要取决于两个因素:接受率(Draft Model猜测正确的比例)和成本比(Draft Model与Target Model的计算成本之比)。理想情况下,我们希望Draft Model既快又准。
3.1 常见Draft Model选型策略
策略一:同系列小模型。最经典的做法是选择同一模型家族中的小版本。例如Target Model为Llama-3-70B时,Draft Model可以选Llama-3-8B。由于架构相同、训练数据相似,小模型的输出分布与大模型有较高的重叠率,典型接受率可达60%-80%。
1
2
3
4
5
6
7 # vLLM 中使用 Llama-3-8B 作为 Llama-3-70B 的 Draft Model
python -m vllm.entrypoints.openai.api_server \
--model meta-llama/Meta-Llama-3-70B-Instruct \
--speculative-model meta-llama/Meta-Llama-3-8B-Instruct \
--num-speculative-tokens 5 \
--use-v2-block-manager \
--tensor-parallel-size 4
策略二:自回归投机(Self-Speculative)。不引入外部小模型,而是用大模型本身的一个”跳层”版本来做Draft。例如70B模型有80层,可以只用前20层来生成候选token,完整80层做验证。这种方案无需额外加载模型,显存开销更小。
策略三:Medusa头部。在Target Model之上训练多个额外的预测头(Medusa Heads),每个头独立预测未来第i个位置的token。不需要独立的Draft Model,但需要额外训练。
策略四:N-gram模型。完全不使用神经网络做Draft,而是用一个简单的N-gram统计模型,基于训练语料或当前上下文的历史来预测下一个token。适合高度重复性的输出(如代码、结构化文本)。
| Draft方案 | 额外显存 | 典型接受率 | 加速比 | 适用场景 |
|---|---|---|---|---|
| 同系列小模型 | 高(需加载完整小模型) | 60%-80% | 2-3x | 通用场景,最推荐 |
| 自回归投机 | 低(共享权重) | 50%-70% | 1.5-2x | 显存受限场景 |
| Medusa Heads | 中(需训练额外头) | 70%-85% | 2-3x | 可训练且追求高接受率 |
| N-gram | 极低 | 30%-50% | 1.3-1.8x | 代码/结构化文本 |
3.2 接受率与加速比的关系
假设Draft Model的接受率为 alpha(每个候选token被接受的概率),候选长度为 K,Draft Model与Target Model的计算成本比为 beta。则平均接受token数为:
1
2
3
4
5
6
7
8
9 # 平均接受的 token 数(几何分布求和)
E[accepted] = (1 - alpha^(K+1)) / (1 - alpha)
# 理论加速比(近似)
speedup = E[accepted] / (1 + beta * K)
# 示例: alpha=0.7, K=5, beta=0.1
# E[accepted] = (1 - 0.7^6) / (1 - 0.7) = (1 - 0.1176) / 0.3 = 2.94
# speedup = 2.94 / (1 + 0.5) = 1.96x
从公式可以看出,接受率越高、Draft Model计算成本越低,加速比越大。当接受率低于50%时,投机解码的收益会显著下降,此时需要重新评估Draft Model的选择或降低K值。
四、vLLM中的Speculative Decoding实战配置
vLLM从0.5.0版本开始原生支持Speculative Decoding,提供了简洁的参数配置。下面通过一个完整的部署示例展示如何在vLLM中启用投机解码。
4.1 基础部署:使用独立Draft Model
1
2
3
4
5
6
7
8
9
10
11
12
13 # 安装 vLLM (确保版本 >= 0.5.0)
pip install vllm --upgrade
# 启动带投机解码的推理服务
python -m vllm.entrypoints.openai.api_server \
--model meta-llama/Meta-Llama-3-70B-Instruct \
--speculative-model meta-llama/Meta-Llama-3-8B-Instruct \
--num-speculative-tokens 5 \
--speculative-draft-tensor-parallel-size 1 \
--tensor-parallel-size 4 \
--gpu-memory-utilization 0.9 \
--max-model-len 4096 \
--port 8000
关键参数说明:
-
1--speculative-model
:指定Draft Model的HuggingFace模型名或本地路径
-
1--num-speculative-tokens
:每次投机生成的候选token数,通常设置为3-8
-
1--speculative-draft-tensor-parallel-size
:Draft Model的张量并行度,通常为1(小模型无需切分)
-
1--tensor-parallel-size
:Target Model的张量并行度
4.2 使用N-gram Draft Model(无需额外模型)
1
2
3
4
5
6
7
8
9 # 使用 N-gram 投机解码,无需加载额外的 Draft Model
python -m vllm.entrypoints.openai.api_server \
--model meta-llama/Meta-Llama-3-70B-Instruct \
--speculative-model "[ngram]" \
--num-speculative-tokens 5 \
--ngram-prompt-lookup-max 4 \
--ngram-prompt-lookup-min 2 \
--tensor-parallel-size 4 \
--port 8000
N-gram模式通过
1 | --ngram-prompt-lookup-max |
和
1 | --ngram-prompt-lookup-min |
控制匹配窗口大小。它会在当前生成上下文中查找N-gram匹配来生成候选token,特别适合代码补全和RAG场景(因为输出通常与输入有较高重复性)。关于vLLM的基础部署,可以参考我们的vLLM推理参数调优全攻略。
4.3 性能基准测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34 import asyncio
import time
from openai import AsyncOpenAI
client = AsyncOpenAI(base_url="http://localhost:8000/v1", api_key="dummy")
async def benchmark_speculative(prompt, num_requests=20):
# 测试投机解码的性能
results = []
for _ in range(num_requests):
start = time.time()
response = await client.completions.create(
model="meta-llama/Meta-Llama-3-70B-Instruct",
prompt=prompt,
max_tokens=256,
temperature=0.7,
)
elapsed = time.time() - start
output_tokens = response.usage.completion_tokens
results.append({
'time': elapsed,
'tokens': output_tokens,
'tps': output_tokens / elapsed
})
avg_tps = sum(r['tps'] for r in results) / len(results)
print(f"Average throughput: {avg_tps:.1f} tokens/s")
return avg_tps
async def main():
prompt = "Please implement quicksort in Python and explain its time complexity."
await benchmark_speculative(prompt)
asyncio.run(main())
典型测试结果(Llama-3-70B + Llama-3-8B,4x A100 80GB):
| 配置 | 吞吐量 (tokens/s) | 平均接受token数 | 加速比 |
|---|---|---|---|
| 无投机解码 (K=0) | ~35 | 1.0 | 1.0x (基准) |
| K=3 | ~55 | 2.1 | 1.57x |
| K=5 | ~72 | 2.9 | 2.06x |
| K=8 | ~68 | 3.2 | 1.94x |
可以看到,K=5时性能最优,K=8时虽然接受token数更多,但Draft Model的计算开销和验证开销也随之增加,导致加速比反而下降。因此在实际部署中需要根据模型组合调优K值。更多推理框架对比可以参考四大推理框架性能基准测试实战。

五、投机解码的变体与前沿方案
除了经典的Speculative Decoding,学术界和工业界还发展出了多种变体方案,各有适用场景。
5.1 Medusa:多头并行投机
Medusa不使用独立的Draft Model,而是在Target Model的最后一个隐藏层上添加K个额外的线性头,每个头直接预测未来第i个位置的token。训练时冻结原始模型,只训练这些额外头部。推理时一次前向传播就能得到K个候选token,无需Draft Model的自回归生成。
Medusa的优势是Draft生成几乎零成本(只是几个线性层),但接受率略低于独立Draft Model方案,因为每个头是独立预测的,没有利用已生成候选token的上下文信息。
5.2 EAGLE:基于特征的自回归投机
EAGLE(Extrapolation Algorithm for Greater Language Modeling Efficiency)是当前最先进的投机解码方案之一。它使用Target Model的隐藏层特征(而非token embedding)作为Draft Model的输入,训练一个轻量的自回归头来生成候选序列。由于利用了丰富的隐藏层信息,EAGLE的接受率显著高于传统方案,在多个基准测试中达到3-4倍加速。
1
2
3
4
5
6 # vLLM 中使用 EAGLE (需 vLLM >= 0.6.0)
python -m vllm.entrypoints.openai.api_server \
--model meta-llama/Meta-Llama-3-70B-Instruct \
--speculative-model "eagle://yuhuili/EAGLE-Llama-3-70B-Instruct" \
--num-speculative-tokens 8 \
--tensor-parallel-size 4
5.3 Lookahead Decoding:无需Draft的并行解码
Lookahead Decoding完全不需要Draft Model,而是通过Jacobi迭代并行生成多个token。它维护一个”n-gram池”,在每次前向传播中同时生成主路径和若干候选路径,通过验证候选路径来加速。虽然加速比通常低于Speculative Decoding(1.5-2x),但完全不需要额外模型或训练,配置最简单。
5.4 方案对比总结
| 方案 | 需要Draft Model | 需要训练 | 典型加速比 | 显存额外开销 | vLLM支持 |
|---|---|---|---|---|---|
| Speculative Decoding | 是(独立小模型) | 否 | 2-3x | 高 | 原生支持 |
| N-gram Speculative | 否 | 否 | 1.3-1.8x | 极低 | 原生支持 |
| Medusa | 否(多头) | 是 | 2-3x | 低 | 原生支持 |
| EAGLE | 是(特征头) | 是 | 3-4x | 低 | 原生支持 |
| Lookahead Decoding | 否 | 否 | 1.5-2x | 极低 | 原生支持 |
六、生产部署的实践建议与踩坑指南
6.1 K值(候选token数)调优
候选token数K是影响性能的最关键参数。K太小则投机收益有限,K太大则Draft Model的计算开销和拒绝后的浪费增加。建议:
- K=3-5:适用于大多数场景,平衡了投机收益和开销
- K=5-8:当接受率较高(大于70%)时使用,如代码生成、结构化输出
- K=2-3:当接受率较低(小于50%)时使用,避免浪费
6.2 显存管理注意事项
使用独立Draft Model时,额外显存开销取决于Draft Model大小。Llama-3-8B作为Draft Model大约需要16-20GB显存(FP16)。在多卡部署中,Draft Model通常放在第一张卡上(TP=1),需要确保该卡有足够显存。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 # 检查显存使用情况
nvidia-smi --query-gpu=index,memory.used,memory.total --format=csv
# 如果显存不足,可以:
# 1. 降低 gpu-memory-utilization
# 2. 使用更小的 Draft Model
# 3. 使用 N-gram 或 Medusa 方案代替独立 Draft Model
# 4. 对 Draft Model 使用量化
python -m vllm.entrypoints.openai.api_server \
--model meta-llama/Meta-Llama-3-70B-Instruct \
--speculative-model meta-llama/Meta-Llama-3-8B-Instruct \
--speculative-draft-tensor-parallel-size 1 \
--num-speculative-tokens 5 \
--tensor-parallel-size 4 \
--gpu-memory-utilization 0.85 \
--enforce-eager # 禁用 CUDA Graph 以节省显存
6.3 与其他优化技术的兼容性
Speculative Decoding可以与vLLM的其他优化技术叠加使用,但需注意:
- PagedAttention:完全兼容,vLLM内部自动处理。关于PagedAttention的原理可以参考PagedAttention深度解析
- Continuous Batching:完全兼容,每个请求独立进行投机解码
- CUDA Graph:兼容但需注意显存,投机解码会增加graph capture的复杂度
- Prefix Caching:兼容,但前缀匹配可能导致Draft Model输出不一致,需测试
- FP8/INT8量化:Target Model可量化,Draft Model建议保持FP16以保证接受率
6.4 监控接受率
在生产环境中监控接受率是评估投机解码效果的关键。vLLM提供了prometheus metrics来追踪:
1
2
3
4
5
6
7 # 查询 vLLM 的 Prometheus metrics
curl http://localhost:8000/metrics | grep speculative
# 关键指标:
# vllm:speculative_token_acceptance_rate - 接受率
# vllm:speculative_accepted_tokens_total - 累计接受token数
# vllm:speculative_draft_tokens_total - 累计草稿token数
如果接受率持续低于40%,建议更换Draft Model、降低K值、或切换到N-gram方案。在显存受限的场景下,N-gram方案虽然加速比略低,但零额外显存开销,是不错的备选方案。
总结
Speculative Decoding是当前最实用的大模型推理加速技术之一,其无损保证的特性使其可以安全地应用于任何场景。核心要点回顾:
- 原理:小模型猜测+大模型验证,通过修正拒绝采样保证输出分布完全一致
- Draft Model选型:同系列小模型最简单可靠,EAGLE方案接受率最高,N-gram零成本适合特定场景
- K值调优:3-5是通用推荐值,需根据接受率动态调整
- 加速效果:典型2-3倍加速,EAGLE可达3-4倍,对吞吐量和延迟均有显著改善
- 生产部署:vLLM原生支持多种方案,通过metrics监控接受率持续优化
随着大模型参数规模持续增长,自回归解码的串行瓶颈将更加突出。Speculative Decoding及其变体方案(EAGLE、Medusa等)为我们提供了一条在不损失精度的前提下大幅提升推理效率的有效路径。在实际部署中,建议从同系列小模型方案起步,根据接受率监控数据逐步调优,必要时切换到EAGLE等高级方案以获取更高加速比。
汤不热吧