vLLM优化配置实战:从KV cache到吞吐延迟调优

发布时间:2026/10/21 12:23:21
vLLM优化配置实战:从KV cache到吞吐延迟调优
1. 为什么vLLM的优化配置值得逐行研究我最早接触vLLM时和大多数人一样第一件事就是跑一个默认参数的推理服务能出结果就万事大吉。真正让我开始认真研究docs/configuration/optimization.md这份文档的契机是一次实测同样的模型、同样的并发压力默认配置下P99延迟直接翻出两倍多而吞吐量还低了一截。当时我以为是机器问题后来才发现是几个默认开关和资源比例没有按场景调整。先说结论vLLM在高并发、长上下文、流式输出的场景下优势非常明显但这些优势全部建立在“配置正确”的前提上。optimization.md对应的就是vLLM服务端的性能与显存优化参数包括KV cache管理、预填充策略、批处理调度、量化推理后端等。这份文档解决的核心问题只有一个在显存有限、并发不确定、模型大小不一的情况下怎么把每一张卡的性能压榨出来同时不让服务质量崩掉。适合读这份文档的人大概有三类第一类是做大模型服务的后端工程师服务上线前要确定并发上限和显存规划第二类是负责压测和容量评估的SRE需要知道哪些参数会影响延迟分布第三类是自己在单卡或双卡上跑开源模型的开发者想在不买新卡的前提下提升吞吐。不管你是哪一类建议按本文的顺序把配置项捋一遍而不是看到哪个参数像就调哪个。vLLM的优化文档有个特点它不像框架的普通接口文档那样只罗列参数名和默认值它更接近一份“性能调优手册”很多参数之间是相互影响的。比如你改了gpu_memory_utilization会影响KV cache可用空间KV cache空间变了max_num_batched_tokens的合理取值也会变这两个变了实际能支撑的并发又不同。所以单独看任何一个参数都无法真正理解文档要看它们构成的系统。2. 吞吐、延迟与显存三个目标背后的配置博弈2.1 先弄清优化到底在优化什么vLLM的优化目标通常分成三块吞吐量throughput、TTFTTime to First Token首Token延迟、TPOT/ITL每个输出Token的延迟或Token间延迟。这三者在资源占用上是互相挤占的。optimization.md里的每一项配置本质都是在调整这三者之间的天平。默认情况下vLLM倾向于用连续批处理来提升吞吐也就是让多个请求在推理引擎的调度中穿插执行。每次调度时引擎会把当前所有等待中的序列拼进一个batch然后并行算完当前步。这个设计对GPU利用率非常友好但代价是单请求的延迟会随着并发上升。你看到“并发高但首Token很慢”的时候先别急着怀疑网络大概率是调度与KV cache参数没配合好。理解这个博弈是理解整份优化文档的前提。后面所有配置你都可以回到这个三角关系里问自己我现在是要压低延迟还是要拉高吞吐还是要在显存受限时保住更多请求答案不同参数走向完全不同。2.2 显存是怎么被瓜分的GPU显存里同时躺着三样东西模型权重、KV cache、推理过程中的中间激活值。optimization.md中反复出现的gpu_memory_utilization就是用来控制“前两者”总占用上限的。默认值通常是0.9意思是允许vLLM拿90%的显存其中扣掉模型权重后剩下的都自动分配给KV cache。这里有个新手常踩的坑把gpu_memory_utilization提到0.95甚至0.98以为能多塞点缓存。实际情况是如果你的模型权重本身就占了很大比例剩下的空间可能不足以支撑vLLM需要的最小KV cache服务会直接启动失败报一个类似于“Not enough memory”的错误。所以在调这个参数前先确认你的模型权重占据了多少显存留出至少10%-15%的余量给CUDA context、推理框架运行时和临时激活值。另外显存分配还会受到max_model_len的影响。KV cache按“最大序列长度”做预留如果你设置了一个非常大的max_model_len引擎必须按这个上限预分配KV cache块。哪怕实际请求都是短文本显存也已经按长序列预留了。反过来把max_model_len设置为接近你业务的真实上限能立刻释放大量显存用于并发。2.3 KV cache块大小和高并发的关系vLLM的KV cache不是一次性给每个请求分整段显存的而是以block为单位按需分配。block大小由block_size决定文档里通常建议保持默认。这个设计的好处是显存利用率高序列长短不一也不会互相浪费坏处是block太小会让管理开销变大block太大则长序列增长时可能有一整块只用了很小一部分。在实际压测中我发现一个规律并发请求越长、块数越多block_size对性能的影响越明显。短文本场景下block_size调大调小区别不大长文本、高并发的场景稍微偏小的block能减少末尾浪费但会让调度稍微多花点时间。这个参数一般保持默认就好不建议业务侧随意改。还有一个容易被忽略的关联参数是num_gpu_blocks_util类的动态信息它会告诉你在运行中KV cache block的占用比例。如果接近90%以上说明系统已经接近容量上限继续加并发会出现排队或拒绝。此时与其调gpu_memory_utilization不如先压max_model_len或换量化权重因为block耗尽意味着物理空间确实不够了。3. 连续批处理与预填充拆分并发吞吐的两个核心玩法3.1 连续批处理不是简单的“多请求排队”很多人以为vLLM的高吞吐来自“把多个请求塞进一个batch”其实没那么简单。真正关键的是连续批处理continuous batching每个请求内部的解码步不是等整批全部跑完再进入下一步而是任何一个序列只要准备进入下一步就可以被调度器放进正在执行的batch里。这样新请求能及时插入、已完成请求能立刻让出位置GPU始终在算而不是在等。vLLM默认是启用连续批处理的你不需要专门开一个开关。需要你操心的是max_num_batched_tokens或对应版本里的max_num_seqs。前者限制了一次调度中参与计算的Token总量后者限制了batch里的最大序列数。这个上限决定了单次前向传播中KV cache的访问跨度。跨度越大显存带宽压力越大但吞吐上限也越高。如果你的业务是大量短请求比如对话助手建议让max_num_seqs高一些从而允许更多并发请求同时进入调度。如果业务是长文档总结、代码生成这类单请求长序列则更适合调高max_num_batched_tokens让一个长序列的多个Token在单次前向中被批量处理而不是被短请求频繁打断。3.2 Chunked Prefill把长提示词的预填充切成小块长提示词请求到达时vLLM会先进入预填充阶段把整个提示词算一遍生成KV cache然后才进入解码阶段。传统方式是“一次性全部算完”这个阶段GPU算力用得很猛但显存带宽用不上而且整个阶段内该请求不会产出任何可读的TokenTTFT会被拉得很长。Chunked prefill把预填充切成多个小段允许预填充和解码阶段交错执行。直观效果是一个长提示词的请求进来了它不需要独占GPU跑完预填充引擎可以先算其中一段穿插几个其他请求的解码步再回来继续算下一段。这样单个长请求的TTFT没有减少但整个batch的吞吐上去了其他短请求也不会被长请求“堵死”。这就是optimization.md里chunked_prefill相关参数的用意。如果你的场景里存在“少数超长提示词”和“大量短请求”混合这个功能几乎是必需品。默认情况下新版vLLM已经默认启用或建议启用但你需要关注它的配合参数chunk大小决定了每次预填充最多占用的Token/序列比例。切得越小调度越灵活但增加了计算次数切得越大长请求的TTFT越短但可能阻塞其他请求。我建议的实测路径是先保持默认构造一组混合负载然后逐步缩小 chunk size观察吞吐变化。如果吞吐没有明显提升就说明长请求占比还不够高没必要继续切小。3.3 调度器与队列长度并发上限其实是你自己设的optimization.md里还有一个值得注意的点是请求调度策略和队列上限。vLLM默认根据显存和配置计算能容纳多少KV cache block并据此决定当前能同时处理多少个序列。如果你没有显式调整max_num_seqs实际并发能力完全由“KV cache可用block数”反推。这里我踩过一次狠的某次我把max_model_len调大了一倍结果KV cache可容纳的序列数骤降压测时并发一高就大量排队还以为代码有问题。查了半天才发现是KV cache总容量不变的情况下单个序列的块数增加能服务的并发自然下降。所以当你发现并发上不去时先算一笔账max_model_len × 层数 × 头数 × 精度字节数就是每个序列占用的KV cache量用它去除总KV cache容量才是理论上的并发上限。调度队列如果超出上限请求会进入等待队列。等待队列本身不在显存中占KV cache但它占内存且会直接影响用户体验。如果你有严格的SLA建议把队列长度上限调低宁可返回“繁忙”也不要无限排队。这属于产品策略问题但配置上确实是由你手上的参数决定的。4. 服务端参数优化从实际压测看前后端配合4.1 并行采样与beam search默认配置不能满足所有场景optimization.md里对采样相关参数着墨不多但它在服务端配置里却极其关键。很多人以为采样参数只影响结果质量不影响性能。实际上vLLM对单个请求的n个并行采样和beam search的处理方式完全不同。比如你设置了n4也就是一个请求回来4条候选vLLM内部会把它当成4条独立的序列去调度每条都占用独立的KV cache block。这个行为在文档里虽然没有大篇幅强调但你必须清楚一个并发请求拉4条结果相当于4倍并发。如果你压测时只数“请求数”没数“序列数”性能数字会非常不可信。beam search同理vLLM虽然支持但它的beam状态管理要维护比普通采样更多的候选。从性能角度我个人的建议是能用普通采样让应用层多路请求就不要用beam search。因为beam search的中间状态存储与恢复完全在引擎内完成对KV cache的占用方式比普通采样更复杂而且很难用gpu_memory_utilization直接约束。做部署评估时请把“峰值并发下的总序列数”而不是“请求数”作为容量指标。这是一个非常容易被忽略、但影响规划准确性的核心细节。4.2 前缀缓存Prefix Caching省算力的开关也是显存刺客前缀缓存是vLLM针对“多轮对话、多请求共享相同前置内容”场景的优化。它会把某个Prompt的前缀计算出来的KV cache块缓存住下一个请求如果前缀相同直接复用不用重新计算。对系统提示词很长的对话应用这个功能能把吞吐提升一大截。但它的代价也很直接缓存要占KV cache空间。如果前缀复用率不高缓存占用的块会造成浪费。文档中关于前缀缓存的建议是要么通过参数显式控制要么在系统提示词变动频繁时关闭它。因为提示词只要改一个字符缓存就全部失效。我在项目里验证过系统提示词基本固定、只有尾部用户输入变化时前缀缓存能减少大概30%-40%的重复预填充时间。但一旦提示词模板按版本频繁迭代收益就会归零还剩下一堆过期的缓存块在显存里等淘汰。这种时候不如关掉缓存把显存让给KV cache扩容。4.3 日志、监控与额外开关别让服务端把吞吐吃掉除了文档焦点参数服务端还有一些容易被忽略但对性能有实际影响的配置。比如日志级别调成debug时每个请求的调度细节都会输出日志写入会争抢CPU时间片高并发下性能下降肉眼可见。上线时务必确认日志级别为info或warning。另一个是流式输出与skip_special_tokens这类的后处理参数。虽然它们看起来和性能无关但某些版本中后处理逻辑会阻塞工作线程。此类问题通常不会出现在单请求测试里只会在并发拉高后暴露。碰到类似情况建议用profiling工具看线程状态别凭感觉改配置。文档里通常还会提到与torch.compile和CUDA graph相关的开关。CUDA graph能减少kernel启动开销对短序列、小batch场景尤其明显。但CUDA graph的捕获过程对动态shape敏感如果你的输入长度变化太剧烈捕获可能失败或回退。实际部署中我会优先开启CUDA graph然后压一轮请求长度分布若发现频繁回退再关掉对比效果。5. 量化、权重加载与推理后端的取舍5.1 量化的显存收益和精度损失别只看一个维度显存不够用时量化通常是第一反应。vLLM支持多种量化方案比如常见的INT8、INT4以及某些更细粒度的分组量化。optimization.md中涉及量化相关的内容核心思想是“以少量精度损失换取显存空间和计算速度”。不同的量化方案在显存节省和计算加速上的表现差异很大。以INT8量化为例它能比较稳固地把KV cache相关显存需求降下来同时对大多数业务场景的精度影响在可接受范围内。INT4则更激进显存占用更低但部分模型上的输出质量下降明显尤其对数学、代码这类任务。我建议不要一次性把所有层全部量化到底。vLLM允许更细粒度地控制哪些层被量化但实际操作里多数人直接用统一配置。如果你在业务中发现了量化后的质量下降尝试保留某些敏感层的原精度是一个比整体回退更好的折中方案。5.2 权重加载精度与序列长度的真实关系优化文档中有一个很容易被忽略的参数是权重加载的数据类型与KV cache的存储类型。KV cache不一定要和模型权重用同样的精度比如权重用FP16KV cache可以单独设置为INT8。这会带来两个效果KV cache占用减半可容纳的并发序列数上升同时解码过程中的KV cache读取带宽压力下降。代价是精度下降长序列下误差可能累计。我测试过某个70B级别的模型在开启KV cache INT8后短文本对话场景输出几乎无感知但在长文档生成时少数段落会出现轻度重复或跑偏。紧急情况下这个配置能救命生产环境请务必在目标场景上做充分评测。此外dtype的设置要与衡量的语义对齐。很多人只看显存变化却忽略了切换dtype之后吞吐也可能提升。因为显存带宽有时才是瓶颈精度降低意味着同样时间能搬更多数据。反过来如果你的瓶颈在算力不在显存量化带来的吞吐提升就没那么明显。5.3 推理后端的选择CPU offload与多卡方案不要盲开部分场景下显存不够文档会建议使用CPU offload也就是把部分权重放到内存里计算时再搬到显卡。这个功能听起来像“白嫖显存”但实际上CPU到GPU的搬运带宽远低于显存带宽只要offload的权重被频繁访问性能就会严重恶化。我的经验是CPU offload只适合追求“能跑起来”的场景比如演示、调试、偶尔推理一旦进入持续在线服务这个方案基本会让TPOT翻好几倍。预算允许的情况下用多卡张量并行把模型切分到多张卡上比offload靠谱得多。多卡并行时也要注意optimization.md中关于Tensor Parallel的设置不是越大越好。卡间通信开销会随卡的增多而上升。比如一个70B模型用4卡能跑用8卡也能跑但8卡的通信开销可能让部分算力被浪费。最稳妥的做法是跑一个可复现的压测脚本固定同一组输入逐一比较不同并行度的延迟而不是背参数表。6. 从启动日志读到优化空间一套可复制的调优路径6.1 启动日志里那些数字的真实含义vLLM启动时会打印一大段日志里面包含“GPU memory usage”“KV cache size”“maximum number of sequences”等信息。很多人瞟一眼就过去了其实这些数字是第一个该看的优化指标。我通常的读法是这样的先看KV cache占显存的比例如果低于20%说明模型权重占比太高可优化的空间有限要么换量化要么加卡。如果比例在40%-60%说明配置比较合理可以继续微调max_model_len和并发上限。如果比例高于70%说明并发容量充足但要注意长序列场景下是否有溢出风险。这个“比例直觉”来源于多次压测经验比任何文档公式都实用。你不需要手动算每一步启动日志已经把结果算好了。你只需记住KV cache比例不是越高越好而是要与你的业务请求长度分布匹配。6.2 一次完整调优该按什么顺序操作调优最忌讳一上来就乱改参数。我整理了一套顺序每次新场景都按这个路径走省掉大量无效排查第一步固定模型与硬件用默认配置启动记录启动日志中的KV cache大小和峰值并发估计。第二步用业务侧的真实请求样本做一轮压测记录TTFT、TPOT和吞吐基线。第三步按max_model_len、gpu_memory_utilization、max_num_seqs的顺序逐个调整每次只改一个参数重新压测对比基线。第四步开启前缀缓存并构造复用前缀的请求验证收益。第五步显存仍不足时考虑KV cache量化或权重量化并做精度对比。第六步全部参数确定后用混合负载做最终压测确认调度是否稳定。这个顺序背后的逻辑是先确定显存边界再调整并发和批处理最后再做量化妥协。倒过来操作很可能出现“量化后吞吐上去了但并发增加了调度不稳”的情况反而更难排查。6.3 哪些配置组合最容易“翻车”最后分享几个我亲测容易踩坑的配置组合写在这儿给你当避雷清单。第一gpu_memory_utilization设置过高 同时开启前缀缓存。前缀缓存本身要占KV cache显存利用率拉满时缓存空间不足可能导致大量缓存被提前淘汰前缀缓存形同虚设。第二max_num_seqs调得过高 短并发请求极多调度器需要频繁切换序列CPU开销上升反而压低了有效吞吐。第三chunked prefill的chunk设得非常小 长Prompt占比高预填充被切得太碎计算重复次数增多TTFT不降反升。这些组合在文档里看起来都是“独立的”实测中却会互相放大问题。所以配置的正确姿势不是把某个点调到极致而是把所有参数按业务画像压在一个平衡区间。这个区间只能靠压测找没有公式。在做完这一轮配置之后我的体会是vLLM的optimization.md不是一份“填参数”的文档而是一张显存与计算资源的分配地图。真正吃透它之后你对任何推理服务的容量规划都会有个清晰的分析框架。碰到新模型、新显卡、新业务场景时直接套用上面的调优路径重新走一遍大多能在一个小时内得到接近合理的配置剩下的偏差就交给更细粒度的压测去磨了。