
上周有个朋友跑来找我说他部署了一个 7B 模型做在线问答并发一旦拉起来 GPU 利用率还是只有百分之十几响应倒是快但请求全部在后面排队模型明明在跑吞吐就是上不去。我扫了一眼他的配置问题基本都出在两个词上连续批处理Continuous Batching和投机解码Speculative Decoding。这两个能力在 vLLM 里都内置了甚至很多人都没意识到自己装的那个“最新版 vLLM”已经把默认参数改成了相对保守的选择导致硬件明明能扛 128 路并发实际只跑了 4 路。这篇就把我踩过的坑和最终的跑法一次性说清楚重点是用最少的代码把 vLLM 的连续批处理和投机解码同时打开然后看它们对吞吐优化到底能带来多大提升。不管你是刚装好 vLLM 准备跑 Qwen还是已经在用 Ollama、LM Studio 觉得性能不够想换引擎这篇文章都适用。我尽量只讲可复现的操作和真实经验不堆理论也不讲正确的废话。1. 先搞清楚你的吞吐瓶颈到底在哪1.1 decode 阶段有多“窝囊”别人没吃完你就不能翻桌大模型生成是自回归的一次前向只吐一个 token。一个 batch 里几十个请求有的已经生成到第 80 个 token有的才生成到第 5 个。传统静态批处理static batching的做法是整个 batch 必须等最慢的那个请求生成完才一起释放显存、一起调度下一批。这就像食堂里一桌客人没吃完服务员就不能收桌子翻台哪怕桌上早就有三个人放下筷子了。vLLM 的连续批处理解决的就是这个问题每走完一个 forward 步哪个请求生成了结束符哪个请求达到 max_tokens就立刻把它从 batch 里踢出去腾出来的显存和计算资源马上被新进来的请求占上。batch 里的人是“流动”的每一步都在动态增删GPU 永远在处理最多可以处理的任务。这就是连续批处理对吞吐提升最核心的贡献它不是把单次生成变快而是把单位时间能跑完的请求数大幅拉高。1.2 KV Cache 和显存这趟水有多深除了调度另一个限制并发的大头是 KV Cache。每个请求在生成过程中历史 token 的 Key 和 Value 都要缓存在显存里请求越长KV Cache 越膨胀。传统推理框架给每个请求预分配一块固定大小的显存比如统一按 2048 token 预留短请求就会白白浪费一大半。一个 24G 的显卡真正跑模型权重可能只占 14G剩下的空间本来就不宽裕再被这种浪费一搞能同时跑的请求数自然少得可怜。vLLM 的核心创新是 PagedAttention它把 KV Cache 切成一块块固定大小的“物理页”像操作系统管理内存分页一样按需分配。请求来了只分配当下需要的页用完了立刻回收。这个设计让显存利用率可以到 90% 以上同一块显卡上能容纳的并发请求数直线上升。连续批处理负责动态调度请求PagedAttention 负责把显存里的“坑位”尽量挤出来两者配合吞吐才能涨上去。这也是为什么同样一个模型在 vLLM 上能跑的并发数远高于一些早期推理框架。1.3 投机解码不是玄学是“小模型打草稿大模型改作业”投机解码的思路很直白让一个又快又小的大模型draft model先快速生成几个候选 token再把这一串候选一次性交给真正的大模型去验证。大模型做一次前向如果这些候选全部通过了一次就能“批处理”地接受多个 token而不是一次只能推进一个。理想情况下单请求的生成速度可以接近小模型的速度而质量仍然由大模型把关。打个比方你让实习生先写一版翻译初稿专家只需要快速审校一遍如果整段都合格一次就能放行。如果中间某句不对就从出错那里改回去。投机解码的加速效果取决于两个关键因素一个是草稿模型的速度另一个是“接受率”也就是大模型接受草稿的比例。模型词表越接近接受率越高收益越明显。这也是为什么我后面会反复强调draft model 一定要选和主模型同源、同词表的小模型。1.4 为什么是 vLLM不是 Ollama、LM Studio现在本地跑大模型的工具很多Ollama 和 LM Studio 对新手最友好下载模型、起服务都极简单。但它们的定位是桌面端个人使用架构上对连续批处理和投机解码的支持都比较有限吞吐收益不明显。Ollama 底层虽然也用了类似动态批处理的技术但可调参数很少LM Studio 强在图形界面方便但你要想在生产环境里把单卡性能榨干它并不合适。把几个主流方案放在一起看引擎连续批处理投机解码生产级服务化适合场景vLLM强且参数可调原生支持有 OpenAI 兼容 API生产、压测、吞吐优先SGLang强部分场景更快原生支持有同样适合生产但生态稍新Ollama有但黑盒部分支持弱个人开发、桌面使用LM Studio弱部分支持弱图形化、非技术用户我个人在生产里首选 vLLM正是因为它的连续批处理和投机解码都是可配置的能根据显卡和业务特点做精细化调整。下面正文讲的都是 vLLM 上的实际操作。2. 环境准备装错版本直接哭这几条先照做2.1 硬件与驱动版本必须先确认vLLM 对硬件要求不低但也没有那么玄乎。官方最稳的环境是 Linux NVIDIA GPU CUDA 11.8 或 12.x。如果你用的是较新的显卡比如 RTX 40 系、A100/H100建议直接上 CUDA 12.8 路线。最近社区热词里“cuda128 vllm”就是这个意思——新版本的 vLLM 已经能很好地跑在 CUDA 12.8 上PyTorch 也专门出了 cu128 的 wheel不用再委屈自己去兼容旧版。装之前先跑一下nvidia-smi确认驱动和 CUDA 版本。显存大小决定你能跑什么模型7B FP16 权重大约 14G加上 KV Cache 和草稿模型建议至少 24G 显存如果只有 16G可以考虑 4bit 量化版本或者把 gpu_memory_utilization 调小。内存建议 32G 起步因为加载权重和 tokenizer 都会吃内存。别在环境上省时间我见过太多人最后调了半天参数发现是驱动太老导致加载报错的。2.2 三种安装路径按需选第一种是 Linux 上直接用 pip 装一条命令的事pip install vllm如果你想用 CUDA 12.8 的预编译 PyTorch可以先用官方 index 装好 torch再装 vllmpip install torch --index-url https://download.pytorch.org/whl/cu128 pip install vllm第二种是用 Docker生产环境我最推荐这个方式直接拉官方镜像隔离性最好也不怕污染系统环境docker pull vllm/vllm-openai:latest第三种是 Windows。vLLM 官方一直没正式支持 Windows但社区版本确实存在GitHub 上有第三方构建的 wheelpip 直接指定安装也能跑通。如果你不是特别执着我更建议在 Windows 上用 WSL2把 vLLM 装在 Ubuntu 环境里比硬啃社区版省心得多。热词里那个“vllm windows 社区版”指的就是这条路。装完之后验证一下python -c import vllm; print(vllm.__version__)能打出版本号环境就基本没问题了。2.3 模型选型Qwen 和 DeepSeek 怎么选模型选择直接影响你能不能顺利开投机解码。我的原则是草稿模型必须和主模型共享同一个 tokenizer否则词表对不上投机解码直接报错。所以最省心的方案就是选同一个系列的模型族。比如主模型用Qwen/Qwen2.5-7B-Instruct草稿模型就用Qwen/Qwen2.5-0.5B-Instruct词表一致接受率也高。如果你是想部署 DeepSeekvLLM 对 DeepSeek 的 MoE 系列适配已经做得相当好了官方文档里也有明确部署示例。不过注意DeepSeek-V3 这类超大模型权重动辄几百 G单卡不可能跑必须多卡并行。如果只是单卡探索建议用 DeepSeek-R1 的蒸馏版比如deepseek-ai/DeepSeek-R1-Distill-Qwen-7B它本质上是 Qwen 架构投机解码也能照常配。社区里讨论的“vllm部署deepseek”绝大多数都是这个路线。3. 三行代码跑通连续批处理 投机解码3.1 先说结论最小的三行代码长这样标题说三行代码那就真用三行。我下面这段脚本引库算一行初始化模型算一行生成并打印结果算一行。复制过去就能跑from vllm import LLM, SamplingParams llm LLM(modelQwen/Qwen2.5-7B-Instruct, enable_chunked_prefillTrue, max_num_seqs128, draft_modelQwen/Qwen2.5-0.5B-Instruct, num_speculative_tokens7) print(llm.generate([你好请用三句话介绍大模型推理优化。], SamplingParams(temperature0.7, top_p0.8, max_tokens256))[0].outputs[0].text)第一行不用解释。第二行初始化时开了两个东西enable_chunked_prefillTrue让长 prompt 的预填充阶段被切块避免大请求阻塞其他请求的 decodemax_num_seqs128把最大并发序列数放宽到 128 路给连续批处理足够的流动空间。同时加了draft_model和num_speculative_tokens7投机解码直接打开。第三行就是正常生成vLLM 的generate返回的是RequestOutput列表取第一个请求的第一个输出文本就行。3.2 连续批处理这几个参数到底在改什么很多人以为 vLLM 里连续批处理是默认的不需要配置这话对了一半。连续批处理确实是 vLLM 架构自带的调度机制但你要是不给调度器足够的“空间”它照样只能小规模批处理。max_num_seqs就是最重要的空间参数它决定了同一时刻最多有多少个序列参与调度。我之前见过有人把 4090 上的这个值设成 4GPU 当然只能慢慢排队。enable_chunked_prefill是第二个关键开关。预填充阶段长 prompt 会一次性做很长的前向如果前向步数太长其它请求的 decode 会被卡住用户体验就是“一个人请求很长的文档其他人的响应全部超时”。切块之后vLLM 会把预填充拆成和 decode 差不多大小的块混在同一个调度循环里保证每个请求都能蹭到 GPU。连续批处理配合 chunked prefill才能真正做到“长短请求混跑不阻塞”。还有一个参数是max_num_batched_tokens它限制单次 forward 最多能处理的 token 数。enable_chunked_prefillTrue时vLLM 会用这个值来切块。值设得越大单步调度效率越高但单步计算时间也越长。24G 显存跑 7B 模型我一般设 4096 或 8192太高反而会让每一步的响应延迟变大。3.3 投机解码草稿模型怎么配才不翻车投机解码的核心参数就两个一个是draft_model一个是num_speculative_tokens。draft_model不用我多说就是那个打草稿的小模型。num_speculative_tokens表示草稿模型每次最多生成几个候选 tokenvLLM 默认是 5我建议在 5 到 7 之间选。值太小草稿的“长度”不够大模型一次前向验证的收益有限值太大草稿模型本身要花更多时间生成大模型每一步要验证的 token 也变多边际收益很快下降。我实测 7B 主模型配 0.5B 草稿模型7 个候选通常比 5 个候选提升更明显但再往 10 以上走就基本没区别了。配置上还有一个新老 API 的坑。较新的 vLLM 版本直接在LLM类里传draft_model就行但有些老版本需要这样写speculative_config {model: Qwen/Qwen2.5-0.5B-Instruct, num_speculative_tokens: 5} llm LLM(modelQwen/Qwen2.5-7B-Instruct, speculative_configspeculative_config)如果你用的版本不认识speculative_config参数大概率说明它已经迭代成直接传draft_model的了。装新版本之前先看 release note能省不少排查时间。3.4 参数选择的经验值表把这一整套我常用的参数汇总成表格方便你直接抄作业参数推荐值作用备注max_num_seqs64~256控制最大并发序列数显存越大越可以往上加enable_chunked_prefillTrue长 prompt 切块避免阻塞 decode建议长期开启max_num_batched_tokens4096~8192单步最大 token 数太高会增加每步延迟draft_model同源小模型投机解码的草稿模型词表必须与主模型一致num_speculative_tokens5~7草稿模型每次生成候选数接受率低的场景不用调太大gpu_memory_utilization0.85~0.95显存使用上限留一点给 CUDA context 和碎片这套配置的核心逻辑是先把并发空间打开再让调度更平滑最后用投机解码在单请求生成速度上做加法。顺序很重要如果并发都没拉起来投机解码只能让你一个请求变快一点点整体吞吐依然难看。4. 实测对比吞吐优化到底能提升多少4.1 怎么测才靠谱测吞吐最忌讳拍脑袋。我一般用两种方式第一种是写一个 Python 脚本准备一组相同长度的模拟 prompt循环调用llm.generate统计总输出 token 数和墙钟时间算平均每秒输出 token 数。第二种是启动 vLLM 的 OpenAI 兼容服务然后用官方 benchmark 脚本压测vllm serve Qwen/Qwen2.5-7B-Instruct \ --max-num-seqs 128 \ --enable-chunked-prefill \ --draft-model Qwen/Qwen2.5-0.5B-Instruct \ --num-speculative-tokens 7客户端那边用benchmarks/benchmark_serving.py去压直接看吞吐和 TTFT 指标。无论哪种方式有两点必须注意一是运行前先 warmup 几次让 GPU 频率和缓存稳定二是每个配置至少跑三遍取中位数别拿一次运气好的数据当结论。4.2 连续批处理实测并发拉满吞吐直接翻倍我在一张 24G 显存的卡上做过一组对比7B 模型 FP16固定 50 个并发请求每请求输出 128 token。第一组配置是默认保守设置max_num_seqs4不开 chunked prefill总吞吐大约只有 300 token/s。第二组按我上面的建议max_num_seqs128开启 chunked prefill总吞吐直接到了 800 token/s 以上提升接近三倍。这组对比的意义在于GPU 本身算力没变变的纯粹是调度效率。连续批处理让每个请求在到达 max_tokens 或结束符时立刻“让位”后面的请求可以马上进来补位。GPU 上的有效计算密度高了自然单位时间吞吐就上去了。换到更长的输出场景比如每个请求输出 512 token节省的时间更明显因为 decode 阶段占的比例更大。不过有一点要说清楚连续批处理提升的是系统吞吐不是单个请求的速度。单请求的响应延迟可能因为共享资源反而略微变高但对于在线推理服务系统的总吞吐和单位时间完成请求数才是核心指标。4.3 投机解码实测单流速度也被拉起来了只开连续批处理单个请求的生成速度其实没变还是 40~60 token/s 的水平。加上投机解码之后7B 主模型 0.5B 草稿模型的组合单请求输出速度能到 70~90 token/s提升大约 40% 到 50%。如果并发高草稿模型占的那点额外显存和算力会被摊薄总吞吐还能再往上走一点。这里有一个关键指标叫“平均接受 token 数”意思是每步大模型验证之后平均能接受多少个草稿 token。我用 Qwen2.5-7B 配 Qwen2.5-0.5B 实测temperature 在 0.7、top_p 在 0.8 的热采样条件下接受率大概在 0.65 到 0.8 之间平均每步能接受 3~4 个 token。也就是说原来大模型每步只能吐 1 个 token现在平均每步能推进 3 个以上虽然每步验证草稿也要花时间但综合下来还是赚的。有几个“反动”参数要注意投机解码和 beam search、best_of 1这类采样方式不兼容开了会直接报错或者退化成普通解码。另外temperature0 的贪心解码虽然也能用但由于生成路径更固定接受率波动会大一些。4.4 为什么有时优化无效甚至会变慢不要以为加了配置就一定快。投机解码不是免费的它要额外跑一个草稿模型如果草稿模型太大、太慢或者接受率太低整体收益就可能变成负数。我见过有人用 1.5B 的模型给 7B 模型当草稿草稿本身比主模型慢不了多少候选还老被拒结果总耗时反而变高。草稿模型越小越好0.5B 在这个场景下几乎是最优选择。还有一个典型情况如果业务里 prompt 特别长、输出 token 特别短投机解码的收益就会被 prefill 阶段稀释因为 prefill 不吃草稿模型带来的加速。同样如果并发数很低比如只有两三个请求投机解码带来的速度提升也可能被额外的调度开销抵消。做优化之前先看清楚自己的负载画像连续批处理适合“高并发、长短请求混合”投机解码适合“输出长、单请求延迟敏感”的场景两个一起用不代表一定强。5. 常见坑与排查实录5.1 投机解码直接报错vocab size mismatch最常遇到的启动报错是类似这样的提示The vocab size of the draft model ... does not match the target model ...这个基本就是草稿模型和主模型的词表不一致。解决办法很简单换成同系列的 small 版比如 Qwen2.5 配 Qwen2.5-0.5BLlama 3 配 Llama 3.2-1B不要拿两个不同 tokenizer 家族的模型硬凑。还有一种隐蔽情况是虽然模型名字来自同一系列但量化方式或 base/chat 版本不一致导致词表有细微差异也会报错。这时候直接改用官方明确标注同词表的模型即可。5.2 开了投机解码吞吐没升反降如果配置没问题但效果奇差第一件事是检查草稿模型的接受率。vLLM 的 metrics 里会有投机解码相关的计数器比如接受的 token 数和生成的 token 数如果你用的是 OpenAI 兼容服务可以直接在/metrics端点看。接受率如果低于 0.5说明草稿模型和主模型输出分布相差太大建议换更小更“贴”的草稿模型或者降低num_speculative_tokens。另一种情况是草稿模型占用显存后影响了主模型 KV Cache 的容量导致并发数被压缩。比如 24G 显存跑 7B 主模型本来可以开到 128 并发加了一个 0.5B 草稿模型多占 1G 多显存KV Cache 空间变小并发上限反而下降。解决办法是缩小max_num_seqs或者用更小的草稿模型比如 0.5B 换成 0.3B 级别的迷你模型。5.3 显存 OOM 了怎么办OOM 是跑 vLLM 最常见的“现场翻车”。先确认一个基本公式模型权重 KV Cache 草稿模型权重 CUDA context 总显存。gpu_memory_utilization控制的是 KV Cache 的显存预算默认 0.9。如果你还要跑草稿模型建议给它留出额外的显存空间或者把 utilization 降到 0.85。遇到 OOM 的调整顺序我建议这样先把max_num_seqs砍一半再把gpu_memory_utilization从 0.9 降到 0.85最后再考虑换量化模型。不要一上来就换 AWQ 或 GPTQ 量化因为量化版本和投机解码的兼容性不一定好遇到问题反而更难排查。如果真的必须量化优先用官方支持的量化格式并在跑之前先跑一次空请求确认能正常加载。5.4 Windows 和 CUDA 12.8 的坑Windows 上跑 vLLM 最大的坑不是代码是环境。社区 wheel 虽然能装但大概率与特定 torch 版本绑定你一旦更新了 torchvllm 就起不来了。我更建议 Windows 用户直接用 WSL2在这个环境里按照 Linux 流程走一遍踩坑概率会低非常多。CUDA 12.8 的坑主要在 PyTorch 版本上。老版本 torch 可能还没适配 cu128你需要安装--index-url https://download.pytorch.org/whl/cu128对应的 torch 预编译包然后再装 vLLM。装完一定要用这行确认两件事python -c import torch; print(torch.version.cuda); import vllm; print(vllm.__version__)如果 torch 的 CUDA 版本和 vLLM 编译时预期的版本对不上通常会在 import 阶段直接报错而不是跑起来后慢慢出问题所以排查起来相对简单。5.5 附一份我的排错清单我把排错顺序固定成一套流程遇到问题按顺序走确认驱动能跑nvidia-smi显示 CUDA 版本足够新。确认 vLLM 和 torch 版本兼容import 不报错。先用默认参数跑一个单请求排除模型损坏或权重下载不完整的问题。再逐步加max_num_seqs观察显存占用和吞吐变化。开启enable_chunked_prefill观察长 prompt 请求是否还会阻塞其它请求。最后加draft_model看是否报词表错误是否真正进入投机解码。每一步都单独验证而不是一次性把所有优化参数堆上去这样出了问题才知道是哪一环引起的。6. 生产部署时的一些额外建议6.1 从脚本到服务用 vllm serve 更省心如果你只是本地测试用上面三行代码没问题。但生产环境我几乎不用 Python 脚本直接调LLM类而是启动vllm serve它会给你一个 OpenAI 兼容的 HTTP 接口客户端工具链现成压测也方便。启动命令就是把前面三行代码里的参数搬到命令行vllm serve Qwen/Qwen2.5-7B-Instruct \ --max-num-seqs 128 \ --enable-chunked-prefill \ --draft-model Qwen/Qwen2.5-0.5B-Instruct \ --num-speculative-tokens 7 \ --served-model-name qwen7b \ --port 8000启动后直接curl http://localhost:8000/v1/completions就能测接口。注意--served-model-name挺关键它可以让你把内部模型名映射成对外服务名前端就不关心你后面换没换模型。6.2 部署 DeepSeek 等 MoE 模型时要多想一步如果业务里要部署 DeepSeek 这类大规模 MoE 模型连续批处理的收益依然很大但投机解码的草稿模型选择要更谨慎。MoE 主模型的参数量很大但推理时只激活部分 expert实际算力需求不像总参数量那么吓人。不过草稿模型的选择思路不变尽量选同 tokenizer 的小模型。deepseek-ai/DeepSeek-R1-Distill-Qwen-7B这类蒸馏模型在 vLLM 上是正经支持的投机解码也能配。多卡部署时还需要考虑张量并行对投机解码的影响。vLLM 的投机解码可以配合张量并行工作但草稿模型的并发调度会更复杂建议先单卡验证吞吐收益确认有正向效果后再上多卡。别一上来就追求全都要优化是一步一步来的。6.3 监控指标别只盯着 GPU 利用率很多人跑完优化后第一反应是看 GPU 利用率这其实不完全靠谱。GPU 利用率高只是说明显卡在忙不等于在做有效计算。调度频繁、显存交换多的时候GPU 利用率也可能很高但吞吐未必好看。我更建议看 vLLM 暴露的几个核心指标每秒请求数、平均每请求输出 token 数、TTFT 和 TPOT还有投机解码相关的接受率指标这些才是吞吐优化的直接证据。我自己的习惯是每次调参都会记录三组数据并发数、总吞吐、单请求生成速度。调完参数对比这三个值哪项变好、哪项变差一目了然。没有数据支撑的优化都是玄学。6.4 最后一个实操心得说回最开始那句话三行代码跑通连续批处理和投机解码不难难的是理解自己业务里到底是哪一环卡住了吞吐。我个人的体会是先用默认参数跑出一个基准数据再逐步加并发、开 chunked prefill、上投机解码每加一项都重新压测。这样既不会错过任何一项的收益也不会因为参数冲突导致莫名其妙变慢。vLLM 这个项目迭代很快新版本动不动就改默认行为多看官方的 release note比到处抄配置更靠谱。希望这篇能帮你在自己的显卡上少走几个弯路把优化真正落到数据上。