☰
vLLM显存OOM排查与优化:从KV Cache到参数配置
2026/10/2 16:26:10 网站建设 项目流程

1. 显存不足的报错长相:先分清是"内存"还是"显存"问题

凌晨一点,我在给团队跑一个 7B 参数的推理服务。vLLM 加载权重很顺利,等第一个请求进来,终端突然甩出一行torch.OutOfMemoryError: CUDA out of memory...。盯着这块 24G 显存的卡,我的第一反应是"模型才 14G,怎么还能爆?"后来才明白,显存里塞的不只是模型权重。类似问题你大概率也会遇到:本地开发没问题,一上 vLLM 就 OOM;或者模型明明能装下,却总在请求进来时挂掉。这篇文章把我的排查思路、计算方法和最终落地的参数完整写下来,遇到同样的坑可以直接按这个顺序走一遍。

1.1 vLLM 里最典型的 OOM 日志长什么样

先看一个我截过无数次的报错:

torch.OutOfMemoryError: CUDA out of memory. Tried to allocate 512.00 MiB (GPU 0; 23.66 GiB total capacity; 20.10 GiB already allocated; 1.30 GiB free; 20.99 GiB reserved in total by PyTorch)

这段信息里有几个数字很关键:GPU 总容量 23.66 GiB,PyTorch 已经通过缓存分配器 reserve 了 20.99 GiB,真正还给驱动的只有 1.30 GiB free。虽然报错说"尝试分配 512 MiB 失败",但这往往并不意味着显存一点不剩,而是 1.30 GiB 的空闲显存已经被拆成了碎片,凑不出一个 512 MiB 的连续块。

还有一种长得不太一样的报错:

RuntimeError: CUDA error: out of memory

这种一般是 CUDA kernel 执行过程中触发的,很多时候报错之后整个 CUDA context 已经处于不可恢复状态,只能重启进程。相比之下,torch.OutOfMemoryError是 PyTorch 层面拦截下来的,进程不一定会立刻崩,但如果你不做处理,后续请求大概率也起不来。

所以第一步不是急着调参数,而是先确认:是启动阶段 OOM,还是第一个请求进来后 OOM,还是跑了很久之后偶发 OOM。三个阶段对应的原因完全不同,后面我会分别说。

1.2 模型权重明明不大,显存却被吃光的四笔账

很多人习惯用模型目录在磁盘上的大小来估算显存,这是最大的误区。一个 7B FP16 模型,权重文件大概 14GB 左右,但 vLLM 进程要占的显存远不止这 14GB。完整算下来,显存里至少有四笔开销:

  • 模型权重:参数量乘以精度。7B FP16 约 14GB,7B INT4 量化后约 3.5GB,70B FP16 约 140GB。
  • KV Cache:这是 vLLM 显存开销的大头。每生成一个 token,都要把计算过的 Key 和 Value 缓存下来。
  • 中间激活值:forward 过程中产生的临时张量,跟 batch size、序列长度、层数直接相关。
  • CUDA Context / PyTorch 缓存 / CUDA Graph:这部分是很多人没算进去的"隐藏开销",通常 1~3GB 起步,CUDA Graph 捕获时还会额外预留。

我给一个简化计算公式,你可以对着自己的模型 config 算:

模型权重显存(GB) = 参数量 / 10^9 * 每个参数字节数 KV Cache 每 token 显存(Byte) = 2 * 层数 * KV头数 * 每个头的维度 * 精度字节数

举个例子,一个 7B 模型如果是 32 层、32 个 KV head、head dim 128、FP16 精度,那么每个 token 的 KV Cache 大约是:

2 * 32 * 32 * 128 * 2 = 524288 字节 = 0.5MB

如果上下文长度是 4096,那张量就是 0.5MB * 4096 ≈ 2GB。如果你的模型用了 GQA,KV 头数从 32 降到 8,这个数字会直接缩小到四分之一。

所以"14GB 权重 + 2GB KV Cache + 2GB 框架开销 ≈ 18GB"才是真实的显存占用。很多人拿 24G 显卡跑 7B,觉得绰绰有余,结果一上 32K 上下文,KV Cache 直接冲到 16GB,OOM 自然就来了。

2. vLLM 的显存分配机制:为什么默认配置也会爆掉

2.1 启动时先按 90% 容量圈地

vLLM 跟普通的 HuggingFace transformers 推理脚本不一样,它不是"用多少显存分配多少",而是在启动时先按一个比例圈地,把大部分显存规划成 KV Cache 池。这个比例默认是gpu_memory_utilization=0.9,也就是最多使用 GPU 总显存的 90% 用于模型执行,包括模型权重、KV Cache 和中间激活。

这样做的好处是稳定:显存提前规划好,KV Cache 按 page 分配,不需要频繁向 CUDA runtime 申请大块内存。但问题也在这:如果你的模型权重本身就很大,或者有其他进程占用了显存,vLLM 依然会按"总容量 * 0.9"去预估可用的空间,等真正加载权重时发现不够,就只能在启动时直接 OOM。

vLLM 启动日志里通常会打印类似下面的信息:

INFO: Maximum concurrency for 4096 tokens per request: 23 INFO: GPU blocks: 15238, blocks world size: 1

这两个数字可以直观告诉你当前配置还能支持多大并发。如果GPU blocks很小,说明 KV Cache 被压得很惨;如果启动阶段就 OOM,那往往连这个日志都看不到。

2.2 总容量够,但"连续空间"不够

CUDA 的显存分配和 malloc 有点像,每次请求需要一段连续的显存地址。PyTorch 的 caching allocator 会把已经释放的显存块留在自己的 cache 里,而不是立刻还给 CUDA。这些缓存块虽然能被 PyTorch 复用,但如果大小不匹配,就可能出现"总空闲空间还有 1.3GB,但你要 512MB 却给不了"的情况。

用停车场来类比:1.3GB 空闲显存是 30 个分散的 40MB 小空位,而 vLLM 需要的是一个 512MB 的连排车位。这时候不是说显存不够,而是"连续空间不够"。碎片化在长时间运行、频繁扩容 batch、反复加载/卸载模型时特别常见。

还有一个隐患:CUDA context 本身会在每个进程里占一定显存,多个进程同时跑在同一张卡上,每个进程都会消耗额外的 context、cuBLAS workspace、CUDA 事件对象等显存。你可能在nvidia-smi里看到每个进程只吃了几百 MB,但叠加起来,可用空间就所剩无几了。

3. 一次完整的排查链路:从报错到定位根因

3.1 先看 nvidia-smi,把现场固定下来

遇到 OOM,第一件事不是改参数,而是看一眼现场。在另一个终端执行:

nvidia-smi

重点看两件事:GPU 显存的总容量、已用、空闲,以及当前有哪些进程占用了 GPU。有时候你以为是 vLLM 的问题,结果发现一个残留的 Python 进程还占着 6GB 显存,一个 Jupyter notebook 占着 2GB,再加一个桌面合成器占几百 MB,留给 vLLM 的自由空间早就不是 24GB 了。

想看得更精炼,可以用:

nvidia-smi --query-gpu=index,memory.total,memory.used,memory.free,utilization.gpu --format=csv

如果看到某个陌生进程占显存,可以用fuser -v /dev/nvidia*找到对应 PID,再ps -ef | grep <pid>确认身份。注意别闷头 kill,先确认是不是别人的服务。

这一步同时要确认你用的是不是CUDA_VISIBLE_DEVICES把进程限制到了某张卡。vLLM 在容器里跑时,宿主机nvidia-smi看到的是整机显存,容器内看到的可能是整机显存,也可能被NVIDIA_VISIBLE_DEVICES限制,先搞清楚边界。

3.2 用一张表估算模型真实显存开销

下面这张表是我平时做预算时用的,单位 GB,粗算,适合先用 5 分钟做出判断:

模型规模FP16 权重4bit 量化权重4K 上下文 KV Cache(GQA 模型参考)框架开销参考
7B约 14约 3.50.5 ~ 22 ~ 3
13B约 26约 6.50.8 ~ 33 ~ 4
34B约 68约 171.5 ~ 55 ~ 8
70B约 140约 351.5 ~ 58 ~ 12

注意 KV Cache 区间为什么这么宽:不同模型的 GQA/MHA 结构不同,KV head 数量差几倍很正常。以 LLaMA-2-7B 这种 MHA 结构为例,4K 上下文 KV Cache 接近 2GB;换成 GQA 头数只有 8 的模型,同样 4K 上下文只要 0.5GB 左右。所以不要背死数值,一定要看模型自己的 config。

算完大致开销后,再对比你的显卡总容量。如果权重 + KV Cache + 框架开销已经超过显存容量的 90%,那无论怎么调gpu_memory_utilization都只是拆东墙补西墙,必须降 context、降并发或者上量化。

3.3 用"最小配置"二分定位问题

如果一时算不清,还有更粗暴的办法:把 vLLM 的配置压到最小,看能不能跑起来。比如:

CUDA_VISIBLE_DEVICES=0 vllm serve /path/to/model \ --max-model-len 256 \ --max-num-seqs 1 \ --gpu-memory-utilization 0.6 \ --enforce-eager \ --swap-space 0

如果这个配置都 OOM,那大概率不是参数问题,而是模型权重加载、驱动兼容性或者 CUDA context 出问题。如果这个配置能正常启动,就按顺序逐步放宽:先提高--max-model-len到 2048,再提高到 4096,再加大--max-num-seqs。每改一个参数跑一次,哪个阶段爆了,问题就定位在哪个变量上。

这个"二分法"我用了很多次,比瞎猜高效得多。尤其是有时候你以为max_model_len没问题,但把并发调上去之后,KV Cache 总量被 batch 乘以序列长度放大,OOM 立刻出现。

4. 解决 OOM 的几种有效手段:按优先级排列

4.1 max_model_len:最直接的显存开关

KV Cache 占用和上下文长度是线性关系。如果你业务场景根本用不到 32K 上下文,就不该让它默认继承模型配置里的max_position_embeddings。很多模型默认上下文是 131072,但你要真按这个值去跑,显存预算直接爆炸。

我通常这样压:

CUDA_VISIBLE_DEVICES=0 vllm serve /data/model/Qwen2.5-7B-Instruct \ --max-model-len 4096 \ --gpu-memory-utilization 0.9

这样做最明显的变化是,KV Cache 总量立刻从几 GB 级降到几百 MB 级,给中间激活和 CUDA Graph 留出充足空间。需要注意,max_model_len不是越低越好,如果改得太短,长文档会直接被截断,业务上可能有损失。先看你的历史请求分位,90% 的请求在多少 token 以内,按这个再留 10%~20% 余量。

4.2 gpu_memory_utilization 与 swap:不要盲目往 1 上调

很多人以为 OOM 就把gpu_memory_utilization调高到 0.95,这其实是个误区。这个参数是"目标使用率上限",不是"能塞多满就多满"。调太高会让 PyTorch 和 CUDA 没有余量处理突发的中间激活,反而更容易在请求高峰时 OOM。

我一般默认给 0.85,激进一点给 0.9,但不会超过 0.93。如果你显卡同时还被别的进程占用,那就要先按实际空闲量折算,比如总显存 24G,已经被占 4G,那 vLLM 的gpu_memory_utilization=0.9其实会按 24G * 0.9 = 21.6G 来圈地,但实际只有 20G 可用,照样 OOM。这种场景要手动把gpu_memory_utilization调到 0.8 甚至更低。

swap-space是另一个常用参数,默认 4,代表允许用 4GB CPU 内存做 KV Cache 的 swap。显存不足时调大它确实能避免 OOM,但代价是性能下降,因为 CPU 跟 GPU 之间搬数据比纯显存慢太多。显存够用的情况下,我甚至会设--swap-space 0,杜绝隐性的内存 swap。

4.3 量化:用精度换显存,损失通常可接受

如果模型权重太大,优先考虑量化。vLLM 支持多种量化格式,比如 AWQ、GPTQ、FP8,模型权重从 FP16 降到 INT4,显存占用可以减少到原来的四分之一左右。以 70B 模型为例,FP16 权重 140GB,单卡基本别想;AWQ 4bit 量化后大约 35GB,一块 48G 或 80G 的显卡就能跑起来,配合 4K 上下文,显存仍然可控。

使用方式很简单,只要模型路径是量化过的权重就行。比如:

CUDA_VISIBLE_DEVICES=0 vllm serve /models/llama-2-7b-awq \ --max-model-len 4096 \ --quantization awq

部分情况下量化后模型质量会有小幅下降,但对绝大多数推理业务来说,4bit 量化的损失远小于"因为显存不足直接不能用"的损失。如果模型本身没有量化权重,可以先在自己环境里做 AWQ/GPTQ 转换,vLLM 官方文档里都有对应脚本。

4.4 单卡放不下,就用张量并行把模型"劈"到多卡

当单卡显存是真的不够时,张量并行是最直接的思路。vLLM 里用--tensor-parallel-size指定参与推理的 GPU 数量:

CUDA_VISIBLE_DEVICES=0,1 vllm serve /data/model/Mixtral-8x7B-Instruct \ --tensor-parallel-size 2 \ --max-model-len 4096

这个参数会把模型权重和 KV Cache 按张量维度切分到多张卡上,每张卡只需要存一部分。例如一张 24G 卡放不下 70B FP16,两张 48G 卡做张量并行,通常就能把它跑起来。但你要注意,张量并行越卡间通信越重,如果是 PCIe 而不是 NVLink,性能可能下降明显。用之前先确认机器拓扑,别为了"装下"把"跑得动"丢了。

4.5 并发数和采样配置也值得检查

很多人的注意力全在模型大小上,忽略了并发数。vLLM 默认--max-num-seqs是 256,意味着最多同时处理 256 个序列,每个序列都会占住一段 KV Cache。如果你的显存预算本来紧张,这个值会迅速把 KV Cache 池吃穿。

建议先观察业务实际并发。如果只是内部工具或者小团队用,--max-num-seqs 16甚至8就够;如果是对外 API,再结合压力测试上调。另外,beam search 的num_beams也会让 KV Cache 按倍数增长,因为每次要同时维护多条候选序列。不用 beam search 时,把best_of、n这类参数尽量设小,别让采样逻辑偷偷吃掉几倍的显存。

5. 那些容易被忽略的隐性 OOM 原因

5.1 同一块 GPU 上住了太多进程

我在现实里遇到最多的 OOM 原因,不是 vLLM 配置,而是同一张卡被多个进程共享。别以为nvidia-smi显示总容量 24G,就真的还有 24G 可用。桌面环境、浏览器硬件加速、其他 Python 进程、甚至监控 agent 都可能占显存。

如果你想完全独占一张卡跑 vLLM,最稳妥的做法是显式指定:

export CUDA_VISIBLE_DEVICES=0 vllm serve /path/to/model ...

如果你有多张卡,可以分散部署多个模型:

CUDA_VISIBLE_DEVICES=0 vllm serve model-a --port 8000 CUDA_VISIBLE_DEVICES=1 vllm serve model-b --port 8001

注意 vLLM 的gpu_memory_utilization是按"当前可见 GPU 总显存"来算的,它不会自动扣除其他进程占用的部分。所以只要你机器上还有别的显存占用,就要手动留出足够余量。

5.2 CUDA graph 和 PyTorch 缓存带来的显存幻象

vLLM 默认会启用 CUDA Graph 来减少 kernel launch 的开销,这会提前捕获并固定一部分显存。好处是推理延迟更稳定,坏处是显存占用会明显增加,尤其是你设置了比较高的max_num_seqs时,Graph 捕获的内存块会更大。

如果你显存非常紧张,可以临时用--enforce-eager禁掉 CUDA Graph。这一步会牺牲一部分吞吐,但往往能让 OOM 立刻消失,适合排查问题或者临时上线兜底。

PyTorch 的缓存分配器也会"只进不出"。vLLM 在启动时做 memory profiling,会触发一些分配和释放,把一些显存块留在 PyTorch 的 cache 里。所以在nvidia-smi里看到 vLLM 进程占着 20GB,不代表这 20GB 都被用到了,也可能是 PyTorch 预留的空闲块。

5.3 驱动/CUDA 版本与容器环境也会"伪装"成 OOM

少数情况下,OOM 并不是显存不够,而是 CUDA 环境出了问题。比如驱动版本和 PyTorch/CUDA 版本不匹配,CUDA context 初始化异常,或者容器内/dev/nvidia*设备映射不完整。这类问题往往伴随着其他错误,比如CUDA kernel errors might be asynchronous或者显存查询结果异常。

遇到非常莫名其妙的 OOM,先做一次简单的"裸跑测试":写一个几行的 PyTorch 程序,在目标显卡上分配 1GB 张量再释放,确认 CUDA 环境和驱动正常。如果这一步都失败,就不用继续折腾 vLLM 参数了,先去修环境和驱动。

5.4 多个模型硬塞一张卡

很多人为了省机器,想在一个 vLLM 进程里同时加载多个模型,或者在同一张卡上并行跑两个不同模型的服务。这种做法不是不行,但显存会变成叠加压力。比如两个 7B FP16 模型,光权重就是 28GB,24G 卡根本塞不下。建议要么给每个模型分配独立显卡,要么先把模型量化到 4bit,再给上下文和并发留出足够余量。

如果你确实要单卡部署多模型,我的习惯是先把每个模型单独跑通,记录各自峰值显存,再确认总和低于显卡容量的 75%~80%,否则毫无意外会 OOM。

6. 我的经验值:显存预算表和一套稳妥启动参数

6.1 我常用的显存预算参考表

以下是我在实际部署时大概会参考的经验表,前提是 FP16/4bit 精度、单请求或低并发、上下文 4K 左右:

GPU 显存FP16 模型建议4bit 量化模型建议推荐 max_model_len
16G7B,但只能短上下文低并发13B 以内2048
24G7B 宽松;13B 紧张13B/14B 宽松4096
48G13B/14B 宽松;34B 勉强70B 低并发4096~8192
80G34B 相对宽松70B 可跑到 8K 上下文8192

这张表不是硬标准,但能帮你快速判断一个模型和显卡组合是不是"听起来能跑、实际必爆"。真正上线前,我会再用前面的公式精确算一遍权重加 KV Cache,不会只靠直觉。

6.2 一套经过实测的启动参数模板

以一张 24G 显存跑 7B/8B 模型为例,我实测下来比较稳的启动命令是:

CUDA_VISIBLE_DEVICES=0 vllm serve /data/model/Qwen2.5-7B-Instruct \ --max-model-len 4096 \ --max-num-seqs 16 \ --gpu-memory-utilization 0.9 \ --swap-space 4 \ --tensor-parallel-size 1

如果仍然偶发 OOM,我会按这个顺序降级:

CUDA_VISIBLE_DEVICES=0 vllm serve /data/model/Qwen2.5-7B-Instruct \ --max-model-len 2048 \ --max-num-seqs 8 \ --gpu-memory-utilization 0.8 \ --swap-space 8 \ --enforce-eager

--enforce-eager是压箱底的策略,一般不建议长期开启,因为它会牺牲一部分延迟和吞吐;但如果你只是想快速恢复服务,它比通宵调参现实得多。

6.3 顺手做的显存监控

最后建议你顺手做个显存监控,别等日志报警才知道 OOM。最简单的是:

watch -n 1 nvidia-smi

但生产环境我一般用定时采集:

nvidia-smi --query-gpu=index,memory.used,memory.total,utilization.gpu --format=csv -l 60

把结果写到文件,配合你现有的监控系统,就能看到显存使用曲线。当已用显存接近总量 90% 并且持续上涨时,大概率是 KV Cache 或并发请求在吃掉最后那点余量,这个时候提前扩容或限流,比事后看torch.OutOfMemoryError舒服得多。

踩了这么多次坑之后,我最深的体会是:显存爆掉不是"运气差",而是对 vLLM 的内存模型预估不准。启动前花两分钟按权重加 KV Cache 加框架开销算一遍,再调好max_model_len和gpu_memory_utilization,能避免绝大多数 OOM。遇到问题也别急着怪 CUDA,先nvidia-smi看现场,再对照这篇文章的排查链路走一遍,基本都能定位。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询