3.2 显存预算与调度调优:把 GPU 喂饱又不 OOM 并行策略定好之后,模型已经稳稳摊在卡上了。但这时候你跑压测,往往会看到一个让人抓狂的现象:GPU 利用率只有 30%40%,吞吐远没到预期,可一旦把并发调高,立刻 OOM 崩掉。 问题不在并行,在于显存预算和调度没调好——GPU 一半时间在等数据、在空转,另一半时间又因为 batch 太大直接溢出。 这一节我们讲高并发调优里最值钱的一组旋钮:KV Cache 显存预算、Chunked Prefill、连续批处理上限,以及抢占与回收。它们共同决定了一个核心指标——在绝不 OOM 的前提下,你的 GPU 到底能被喂多满。 先算账:显存到底花在哪了 要调好,先得知道一张卡(以常见数据中心卡 80GB 显存为例)的显存是怎么没的。
并行策略定好之后,模型已经稳稳摊在卡上了。但这时候你跑压测,往往会看到一个让人抓狂的现象:GPU 利用率只有 30%~40%,吞吐远没到预期,可一旦把并发调高,立刻 OOM 崩掉。 问题不在并行,在于显存预算和调度没调好——GPU 一半时间在等数据、在空转,另一半时间又因为 batch 太大直接溢出。
这一节我们讲高并发调优里最值钱的一组旋钮:KV Cache 显存预算、Chunked Prefill、连续批处理上限,以及抢占与回收。它们共同决定了一个核心指标——在绝不 OOM 的前提下,你的 GPU 到底能被喂多满。
要调好,先得知道一张卡(以常见数据中心卡 80GB 显存为例)的显存是怎么没的。大致分给三块:
关键认知:高并发时,KV Cache 往往比权重还吃显存。 谁的 KV 缓存能多塞,谁就能多扛并发。这就是为什么 gpu-memory-utilization 这个参数如此要紧——它本质上是在给 KV Cache 池划地盘。
光说「KV 很吃显存」太虚,我们算一笔具体的账。假设某层每个 token 的 KV 占用(Key+Value 合计)约为:
那么 KV Cache 池能容纳的「token 总数」≈(KV 池字节数)/ 0.12 MB。若 gpu-memory-utilization 留出 30GB 给 KV 池,则约可存 30×1024/0.12 ≈ 25 万 token 的 KV。如果每个并发序列平均长度 2000 token,那大约能同时容纳 100 多个序列。这个数字直接决定了 max-num-seqs 能设多高——它不是你拍脑袋定的,是显存算出来的。
注意上面是简化模型,真实还要扣掉碎片(PagedAttention 用分页管理,仍会有页内浪费)、不同序列长度差异、以及激活值的峰值占用。但「先算上限再留 20% 余量」这个动作,能让你避开绝大多数 OOM。
vLLM 用 gpu-memory-utilization(简写 --gpu-memory-utilization,取值 0~1)告诉框架「显存里最多拿出百分之多少给我管」。框架会先预留权重和激活的空间,剩下的按比例划给 KV Cache 池。
0.9:框架最多用 90% 显存,KV Cache 池更大,能扛更多并发序列。我的主张:不要一上来就设 0.95 搏命。稳妥起点是 0.85~0.90,压测时观察是否出现 OOM 或频繁抢占,再微调。如果你发现显存还剩一大截、并发却卡住不动,八成是这个值设保守了——往上调,往往立竿见影。
这是高并发延迟优化的一个关键机制。传统 prefill(处理输入 prompt、生成首批 KV)是「一次性」的:一个超长 prompt 要算完才能把卡让出来做 decode,期间所有 decode 请求都被阻塞,尾延迟飙升。
Chunked Prefill 把长 prompt 切成小块(chunk),每块算完就腾出空隙插入 decode 请求,让 prefill 和 decode 在微观层面交错进行。代价是 prefill 本身稍慢一点,但整体吞吐和尾延迟显著改善——对「输入长、并发高」的场景几乎是必开项。
为什么它会改善延迟而非只是吞吐?关键在于 decode 阶段对延迟极度敏感:每个 decode 步通常只生成一个 token,本应毫秒级返回,但如果它被一个长 prompt 的 prefill 完全阻塞,就要等整个 prefill 跑完——一个 32k token 的 prefill 可能要几百毫秒甚至上秒,这一等就把所有在线 decode 请求的尾延迟直接拉爆。Chunked Prefill 把这块大任务切碎,让 decode 能「见缝插针」,等于把长 prompt 的延迟成本摊薄到多个小步里,每个 decode 请求每次只多等一个 chunk 的时间,而非一整段 prefill。
实战建议:通过启用 chunked prefill 相关参数(名称随版本可能写作 enable-chunked-prefill 之类,请以官方文档为准),配合合理的最大 chunk 长度,让长 prompt 不再成为吞吐杀手。一个判断:如果你的服务里 prompt 普遍很长(如文档问答、代码补全),不开 Chunked Prefill,高并发下 P99 会难看到没法上线。它和 max-num-batched-tokens(一次批处理的总 token 上限)是联动的——chunk 大小受这个总上限约束,调的时候要一起看。
一个常见误区:有人担心开 Chunked Prefill 会让 prefill 本身变慢、拖长 TTFT。确实,把 prefill 切碎会引入一些调度开销,单次 prefill 的总耗时可能略增;但 TTFT 的感知主要来自「首个 token 何时返回」,而 Chunked Prefill 让首个 token 仍能在第一块 chunk 算完时就开始生成,配合 decode 交错,整体 TTFT 通常反而更稳。权衡上,长 prompt 高并发场景几乎总是「开比不开好」。
连续批处理(第二章 2.3 讲过)能把大量请求动态拼成一波。但拼多少是有上限的,由两组参数共同约束:
max-num-seqs:KV Cache 池里同时能容纳多少个序列。直接受 KV 池大小限制。max-num-batched-tokens:一个调度步里总 token 数上限(含 prefill 的 prompt token + decode 的已生成 token)。它防止单步计算量过大把显存或算力打爆。调优陷阱:只抬 max-num-seqs 但忘了 max-num-batched-tokens,会出现「序列进来了却攒不到一波算」的尴尬;反之只抬后者不抬前者,KV 池先满、序列进不来。我的经验值:短序列场景 max-num-seqs 可推到数百;长上下文(如 32k+)每个序列占的 KV 巨大,两者都要按比例降,否则 KV 池秒满。
当 KV Cache 池真的被占满、新请求又来了,vLLM 必须做抢占(preemption):把某些正在跑的序列的 KV 腾出来。腾的方式有两种,这是调优里最容易被忽视的取舍:
判断:如果你的机器内存充足、且被抢占是偶发,Swap 更友好;如果内存也紧、且追求极限吞吐,Recompute 更简单但要做好尾延迟变差的预期。无论哪种,频繁抢占本身就是一个强烈信号——说明你的 max-num-seqs 或 gpu-memory-utilization 设得超出了硬件承载,该降,而不是硬扛。
前面给的清单是「动作」,但动作要有反馈才叫调优。我建议你压测时至少盯四个指标,它们能直接告诉你卡在哪:
把这四指标和前面的速查表对照,调优就从「猜」变成了「读图下针」。我个人的习惯是:每改一个旋钮,就跑同一份压测脚本,截四指标的前后对比,连续做几轮,曲线自然告诉你甜点在哪里。
把上面串起来,给你一份高并发调优的实战顺序,建议照着跑压测、边测边调:
下面是一张「现象 → 原因 → 先动哪个旋钮」的速查表,是我自己排障时反复用的:
| 现象 | 最可能原因 | 先试的旋钮 |
|---|---|---|
| GPU 利用率低、吞吐上不去 | KV 池太小 / max-num-seqs 太保守 | 上调 gpu-memory-utilization、max-num-seqs |
| 高并发 OOM | KV 池或 batch 太大 | 下调 gpu-memory-utilization,或降 max-num-batched-tokens |
| P99 延迟飙升、长 prompt 明显 | 未开 Chunked Prefill | 启用 chunked prefill |
| 频繁抢占、部分请求极慢 | 并发超过 KV 池承载 | 回调 max-num-seqs,或升 gpu-memory-utilization(留余量内) |
| 跨节点并行后吞吐反降 | TP/EP 跨节点通信吃收益 | 收回节点内,PP 兜底 |
很多人以为调优是「把吞吐推到无限高」,其实终点是一个平衡态:GPU 利用率稳定在 80%~90%、P99 满足业务 SLA、且连续压测不再出现 OOM 或频繁抢占。到了这个态,再加并发只会增加延迟风险,没有收益。所以「找到甜点」比「推到极限」更重要——这也是我把 max-num-seqs 强调为「压到临界再回调」而非「越大越好」的原因。
记住:本章给你的不是一组固定数字,而是一套「算预算 → 设起点 → 压测 → 读指标 → 回调」的闭环方法论。换硬件、换模型、换 vLLM 版本,这套方法都通用;死记某个博客里的「最佳参数」反而会害你。
最后点破一个新手误区:很多人把「OOM」当成纯显存问题,于是只去降 batch。但高并发下的 OOM 常常根子在「并行没选对」——比如 EP 没开导致专家权重全堆在少数卡,单卡 KV 池再怎么让也放不下全部并发。所以当你频繁 OOM 时,先回 3.1 复查并行,再回来动本节的显存旋钮,顺序别颠倒。
高并发调优的本质,是在「显存预算」和「并发度」之间找甜点:gpu-memory-utilization 决定 KV Cache 池多大,max-num-seqs / max-num-batched-tokens 决定能同时喂多少个序列,Chunked Prefill 决定长输入不堵路,抢占机制决定爆了怎么兜底。四个旋钮联动调,才能把 GPU 从空转拉到饱和,又不踩 OOM 的雷。
给你的一句话带走:并行解决「算得动」,调度解决「算得满」;先按显存算清 KV 上限再设并发,打开 Chunked Prefill,把 max-num-seqs 压到临界再回调——这是高并发调优的三板斧。下一章我们将把这些原理落到真实部署案例里,看一个完整的生产级 DeepSeek V4 服务是怎么搭起来的。