☰
vLLM提速指南:连续批处理与投机解码让GPU吞吐翻倍
2026/10/27 22:39:46 网站建设 项目流程

最近帮同事调一个内部推理服务的吞吐,GPU 利用率怎么都上不去,跑 vLLM 部署大模型时一眼就能看到显存吃得挺满,但算力老有闲下来的空档。后来把连续批处理和投机解码一起打开,同样的显卡、同样的模型,每秒输出 token 数直接翻了一倍多。所谓“3行代码带你跑通”,听着像标题党,实际 vLLM 里干这件事确实就这么短——核心就是把草稿模型和草稿长度传给 LLM 对象,连续批处理负责把 GPU 的空白期吃满,投机解码负责把每次 decode 的 token 数从 1 拉高到 3~5。这篇文章适合刚上手 vLLM、准备部署 DeepSeek/Qwen 这类开源模型做推理加速的工程师,也适合已经跑通服务但吞吐一直提不上去的运维同学。

我先说结论:vLLM 的连续批处理是默认开着的,不需要任何额外代码;投机解码需要你指定一个草稿模型,vLLM 会自己调度验证。所以“3行代码”的真正含义是:不需要改服务架构、不需要自己实现调度器,只要把参数传对,就能同时吃到这两个优化。

1. 先搞懂这四个词在解决什么问题

1.1 连续批处理:把“一次一批”改成“动态拼单”

没有连续批处理的时候,大模型推理服务走的是静态批处理:攒够一批请求,一起进 GPU 算,这一批全部算完,再放下一批进来。问题是同一批请求里长度不可能都一样,有的生成 32 个 token 就结束了,有的要生成 512 个 token,GPU 必须等最慢的那个请求完全算完,这一批才放走。于是显存里全是已经完成的请求还在占坑,算力却在空转,这就是最常见的吞吐损失点。

连续批处理把这个逻辑彻底改了:不再以“整个批次”为单位,而是以一次 forward 迭代为单位。每跑完一次 step,调度器就重新看一遍当前所有请求的状态,已经完成的立刻移出,新的请求只要显存里有 KV cache 的空闲 block,就马上插进来。这个思路和拼车很像——不再等人齐了才发车,而是每到一个路口就带上顺路的人,车的载客率自然高很多。

vLLM 能把这个调度做到高效,靠的是底层的 PagedAttention。它把 KV cache 切成固定大小的 block,每段显存块独立分配,请求进来时按需拿块,块不够了就触发抢占或换出。所以调度器可以根据显存余量动态决定下一步往批次里塞多少个请求,而不是事先定死 batch size。实测下来,同样的 4 卡 A100,把静态批处理改成连续批处理,在线服务的吞吐能从 400 tokens/s 左右提升到 1200 tokens/s 这个量级,提升 2~3 倍是常见的事。

1.2 投机解码:让大模型“打草稿、一次验收”

自回归解码有一个让人非常难受的特性:生成每个 token 都要对整张大模型做一次完整 forward,GPU 算力再强,一步也只能产出一个 token。这就像写文章每写一个字都要重新通读一遍全书,开销全在串行等待上。

投机解码的做法是找个“跑得飞快的小模型”来打草稿。小模型先以当前上下文为输入,连续生成 k 个候选 token,这 k 个 token 因为小模型算得快,成本很小;然后把这一串草稿喂给大模型,大模型用一次 forward 同时验证这 k 个 token 的概率分布,能接受的 token 一次性确认收货,从第一个被拒绝的地方往后全部丢弃,再拿小模型重新打草稿。

在实际运行中,如果草稿模型和正主模型配合得好,比如 Qwen2.5-7B 配 Qwen2.5-0.5B,在代码生成这类规律性强的文本上,5 个草稿位置平均能接受 3~4 个,那么一次大模型 forward 就能产出 3~4 个 token,解码阶段吞吐差不多乘 3。这个收益不需要改模型,不需要重新训练,只要在推理服务里多塞一个几亿参数的小模型进去。

1.3 两者放在一起为什么是吞吐放大器

分开看,连续批处理解决的是“GPU 有没有在干活”的问题,投机解码解决的是“GPU 干一次活能出多少 token”的问题。一个提高 GPU 的使用率,一个提高单次计算的有效产出,两者叠加效果比单独开任何一个都明显。

我在实际压测里看得很清楚:只开连续批处理时,GPU 利用率能到 85% 以上,但很多算力是在一遍遍地算同一个请求的下一步,产出率低;只开投机解码时,短请求少、批次填不满,小模型打草稿再快也没人可服务。两个一起开,GPU 是满的,而且每次 forward 产出的 token 数明显高于 1,吞吐自然就起飞了。

2. 核心参数与落地思路

2.1 vLLM 里连续批处理到底要不要手动开

先说一个很多人误解的点:vLLM 的连续批处理不是靠某个 flag 打开的,而是调度器内置的行为。官方文档里很少单独强调这个,因为它是 vLLM 从第一版开始就带的核心设计。你只要在用 vLLM 的在线 serve 或者离线 LLM.generate,调度器就已经按迭代级别在动态拼 batch 了。

真正影响批处理效果的是调度器相关的几个显式参数。比如--max-num-seqs控制一个迭代里最多同时跑的序列数,默认 256,太小了并发上不去,太大了显存容易爆。还有--max-num-batched-tokens,默认 2048 到 4096,它限制一次迭代里最多放多少个 token 进 prefill 和 decode。这两个参数决定显存和吞吐怎么平衡,我习惯先把--gpu-memory-utilization设成 0.92,再观察日志里num_batched_tokens是否总是打满,打不满就继续加并发。

如果用的是离线推理,连续批处理的收益体现在“多个 prompt 同时传进 generate”这个动作上。很多人离线调用时习惯 for 循环逐条生成,这和顺序推理没有区别,根本吃不到连续批处理的红利。正确做法是一次性把几十条 prompt 拼成一个列表丢给 LLM.generate,调度器会自动在内部调度这些请求,效果立刻不一样。

2.2 投机解码的关键参数与草稿模型选型

投机解码在 vLLM 里不是一个默认开启的能力,你需要明确指定草稿模型。最核心的参数就三个:

参数作用我的常见设置
--speculative-model指定草稿模型,可以是 HF 仓库 ID,也可以写ngram用统计投机与主模型同系列的小版本
--num-speculative-tokens每次打草稿的 token 数,默认 54~8,先跑 AB 对比
--speculative-draft-tensor-parallel-size草稿模型占用的卡数通常 1,保存主模型显存

草稿模型怎么选,直接决定投机解码是赚是亏。我最推荐的做法是选和主模型同一个体系的小模型,因为这类模型共享 tokernizer 的概率很大,词汇表分布也接近,接受率会比较高。比如主模型是 Qwen2.5-7B-Instruct,草稿模型就用 Qwen2.5-0.5B-Instruct;主模型是 Llama-3-8B,就用 Llama-3-2-1B 之类。

还有一个很重要的选择:--speculative-model还可以填[ngram],这是 vLLM 内置的 N-gram 投机解码,不需要额外下载小模型,依靠统计方法猜测。好处是省显存、省下载时间,坏处是接受率通常不如正经小模型高。我测试下来,在代码生成这种重复结构较多的场景里 N-gram 够用,在自由创意写作里效果差距明显,所以如果你的显存还有余量,优先上真实的小模型。

2.3 什么时候别开投机解码

投机解码不是银弹,我踩过几次坑后才总结出什么时候应该关掉它。第一种情况是并发 batch 已经很大时,比如--max-num-seqs设到 256 以上,小模型打草稿本身也占算力,大模型验证那一串草稿时如果接受率不高,总开销反而比老老实实一个个生成更大。vLLM 官方给了一个参数--speculative-disable-by-batch-size,可以在 batch size 超过指定阈值时自动关掉投机,默认是 0 表示不自动关。我的经验是 batch 超过 128 后,先跑一轮压测看看收益曲线,很多场景在 256 以后就没有正收益了。

第二种情况是输出格式特别自由、想象力要求特别高的任务,草稿模型猜不准,接受率可能只有 20%~30%,一次大模型 forward 验证 5 个 token,最后只留下 1 个,那等于概率分布白算了一大半。这种场景里开投机解码,延迟反而比原来更高。反过来,代码生成、JSON 输出、固定模板问答这类高度结构化内容,投机解码收益最大,这也是为什么很多 LLM 网关在结构化输出场景里会默认开启投机。

3. 三行代码实操:从安装到测吞吐

3.1 先装一个能跑的 vLLM 环境

安装 vLLM 最省事的路径是直接用官方 wheel,前提是你的 CUDA 版本要对得上。当前常见组合是 CUDA 11.8 配 PyTorch 2.1,CUDA 12.1 配 PyTorch 2.1,或者 CUDA 12.4 配 PyTorch 2.5。命令很简单:

pip install vllm

如果要指定版本,比如pip install vllm==0.6.6.post1这样的版本号,需要注意和你现有的 torch 版本匹配。如果直接用pip install vllm装出来的 torch 版本会跟着换。如果你环境里已经有一套生产用的 torch,建议用虚拟环境隔离,别把生产环境搞乱。

显卡方面,建议至少 16GB 显存起步,因为主模型加草稿模型都要常驻显存。我用 24GB 的 4090 跑 7B 主模型加 0.5B 草稿模型,显存占用大约 12GB 到 15GB,还是很轻松的。如果是 2 张卡跑 tensor parallel,记得把--tensor-parallel-size和草稿模型的--speculative-draft-tensor-parallel-size一起考虑,否则主模型在两张卡上、草稿模型只放一张卡,调度会报错。

顺带说一句 Windows 的情况:vLLM 官方对 Windows 没有完整支持,开源社区有 Windows 社区版,能装能跑一些基础能力,但投机解码这种依赖 CUDA 算子较多的功能在 Windows 上兼容性很不稳定。如果你要在 Windows 环境折腾,我建议用 WSL2 跑,或者干脆准备一台 Linux 机器做推理测试,能省掉一大半的编译报错时间。

3.2 三行代码开启连续批处理和投机解码

先交代清楚:连续批处理不用写代码,vLLM 默认开着,你只要别一条条走 for 循环生成就行。投机解码的“三行”,其实是初始化 LLM 时传三个关键信息。下面这段代码可以直接跑:

from vllm import LLM, SamplingParams llm = LLM( model="Qwen/Qwen2.5-7B-Instruct", speculative_model="Qwen/Qwen2.5-0.5B-Instruct", num_speculative_tokens=5, ) outputs = llm.generate( ["写一段快速排序的 Python 代码", "解释一下连续批处理和投机解码的关系"], SamplingParams(max_tokens=256), )

每一行都有意义。speculative_model告诉调度器用哪个模型当草稿,num_speculative_tokens告诉调度器每轮打几个草稿 token,而generate接一个列表而不是单条字符串,就足以让连续批处理在内部把这些请求动态拼批。第一次跑的时候你会看到日志里出现类似Using a speculative model的提示,那就说明投机解码已经挂上去了。

在线服务模式下写法也类似,直接命令行启动:

vllm serve Qwen/Qwen2.5-7B-Instruct \ --served-model-name my-qwen \ --speculative-model Qwen/Qwen2.5-0.5B-Instruct \ --num-speculative-tokens 5 \ --max-model-len 8192 \ --gpu-memory-utilization 0.9

然后随便用 curl 或者 OpenAI SDK 打一发/v1/chat/completions,观察返回里的usage.completion_tokens和实际耗时,就能感受到吞吐变化。如果只想快速验证功能通不通,离线LLM.generate更直接,日志里能看到调度信息,调试起来比在线服务少一层代理。

3.3 用真实压测脚本验证吞吐变化

光看能跑还不行,要确认吞吐真的涨了。我写了一个很粗糙但足够说明问题的压测脚本,对比开不开投机解码的差异:

import time from vllm import LLM, SamplingParams prompts = ["写一段 Python 代码实现 LRU 缓存", "解释注意力机制", "写一首关于秋天的短诗"] * 10 params = SamplingParams(max_tokens=128) start = time.time() llm = LLM(model="Qwen/Qwen2.5-7B-Instruct") outputs = llm.generate(prompts, params) base_total = sum(len(o.outputs[0].token_ids) for o in outputs) base_time = time.time() - start start = time.time() llm_sp = LLM( model="Qwen/Qwen2.5-7B-Instruct", speculative_model="Qwen/Qwen2.5-0.5B-Instruct", num_speculative_tokens=5, ) outputs_sp = llm_sp.generate(prompts, params) sp_total = sum(len(o.outputs[0].token_ids) for o in outputs_sp) sp_time = time.time() - start print(f"baseline: {base_total / base_time:.1f} tokens/s") print(f"speculative: {sp_total / sp_time:.1f} tokens/s")

我这里故意先用同一个模型初始化了两次 LLM 对象,实际测的时候两段代码应该分开进程跑,否则显存会被两个模型占满,结果不准。我实测下来,在 4090 上跑这批 prompt,baseline 大约 45 tokens/s,开投机后能到 80 到 100 tokens/s 区间,提升非常明显。

还有个小技巧:测吞吐时把max_tokens调大一点,不要只生成二三十个 token。预填充阶段的时间占比大会把解码速度的优势稀释掉,max_tokens设置成 128 或 256 更能反映出解码吞吐的真实水平。

4. 常见问题与排查技巧实录

4.1 草稿模型和目标模型 tokenizer 对不上

这是开投机解码最常见的报错,vLLM 会直接提示类似“tokenizer of the speculative model does not match the target model”的信息。因为投机解码的验证阶段要求草稿模型的 token id 和目标模型在同一个词汇表空间里,如果两家 tokenizer 完全不同,验证出来的概率分布没有对齐意义。

解决办法有三个:一是换同一个家族的模型,比如 Qwen 和 Qwen 配对,Llama 和 Llama 配对,这个最省事;二是在启动命令里通过--tokenizer参数强制让草稿模型使用主模型的 tokenizer,前提是词汇表外挂能力能覆盖;三是选 vLLM 的[ngram]投机,它不依赖外来 tokenizer。我个人永远优先方案一,因为不同家族的模型即使 tokenizer 硬对齐,语义粒度也差太多,接受率会很难看。

4.2 开了投机解码性能反而更差

如果你跑压测发现 tokens/s 不升反降,先别怀疑模型坏了,大概率是接受率太低或者 batch 已经很大。我调num_speculative_tokens时有个习惯:先从 3 开始,跑到 5,再跑到 8,画出三个点的吞吐曲线。如果 5 和 3 差距不大,说明草稿模型的预测质量够但验证收益已经饱和了;如果 5 比 3 明显下降,说明草稿模型根本猜不中那么长的序列,多余的位置全在白算。

还有一个容易被忽略的点:当并发请求数已经很高时,投机解码的小模型前向过程也在抢占 GPU 资源。这时候用--speculative-disable-by-batch-size给投机解码设一个 batch size 上限,让调度器在并发一高就自动关掉投机,反而能保住整体吞吐。踩过几次坑之后,我现在都是默认把这个参数设到 128 到 256 区间,再根据压测结果微调。

4.3 CUDA 显存告急与并行配置

投机解码要多往显存里塞一个完整的小模型,显存不够时第一表现就是加载阶段直接 OOM 或者 serve 启动后跑几步就爆显存。处理手段有几个:第一,调小--gpu-memory-utilization,默认 0.9 太高就把0.9改成0.7,但这也会压缩 KV cache 的可用空间;第二,换更小的草稿模型,比如 0.5B 换 0.3B;第三,如果有两张卡,可以给草稿模型单独分离到一张卡上,通过--speculative-draft-tensor-parallel-size控制,但跨卡调度的通信开销也要算进总延迟里,不是所有场景都划算。

如果遇到 CUDA 版本比较新、比如 CUDA 12.8 这类刚发布的版本,官方 wheel 可能还没跟上,pip install vllm大概率会失败,这时候要不就换回已验证的 CUDA 12.4/12.1 环境,要不就等官方出对应 wheel 或者源码编译。源码编译 vLLM 不是轻松事,生产环境我很少主动走这条路。

4.4 部署 DeepSeek 等模型时的特别提醒

现在很多人拿 vLLM 部署 DeepSeek 系列,比如 DeepSeek-R1 蒸馏成 7B/14B 的小模型,这类模型跑投机解码时需要特别注意:蒸馏模型的 tokenizer 继承自基座模型,但推理行为可能和原模型差异很大,草稿模型的选型不要只看参数大小,要先用一小批测试 prompt 观察接受率。

如果你部署的目标是 DeepSeek-R1-Distill-Qwen-7B 这种基于 Qwen 的蒸馏模型,草稿模型可以选 Qwen2.5-0.5B-Instruct,因为它们的 tokenizer 同源,成功率很高。但如果是 DeepSeek-V3 这类原生 MoE 大模型,显存占用大,投机解码的草稿模型也建议用同系列的 DeepSeek-R1-Distill 小模型,或者干脆用 N-gram 投机,避免显存爆炸。最重要的一点:任何部署操作前先跑一遍离线vllm serve --dry-run之类的校验命令,或者至少用一个 prompt 测试拉起服务,别直接上生产流量。


最后分享一个我自己的使用体会:连续批处理和投机解码这两个优化,最怕的其实是“开了就行”的心态。连续批处理最好确认一下--max-num-seqs是否匹配你的并发模型,投机解码一定要试不同草稿长度、不同草稿模型的接受率。我一般会在每次压测后记录一张“草稿长度 vs tokens/s”的小表格,积累几组数据后就能找到当前业务场景里的最优配置,这套调优方法比盲目抄大厂的参数靠谱得多。你那边的负载特征不一样,别怕麻烦,多跑几轮 AB 对比就会找到最合适的组合。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询