做过推理服务的人都有这种体会:模型参数量上去之后,单 token 生成时间死死卡在几十毫秒,用户问一句话要等好几秒。LLM 推理速度不够快,这个问题卡住了不少人。推测解码(Speculative Decoding)是这两年开源社区里讨论度最高的加速手段之一,而 VLLM 社区把这个原本停留在论文里的技巧做成了开箱即用的功能。这篇文章不打算堆公式,而是用一个“实习生写初稿、专家一次性审阅”的视角,把推测解码的原理讲明白,再带你实际配通 vLLM 的推测解码服务。你会看到,同样的模型、同样的显卡,在合适场景下每秒吐词数量实打实翻倍。
这篇文章适合三类人:刚接触大模型推理的算法工程师,想在不换卡的前提下提升服务响应速度的开发者,以及所有被 LLM 逐字生成速度折磨过的玩家。我会把推理慢的根源、推测解码为什么省时间、vLLM 里怎么配置、常见坑怎么排,按照从原理到实战的顺序讲清楚。你不需要先读一字不差的论文公式,只要知道大模型每个 token 是怎么“蹦”出来的,就能跟上后续所有内容。
1. 从“一个字一个字蹦”说起:LLM 推理为什么慢
1.1 自回归解码的每一步,代价比你想的大
所有主流大语言模型都是自回归架构,意味着它一次只生成一个 token,然后把新词拼到已有序列最后,继续作为输入来预测下一个词。这个机制有一个直观后果:生成第 100 个 token 之前,你必须先把前 99 个都生成完,串行没有出口。你可以在接口里设置 max_tokens,但解码过程本质上还是“链条式”的。
但是,每一环“链条”的成本,远比想象中高。生成一个新 token 并不是只做一次简单预测,而是要把当前的整段序列重新喂给模型,穿过 Embedding 层、几十层 Transformer、最后一层投影到词表,才能得到下一个词的分布。即便现在的推理引擎都实现了 KV Cache,把历史 token 计算出的 Key 和 Value 缓存起来,免去了历史部分的重算,模型每一层的全部权重参数依然要从显存中完整读一遍,参与计算。这一步是省不下来的。
我见过很多人惊讶于一个现象:不管 7B 还是 13B 的模型,单 token 生成时间往往都在几十毫秒,而且显卡利用率看起来并不高。原因在于 LLM 推理受限于显存带宽,而不是计算算力。粗略估算一下:7B 模型的 FP16 权重约有 14GB,主流旗舰卡显存带宽大概 2TB/s,即使不做任何计算,光把权重读一遍就需要至少 7ms。所以生成一个 token 的时间,在这种配置下很难低于这个门限,计算单元大部分时间都在等待数据。
1.2 卡脖子的不是算力,是显存带宽
有人会觉得,KV Cache 不是已经把历史部分的重复计算都消掉了吗?确实消掉了,但它解决的是“同一个历史状态的 K/V 要不要重算”的问题,解决不了“每一轮都要把权重读一遍”的带宽问题。权重就那么大,显存带宽就那么多,单 token 生成时间因此存在一个物理下限。只要解码还是逐 token 串行,总延迟就得乘上 token 数量。
这里可以做一个更直观的类比:把显卡想象成一个图书馆,模型权重是书架上的全部藏书,生成一个 token 相当于把整套书从头快速翻一遍,然后写下一个词。哪怕你只需要写一个词,也必须把整本书翻完,因为模型每一层都依赖这些权重。翻书的速度受限于书架的“带宽”,而不是你写字的速度。所以模型越大,这套书越多,翻一遍花的墙钟时间就越长,单 token 生成速度自然上不去。
这个结论对后续理解推测解码非常关键。我们优化的本质,不是让“翻一遍书”变得更快,而是想办法尽量少“翻书”。自回归解码之所以昂贵,是因为每生成一个 token 就要完整翻一遍书。如果有一天能一次翻书、同时写好几个词,成本就被摊薄了。
1.3 一个死结:自回归模型没法一步跨两个 token
既然串行是求解慢的直接原因,一个自然的想法是:让模型一次性多生成几个 token。但自回归模型的结构决定了它做不到——要预测第 n+1 个 token,必须先把第 n 个 token 的真实结果作为输入喂进去。这就陷入了一个死结:想跳步,却没有跳板。推测解码恰恰是通过引入一个“打草稿”的小模型,把这个跳板搭了起来。
2. 推测解码:小模型打草稿,大模型来验收
2.1 核心思想:草稿、验证、修正
推测解码的核心思想可以概括成六个字:草稿、验证、修正。流程分三步。第一步,让一个轻量级草稿模型从当前上下文出发,按照和正常解码一样的方式,快速生成 K 个候选 token;第二步,把这 K 个 token 拼接到序列尾部,一次性送入目标大模型,做一次前向传播;第三步,大模型同时得到这 K 个位置各自对应的概率分布,逐一判断草稿模型在当前位置猜得对不对,对的留下,不对的从第一个出错位置开始全部推翻,并用自己的结果补上。
这就是“实习生写初稿、专家一次性审阅”的由来。实习生写得快但水平有限,专家不必每句话都单独找实习生重新写,而是一次拿到一版初稿,从头翻到尾,把不合格的标记出来自己改。放在模型里,专家只需要一次前向传播,就能同时评估 K 个位置的答案,这就是加速的核心来源。
为什么大模型能“同时评估 K 个位置”?因为这次前向传播的输入不是“只预测下一个 token”,而是把草稿模型生成的 K 个 token 全部拼在序列后面。目标模型依次对每个位置做预测,相当于同时完成了 K 个预测任务。这跟我们让目标模型自己一步一步生成不一样,因为这里的 K 个输入 token 是草稿模型给的,不需要等目标模型逐个产出来再喂回去,所以可以并行。
2.2 接受与拒绝:为什么这不是偷工减料
如果目标模型是贪心解码,验收规则特别直白:看草稿 token 是否等于目标模型在那个位置选出的最大概率 token。是,就接受并继续比较下一个;不是,就从这个位置截断,后面草稿全部作废,改用目标模型自己的选择。这样一轮下来,有效产出就是这个被截断的位置数量。如果 K=5,你运气好,5 个全中,那这一轮就一次生成了 5 个 token,相当于把五轮自回归合成了一轮。
采样的场景略有不同,草稿 token 即使不是 argmax,也可以根据目标分布和草稿分布的概率比值决定是否接受。这套接受-拒绝规则不是拍脑袋设计的,它保证了最终采样结果和目标模型直接采样在分布上完全一致。所以我一直建议把推测解码理解成无损加速——你看到的内容风格和原本模型的随机性没有变化,变的只是每一秒能吐出的 token 数量。
有人会担心:被拒绝的 token 是不是白算了?不是。即便草稿前 5 个 token 里只接受了 2 个,大模型也已经拿到了从第 2 个位置开始往后全部位置的概率分布,接下来它完全可以在这个被拒绝的位置重新采样。也就是说,草稿模型提供了一个“跳板”,让大模型在这一次前向传播里同时看到了未来多个位置的信息。就算跳板短了,也仍然省掉了一次大模型自回归的轮次,只是收益变小而已。
2.3 为什么两个模型相加,反而比一个大模型跑得快
有人会脱口而出:这不是要跑两个模型吗?算力开销更大了吧。确实,如果草稿模型每生成一个 token 的时间不可忽略,总开销会增加。但只要草稿模型足够小,它单 token 生成时间比大模型低一个数量级,整个账就划算。
我算一个直观的例子。假设大模型单 token 耗时 10ms,草稿模型单 token 耗时 2ms,草稿长度 K 取 5,且接受率理想情况下很高。原生生成 5 个 token 需要 5×10=50ms。推测解码的流程则是:草稿模型生成 5 个 token 花 5×2=10ms,大模型一次前向传播验证存量序列加 5 个新 token 花大概 10ms,加起来 20ms,省了一半多。如果草稿模型再小一点、快一点,K 再合适一点,速度可以直接拉出去两三倍。
但你要问,接受率不高怎么办?比如这 5 个里面只接受了 2 个,相当于大模型一次前向后,有效产出只有 2 个 token,原生跑 2 个 token 只需要 20ms,而推测解码花了 10+10=20ms,基本打平。如果接受率更低,甚至变慢。所以推测解码省不省,关键看两项:草稿模型的对齐度(接受率)和草稿长度 K。这也是后面调参的核心。
3. VLLM 社区的落地:从论文到生产可用
3.1 为什么是 vLLM:架构带来的天然优势
推测解码的概念很早就在学术圈出现了,但真正把它推到生产环境,靠的是以 vLLM 为代表的开源推理引擎。vLLM 在社区里口碑好,不光是单元测试写得多,而是它的架构天然适合承载这类“多模型协作”玩法。
首先,vLLM 用分页显存管理机制做 KV Cache,按页分配,避免了静态预分配带来的显存浪费,这让同时加载目标模型和草稿模型成为可能。小模型虽然只有几百 MB,但也是额外的显存开销,显存管理不高效的话,开这个功能还不够腾地方。
其次,vLLM 的调度器对多步推测做了专门支持。推测解码有一个跟普通自回归不一样的特性:每一轮里,草稿模型要连续生成 K 个 token,这就涉及到一个“多步生成 + 一次性验证”的调度流。普通调度器处理不了这种“先连续走几步、再回头验证”的逻辑。VLLM 社区在调度层做了改造,才能让用户只要传两个模型路径就能开启这个功能。
另外,vLLM 的动态批处理调度很成熟。多请求并发时,一个新的请求进来不必傻等队头清空,可以随时插入。推测解码和这种动态批处理结合后,低并发场景下尤其受益,因为单个请求的延迟可以大幅下降。这也是为什么很多人在本地搭 vLLM 服务而不是直接用闭源方案来做实验。
3.2 草稿模型选型:别只看参数量
草稿模型不是随便拿一个语言模型就能用的,社区里最常踩的坑就是词表对齐问题。推测解码机制要求草稿模型和目标模型在 tokenizer 的 vocabulary 上是兼容的,否则你拿草稿模型输出的 token id 去大模型里算概率,对不上号。最省事的做法是选同一系列、同一词表的小模型。比如你部署一个 7B 的模型,优先考虑它作者自己放出的 0.5B/1B 小兄弟;如果跨家族混搭,先确认 vocabulary size 一致,否则会出现概率分布维度不匹配,直接跑不起来。
其次是容量选择。草稿模型太小,接受率低,白干;太大,生成 K 个 token 的时间压不下来,还是白干。社区里比较通用的经验是:目标模型在 7B 左右,配 0.5B 到 1B 的草稿模型;13B 级别可以配 1B 到 3B;更大的 70B 级别,草稿模型也不宜超过 7B。这只是起步经验值,具体还要看实测接受率。我的建议是:代码里预留一个开关,上线前用一小段评测集分别测不同草稿模型和不同 K 值,把接受率和端到端延迟一并打出来,不要让“感觉”替你做决定。
还有一点容易被忽略:草稿模型的结构越接近目标模型,越容易产生“预测一致”。一个与目标模型同结构的缩小版,通常比一个结构完全不同的通用小模型表现更好。因为它们的概率分布先验更接近。你在社区里看到的许多成功案例,用的基本都是同构小模型。这不是强制要求,但能降低很多调参成本。
3.3 实测加速效果与适用边界
VLLM 社区和各个自建用户放出的实测数据,大致集中在以下规律:单请求、低并发、长回复场景下,加速比最明显,常见 1.5 到 2.5 倍;短回答和首字时间几乎没有帮助,因为首字之前没有草稿可打;高并发下加速比会回落,原因是大模型本身就忙着处理多个请求,显存带宽已经接近饱和,草稿验证带来的边际价值变小。
| 使用场景 | 加速体感 | 原因 |
|---|---|---|
| 单请求长回答 | 1.5 到 2.5 倍 | 草稿模型用低成本预测,大模型一次验证多位置 |
| 多并发高吞吐 | 接近 1 倍甚至无收益 | 大模型带宽饱和,草稿验证边际收益小 |
| 短问答 | 几乎无感 | 首 token 前没有可验证的草稿,加速被首字延迟淹没 |
| 小模型 | 很难超过 1 倍 | 找不到更小的同词表草稿模型,成本和收益倒挂 |
我自己的体感也是这样。某个 7B 模型在单路下,默认约 30 token/s;开启推测解码用 1B 草稿模型后,稳定在 55 到 65 token/s。但并发数拉到 8 以上后,整体吞吐变成了另一种瓶颈,推测解码的收益就不明显了。它不是银弹,但适用场景恰好是很多个人开发者和中小团队最常用的场景:一个推理服务,低并发,交互式聊天。
4. 手把手配置:在 vLLM 里开启推测解码
4.1 环境准备与模型下载
我默认你已经装好 vLLM。如果还没有,Python 3.10 以上,一张支持 CUDA 的显卡,然后直接安装 vLLM 即可。这里特别提醒一句:观察安装后的版本号,推测解码这个功能在早期版本里名字和参数有过变动,尽量用近半年的新版本,老版本文档不一定对得上。
接着准备好两套模型权重。目标模型用你计划部署的模型,草稿模型按上面说的原则选。从模型分享平台下载两条命令就能拿到:一条拉目标模型,一条拉草稿模型。我用的是本地目录方式:先分别下载,再让 vLLM 加载本地路径。好处是断网环境也能跑,也方便对比不同草稿模型。下载完务必检查一下 word embeddings 层的输出维度,如果目标模型词表大小是 32000,草稿模型也得是 32000,否则后续会报 shape mismatch。
4.2 启动参数与代码示例
开启推测解码,核心就是两个参数:speculative-model 指向草稿模型,num-speculative-tokens 指定每轮草稿长度。下面是一个最简命令行示例:
vllm serve /path/to/target-model \ --speculative-model /path/to/draft-model \ --num-speculative-tokens 5 \ --max-model-len 8192 \ --gpu-memory-utilization 0.8 \ --port 8000如果你更喜欢用 Python API 起服务,等效的代码是:
from vllm import LLM, SamplingParams llm = LLM( model="/path/to/target-model", speculative_model="/path/to/draft-model", num_speculative_tokens=5, gpu_memory_utilization=0.8, ) sampling_params = SamplingParams(temperature=0.0, max_tokens=512) outputs = llm.generate(["写一份项目进度汇报的提纲"], sampling_params)这里有个关键点:vLLM 启动时,如果显存不够同时放两个模型,会先报显存错误,这时候不要急着换显卡,先把 gpu_memory_utilization 调低,或者把 max-model-len 调小。草稿模型虽然小,但推理时要保留自己的 KV Cache,这部分显存经常被人忽略。
还有一个小经验:开推测解码后,跟采样相关的参数尽量复制生产环境的值。比如生产环境用 temperature=0.7,那么压测和评测也都要用 0.7。因为采样温度会影响接受率,温度高的时候,草稿 token 被接受的概率会下降,你在 temperature=0 下测出来的加速比,不代表线上真实效果。
4.3 如何科学记录和解读性能指标
开完功能,很多人直接看终端里每秒生成速度,觉得快了就完事。这不够。我建议做一组对照实验,变量只有“是否开启推测解码”,用同一批 prompt、同一个最大长度、同样的采样参数测三遍取中位数。记录下来三个观测:首 token 延迟、端到端耗时、吞吐量。
进一步可以看接受率。vLLM 的服务指标里会暴露 speculative 相关的统计,不同版本字段名略有差异,但基本上能看出每一轮草稿 token 里有多少被接受。这个数值是排查性能问题最重要的放大镜。如果接受率低于 50%,先别折腾大模型侧,换个草稿模型或者调小 K 往往立竿见影。
如果追求更精细的调参,把 num_speculative_tokens 从 1 依次调到 3、5、7,每个值各压测一轮,画出“K 值 vs 加速比”的曲线。超过某个点后接受率会随 K 增大而下降,因为草稿模型越往后预测越不准,加速比曲线就会出现拐点。找到拐点附近的值,就是你这个组合的最佳参数。这种调参方法不仅适用于推测解码,也适合很多带超参的推理优化,记录下每轮配置和结论,方便回溯。
5. 掉坑记录与排查方法
5.1 常见问题速查表
把我在社区里和实际使用中见过的问题汇总一下,按频率排:
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 启动时报 vocab size 不匹配 | 草稿模型与目标模型词表不一致 | 检查两个模型的 config 文件;换同族模型 |
| 显存不足无法启动 | 同时加载两个模型,KV Cache 预留不够 | 降低 gpu_memory_utilization;调小 max-model-len;换更小草稿模型 |
| 开启后速度反而变慢 | 接受率太低;草稿模型逐 token 耗时太高 | 查看接受率;换更小草稿模型;调低 K 值;确认并发数是否已很高 |
| 响应结果与不开启时不一致 | 采样参数不一致;随机种子影响 | 对比同一采样参数;区分贪心采样和随机采样的噪声 |
| 验证阶段显存溢出 | 估算显存时漏了验证阶段的额外中间激活 | 调小 num_speculative_tokens;减小最大 batch token 限制 |
这张表来自我自己的排查笔记。我想强调最后一行:很多人开推测解码时只算了模型权重和 KV Cache,忘了大模型在验证 K 个位置时,中间激活值也会变大,K 越大,激活峰值越高。对付它的办法也很简单:K 回调,别贪。
5.2 当接受率不高:先分清草稿太弱还是 K 太大
诊断思路其实不难。先固定 K=3,跑一个小批量评测,如果接受率还是低,基本可以确定是草稿模型和目标模型对齐度不够,换草稿模型。如果 K=3 接受率不错,K=7 接受率断崖,那说明不是草稿模型本身差,而是它做不了那么长远的预测,把 K 调回 4 或 5 就好。
有些情况下接受率不低,端到端却没变快,那问题出在草稿模型生成速度上。你可以单独给草稿模型计时,看看它每生成一个 token 要多久。如果它跟大模型单 token 耗时差不到五倍以上,整个流程收益就不会大。记住一个拍脑袋的参考线:草稿模型单 token 耗时至少要比目标模型低一个数量级,推测解码才划算。
这里补充一个技巧:开着服务看 GPU 利用率。推理过程受限于带宽时,GPU 利用率不会忽高忽低,但如果你发现 GPU 算力块一直很闲而整机速度没提升,多半是调度层面的开销,把 K 调大一些能摊薄每轮调度的固定成本。如果 K 调大后接受率下降明显,那就需要换一个预测能力更强一点的草稿模型,而不是继续跟 K 较劲。
5.3 什么场景下不要开推测解码
我也见过团队把推测解码供起来,觉得未来所有推理都必须开。但实际工程里不是这样。如果你的服务已经跑在大 batch 高并发下,GPU 带宽接近饱和,推测解码的收益会被稀释,甚至因为多跑一个小模型,吞吐不升反降。这时候建议关闭。
另外,如果你的场景主要是短问答,每次生成不超过几十个 token,推测解码省下来的主要是后半段,首 token 没有收益,等于白忙。还有,如果你用的目标模型本身已经非常小,草稿模型很难选更小的同词表模型,收益空间有限。我的建议是务实:先上基准测试,让数据说话,别为了“用上新功能”而强行开启。
我在实际使用中还有一个体会:推测解码最好和别的加速手段搭配,但不要叠太多。有人既开推测解码,又开量化,又开各种投机式优化,结果排查问题时不知道瓶颈在哪。建议一次只改一个变量,逐步叠加,每次记录性能。这种做法看起来慢,实际上最能帮你摸清当前组合的真实能力。
我现在的习惯是,每次更换目标模型或者更新推理引擎版本后,都重新跑一遍上面这组基线测试。模型换代后,最优的草稿模型和 K 值常常会变,之前测出来的甜点位只能当参考坐标。把这个流程沉淀成脚本,以后任何新模型上线都能快速找到自己的配置。这也是推测解码这个技术教会我的事:不要凭感觉优化,跑数据、看接受率、记录配置,剩下的事情时间会告诉你。