
说实话这个标题里的“3行代码”其实带点标题党。真正让我在推理吞吐上拿到翻倍收益的不是某一段炫技代码而是vLLM连续批处理配合投机解码之后整套启动参数和调度逻辑的重新理解。这篇文章把我从基线测试到参数调优的完整过程写出来包括哪些参数可以直接照抄、哪些要根据你的模型和显存现场改以及我踩过的掉速坑。适合已经能跑通vLLM、但对吞吐优化还停留在“换个更大的卡”阶段的人。1. 先把“3行代码”摆出来一份可以照抄的启动配置1.1 三行关键参数和它们各自管什么我先直接给结论。如果你用的是vLLM的OpenAI兼容服务核心配置是这样一组启动参数vllm serve Qwen/Qwen2.5-7B-Instruct \ --tensor-parallel-size 1 \ --max-num-seqs 256 \ --enable-chunked-prefill \ --speculative-model Qwen/Qwen2.5-0.5B-Instruct \ --num-speculative-tokens 5其中真正起决定性作用的是最后三行--enable-chunked-prefill开启分块预填充。它让长提示词的prefill阶段不要一次性占满GPU而是切成小块和decode混合执行。这是连续批处理能跑得顺的关键前置不是可有可无的开关。--speculative-model Qwen/Qwen2.5-0.5B-Instruct指定一个“草稿模型”。它负责快速猜测接下来的几个token再由7B主模型一次性验证。--num-speculative-tokens 5让草稿模型一次猜几个token。5是我试下来收益和风险比较平衡的值不是越大越好。如果你不走服务化而是直接在Python里调用那更接近“3行代码”的形态from vllm import LLM, SamplingParams llm LLM( modelQwen/Qwen2.5-7B-Instruct, speculative_modelQwen/Qwen2.5-0.5B-Instruct, num_speculative_tokens5, ) outputs llm.generate([给我写一段Python快速排序], SamplingParams(temperature0))我理解标题里说的“3行代码”本质就是这个不换模型、不写自定义算子、不碰CUDA kernel只靠vLLM暴露出来的调度与投机解码配置把同一张卡的产出逼出来。后文我会解释为什么这几行配置能生效以及它们各自的收益边界在哪。1.2 我的实测环境和版本选择先交代环境因为吞吐数据脱离硬件和版本没有意义。我用来跑通这套配置的机器是单张A100 40GPyTorch 2.xCUDA 12.4vLLM版本在0.7系列。主模型是Qwen2.5-7B-Instruct草稿模型是Qwen2.5-0.5B-Instruct词表完全一致这很重要。如果你在Windows上折腾我先泼一盆冷水。vLLM在Windows上虽然有社区版但很多高级特性、显存管理和CUDA图模式的稳定性都不如Linux。我见过不少人在Windows上开投机解码直接报torch.cuda.OutOfMemoryError不是参数问题是平台兼容问题。生产环境老老实实用Linux容器。另外如果你装了最新的CUDA 12.8记得选和它匹配的vLLM版本否则大概率要自己编译编译一次够你喝一壶的。至于LM Studio、Ollama和vLLM/SGLang的对比我的观点是个人尝鲜用Ollama没毛病但你要做吞吐优化、要精确控制批处理大小和投机解码参数Ollama暴露出来的配置项太少了。SGLang和vLLM是直接竞品我在另一篇文章里拆过两者的调度差异这篇我们聚焦vLLM。2. 连续批处理吞吐翻倍的第一个来源藏在一个默认参数里2.1 从“一锅端”到“流水线”连续批处理的调度逻辑很多人没意识到vLLM的连续批处理技术continuous batching其实是吞吐提升的第一大来源投机解码是叠加在上面的第二层收益。要先理解连续批处理得先看看传统推理服务是怎么干的。传统静态批处理的做法是攒够一批请求同时送进GPU等这一批全部生成完毕再统一释放显存、接收下一批。问题很明显一批里面只要有一个长序列没生成完其他早该结束的短序列也得陪着等新来的请求哪怕很紧急也得等当前这整批清空。这就像食堂打饭一锅菜没炒完所有人都得在窗口前干等着。连续批处理的思路完全不同。它把调度粒度从“请求级”降到“token级”GPU每执行一次前向调度器会重新看一遍当前所有还在生成中的序列哪些能继续跑、哪些已经生成了结束符就立刻腾出显存槽位把等待队列里的新请求塞进来。这样一来短请求不会被长请求拖死新请求也不用等一整批结束GPU的利用率自然就上去了。这个机制在vLLM里其实是默认开启的但你未必吃到了它的红利因为默认的--max-num-seqs偏保守。假如默认值是64而你的显存明明能同时塞下256个序列那连续批处理再聪明也只能在64个槽位里转悠吞吐天花板被锁死了。2.2 chunked-prefill为什么它和连续批处理是天生一对连续批处理解决了“请求怎么在batch里进出”的问题但还留了一个尾巴prefill预填充阶段。一个请求如果带着很长的提示词进来prefill计算量很大会一次性霸占GPU很长时间。如果按照传统方式把整个prompt的prefill一次性做完这段时间里其他序列的decode全部被阻塞batch里明明有256个槽位实际能用的只有正在prefill的那一个。chunked-prefill就是来收拾这个局面的。它把一次prefill拆成多个小段每算完一小段就让调度器插几个decode步骤进去。你可以把它理解成一个大任务不再独占CPU而是每跑一会儿就让出时间片让大家轮流用。这样GPU的流水线始终是满的不会出现“一个人吃饭、所有人围观”的尴尬。所以我的建议是调大--max-num-seqs的同时一定要配合--enable-chunked-prefill。两者配合的效果我在后面验证部分会给数据。有些vLLM新版本可能在特定条件下默认开启了chunked prefill如果你的版本提示这个参数已经默认启用去掉也不会报错但如果你的服务日志里能看到chunked prefill相关配置就说明它在工作。2.3 先测一次基线再谈优化我强烈建议你在动任何优化参数之前先跑一次基线。优化的前提是可测量否则你都不知道改完参数是变快还是变慢。最省事的办法是用vLLM自带的离线吞吐基准python -m vllm.benchmark.benchmark_throughput \ --model Qwen/Qwen2.5-7B-Instruct \ --tensor-parallel-size 1 \ --num-prompts 1000 \ --input-len 512 \ --output-len 256我环境下的基线大概是默认--max-num-seqs64时稳定吞吐约1100 tokens/s。注意这个数字只对“我这台机器这个数据集”有意义但同一环境下的相对关系是有参考价值的。拿到基线后再逐步加参数才不会变成玄学调优。3. 投机解码用“草稿验证”缩短单token产出时间3.1 草稿模型的工作方式猜得越快验证越省连续批处理解决的是“GPU满负荷运转”的问题但单次decode仍然只生成一个token。投机解码speculative decoding解决的是“单次前向产出太少”的问题思路非常反直觉让一个更小更快的模型先猜接下来K个token然后大模型一次前向把这些猜测全部验证掉。这里的关键在于大模型验证K个token的成本远小于大模型自己逐个生成K个token的成本。因为大模型验证时可以并行处理这K个token对应的位置本质上是“一次前向多个输出位置同时算概率”。而在传统自回归里每个token必须等前一个token算完才能开始这个串行依赖才是慢的根源。用个不严谨但好懂的说法你让实习生先写5行代码然后你一次看完觉得没问题就全用了而不是每一行都自己从头写。只要实习生水平别太差你的产出速度一定比逐字写快。投机解码的数学收益取决于“接受率”accepted rate大模型对草稿模型猜的token有多大比例愿意放行。如果草稿模型的接受率是α草稿长度是K理想情况下每个验证步产出的token数期望大约是[ E \frac{1 - \alpha^{K1}}{1 - \alpha} - 1 ]这个公式你不用硬记记住两个极端就行α接近0.8以上时K5通常能带来2到3倍的产出提升α在0.4以下时收益会被验证开销吃掉甚至不如不开。这也是为什么很多人开了投机解码反而掉速——不是vLLM的问题是草稿模型选得不好。3.2 草稿模型怎么选同源、同词表、尺寸合适我踩过最大的坑是草稿模型和目标模型词表不一致。当时我图省事拿了一个很流行的通用小模型当草稿结果目标模型生成的token ID和小模型猜的token ID根本对不上接受率接近0速度反而比不开投机还慢。后来我才理解投机解码验证的是token序列不是字符串两个模型的词表必须完全一致否则“猜”和“验”根本不在同一个坐标系里。所以草稿模型的第一原则是和目标模型同源、同词表。比如目标模型用Qwen2.5-7B草稿模型就选Qwen2.5-0.5B或1.5B目标模型用Llama-3.1-8B草稿模型就选Llama-3.1-8B的同系列小模型。尺寸方面我的经验是草稿模型大概为主模型的十分之一到五分之一比较合适太大了挤占显存主模型跑不快太小了接受率低投机没意义。如果你部署的是DeepSeek这类通过蒸馏得到的开源模型要特别注意草稿模型是否和目标模型用了同一套tokenizer。不要只看名字像不像要在启动后观察日志或者实测接受率。vLLM里也有n-gram草稿、EAGLE这样更高级的草稿策略但新手我不建议一上来就碰先用标准草稿模型把链路跑通比追求极限收益重要。3.3 采样参数对投机解码的影响temperature越高收益越缩水一个经常被忽略的事实投机解码的接受率和采样参数强相关。当你用greedytemperature0时目标模型的输出是确定的草稿模型猜中的概率最高。一旦你调高temperature或者把top_p压得很低目标模型会在概率分布上随机采样草稿模型哪怕概率排第一也经常因为随机性被拒绝。换句话说你的业务如果要求高随机性、高多样性输出投机解码的收益会明显打折。我在实测里发现temperature从0调到0.7同样的草稿模型整体吞吐能掉20%到30%。不是说不能用而是你要有预期投机解码在低温度场景下收益最稳定。如果你做的是代码生成、结构化输出、固定格式填充这类任务放心大胆开如果你做的是创意写作、开放对话先小流量验证一下再说。另外--num-speculative-tokens不是越大越好。草稿模型一次猜的token越多大模型验证时的显存和计算开销也越大而且猜得越长后半段接受率通常越低边际收益递减。我试过3、5、7、10最终5是综合最优。10的话显存涨了一截吞吐反而和5差不多因为多出来的猜测大部分被拒绝了。4. 从安装到压测完整复现这套优化的链路4.1 安装和版本选择别让环境坑了你安装vLLM本身不复杂复杂的是版本匹配。我推荐在干净的虚拟环境里装避免和PyTorch相关的包冲突。pip install -U vllm如果你要跑CUDA 12.8务必确认当前vLLM版本是否提供了对应的预编译wheel。装完之后先别急着跑投机解码跑一次vllm serve主模型确认环境本身没问题。如果启动阶段就报torch.cuda相关错误先排查CUDA驱动和容器运行时不要急着怀疑参数。Windows这一节我再多说一句。vLLM的Windows社区版在简单推理场景下可用但我在社区版上跑--enable-chunked-prefill加投机解码的组合时遇到过CUDA graph捕获失败和显存碎片化的问题。如果你是个人电脑想体验用WSL2的Docker镜像都比原生Windows稳。生产环境直接上Linux不要纠结。4.2 启动命令与“到底生效了没有”的确认方法完整启一套带优化的服务我用的命令类似这样vllm serve Qwen/Qwen2.5-7B-Instruct \ --tensor-parallel-size 1 \ --max-num-seqs 256 \ --max-num-batched-tokens 8192 \ --enable-chunked-prefill \ --speculative-model Qwen/Qwen2.5-0.5B-Instruct \ --num-speculative-tokens 5 \ --gpu-memory-utilization 0.92 \ --port 8000--max-num-batched-tokens控制一次前向最多处理多少个token。调大--max-num-seqs之后建议同步观察显存适当调整这个上限防止显存溢出。启动之后怎么确认投机解码真的生效了看日志。vLLM启动时如果成功加载草稿模型日志里会出现类似Speculative decoding、draft model的字样。如果不生效它通常会在启动阶段直接报错或警告而不是默默忽略。另一个办法是看显存占用加载了小模型后显存占用会比单模型启动时高出一截。如果你瞄一眼显存和单模型一模一样那投机解码大概率没配上。chunked prefill和连续批处理有没有生效可以通过观察服务负载和吞吐变化来判断也可以在日志里看调度配置打印。不过我更推荐用压测数据说话毕竟配置打印了不等于效果达标。4.3 用离线脚本压测记录三档对照数据我把优化过程分成三档对比这样能清楚看到每个参数的贡献配置吞吐tokens/s备注默认参数基线约1100max-num-seqs保持64调大max-num-seqs chunked prefill约1750显存占用明显上升再叠加投机解码K5约2800草稿模型同源0.5B这套数据是在我环境下的离线基准测出来的输入长度512、输出长度256、1000条prompt。你可以看到连续批处理相关参数把吞吐拉高了约60%投机解码在此基础上又叠加了约60%。两个优化方向不冲突先把连续批处理吃到嘴里再谈投机。如果你想压测的是OpenAI兼容服务而不是离线脚本vLLM仓库里的benchmark_serving.py更合适python benchmark_serving.py \ --model Qwen/Qwen2.5-7B-Instruct \ --tokenizer Qwen/Qwen2.5-7B-Instruct \ --request-rate 10 \ --dataset sharegpt \ --num-prompts 500在线压测时除了看吞吐还要盯住TTFT首token延迟和TPOT每token延迟。优化吞吐的前提是不能无限牺牲延迟。开投机解码之后TPOT通常会下降因为一个验证步能吐出更多token但如果你把--max-num-seqs调得过大请求排队时间变长TTFT可能上升这在交互式场景里需要权衡。5. 开投机反而掉速排查链路与调参顺序5.1 现象配置都对但吞吐不升反降我见过不止一次有人严格按照文档开了投机解码压测出来比不开还慢。这时候不要慌按下面几条逐个排查。第一条是草稿模型词表不一致前面说过了token ID对不上接受率直接崩溃。第二条是采样参数太激进temperature偏高。第三条是显存不够导致swap或者preemption日志里如果出现preemption字样说明显存已经吃紧草稿模型挤占了主模型的空间主模型被频繁抢占性能自然崩。还有一类情况是目标模型本身太小比如你在跑3B、1.5B这种模型草稿模型很难比它“明显更快”投机解码的验证开销反而成了净负担。投机解码吃的是“大模型大而慢小模型小而快”的剪刀差如果剪刀差不够大这个方案就不成立。5.2 排查顺序从日志到消融实验我的习惯是先确认启动日志没有报错再确认草稿模型加载后显存有变化接着做消融实验。消融实验的方法很简单同一份压测数据依次跑“纯基线”、“只开连续批处理相关参数”、“只开投机解码”、“全开”。每次只加一个变量对比吞吐和延迟曲线。不需要复杂的监控系统vLLM自带的benchmark工具够了。如果在“只开投机解码”这一档就掉速直接换草稿模型或减小K。如果这一档正常全开反而掉速重点查显存上限和批处理参数。我还遇到过一种情况主模型和草稿模型都加载成功了但因为模型量化方式不兼容实际推理没走投机路径。这种问题在日志里通常有warning建议更新到较新版本再试。5.3 我推荐的调参顺序先拿稳收益再叠加不要一上来就把所有参数拉满否则出了问题你根本不知道是哪一步导致的。我推荐的顺序是先调连续批处理把--max-num-seqs从默认值往上涨配合--enable-chunked-prefill观察吞吐到哪一步停止增长、显存什么时候逼近上限。稳定之后开投机解码先选同源草稿模型--num-speculative-tokens从3开始跑出接受率和吞吐。逐步调K3、5、7各跑一轮选吞吐峰值对应的K。通常5够用超过7收益递减。最后再回头看延迟指标如果TTFT超标适当降低--max-num-seqs或者限制并发而不是直接关掉投机解码。还有两个细节容易被忽略。一是--gpu-memory-utilization留一点余量别把显存吃到100%否则草稿模型和主模型之间稍微波动一下就开始preemption。二是长prompt场景下投机解码只优化decode阶段对prefill没帮助甚至因为草稿模型占显存而拖累prefill。如果你主要处理超长输入连续批处理和prefix caching可能比投机解码更值得投入。5.4 别指望魔法什么场景收益有限说了这么多我得浇点冷水。投机解码不是万能的。你的输入如果以极长prompt为主那整体时间大头在prefilldecode阶段的加速再猛也拉不高总吞吐你的业务如果强制temperature很高接受率掉了收益自然缩水你的GPU如果本来就很空闲只跑一两个并发请求连续批处理连batch都凑不满投机解码的验证开销还会让单请求延迟略微升高。我个人的经验是这套组合拳最适合高并发、低temperature、输出长度相对稳定的服务化场景。先把这些条件对齐然后再去压榨每一个参数你会看到吞吐像挤牙膏一样一点一点往上走那种感觉比直接换卡有意思多了。