同一张 A100、同一个 14B 模型,别人的服务每秒吐 60 个 token,你的只有 25 个——差距往往不在模型,而在解码方式。推测解码(Speculative Decoding)是目前性价比最高的大模型推理加速手段之一:用一个小模型抢先”猜”出后面几个 token,再让大模型一次性批量验证,猜对就白赚,猜错就丢弃。它最迷人的地方在于——数学上可证明输出分布与原模型完全一致,也就是说加速是免费的,质量零损失。本文从瓶颈本质讲到 vLLM 生产配置,把这套方案落到能跑的命令上。
一、先搞清楚:大模型推理慢在哪
很多人以为推理慢是”算力不够”,其实自回归生成阶段(decode 阶段)几乎不吃算力,吃的是显存带宽。原因很直接:生成第 N 个 token 时,GPU 必须把整个模型的权重从显存搬到计算单元一次,而这一次搬运只为了算出 1 个 token。一个 14B 的 FP16 模型权重约 28GB,若显存带宽是 2TB/s,理论上每个 token 至少要 14ms——无论你的 GPU 算力多富余,这个搬运成本躲不掉。
这就形成了一个荒谬的现实:decode 阶段的 GPU 算力利用率常常只有个位数百分比,绝大部分时间在等数据。与之相对,prefill(处理输入 prompt)阶段因为能并行处理成百上千个 token,算力利用率能拉到很高。结论呼之欲出:如果能让一次权重搬运顺带验证多个 token,单位时间产出就能成倍提升。这正是推测解码的立足点,也是它和量化、蒸馏等手段的根本区别——后两者是把模型变小,它是把每一次搬运榨干。关于显存与吞吐的系统性优化,可以对照阅读 大模型推理成本优化:从吞吐到模型路由。
二、核心机制:抢跑 + 批量验证 + 拒绝采样
推测解码需要两个模型:一个便宜的草稿模型(draft model,通常是同系列的 0.5B~1.5B 小模型)和一个昂贵的目标模型(target model,即真正要用的大模型)。一轮迭代分三步:
- 抢跑:草稿模型自回归地连续生成 k 个候选 token(k 常取 3~7),因为它小,这一步很快。
- 批量验证:把这 k 个候选一次性喂给目标模型做一次前向,得到每个位置上目标模型自己的概率分布。注意这只是一次权重搬运,代价约等于原来生成 1 个 token。
- 拒绝采样裁决:从左到右逐位比对。若目标模型也认可该 token 就接受;一旦某位被拒绝,丢弃该位及其后所有候选,并按修正后的分布重采样一个 token,然后进入下一轮。
为什么能保证”零质量损失”
关键在第三步的拒绝采样。设草稿模型给出的概率为 q(x)、目标模型为 p(x),算法以 min(1, p(x)/q(x)) 的概率接受候选;被拒绝时,从归一化后的残差分布 max(0, p(x)-q(x)) 中重新采样。可以证明这样得到的 token 序列,分布与”直接用目标模型逐个采样”完全相同。所以推测解码不是近似、不是有损压缩,它只是把串行计算重排成了并行验证。这一点是它与 本地大模型量化部署:GGUF 与 llama.cpp 调优 或 模型蒸馏实战:知识蒸馏压缩大模型落地 的本质分野:后两者用精度或能力换速度,推测解码不换。
需要牢记的隐含前提:草稿模型与目标模型必须共享同一套 tokenizer 和词表。用 Qwen 的小模型给 Llama 做草稿,token id 对不上,结果必然错乱。所以选草稿模型的第一原则是”同家族、同词表”,比如 Qwen2.5-0.5B 配 Qwen2.5-14B。
三、四种主流实现路线怎么选
“草稿从哪来”决定了工程复杂度。目前落地成熟度较高的有四条路线,下表按部署成本从低到高排列(加速比为社区常见实测区间,务必在自己业务的真实 prompt 上复测):
| 路线 | 草稿来源 | 额外显存 | 常见加速比 | 最适合场景 |
|---|---|---|---|---|
| n-gram / Prompt Lookup | 直接从 prompt 与已生成文本里检索重复片段 | 几乎为 0 | 1.3~2.5× | 摘要、RAG 问答、代码补全等大量原文复述的场景 |
| Draft Model(独立小模型) | 同家族小尺寸模型 | 小模型权重(0.5~1.5GB) | 1.5~2.5× | 通用对话、指令任务,最经典也最稳 |
| Medusa(多解码头) | 在目标模型上加装多个预测头 | 头参数,需训练 | 1.6~2.8× | 能自行训练、追求极限吞吐的自研团队 |
| EAGLE 系列 | 基于隐藏状态的轻量自回归头 | 较小,需训练 | 2~3× | 已有训练管线、愿投入调优成本 |
给一句实用建议:先上 n-gram,再考虑 draft model,最后才碰 Medusa/EAGLE。n-gram 路线零训练、零额外权重,在 RAG 与文档问答这类”答案大量引用原文”的业务里效果出奇地好,因为模型接下来要吐的字,往往就在 prompt 里躺着。反过来,如果你的业务是开放式创作,输出与输入几乎没有字面重叠,n-gram 的接受率会低到形同关闭,此时才值得付出小模型的显存代价。
四、最小可验证实验:transformers 一行开启
在写生产配置前,强烈建议先在单机上跑一遍最小实验,亲眼确认加速是否存在。Hugging Face transformers 已内置 assisted generation,只要多传一个 assistant_model 参数:
import time, torch
from transformers import AutoModelForCausalLM, AutoTokenizer
TARGET = 'Qwen/Qwen2.5-7B-Instruct'
DRAFT = 'Qwen/Qwen2.5-0.5B-Instruct' # 同家族、同词表,这是硬性前提
tok = AutoTokenizer.from_pretrained(TARGET)
target = AutoModelForCausalLM.from_pretrained(TARGET, torch_dtype=torch.float16, device_map='cuda')
draft = AutoModelForCausalLM.from_pretrained(DRAFT, torch_dtype=torch.float16, device_map='cuda')
msg = [{'role': 'user', 'content': '用三段话解释显存带宽为什么限制大模型推理速度'}]
ids = tok.apply_chat_template(msg, add_generation_prompt=True, return_tensors='pt').to('cuda')
def bench(tag, **kw):
torch.cuda.synchronize(); t0 = time.time()
out = target.generate(ids, max_new_tokens=256, do_sample=False, **kw)
torch.cuda.synchronize(); cost = time.time() - t0
n = out.shape[-1] - ids.shape[-1]
print(f'{tag}: {n} tokens / {cost:.2f}s = {n / cost:.1f} tok/s')
return out
base = bench('baseline')
spec = bench('speculative', assistant_model=draft)
# 关键校验:贪心解码下两者输出必须完全一致,否则说明实现或词表有问题
print('输出一致:', torch.equal(base, spec))
最后那行 torch.equal 是整个实验的灵魂。贪心解码(do_sample=False)下推测解码的输出应当与基线逐 token 完全相同;如果不同,别急着庆祝速度,先去查 tokenizer 是否同源、dtype 是否一致。另外若你走 n-gram 路线,连草稿模型都不用加载,把参数换成 prompt_lookup_num_tokens=10 即可,特别适合先在 CPU 有限的机器上试水。
五、生产落地:在 vLLM 里开启推测解码
单机脚本只能证明原理,真正上线还得靠推理引擎。vLLM 从 0.5/0.6 系列起支持推测解码,注意不同版本参数名有过合并调整,务必以 vllm serve --help 的实际输出为准,不要照抄博客(包括本文)里的旧写法:
# 路线 A:独立草稿模型(vLLM 0.6.x 风格的分离式参数)
vllm serve Qwen/Qwen2.5-14B-Instruct \
--speculative-model Qwen/Qwen2.5-0.5B-Instruct \
--num-speculative-tokens 5 \
--gpu-memory-utilization 0.90 \
--max-model-len 8192 \
--port 8000
# 路线 B:零额外权重的 n-gram 草稿(适合 RAG / 摘要类业务)
vllm serve Qwen/Qwen2.5-14B-Instruct \
--speculative-model '[ngram]' \
--num-speculative-tokens 5 \
--ngram-prompt-lookup-max 4 \
--port 8000
# 较新版本已把这些参数收敛进一个 JSON,形如:
# --speculative-config '{"model": "Qwen/Qwen2.5-0.5B-Instruct", "num_speculative_tokens": 5}'
# 升级 vLLM 后第一件事就是核对参数名,否则进程会直接启动失败
vllm serve --help | grep -i specul
这里有个容易被忽略的坑:推测解码会挤占 KV Cache 的显存预算。草稿模型自己要占显存,验证阶段还要为 k 个候选临时扩展 KV Cache。所以开启后如果发现并发能力下降、报显存不足,不是配置写错了,而是要相应下调 –max-num-seqs 或 –max-model-len 做再平衡。vLLM 本身的部署与调优细节,可以回看 vLLM 部署实战:高吞吐 LLM 推理服务调优。
六、接受率:这一个指标决定成败
推测解码不是”开了就快”。它是一场赌博:猜对了赚时间,猜错了草稿模型那几次前向就白烧。衡量赌局盈亏的核心指标叫接受率(acceptance rate),即草稿 token 被目标模型接受的比例。vLLM 会在日志里周期性打印 Draft acceptance rate 与 System efficiency,这是上线后第一个要接进监控的数字。
| 指标 | 含义 | 健康参考 | 不健康时的动作 |
|---|---|---|---|
| Draft acceptance rate | 草稿 token 被接受的比例 | > 0.6 可观,> 0.75 优秀 | 低于 0.4 建议直接关闭,或换更强草稿模型 |
| num_speculative_tokens (k) | 每轮抢跑几个 token | 通常 3~7 | 接受率高可调大,低则调小到 2~3 |
| System efficiency | 综合加速效率 | > 1 才算赚 | 接近或低于 1 说明净亏,回退基线 |
| 输出端到端 tok/s | 用户真实感知速度 | 对比基线看提升幅度 | 无提升就别为复杂度买单 |
k 值不是越大越好
直觉上 k 越大一轮能赚越多,但接受是连乘关系:一旦第 2 个 token 被拒,后面第 3、4、5 个再准也全部作废。假设单 token 接受率为 0.8,那么连续 5 个全被接受的概率只有 0.8 的五次方,约 0.33。也就是说 k 从 5 加到 10,后半段的草稿计算基本是纯浪费,还额外拉长了每轮延迟。实践中的做法是从 k=4 起步,按接受率上下微调,能力允许时优先启用引擎的动态 k(部分实现会根据实时接受率自动伸缩)。
采样温度会直接影响接受率
这条经验很多人踩过:在 temperature=0 的确定性场景(代码生成、结构化抽取、分类)接受率往往很高;一旦把温度调到 0.9 做开放创作,目标模型的分布变平坦、随机性上升,草稿越来越难命中,接受率随之下滑。所以同一套服务里,不同业务线的加速收益可能天差地别——建议按业务分池部署,而不是全局一刀切开启。落到接口层,用 LiteLLM 多模型网关:统一接入与成本路由 把高温创作类请求路由到未开启推测解码的实例,是成本更低的做法。
# 上线前的自测脚本:同一批真实业务 prompt,对比两个端点的 tok/s
import time, requests
PROMPTS = [line.strip() for line in open('real_prompts.txt') if line.strip()]
ENDPOINTS = {'baseline': 'http://127.0.0.1:8000/v1', 'spec': 'http://127.0.0.1:8001/v1'}
for tag, base in ENDPOINTS.items():
total_tok, total_cost = 0, 0.0
for p in PROMPTS:
t0 = time.time()
r = requests.post(base + '/chat/completions', json={
'model': 'Qwen/Qwen2.5-14B-Instruct',
'messages': [{'role': 'user', 'content': p}],
'max_tokens': 256, 'temperature': 0,
}, timeout=120).json()
total_cost += time.time() - t0
total_tok += r['usage']['completion_tokens']
print(f'{tag}: {total_tok / total_cost:.1f} tok/s (n={len(PROMPTS)})')
务必用真实业务 prompt 而非”讲个笑话”来测。基准数据集上的 2 倍加速,换到你那种”塞了 6000 字上下文再问一句”的场景,结论可能完全反转。测完把 tok/s、接受率一起打进可观测系统,可参考 Langfuse 实战:LLM 应用链路与成本可观测 的埋点思路。
七、什么情况下不该开推测解码
它不是万能开关,以下四种情况开了反而更慢或更贵:
- 高并发吞吐优先的场景:推测解码优化的是单请求延迟(TPOT)。当 batch 已经很大、GPU 算力本就跑满时,验证阶段的额外计算会挤压整体吞吐,得不偿失。它最适合”并发不高但要求响应快”的交互式业务。
- 显存已经吃紧:草稿模型加上扩展的 KV Cache 都要吃显存,本就靠 –gpu-memory-utilization 0.95 硬撑的服务,开了大概率直接 OOM。
- 接受率长期低于 0.4:说明草稿模型跟目标模型”想的不是一回事”,白烧算力,果断关闭或换草稿模型。
- 输出极短的任务:只生成十几个 token 的分类、打标签任务,收益被固定开销吃掉,还引入了额外故障面。
另一个常被误解的点:推测解码和思维链类长推理并不冲突,反而在长输出场景收益更明显——毕竟输出越长,decode 阶段占比越大。若你的服务同时要处理长思维链,可以结合 推理模型实战解析:思维链原理与算力调优指南 一起做容量规划。
八、小结:一张决策清单
把本文压缩成可执行的四步:第一,确认业务是延迟敏感型且并发中低,否则不必折腾;第二,先用 n-gram 零成本试水,RAG/摘要类业务常常到此就够;第三,需要通用加速再上同家族小模型作草稿,k 从 4 起步,用真实 prompt 实测 tok/s 与接受率;第四,把接受率与 System efficiency 接进监控,低于阈值自动告警或回退基线。
推测解码代表了一类很聪明的工程思路——不去改模型,而是重新组织计算的时序,把被带宽浪费掉的算力捡回来。在显卡供给持续紧张的当下,这种”不损失质量的免费加速”比任何调参技巧都值得优先投入。下一步若想继续压成本,可以把它与量化、模型路由叠加使用,形成从单请求延迟到整体账单的完整优化链路。




