推测解码实战:让大模型推理提速 2 倍

同一张 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,即真正要用的大模型)。一轮迭代分三步:

  1. 抢跑:草稿模型自回归地连续生成 k 个候选 token(k 常取 3~7),因为它小,这一步很快。
  2. 批量验证:把这 k 个候选一次性喂给目标模型做一次前向,得到每个位置上目标模型自己的概率分布。注意这只是一次权重搬运,代价约等于原来生成 1 个 token。
  3. 拒绝采样裁决:从左到右逐位比对。若目标模型也认可该 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 与已生成文本里检索重复片段几乎为 01.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 接进监控,低于阈值自动告警或回退基线。

推测解码代表了一类很聪明的工程思路——不去改模型,而是重新组织计算的时序,把被带宽浪费掉的算力捡回来。在显卡供给持续紧张的当下,这种”不损失质量的免费加速”比任何调参技巧都值得优先投入。下一步若想继续压成本,可以把它与量化、模型路由叠加使用,形成从单请求延迟到整体账单的完整优化链路。

上一篇 LiteLLM 多模型网关:统一接入与成本路由
下一篇 技术面试与团队招聘:技术 Leader 选人方法论