1. 大模型推理为什么慢:从自回归生成的本质说起
但凡在生产环境部署过大模型推理服务的人,都会对同一个问题印象深刻:吞吐上不去,延迟下不来,GPU利用率还忽高忽低。你花了大价钱买的加速卡,跑起生成任务来像是被什么东西卡住了喉咙——明明算力还有富余,但每秒吐出来的token数就是那么可怜。这个问题的根源,得从自回归生成的机制讲起。
大模型生成文本的过程是逐token进行的。每生成一个token,模型都要完整地跑一遍前向计算:从嵌入层到几十层Transformer块,再到最后的输出投影和采样。这个过程里,真正用于“计算”的时间其实占比很小,大量时间花在了把模型权重从显存搬到计算单元这件事上。业内管这叫内存带宽瓶颈(memory-bound),意思是你的算力单元再强,数据喂不过来也是白搭。
用一个生活化的类比:这就像一家餐厅,厨房里有一位米其林大厨(GPU的算力),但食材仓库在很远的地方(显存),每次做一道菜(生成一个token),都要派人去仓库取一次食材(加载权重)。大厨做菜只要几秒钟,但取食材来回要几分钟。结果就是大厨大部分时间在等食材,而不是在做菜。
具体到数字上,一个70亿参数的模型,以FP16精度存储,权重占用约14GB。每生成一个token,这14GB的数据都要从显存完整读一遍。假设显存带宽是2TB/s,那么光是读权重就需要7ms。而生成一个token的计算量大约是2×7B=14G FLOPs,在算力为300 TFLOPS的加速卡上只需要约0.05ms。计算时间和数据传输时间差了整整两个数量级,这就是自回归生成慢的核心原因。
更糟糕的是,这个瓶颈在batch size较小时尤为明显。当batch size=1时,每次前向计算只为一个序列服务,算力利用率极低。虽然增大batch size可以摊薄权重加载的成本(一次加载服务多个序列),但在线服务场景下,用户的请求是陆续到达的,你很难总是凑满一个大批次。而且增大batch size会显著增加首token延迟,用户体验反而变差。
所以问题的本质是:自回归生成的串行特性,导致每一步的计算量太小,无法充分利用硬件算力,时间都浪费在搬运权重上了。要提速,核心思路就是让每一步“多算一点”,把闲置的算力利用起来。推测解码正是沿着这个思路走出的一条漂亮路径。
2. 推测解码的核心思路:用小模型“猜”,用大模型“验”
推测解码(Speculative Decoding)的想法其实非常直觉:既然大模型每步只生成一个token太浪费,那能不能让一个更小、更快的模型先一口气猜出好几个token,然后让大模型一次性验证这些猜测对不对?如果猜对了,就直接采纳,相当于大模型一步生成了多个token;如果猜错了,就回退到第一个错误的位置,由大模型重新生成那一个token。这样一来,在猜测准确率较高的情况下,大模型的实际生成步数大幅减少,整体速度就上去了。
这个思路的关键洞察在于:验证多个token的计算成本,和生成一个token的计算成本几乎相同。因为验证时,大模型可以并行地对所有猜测位置计算概率分布,这是一次前向传播就能完成的事情。换句话说,你让大模型做一次前向计算,本来只能生成一个token,现在可以验证K个token。只要猜对的token数超过1,你就赚了。
2.1 草稿模型与目标模型的角色分工
在推测解码的框架里,有两个角色:
- 目标模型(Target Model):就是你原本要部署的那个大模型,比如70B、13B级别的模型。它负责最终的验证和输出,保证生成质量不打折扣。
- 草稿模型(Draft Model):一个参数量小得多的模型,比如目标模型的十分之一甚至更小。它负责快速生成候选token序列。
草稿模型的选择很灵活。可以是同系列的小模型(比如目标模型是70B,草稿模型用同架构的1.5B或3B版本),也可以是一个专门蒸馏出来的小模型,甚至可以用n-gram统计模型来做草稿。选择的核心原则是:草稿模型要足够快,同时预测准确率要尽量高。如果草稿模型太慢,那猜测本身就成了瓶颈;如果准确率太低,大模型频繁回退,加速效果也会大打折扣。
2.2 验证机制:如何保证输出质量不降级
推测解码最精妙的地方在于它的验证机制。它不是简单地“猜对了就用,猜错了就扔”,而是通过一种修正采样的方式,保证最终输出的概率分布和直接用目标模型生成完全一致。这意味着推测解码是无损的——它不会改变模型的输出质量,只是让生成过程变快了。
具体来说,假设草稿模型对某个位置给出了概率分布q(x),目标模型给出概率分布p(x)。推测解码会从草稿模型中采样一个token x,然后以min(1, p(x)/q(x))的概率接受它。如果拒绝,就从修正后的分布(p(x)-q(x))_+中重新采样。数学上可以证明,这样得到的最终分布恰好就是p(x)。这个证明基于拒绝采样(Rejection Sampling)的理论,是推测解码能够做到无损加速的数学基础。
实际工程中,很多实现会简化这个流程:直接比较草稿模型和目标模型对同一个token的概率,如果目标模型的概率不低于某个阈值,就接受。这种做法在大多数情况下效果也很好,但严格来说不是无损的。如果你对输出质量有严格要求,建议采用标准的拒绝采样方案。
3. 生产落地的关键参数与性能模型
把推测解码从论文搬到生产环境,光理解原理是不够的。你需要搞清楚哪些参数会影响加速效果,以及如何根据你的硬件条件和业务场景来调优。这一块我踩过不少坑,下面把关键参数和性能模型拆开讲。
3.1 草稿长度K怎么选:不是越大越好
草稿长度K指的是草稿模型每次猜测的token数量。直觉上K越大,大模型一次验证能采纳的token越多,加速比越高。但实际上K的选择是一个收益递减的过程。
假设草稿模型的单token接受率为α(即草稿模型猜的token被目标模型接受的概率),那么猜测K个token时,期望被接受的token数量是:
E[accepted] = (1 - α^(K+1)) / (1 - α)
当α=0.8时,K=1的期望接受数是0.8,K=3是1.95,K=5是2.95,K=8是4.16。可以看到K从1增加到5,收益很明显;但从5增加到8,收益就小很多了。而与此同时,草稿模型生成K个token的时间是线性增长的。所以存在一个最优K值,使得加速比最大。
根据我的实测经验,在大多数场景下,K取4到8之间比较合适。具体选多少,取决于草稿模型的接受率和草稿模型相对于目标模型的速度比。如果草稿模型非常快(比如参数量只有目标模型的1/20),可以适当增大K;如果草稿模型本身就不快,K取小一点更划算。
3.2 接受率α的影响因素与提升方法
接受率α是决定推测解码加速效果的最关键变量。α越高,同样的K值下能采纳的token越多,加速比越大。影响α的因素主要有几个:
- 草稿模型与目标模型的分布匹配度:如果草稿模型是在和目标模型相同的数据分布上训练的,或者本身就是从目标模型蒸馏出来的,α会比较高。实践中,同系列的小模型通常表现不错。
- 任务类型:对于代码生成、结构化输出这类“确定性较强”的任务,α通常较高(0.7-0.9);对于开放式创作、闲聊这类“随机性较强”的任务,α会低一些(0.5-0.7)。
- 温度参数:温度越高,采样越随机,α越低。如果你用temperature=1.0甚至更高,推测解码的加速效果会明显下降。
提升α的方法包括:用目标模型对草稿模型做蒸馏微调、针对特定任务领域训练专门的草稿模型、在推理时动态调整草稿策略等。这些方法各有成本,需要根据你的实际情况权衡。
3.3 加速比的理论上限与实测数据
推测解码的理论加速比可以用下面的公式估算:
Speedup ≈ E[accepted] / (1 + K × r)
其中r是草稿模型单步时间与目标模型单步时间的比值。这个公式告诉我们,加速比受两个因素制约:一是期望接受的token数(越大越好),二是草稿模型带来的额外开销(越小越好)。
以我实际部署过的一个场景为例:目标模型是13B,草稿模型是1.5B,r约为0.12,α约为0.75,K取5。代入公式:
E[accepted] = (1 - 0.75^6) / (1 - 0.75) ≈ 3.29
Speedup ≈ 3.29 / (1 + 5 × 0.12) ≈ 2.06
实测下来,加速比在1.8到2.2之间波动,和理论估算基本吻合。如果换用更小的草稿模型(比如0.5B),r降到0.05左右,但α也会下降到0.6左右,此时:
E[accepted] = (1 - 0.6^6) / (1 - 0.6) ≈ 2.34
Speedup ≈ 2.34 / (1 + 5 × 0.05) ≈ 1.87
反而略低一些。所以草稿模型不是越小越好,要在速度和准确率之间找平衡。
4. 从零搭建推测解码推理服务:完整实操流程
理论讲完了,下面进入实操环节。我会以一个典型的在线推理服务为例,把推测解码的部署流程完整走一遍。这里假设你已经有了一定的推理框架使用经验,比如用过vLLM、TensorRT-LLM或者HuggingFace TGI之类的工具。
4.1 环境准备与模型选型
首先确定你的目标模型和草稿模型。目标模型就是你线上正在服务的那个模型,草稿模型建议优先选择同系列的小模型。比如目标模型是Qwen2.5-14B,草稿模型可以用Qwen2.5-1.5B;目标模型是Llama-3-8B,草稿模型可以用Llama-3.2-1B。同系列模型共享tokenizer和词表,兼容性最好,省去很多麻烦。
硬件方面,草稿模型虽然小,但也要占用显存。如果目标模型已经快把显存吃满了,可能需要考虑用更小的草稿模型,或者用INT8/INT4量化草稿模型来省显存。我一般会预留目标模型显存占用的15%-20%给草稿模型。
# 以vLLM为例,安装支持推测解码的版本 pip install vllm>=0.6.0 # 检查GPU显存 nvidia-smi --query-gpu=memory.total,memory.used --format=csv4.2 配置推测解码参数
以vLLM为例,启动推测解码服务时需要指定草稿模型和相关的推测参数:
python -m vllm.entrypoints.openai.api_server \ --model /path/to/target-model \ --speculative-model /path/to/draft-model \ --num-speculative-tokens 5 \ --speculative-draft-tensor-parallel-size 1 \ --tensor-parallel-size 2 \ --gpu-memory-utilization 0.9 \ --max-model-len 4096几个关键参数说明:
--num-speculative-tokens:就是前面说的K值,建议从5开始调。--speculative-draft-tensor-parallel-size:草稿模型的张量并行度。草稿模型小,通常设为1就够了。--tensor-parallel-size:目标模型的张量并行度,根据你的GPU数量来定。
注意:草稿模型和目标模型的tensor parallel size可以不同,但草稿模型的并行度不能超过目标模型,否则会报错。
4.3 压测与调优:找到你的最优参数组合
服务跑起来之后,别急着上生产,先做一轮压测。压测的目标是找到在你的业务流量特征下,加速比最大的参数组合。需要调的参数主要有两个:K值和草稿模型的选择。
压测时建议用真实的业务请求做输入,而不是随便找几段文本。因为不同任务类型的接受率差异很大,用真实数据才能反映真实效果。我一般会准备200-500条代表性请求,覆盖主要的业务场景。
压测指标方面,重点关注这几个:
| 指标 | 含义 | 期望 |
|---|---|---|
| 首token延迟 | 从请求到第一个token返回的时间 | 不应明显增加 |
| 输出吞吐 | 每秒生成的token总数 | 提升50%以上 |
| 接受率α | 草稿token被接受的比例 | 0.6以上 |
| 加速比 | 推测解码前后的吞吐比值 | 1.5以上 |
如果接受率低于0.5,说明草稿模型和目标模型差距太大,考虑换一个更好的草稿模型。如果加速比不理想但接受率还行,可能是K值没调好,或者草稿模型本身太慢。
4.4 生产环境的动态调整策略
上线之后,流量特征可能会变化。比如白天以短请求为主,晚上以长文本生成居多。不同场景下的最优K值可能不同。这时候可以考虑动态调整K值的策略:根据当前batch的接受率历史,动态增大或减小K。
一个简单的实现方式是:维护一个滑动窗口的接受率统计,如果最近100个batch的平均接受率高于0.8,就把K增大1;如果低于0.6,就把K减小1。K的调整范围限制在2到10之间。这样可以让系统自适应不同的流量模式。
另外,如果草稿模型在某些请求上表现特别差(比如专业领域术语多的请求),可以考虑降级策略:当连续多个batch的接受率低于阈值时,临时关闭推测解码,直接用目标模型生成。等流量特征恢复后再重新开启。
5. 常见问题与排查技巧实录
这一块是我在实际部署中踩过的坑和总结出来的排查经验,很多是文档里不会写的。
5.1 加速比不达预期?先查这三个地方
问题一:草稿模型和目标模型的tokenizer不一致。这是最隐蔽的坑。如果两个模型的词表不同,草稿模型猜出来的token在目标模型里可能对应完全不同的东西,接受率会低到离谱。排查方法很简单:分别用两个模型对同一段文本做tokenize,看结果是否一致。不一致就换同系列的草稿模型。
问题二:K值设置过大导致草稿开销占比过高。前面说过,K越大草稿模型耗时越长。如果你发现GPU利用率很高但吞吐上不去,很可能是草稿模型成了新瓶颈。试着把K减半,看看吞吐是否反而上升。
问题三:batch size太小。推测解码在batch size较小时效果最好,但如果batch size=1且请求本身就很短,加速效果可能不明显。因为推测解码的收益主要来自“一次验证多个token”,如果请求本身只需要生成几个token就结束了,那推测解码还没来得及发挥作用就结束了。建议对短请求做特殊处理,或者设置一个最小生成长度阈值。
5.2 显存不够用怎么办
草稿模型虽然小,但也是额外的显存开销。如果显存紧张,有几个办法:
- 量化草稿模型:用INT8或INT4量化草稿模型,显存占用可以降到FP16的1/2到1/4。草稿模型本身对精度不敏感,量化带来的接受率下降通常很小。
- 共享KV Cache:某些推理框架支持草稿模型和目标模型共享KV Cache,能省不少显存。但实现起来比较复杂,需要框架层面的支持。
- CPU Offload草稿模型:把草稿模型放在CPU上,用的时候再加载到GPU。但这样会增加延迟,只适合对延迟不敏感的场景。
5.3 接受率突然下降的排查思路
如果服务运行一段时间后接受率突然下降,按下面的顺序排查:
- 检查输入分布是否变化:是不是有新的业务场景接入了?新场景的文本特征可能和草稿模型的训练分布不匹配。
- 检查温度参数:是不是有人调高了temperature?温度越高接受率越低。
- 检查草稿模型是否被意外更新:如果草稿模型是动态加载的,确认没有加载错误的版本。
- 检查GPU是否降频:显存温度过高会导致GPU降频,草稿模型变慢,间接影响加速比。
5.4 常见问题速查表
| 现象 | 可能原因 | 解决方法 |
|---|---|---|
| 加速比低于1.2 | 草稿模型太慢或接受率太低 | 换更小的草稿模型或提高草稿模型质量 |
| 首token延迟明显增加 | 草稿模型加载拖慢启动 | 预加载草稿模型,或改用懒加载 |
| 输出质量下降 | 使用了非无损的验证方式 | 切换到标准拒绝采样方案 |
| 显存OOM | 草稿模型占用过多显存 | 量化草稿模型或减小K值 |
| 吞吐波动大 | 流量特征变化大 | 启用动态K值调整策略 |
6. 推测解码的适用边界与替代方案对比
推测解码不是万能的。它在某些场景下效果拔群,在另一些场景下可能还不如不用。搞清楚它的适用边界,比盲目上马更重要。
6.1 什么场景最适合推测解码
根据我的经验,下面这几类场景用推测解码收益最大:
- 代码生成与补全:代码的确定性很强,草稿模型很容易猜对。我实测过代码补全场景,接受率能到0.85以上,加速比超过2.5。
- 结构化输出:比如JSON生成、表格填充,格式约束强,草稿模型猜起来很准。
- 翻译任务:源文本和目标文本有较强的对应关系,草稿模型可以基于源文本做较好的预测。
- 长文本摘要:摘要任务中很多短语和句式可以从原文中直接复制,草稿模型容易猜中。
反过来,开放式创作、角色扮演、高温度采样这些场景,推测解码的加速效果会打折扣。因为输出随机性太强,草稿模型很难猜准。
6.2 与其他推理加速方案的组合使用
推测解码可以和其他加速技术叠加使用,效果往往比单独用更好:
- 与量化结合:目标模型用量化加速,草稿模型也用少量化,两者叠加可以同时享受量化和推测解码的收益。但要注意量化后的接受率可能会略有下降。
- 与连续批处理结合:连续批处理(Continuous Batching)提高GPU利用率,推测解码减少生成步数,两者从不同维度提升吞吐,叠加效果很好。
- 与PagedAttention结合:PagedAttention优化KV Cache的显存管理,让更大的batch size成为可能,间接提升推测解码的收益。
不过要注意,推测解码和投机采样(Speculative Sampling)不是一回事。投机采样是另一种加速思路,它不依赖草稿模型,而是用目标模型自身的中间层输出做预测。两者各有优劣,选择哪种取决于你的具体场景和工程能力。
6.3 什么时候不该用推测解码
如果出现下面这些情况,建议先别上推测解码:
- 显存已经捉襟见肘:没有余量放草稿模型,强行上会导致OOM或者被迫减小目标模型的batch size,得不偿失。
- 请求以极短生成为主:比如只需要生成1-2个token的分类任务,推测解码的收益几乎为零。
- 团队缺乏推理优化经验:推测解码涉及两个模型的协同,调试复杂度比单模型高不少。如果团队对推理框架不熟,建议先把单模型的性能榨干再说。
我个人建议:先在测试环境用真实流量跑一周,对比开启和关闭推测解码的各项指标,用数据说话。不要因为“听起来很厉害”就盲目上线。
7. 我踩过的坑与实战心得
最后分享几个我在实际项目中踩过的坑,都是真金白银换来的经验。
第一个坑:草稿模型选型只看参数量。一开始我以为草稿模型越小越好,选了个0.5B的模型,结果接受率只有0.4,加速比还不如不用。后来换成1.5B的同系列模型,接受率提到0.75,加速比直接翻倍。草稿模型的质量比大小更重要,同系列蒸馏过的小模型往往比通用小模型效果好得多。
第二个坑:忽略了tokenizer的对齐。有一次用了一个不同系列的草稿模型,tokenizer不一样,接受率低到0.2。排查了半天才发现是词表不匹配。从那以后,我选草稿模型的第一条标准就是:必须和目标模型共享tokenizer。
第三个坑:K值设太大导致尾延迟飙升。有一次为了追求吞吐,把K设到了12,结果平均吞吐是上去了,但P99延迟暴涨。因为有些请求草稿模型猜得特别不准,大模型要回退很多次,反而比不用推测解码还慢。后来把K降到6,P99延迟就正常了。K值的选择要兼顾吞吐和尾延迟,不能只看平均值。
第四个坑:没有做动态降级。有一次线上流量突然变化,来了一大批专业领域的请求,草稿模型完全猜不准,接受率掉到0.3,整个服务的吞吐反而比不用推测解码还低。后来加了一个动态降级机制:当滑动窗口内的接受率低于0.5时,自动关闭推测解码,等接受率恢复后再开启。这个机制救了命。
第五个坑:压测数据不代表真实流量。一开始压测用的是公开数据集,加速比很漂亮。上线后发现实际流量下加速比打了七折。原因是公开数据集的文本分布和真实业务差异很大。压测一定要用真实业务数据,哪怕只有几百条,也比公开数据集强。
关于参数调优,我的经验是:先固定K=5,调草稿模型;草稿模型确定后,再微调K。不要同时调两个参数,否则你根本分不清是哪个参数在起作用。另外,每次调整后至少跑500个请求再下结论,样本太少波动太大。
还有一个实用技巧:给草稿模型单独设一个温度参数。有时候目标模型用较高的温度采样,但草稿模型可以用较低的温度(比如0.5),这样草稿模型的输出更确定,接受率反而更高。这个技巧在开放式生成场景下特别有用。
这个方向后续还可以往多草稿模型集成的方向扩展——同时用多个不同风格的草稿模型做猜测,取接受率最高的那个。理论上能进一步提升接受率,但工程复杂度也会相应增加。如果大家有兴趣,我可以再写一篇专门讲多草稿模型集成的实操。