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 GiBPyTorch 已经通过缓存分配器 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 约 14GB7B INT4 量化后约 3.5GB70B 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。如果你的模型用了 GQAKV 头数从 32 降到 8这个数字会直接缩小到四分之一。所以14GB 权重 2GB KV Cache 2GB 框架开销 ≈ 18GB才是真实的显存占用。很多人拿 24G 显卡跑 7B觉得绰绰有余结果一上 32K 上下文KV Cache 直接冲到 16GBOOM 自然就来了。2. vLLM 的显存分配机制为什么默认配置也会爆掉2.1 启动时先按 90% 容量圈地vLLM 跟普通的 HuggingFace transformers 推理脚本不一样它不是用多少显存分配多少而是在启动时先按一个比例圈地把大部分显存规划成 KV Cache 池。这个比例默认是gpu_memory_utilization0.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-gpuindex,memory.total,memory.used,memory.free,utilization.gpu --formatcsv如果看到某个陌生进程占显存可以用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 CacheGQA 模型参考框架开销参考7B约 14约 3.50.5 ~ 22 ~ 313B约 26约 6.50.8 ~ 33 ~ 434B约 68约 171.5 ~ 55 ~ 870B约 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_DEVICES0 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_DEVICES0 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_utilization0.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_DEVICES0 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_DEVICES0,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_DEVICES0 vllm serve /path/to/model ...如果你有多张卡可以分散部署多个模型CUDA_VISIBLE_DEVICES0 vllm serve model-a --port 8000 CUDA_VISIBLE_DEVICES1 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 模型光权重就是 28GB24G 卡根本塞不下。建议要么给每个模型分配独立显卡要么先把模型量化到 4bit再给上下文和并发留出足够余量。如果你确实要单卡部署多模型我的习惯是先把每个模型单独跑通记录各自峰值显存再确认总和低于显卡容量的 75%~80%否则毫无意外会 OOM。6. 我的经验值显存预算表和一套稳妥启动参数6.1 我常用的显存预算参考表以下是我在实际部署时大概会参考的经验表前提是 FP16/4bit 精度、单请求或低并发、上下文 4K 左右GPU 显存FP16 模型建议4bit 量化模型建议推荐 max_model_len16G7B但只能短上下文低并发13B 以内204824G7B 宽松13B 紧张13B/14B 宽松409648G13B/14B 宽松34B 勉强70B 低并发4096~819280G34B 相对宽松70B 可跑到 8K 上下文8192这张表不是硬标准但能帮你快速判断一个模型和显卡组合是不是听起来能跑、实际必爆。真正上线前我会再用前面的公式精确算一遍权重加 KV Cache不会只靠直觉。6.2 一套经过实测的启动参数模板以一张 24G 显存跑 7B/8B 模型为例我实测下来比较稳的启动命令是CUDA_VISIBLE_DEVICES0 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_DEVICES0 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-gpuindex,memory.used,memory.total,utilization.gpu --formatcsv -l 60把结果写到文件配合你现有的监控系统就能看到显存使用曲线。当已用显存接近总量 90% 并且持续上涨时大概率是 KV Cache 或并发请求在吃掉最后那点余量这个时候提前扩容或限流比事后看torch.OutOfMemoryError舒服得多。踩了这么多次坑之后我最深的体会是显存爆掉不是运气差而是对 vLLM 的内存模型预估不准。启动前花两分钟按权重加 KV Cache 加框架开销算一遍再调好max_model_len和gpu_memory_utilization能避免绝大多数 OOM。遇到问题也别急着怪 CUDA先nvidia-smi看现场再对照这篇文章的排查链路走一遍基本都能定位。