
vLLM 投机解码实战:方法选型、参数配置与效果验证
投机解码能把 LLM 推理的吞吐往上提一大截,但 vLLM 里方法有一堆、参数也不少。这篇按官方文档梳理清楚:各种方法适合什么负载、--speculative-config 该怎么写、用哪些指标判断到底有没有提速。
原创。读完你能明确三件事:自己这套负载该选哪种投机解码方法、
--speculative-config具体怎么写、以及用哪些指标验证提速是否真实发生。
大模型推理有个反直觉的地方:decode 阶段往往不是算力不够,而是显存带宽不够。每生成一个 token,都要把整个模型权重从显存里读一遍,GPU 的计算单元大部分时间在等数据。序列越长、batch 越小,这个浪费越明显。
投机解码(speculative decoding)就是冲着这个浪费去的。思路很朴素:既然一次前向只出一个 token 太亏,那就先让一个便宜的方式猜出后面几个 token,再让主模型一次性并行验证。验证的代价和生成一个 token 差不多,但只要猜中,就能一次吐出多个 token。
本文全部配置与参数说明来自 vLLM 官方文档(当前版本 v0.29.0),涉及出处的部分都附了链接,方便你对照原文。
先搞清楚收益是怎么来的
投机解码的流程可以拆成三步:
- 提议:由 drafter 生成 K 个候选 token(K 就是配置里的
num_speculative_tokens)。 - 验证:主模型对这段候选做一次前向,并行算出每个位置的输出分布。
- 接受:从前往后逐个比对,接受匹配的候选前缀,再额外吐出 1 个由主模型自己决定的"奖励 token"。
关键在于接受长度。如果一次验证平均能接受 3 个草稿 token,加上奖励 token,这一次前向就产出了 4 个 token,相当于 4 倍效率。官方的 per-request 指标里,mean_acceptance_length 的定义就是"每个验证步平均产出多少 token(含奖励 token)",取值范围从 1.0(一个都没接受)到 K+1。
所以投机解码的效果完全取决于两件事:草稿质量(接受率高不高)和额外开销(drafter 自己算得贵不贵)。这两点决定了下面该怎么选方法。
还有一个容易误解的点:投机解码是精确的。它的接受/拒绝机制经过设计,能保证最终输出分布和直接用主模型采样一致——它不是在"近似"模型行为,这一点和量化、蒸馏那些会改变输出的手段有本质区别。真正损失的只有速度:当草稿全被拒绝时,你白付了一次 drafter 的开销。所以它不会让模型变笨,只可能让推理变慢。
—— 广告 ——
vLLM 里有哪些方法
按官方文档,vLLM 目前支持这一组方法,可以分为"基于模型"和"免模型"两类:
- 基于模型:EAGLE、MTP(多 token 预测)、Draft Model、PARD(并行草稿模型)、MLP speculator
- 免模型:N-gram、Suffix decoding(后缀解码)
- 进阶:Custom Proposer(自定义提议器,实验性)、Dynamic Speculative Decoding、Adaptive Verification
官方给了一张定性的选型表,我把它整理成中文:
| 方法 | 低 QPS(看延迟) | 高 QPS(看吞吐) | 说明 |
|---|---|---|---|
| EAGLE | 高收益 | 中到高 | 通用性最强的模型方法 |
| MTP | 高收益 | 中到高 | 目标模型原生支持 MTP 时最合适 |
| Draft model | 高收益 | 中 | 需要额外一个草稿模型 |
| PARD(并行草稿) | 高收益 | 中到高 | 草稿模型延迟低 |
| MLP speculator | 中到高 | 中 | 有兼容的 MLP 草稿器时可用 |
| N-gram | 低到中 | 中 | 轻量、开启成本最低 |
| Suffix decoding | 低到中 | 中 | 不需要额外模型,推测深度动态调整 |
| Dynamic Speculative Decoding | 高收益 | 高于基础方法 | 适合 RL 场景或 QPS 波动大 |
| Adaptive Verification | 高收益 | 高于基础方法 | 按草稿置信度决定验证规模,目前仅 DSpark |
这张表只是起点。真实收益取决于你的模型家族、流量模式、硬件和采样设置——同一套配置换台机器可能就从"很香"变成"更慢",后面讲验证方法就是为这件事准备的。
三种上手指路
所有方法都通过同一个入口配置:命令行上传 --speculative-config 一个 JSON 对象,Python 里则是 LLM(..., speculative_config={...})。
上手一:N-gram,零额外模型
最便宜的一档。它不需要任何草稿模型,而是在当前请求已经出现过的文本里做 n-gram 匹配,把匹配到的后续片段当草稿。官方文档对它的定位是"轻量、容易开",适合代码补全、摘要改写、问答这类有明显重复模式的负载。
vllm serve <target-model> \
--speculative-config '{
"method": "ngram",
"num_speculative_tokens": 4,
"prompt_lookup_min": 2,
"prompt_lookup_max": 5
}'prompt_lookup_min 和 prompt_lookup_max 控制匹配窗口的下限和上限,都是整数、最小为 1。如果两个都不填,默认是 5。
上手二:Draft model,配一个独立草稿模型
这是最直观的一种:找一个小模型当草稿器,同一个家族最好。官方给的最小示例:
vllm serve <target-model> \
--speculative-config '{
"method": "draft_model",
"model": "<draft-model>",
"num_speculative_tokens": 5
}'这里有个容易踩的坑:默认情况下 vLLM 要求草稿模型和目标模型共享同一个词表。如果你手上的小模型来自别的模型家族、分词器不同,就得打开跨词表支持:
from vllm import LLM, SamplingParams
llm = LLM(
model="<target-model>",
speculative_config={
"method": "draft_model",
"model": "HuggingFaceTB/SmolLM2-135M-Instruct",
"num_speculative_tokens": 3,
"use_heterogeneous_vocab": True,
},
gpu_memory_utilization=0.5,
)
params = SamplingParams(temperature=0.0, max_tokens=256)
out = llm.generate(["用一句话解释投机解码。"], params)
print(out[0].outputs[0].text)use_heterogeneous_vocab 打开的是官方叫 Token-Level Intersection(TLI) 的算法:初始化时在两个词表之间建立 token 级交集,把草稿 logits 约束到共享 token 上。要注意它只兼容 method=draft_model,而且开启之后暂时不支持概率式的草稿采样(draft_sample_method='probabilistic')。
上手三:EAGLE-3,走草稿头
EAGLE 系列是目前通用性最强的模型方法。它不下载一个独立的完整模型,而是用一个轻量的草稿头(EAGLE head)。配置里把 method 设成 eagle3,model 指向配套的草稿检查点即可。如果目标模型自带 MTP 支持(比如 Gemma 4 的 assistant 检查点),则应该用 "method": "mtp",把 assistant 检查点填到 model。
这里有个官方明确点出的诊断点:如果你用的是 Gemma 4 assistant 检查点,但启动日志里显示 SpeculativeConfig(method='draft_model', ...),说明你装的 vLLM 版本还不包含这条路径的 Gemma 4 MTP 支持。正确做法是升级 vLLM,而不是硬把一个 assistant 检查点塞进通用草稿模型流程里跑。
常用参数逐个说清
--speculative-config 里有一批跨方法通用的键,官方文档列出的主要几个:
| 键 | 类型 | 默认 | 含义 |
|---|---|---|---|
method | string | 无 | 推测方法,常见值:draft_model、ngram、suffix、mtp、eagle3、dflash。不填时 vLLM 会尽量从配置推断 |
model | string | 无 | 草稿模型、EAGLE 头或辅助模型标识。ngram、ngram_gpu、suffix、mtp 通常可以省略 |
num_speculative_tokens | 整数 > 0 | 无 | 每步提议的草稿 token 数。对不能从模型元数据推断的方法,这一项必填 |
draft_tensor_parallel_size | 整数 ≥ 1 | 无 | 草稿模型的张量并行大小 |
max_model_len | 整数 ≥ 1 | 无 | 草稿模型的最大上下文长度 |
parallel_drafting | 布尔 | false | 并行生成草稿 token,只兼容 EAGLE 和 draft model |
rejection_sample_method | string | standard | 可选 standard、synthetic、block |
use_heterogeneous_vocab | 布尔 | false | 允许草稿与目标模型词表不同(TLI),仅 draft_model 可用 |
另外,n-gram 的 prompt_lookup_min / prompt_lookup_max 见上一节;suffix decoding 也有一组专属键,默认值都写在文档里:suffix_decoding_max_tree_depth(默认 24)、suffix_decoding_max_cached_requests(默认 10000,设 0 关闭全局缓存)、suffix_decoding_max_spec_factor(默认 1.0)、suffix_decoding_min_token_prob(默认 0.1)。
num_speculative_tokens 怎么定
这个参数最需要你按自己的负载调,没有万能值。官方示例里出现过 3、4、5、8 等不同数值,说明它跟方法和模型强相关。实践中有两条经验值得记:
一是别一上来就调大。 草稿越长,被接受的比例通常越低,而验证开销基本是固定的一次前向。草稿长度从 5 加到 15,收益可能先涨一点然后进入平台期,甚至因为接受率下降而变差。官方在 AMD 上的那篇实测也提到过这个现象:增大 num_speculative_tokens 在最初几个档位可能提升吞吐,更大的值会导致平台期或吞吐下降。
二是用指标说话,不要凭感觉。 vLLM 提供了 per-request 的投机解码指标,开启方式是在启动服务时加 --per-request-spec-decode-metrics summary(或 detailed)。开启后,响应里会带上 metrics.speculative_decoding 字段,其中最关键的两个是:
mean_acceptance_length:平均每步产出多少 token(含奖励 token),1.0 表示一个都没接受。draft_acceptance_rate:草稿 token 被接受的比例。
判断逻辑很直接:如果接受率长期低于一半,说明这个 drafter 跟你的负载不匹配,换方法或换草稿模型比继续调长度更有效;如果接受长度稳定在 2 以上,再考虑加大草稿长度往上压榨。
效果验证:别只用"感觉快了"
投机解码最常见的翻车方式是:单请求演示时很快,一上生产、并发一高反而更慢。原因是 drafter 也要占算力和显存,高并发下它会和目标模型抢资源。
所以验证要分两种负载各测一遍:
低 QPS、看延迟:单请求、batch 小,重点看首 token 之后的生成速度(TPOT)和端到端时间。这时模型方法(EAGLE、draft model)通常收益最大。
高 QPS、看吞吐:拉满并发,看整体 token/s。这时免模型方法(n-gram、suffix)的优势在于不增加峰值负载——它们不引入额外模型,代价只在你已经有匹配模式时才产生。官方选型表里这两类方法在"高 QPS 吞吐"一列被标成"中"收益,就是这个意思。
官方给了两个可复现的测量入口:仓库里的 examples/features/speculative_decoding/spec_decode_offline.py,以及基准测试 CLI 指南。前者可以直接跑离线对比,后者适合做服务端压测。
服务端还有一组聚合指标可以对账。per-request 的字段和 Prometheus 计数器是能对上的(在全部请求 n=1 的负载下):num_spec_steps 对应 vllm:spec_decode_num_drafts_total,num_draft_tokens 对应 vllm:spec_decode_num_draft_tokens_total,num_accepted_draft_tokens 对应 vllm:spec_decode_num_accepted_tokens_total。用 /metrics 拉这几个数,就能在不改客户端的前提下看线上接受率。
它和其他加速手段怎么配合
投机解码不是唯一一条提速路径,理解它和另外几种手段的关系,能少走弯路。
量化(把权重压到 FP8/INT4)降的是"每读一个权重花多少带宽",属于直接缓解瓶颈;投机解码降的是"每个 token 需要几次前向"。两者是正交的,可以叠加,而且量化的收益更稳、更容易预估。如果你还没做过量化,先做它。
前缀缓存(automatic prefix caching)让多轮对话里重复的前缀不用重算,省的是 prefill。投机解码省的是 decode。真实 agent 负载通常两者都占大头,值得都开。
连续批处理(continuous batching)提升的是 GPU 利用率,会让"一次前向处理多个请求"更划算。这里有个交互点值得注意:batch 越大,decode 阶段本身越接近计算受限,投机解码能榨出的相对收益就越小——这解释了为什么官方选型表把免模型方法在高 QPS 一栏标成"中",模型方法在后一栏从"高"降到"中到高"。
所以一个务实的顺序是:先把量化和前缀缓存开了,把批处理调好,让基础吞吐接近这台机器的上限;再用投机解码去优化剩下那部分延迟。反过来做,很容易在一条本来就受限的路径上叠加复杂度,最后分不清收益来自哪里。
几个容易踩的坑
temperature、top_p 不是 --speculative-config 的字段。 它们是采样参数,别塞进这个 JSON 里。
YAML 配置文件里不要写成转义字符串。 --speculative-config 在命令行上收的是 JSON 对象;在 YAML 配置文件里应该写成嵌套 mapping,而不是把 JSON 转义成一行字符串。
别去设置 vLLM 内部字段。 target_model_config、draft_model_config、target_parallel_config、draft_parallel_config、draft_load_config 这些是运行时自己填的,用户不该碰。
草稿模型和目标模型分布差太远,接受率会很低。 这是推断测解码收益的第一性原因。同家族、同分词器、或者至少在同类数据上训过的草稿器,表现会明显好于随便找个小模型顶上。
流式响应里指标在最后一个 usage chunk 上。 想拿到 speculative_decoding 字段,得开启 usage 上报——请求里设 stream_options.include_usage: true,或者服务端启动时加 --enable-force-include-usage。
一条建议的上手路径
如果你只是想知道投机解码值不值得开:先在你真实的流量样本上跑一遍 n-gram,成本几乎为零,能立刻看出这套负载里有多少可复用的重复模式。n-gram 没收益,说明文本重复度低,直接跳到模型方法。
要上模型方法,就从和你目标模型同家族的 EAGLE 草稿头或官方 MTP 支持开始,这是接受率的上限所在。开完之后固定住并发和输入分布,只看 mean_acceptance_length 和端到端速度这两组数,再决定要不要调 num_speculative_tokens。
最后提醒一句:投机解码是用显存和算力换延迟的技术。如果你的瓶颈本来就在显存容量或者并发度上,它可能会让情况更糟。先确认瓶颈在带宽,再考虑它。
参考:
© 2026 四月
原文链接:https://www.aprilzz.com/tutorials/vllm-speculative-decoding
相关文章
从 2500 亿条缓存里抠出 100TB 内存:Cloudflare 的五个内存优化手法,每个都能直接抄
Cloudflare 对 1.1.1.1 的 DNS 缓存做了 5 个存储层优化,每条目占用从 953 字节降到 420 字节,整个机群省下约 100TB 内存,缓存反而更快了。本文逐条拆解这些可复用的 Rust 内存优化技术。
在 Mac 上搭建本地编程 Agent:llama.cpp + Gemma 4 + MTP 投机解码完整指南
断网也能用的编程 Agent:用 llama.cpp 在 Mac 上跑 Gemma 4 26B,配合 MTP 投机解码把生成速度从 58 提到 72 token/s,再接上支持图片输入的 Pi 终端 Agent。
浏览器主线程为什么贵?一份系统的前端性能指南:拆、批、排、延、绕
从'代码不慢,只是恰好占着主线程'出发,系统讲解浏览器主线程的运作机制与帧预算,给出五大优化策略:拆分长任务、批处理高频事件、优先级队列、延迟非必要工作,以及把工作移出主线程(合成器线程、Worker)甚至直接消除。每个策略都附代码与交互示例。