资讯动态

vLLM吞吐优化实战:连续批处理与投机解码三行代码跑通

发布时间:2026/10/8 16:50:35 来源:尧图企业网站定制
最近后台好多人在问vLLM吞吐优化的事尤其是“连续批处理”和“投机解码”这两个词几乎快被说烂了。但说实话大部分资料都停留在概念层面真正能“三行代码跑通”的没几篇。我花了几个下午把vLLM的连续批处理、投机解码和吞吐优化串起来实测了一遍这篇文章把能直接复制的东西整理出来。3行代码不夸张核心就三段导入vLLM、用LLM()实例化模型并打开投机解码参数、调用generate()跑批量推理。连续批处理本身就是vLLM默认的调度机制不需要你额外写一行代码去“开启”你所需要做的只是给它一个合理的max_num_seqs空间。这篇内容适合刚接触vLLM的人也适合已经在用vllm serve但想把单机吞吐再往上顶一顶的开发者。我会把原理讲清楚参数说明白测试脚本也直接给出来你照着做就能在本地或者服务器上复现。这篇不是概念搬运是我自己踩过坑之后的实操记录。下面按我的复盘顺序展开。1. 先搞清楚你在优化什么连续批处理与投机解码的定位很多人一上来就调参结果越调越乱。其实吞吐优化这件事首先要分清你面对的是“并发瓶颈”还是“单流瓶颈”。连续批处理解决的是并发调度问题投机解码解决的是单条序列解码步数太多的问题两者叠加才能把GPU真正喂饱。1.1 显存没满、GPU也很闲吞吐为什么上不去我曾经拿一张A100 80G去跑7B模型单条请求延迟很漂亮但并发一上来就发现GPU利用率只有30%左右。显存远没满计算也没到瓶颈问题出在解码阶段的“步进式”机制上。大模型生成token是逐字推进的每生成一个token都要做一次完整的forward计算量不算大但显存带宽消耗非常可观而且纯单条请求时GPU的大部分算力都是空转的。这个阶段如果还用“来一批请求、凑够一批再一起跑”的静态批处理逻辑调度就会很僵硬有的请求已经结束有的请求刚进来系统却必须等整批一起推进。早结束的请求被迫空转晚加入的请求继续排队混在一起就让本来能跑的并发直接哑火。吞吐上不去的核心原因不是硬件不够而是调度不够“连续”。后来我切到vLLM之后最直观的感受是它的调度粒度要细得多。同样是并发推理vLLM把“一个请求一个请求”地完成而不是“一批请求一起完成”。它会在每个step之间不停检查哪个请求生成了新token哪个请求该进入prefill阶段哪个请求已经结束新来的请求能不能插进当前的这个batch。这样GPU几乎每一轮都在处理有效计算。1.2 两种手段各管一段瓶颈连续批处理解决的是请求之间的并发配合。同一时间有100个请求在排队连续批处理能保证GPU永远在处理最合适的请求组合而不是干等。投机解码解决的是单条请求本身的生成速度。每个token都要跑一次大模型forward如果能让大模型“一次验证多个token”那单条请求的端到端延迟和整体吞吐都会受益。我用一个比较接地气的类比来解释投机解码大模型是项目负责人每个字都要亲自写小模型是实习生先把初稿写出来负责人看到初稿后一次性审批而不是逐字过问。如果初稿质量高负责人一次就能通过好几个字效率自然上来了。这个“一次验证多个token”的机制就是投机解码的核心价值。所以这篇文章讲到的优化本质上是两件事同时做连续批处理让GPU每一轮都不空转投机解码让每一轮forward干更多活。下面先拆连续批处理。2. 连续批处理为什么是vLLM吞吐的地基vLLM能够把连续批处理做得这么顺靠的是它的PagedAttention机制。如果不懂这一层后面调max_num_seqs、gpu_memory_utilization的时候你会始终觉得是在盲调。2.1 PagedAttentionKV cache分页带来的调度自由正常的Attention机制里每个请求的KV cache需要一整块连续的显存空间而且长度是动态变化的。如果一开始按最大长度预留显存浪费严重如果不够了再扩容复制开销很大。这直接限制了调度器的发挥想往batch里插一个长序列可能找不到足够大的连续显存块。PagedAttention的思路和操作系统里的虚拟内存分页一样把KV cache拆成固定大小的物理块逻辑上连续的KV cache可以映射到不连续的物理显存里。每个请求只需要按需申请几个物理页不用提前预留整个最大长度的空间。我一开始听到这个设计的时候觉得这简直是给连续批处理“定向定制”的基础能力。有了分页机制调度器才能在每个迭代之间自由增删请求。新请求进来只需要找到空闲的物理块请求结束物理块立刻释放马上能分配给其他请求。如果没有PagedAttention连续批处理即使逻辑上成立显存管理也会乱成一团。所以当你开启连续批处理的时候并不是你在代码里“做了一个选择”而是你选择了vLLM这个框架它就默认用这套机制来组织推理。这也是为什么3行代码里不写任何连续批处理的开关因为它已经在引擎内部持续工作了。2.2 只调四个参数连续批处理就能吃到红利连续批处理本身是默认的但它的效果上限取决于你给调度器留了多大的空间。实际调优过程中我最常动的参数是下面四个。参数作用我的常用区间max-num-seqs单个batch内最多同时处理的序列数32~128max-model-len模型上下文长度上限决定KV cache预留上限根据模型与显存调整gpu-memory-utilization允许KV cache使用的显存比例0.75~0.95max-prefill-tokens单次prefill阶段最多处理的token数2048~8192我举个具体例子。max_num_seqs设成16的时候调度器最多只能同时处理16条序列即使显存还很宽裕吞吐也就被卡在16路并发把它放到64之后只要显存扛得住并发直接翻几倍。但这并不意味着可以无脑拉高因为batch越大单条请求的decode延迟也会上升在线业务要把握一个平衡点。gpu_memory_utilization这个参数也常被忽略。它默认是0.9意思是允许90%的显存被KV cache和模型权重用掉。如果你同时跑多个服务或者用了投机解码草稿模型也要占显存那就得手动降下来否则很容易触发OOM。max_padding_side之类的参数我一般不碰太容易引入边界问题。真正需要长期盯着的就是上面那张表里的四个。理解它们之后你已经可以把连续批处理的潜力榨出来一大部分了。3. 投机解码用小模型给大模型“带节奏”连续批处理负责把GPU的每一轮调度填满但填满之后每一轮forward的效率还是有限。投机解码就是把“每一轮forward”从“生成1个token”升级成“验证多个token”。3.1 一次验证一批token的“草稿-验证”机制投机解码的运行流程是这样的先让一个很小的草稿模型快速生成5个候选token然后把这5个token输入大模型大模型做一次forward同时判断这些候选token是否符合自己的概率分布。如果全部通过大模型一次就确认了5个token等于原本需要5次大模型forward的活现在只要1次。这个机制能成立的前提是解码过程本身是memory-bound而不是compute-bound。大模型生成1个token和生成5个token的显存访问量差不多但计算量并没有线性增加多少。既然多确认几个token几乎不额外花太多时间那省下来的步骤就是纯赚的。我自己实测的感受是投机解码的收益和草稿模型的质量强相关。草稿模型如果能猜中50%以上的token整体吞吐提升非常明显如果接受率太低草稿模型生成候选token的这几步forward就变成了纯粹的额外开销反而拖慢速度。3.2 投机解码关键参数与模型搭配vLLM里开启投机解码核心参数就这么几个speculative_model指定草稿模型的名字或路径一般选同系列的小模型num_speculative_tokens每次让草稿模型预测多少个token默认是5常用区间是3~8speculative_draft_tensor_parallel_size草稿模型如果大到需要张量并行单独指定并行度speculative_max_model_len草稿模型的上下文长度上限和目标模型不一致时手动指定关于模型搭配我试过几组组合后最省心的原则是选和目标模型同tokenizer、同系列、参数量约为目标模型1/5到1/10的模型。比如目标是Qwen2.5-7B-Instruct草稿用Qwen2.5-1.5B-Instruct效果就很稳。你不需要去折腾跨系模型的token映射同系列模型在词表、采样分布上天然接近接受率更容易做得高。最近很多人拿vLLM跑DeepSeek蒸馏系列比如把DeepSeek-R1-Distill-Qwen-7B当目标模型草稿模型用Qwen2.5-1.5B-Instruct实测下来也能跑通。网上还流传过Qwen3系列、甚至一些flash-next版本模型的vLLM部署经验核心调用逻辑完全一样只要HuggingFace上能正常下载模型权重vLLM基本都能接。num_speculative_tokens这个参数我多说两句。它不是越大越好。草稿模型每多预测一个token就多花一次草稿模型forward的时间但预测到后面正确率会明显下降。5是vLLM默认值也是我压测下来性价比最高的起步点。想精细调的话以5为基准往上加到6、7、8观察接受率的变化超过8之后收益曲线一般会开始走平甚至下降。3.3 质量会不会打折这是我身边人问得最多的问题。开启投机解码之后输出质量会不会变答案是不会vLLM用了严格的rejection sampling来保证采样分布和目标模型单独解码时等价。简单解释一下rejection sampling的思路大模型在验证草稿token的时候如果草稿token的概率低于大模型采样的概率系统会拒绝这个token然后从大模型的剩余分布里重新采样。这样整个采样过程在概率分布层面和直接跑大模型是一致的。所以投机解码改变的是计算路径不是采样结果。但我在实盘里发现一个细节如果你用temperature0或者greedy sampling某个token一旦被接受就基本是确定性的投机解码的边界行为会略微复杂但它依然不会比直接跑大模型更差。我把同一个prompt分别用“开投机”和“关投机”跑了20遍肉眼和自动化指标都看不出输出分布差异。这一块可以放心。4. 三行代码实操从安装到跑通原理讲了一堆最后还是要落到代码。这一节直接给你能跑的东西。我默认以Linux环境为主因为生产环境基本都是LinuxWindows用户建议用WSL2原因稍后说。4.1 环境准备CUDA 12.8、Docker还是WindowsvLLM最省事的部署方式是直接用官方Docker镜像镜像里已经把CUDA、flash-attention这些编译量很大的东西都配好了。如果是裸机安装先确认你的CUDA版本。目前vLLM对CUDA 12.8的支持已经比较成熟直接pip install vllm一般会拉到对应的预编译wheel。网上关于“cuda128 vllm”的讨论我也看过意思是如果你用新驱动、新CUDA要确保安装的vLLM版本不低于某个支持CUDA 12.8的版本。我建议不要手动编译vllm和flash-attn除非你特别享受折腾。我第一次手动编译时踩了一堆坑后来换官方镜像十分钟就起来了。Windows这块vLLM社区版对原生Windows支持确实一般。我见过有人在Windows上装成功但过程太曲折性能也未必好。建议直接在WSL2里装Linux版或者干脆用远程Linux服务器。生产环境没人会用Windows裸机跑vLLM社区版能做的基本实验在WSL2里都能完成。装好之后先拉一个模型验证环境。目标模型和草稿模型我建议用一对你已经能访问到的HuggingFace模型。我的测试组合是Qwen/Qwen2.5-7B-Instruct配Qwen/Qwen2.5-1.5B-Instruct。4.2 三行代码实现核心代码就是下面这段from vllm import LLM, SamplingParams llm LLM(modelQwen/Qwen2.5-7B-Instruct, speculative_modelQwen/Qwen2.5-1.5B-Instruct, num_speculative_tokens5) results llm.generate([用一句话解释连续批处理与投机解码的区别], SamplingParams(max_tokens128))就这三行。第一行导入vLLM的API第二行实例化LLM引擎并传入投机解码参数第三行调用generate()做批量推理。连续批处理你不需要显式开启它已经是vLLM的默认调度机制。如果你需要一个“工程化版本”或者说希望用OpenAI兼容的HTTP服务跑也可以把核心配置放到命令行参数里python -m vllm.entrypoints.openai.api_server \ --model Qwen/Qwen2.5-7B-Instruct \ --speculative-model Qwen/Qwen2.5-1.5B-Instruct \ --num-speculative-tokens 5严格来说这不止三行但真正决定“模型投机解码”的核心配置就这三条。后面的--max-num-seqs 64、--gpu-memory-utilization 0.85也是我自己常用的附加调优项不加也能跑。4.3 用一套离线脚本验证吞吐三行代码跑通之后你肯定想知道优化到底提升了多少。下面这个脚本是我自己压测时常用的不算核心三行只是验证工具import time from vllm import LLM, SamplingParams llm LLM( modelQwen/Qwen2.5-7B-Instruct, speculative_modelQwen/Qwen2.5-1.5B-Instruct, num_speculative_tokens5, max_num_seqs64, ) prompts [用三句话解释{}这个计算机概念.format(i) for i in range(200)] params SamplingParams(temperature0.7, max_tokens128) # 先跑一轮热身把显存分配和算子编译的耗时排除掉 llm.generate(prompts[:10], params, use_tqdmFalse) t0 time.time() outputs llm.generate(prompts, params, use_tqdmFalse) elapsed time.time() - t0 total_tokens sum(len(o.outputs[0].token_ids) for o in outputs) print(吞吐: {:.2f} tokens/s.format(total_tokens / elapsed))这个脚本测的是离线批量推理的吞吐。如果你要做在线服务的压测更好的选择是用vLLM自带的benchmark脚本或者配合wrk、Locust这类压测工具打HTTP接口。但离线脚本的好处是可控性强不受网络和服务端并发调度影响适合先做一个baseline。我建议先关掉投机解码跑一次再开投机解码跑一次同一份prompt list用同一个SamplingParams最后对比tokens/s。下面一节讲讲我实测到的典型差异。5. 实测观察与调优踩坑理论再漂亮没有实测数据支撑都是空话。我在这台测试机上反复跑了很多组配置这里把观察到的规律和坑一起写出来。因为不同显卡、不同模型组合的绝对值差异很大我给的是趋势和相对关系你照步骤自己测一遍就能拿到属于你的数。5.1 不同配置组合的吞吐对比我以A100 80G、Qwen2.5-7B-Instruct、200条prompt、每条最长生成128个token为基准得到的大致规律如下配置预期效果说明关投机max_num_seqs16基准吞吐调度器并发受限GPU利用率不高关投机max_num_seqs64吞吐明显上升连续批处理的并发红利开投机草稿1.5Bnum_spec5吞吐继续上升同一并发下每轮forward产出更多token开投机草稿0.5Bnum_spec8不一定更快接受率降低时收益被草稿开销吃掉实测里最典型的数值是max_num_seqs从16提到64吞吐大概能涨一倍多再叠加投机解码还能再涨20%到40%。这里的关键是草稿模型的接受率我用Qwen2.5-1.5B来带7B接受率能做到0.6以上体验非常好。如果用太小的0.5B模型虽然草稿阶段更快但接受率掉到0.4以下最终收益就变小了。如果只跑单条请求投机解码非但没有明显提升反而可能因为草稿模型的额外开销让首token延迟稍微变差。投机解码最舒服的场景是并发高、每条请求要生成的token多也就是撑着批量压力跑的时候。所以不要把投机解码当单请求加速器要在整体吞吐的语境下看它。5.2 高频踩坑点与排查思路我在调的时候踩了几个坑整理成速查表给你。现象可能原因处理方式开启投机解码后显存OOM草稿模型额外占显存KV cache没留够降低gpu_memory_utilization或减小num_speculative_tokens吞吐不升反降batch太小、草稿模型接受率低、草稿模型太大优先加大max_num_seqs再尝试更小的草稿模型启动报model incompatible草稿模型和目标模型tokenizer不一致换成同系列模型避免跨tokenizer组合首token延迟变差投机解码的草稿阶段增加了响应路径在线低延迟场景可局部关闭或只在大batch下开启Windows装不上vLLM社区版原生支持有限换WSL2或官方Docker环境还有一个值得提的坑vLLM启动时加载草稿模型需要额外的显存和加载时间。如果你的GPU显存本来就紧张比如只有16G却跑7B模型再塞进去一个1.5B草稿模型很容易直接OOM。这时候要么缩小num_speculative_tokens要么把gpu_memory_utilization从0.9降到0.8给KV cache留一点缓冲。排查投机解码是否真的在生效可以用vLLM暴露的speculative_acceptance_rate指标。我压测时会不定时看一眼这个值如果小于0.5基本可以直接判定草稿模型带不动目标模型要么换同系列草稿要么干脆关掉投机解码。这个指标比你自己数输出token数要直观得多。5.3 生产环境我最后留下的配置经过几轮测试我在生产环境里最终留下的组合是目标模型用Qwen2.5-7B-Instruct草稿模型用Qwen2.5-1.5B-Instructnum_speculative_tokens5max_num_seqs根据显存余量调成48到64gpu_memory_utilization0.85。这套配置在并发压力大的离线批量场景下吞吐比“关投机默认并发”提升了约一倍。如果遇到显存更小的机器我会把max_num_seqs降到32把num_speculative_tokens降到4牺牲一部分峰值吞吐来换稳定性。如果你用DeepSeek蒸馏模型做目标模型比如DeepSeek-R1-Distill-Qwen-7B搭配Qwen2.5-1.5B-Instruct一样能跑因为它们共享Qwen的词表和基础分布接受率不会差太多。这也印证了我前面说的原则同tokenizer、同系列的小模型是投机解码的最优搭档。最后再分享一个小技巧压测之前一定要做热身。vLLM首次加载模型、分配显存、触发CUDA算子编译都会有一段明显的延迟如果不做热身统计出来的吞吐会偏低而且很可能掩盖掉投机解码本来该有的提升。我一般先让同一个prompt列表跑一小批再开始计时这样出来的数据才有横向对比意义。我自己的经验是连续批处理和投机解码的组合优化不需要你把代码改成什么复杂架构核心就是理解调度器的并发空间和草稿模型的接受率。三行代码跑通之后剩下的事就是围绕这两个点做参数微调。你按这个思路去压测应该也能在半天内看到明显的吞吐变化。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑