尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

vLLM显存OOM排查全攻略:从CUDA out of memory到参数调优实战

vLLM显存OOM排查全攻略:从CUDA out of memory到参数调优实战 跑大模型推理的人十个里有九个见过这行红字CUDA out of memory。你要是用 vLLM 做部署这个问题几乎绕不开尤其是刚开始调参那几天跑着跑着就崩崩完一脸懵。vLLM 作为当前大模型推理服务里最主流的框架之一显存管理已经是它最大的卖点——PagedAttention 把 KV cache 拆成块来动态分配理论上比传统方案省一半以上显存可即便如此OOM 的报错还是照出不误。这篇文章就围绕 vLLM 运行过程中的CUDA out of memory做一个全流程拆解。我会从 vLLM 的显存分配机制讲起梳理 OOM 的高频原因给出可复现的排查路径和参数调优方案再附上多模型部署、单机多卡这些真实场景里的处理经验。无论你是刚跑通 vLLM 的新手还是已经在生产环境里被 OOM 折腾过的老手这篇都值得看完尤其是后面那些坑几乎每个我都在实际环境里踩过。1. 先搞明白vLLM 的显存到底花在哪了遇到 OOM 第一反应是“显存不够就换大卡”这话只对了一半。换卡能解决绝对容量不足的问题但很多 OOM 恰恰是 vLLM 自身的显存管理策略和用户参数设置不匹配造成的。想调明白这个问题得先知道显存都被谁吃了。1.1 显存四巨头权重、KV cache、CUDA context 和激活值一张 GPU 上跑 vLLM 服务时显存占用大致可分为四类。第一块是模型权重也就是常说的参数。以 7B 模型为例FP16 精度下光权重就占大约 14GB如果是 13B 就是约 26GB70B 直接到 140GB。这一块是硬占用只要模型加载进来就动不了。第二块是 KV cache。这是 vLLM 的“命根子”也是 OOM 发生最频繁的区域。vLLM 在初始化时会根据你设置的gpu_memory_utilization默认 0.9把 GPU 显存的大部分预留给 KV cache后续推理过程中每个请求的 K 向量和 V 向量都会按需写入这些预留的 cache 块里。请求越长、并发越高KV cache 消耗就越快。它不像普通显存那样靠了才报错而是提前“圈地”圈得太多其他环节就没得用了。第三块是 CUDA context 和运行时开销。很多人忽略这块但它实打实存在。加载 CUDA 内核、cuDNN、TensorRT 的组件会固定吃掉几百 MB 到 1GB 不等的显存多卡环境下每张卡都要摊一份。小显存卡上这块占比会被明显放大。第四块是激活值activation和前向计算过程中的临时张量。推理时中间层输出的临时数据也会占用显存只是比训练小得多但在长序列、大 batch 的极端情况下同样可能成为压垮骆驼的最后一根稻草。1.2 PagedAttention 帮了什么忙又带来了什么新问题vLLM 核心的 PagedAttention 借鉴了操作系统虚拟内存的分页思想。传统推理框架会把某个请求的 KV cache 连续存储如果请求提前结束或长度变化就会产生大量碎片和浪费。vLLM 把 KV cache 切成固定大小的物理块用块表映射到逻辑块按需分配。这个设计确实大幅提升了显存利用率官方数据说吞吐量能到 HuggingFace Transformers 的 24 倍。但烦人的是PagedAttention 的显存是“预分配”的。vLLM 启动时就会按gpu_memory_utilization把绝大部分可用显存全部申请到自己的显存池里包括 KV cache 池而不是用到多少才申请多少。这意味着你看到nvidia-smi里显存占用很高不代表这些显存全都在“有效工作”很多是处于“备而不用”的状态。对应的副作用就是同一个 GPU 上你再想启动第二个模型哪怕那个模型很小也可能因为显存已经被第一个 vLLM 实例圈走而直接 OOM。这不是显存物理上不够而是被预分配策略吃死了。2. 别急着调参先定位你属于哪一类 OOMOOM 不是只有一种死法。我见过很多人在网上抄了一堆参数试了没效果就是因为没弄清楚自己的问题属于哪一类。下面这六种是我在实际环境里总结出的高频类型你可以对号入座。2.1 类型一模型权重就已经塞不下这类最简单也最容易判断。启动 vLLM 后立刻报 OOM日志里会出现类似torch.OutOfMemoryError: CUDA out of memory. Tried to allocate 512.00 MiB的提示而nvidia-smi里显示显存几乎全满。比如你用一张 16GB 的 V100 或 T4 推理 FP16 的 13B 模型光权重就 26GB那无论如何都会 OOM。硬件规格不达标系统性地无法加载。解决办法有几种。最直接的是换更大的卡其次是降低精度用 AWQ、GPTQ 这类 4bit 量化模型13B 权重能压到 7GB 左右再者是切分模型利用多卡把权重拆开最极端的是开 vLLM 的 CPU offload让部分权重放内存代价是速度断崖式下跌。这类情况没有花里胡哨的技巧就是做加减法。2.2 类型二KV cache 预留太多挤占了权重或临时显存真正让人头疼的是这一类。模型能加载进去跑短请求没问题但一旦并发起来或请求变长就开始随机 OOM。或者反过来加载就报错但是把gpu_memory_utilization从默认的 0.9 调小到 0.7问题就消失了——这说明权重加 CUDA context 的固定开销加上你预留的 KV cache 池超出了物理显存总量。vLLM 默认按 90% 的显存来建池剩下的 10% 留给 CUDA context 和激活值。如果你用的是消费级显卡显存本身不大90% 可能真的太多了。而且有些环境下显卡驱动、其他进程也会占一部分显存vLLM 计算可用显存时未必能完全感知一来二去就爆了。面对这一类第一板斧就是把gpu_memory_utilization往下调。调到 0.85、0.8 甚至 0.7看启动日志里 KV cache 的分配数量变化。注意调太低会导致 KV cache 太少很容易在推理中途报No available memory for the cache blocks之类的错误那是另一种“软 OOM”同样需要警惕。2.3 类型三max_model_len 设置过大max_model_len是 vLLM 中决定 KV cache 容量的另一个关键参数。它代表模型支持的最大序列长度也就是 prompt 加生成文本的总 token 数。这个值设得越大vLLM 就需要为每个序列预留越多的 KV cache 块。举个例子同样的显存池max_model_len4096时能支持的并发序列数量是max_model_len8192时的近两倍。如果你模型本身支持的窗口是 32K但业务场景根本用不到那么长那你就白白牺牲了并发能力。很多生产环境里的 OOM 往往不是显存总量不够而是 KV cache 池被长上下文塞爆了报错时会提示The models max seq len (xxx) is larger than the maximum number of tokens that can be stored in KV cache。看到这类日志优先检查max_model_len是不是设得太离谱。2.4 类型四并发和吞吐参数挤压显存空间max_num_seqs控制最多并行处理的序列数max_num_batched_tokens控制单次前向传播能处理的 token 上限。这两个参数值越大单次推理的 batch 就越大激活值和临时张量占用就越高。默认值通常比较保守但如果你手动调大了它们又遇到显存中临时分配失败那就得考虑是不是它们造成了瞬时显存峰值。这类 OOM 的典型特征是服务刚启动时一切正常跑了一段时间、并发一上来才报错。报错信息里通常能看到Tried to allocate ... MiB后面跟着threw CUDA memory error而且往往伴随着前向计算过程中的张量分配失败。2.5 类型五同卡多模型部署互相“打架”vLLM 支持在同一张卡上起多个实例、部署多个模型但前提是你手动把显存预算分清楚。很多人图省事第一个模型用默认 0.9 启动第二个模型再用默认 0.9 启动那第二个必然 OOM——显存早就被第一个实例圈走了。这就像两个人合租一间房第一个人把衣柜全占了第二个人只能坐地上。更隐蔽的是即便你给两个模型分别设置gpu_memory_utilization0.4也可能 OOM因为 vLLM 计算时是基于整卡显存去预留的但实际显存占用还包含 CUDA context、驱动开销和碎片化余量多个实例叠加后这些固定开销会被放大。2.6 类型六外部显存碎片和他进程抢占最后这类不常见但很恶心。显卡上如果还跑着别的任务比如另一个 Python 进程、数据加载线程、或者别的框架没释放干净显存就会变得支离破碎。vLLM 启动时预留显存池需要连续的大块显存如果内存碎片化严重哪怕总量够也可能分配失败。还有 NVIDIA 驱动版本的 bug 会导致显存释放不彻底尤其是频繁创建销毁显存池的应用跑完退出后nvidia-smi里显存占用还是很高但实际已经没进程在用了。这时候重启机器或者重置 GPU不一定所有环境都支持nvidia-smi --gpu-reset生产环境慎用能解决。3. 排查 OOM 的四步走照着做就完事定位类型比盲目调参重要得多。我在这套流程上吃过亏后来总结成四步基本能覆盖 95% 的 vLLM OOM 场景。3.1 第一步看现场记录完整报错日志不要一看到 OOM 就慌先把完整的日志拖出来看。vLLM 启动日志本身就是信息量极大的排查入口。它启动时通常会有这么几行Before model init, free memory: 23.41 GB After model init, free memory: 9.37 GB Maximum concurrency for 4096 tokens per request: 8x这三行数据非常关键。Before model init显示模型加载前可用显存After model init显示加载完模型权重后剩余显存两者之差就是权重的实际占用。后面的Maximum concurrency则直接告诉你在你设置的max_model_len下当前显存池最多支持多少个并发序列。如果启动时还剩不少内存但跑一段时间后才 OOM那就要去翻推理过程中的报错信息重点看是“预分配 KV cache 池不足”还是“临时张量分配失败”这两者对应的解决方向截然不同。这里教你一个小技巧vLLM 报错经常会把真正的原因埋在一大堆 traceback 下面。别急着去翻最底部先看最上面抛出的第一个异常类型那才是核心。如果看到torch.OutOfMemoryError后面跟着具体的Tried to allocate数值就说明是运行时显存分配失败如果看到 vLLM 自定义的NoAvailableMemoryForCache之类的内部错误那更可能是 KV cache 池配置问题。3.2 第二步用 nvidia-smi 看整体显存拓扑另开一个终端执行nvidia-smi看清几件事总显存是多少当前各进程分别占了多少显存GPU 利用率是高是低有没有其他残留进程没释放显存。nvidia-smi最下面会列出所有使用显存的进程注意看有没有 Python、Triton、隧道的进程还挂着。比如之前跑 Transformers 的进程 CtrlC 杀掉后没彻底退出那显存就会一直被占着vLLM 再启动必然 OOM。更详细一点可以用nvidia-smi --query-gpumemory.total,memory.used,memory.free --formatcsv去看简洁版配合watch -n 0.5 nvidia-smi可以做实时监控。在服务运行中观察显存变化趋势能帮你判断 KV cache 池到底跑到多满才崩。3.3 第三步用最小可复现配置做二分定位如果 OOM 稳定复现最快的方式是做“减法实验”。先把参数压到最小确认能跑通再逐步把参数往回收定位是哪个参数触发了 OOM。具体顺序建议这样先设--gpu-memory-utilization 0.5同时--max-model-len 2048并发拉低测试能否启动并完成推理能通过后先把max-model-len调回业务需要的长度测试是否 OOM再调高gpu-memory-utilization观察 KV cache 分配量的变化最后再把并发参数max-num-seqs、max-num-batched-tokens拉高。哪一步开始报错问题就在哪个参数上接下来做针对性调优而不是盲目抄别人的配置。这一步我每次排查都会做虽然花时间但最有效。3.4 第四步结合 vLLM 日志里的 KV cache 数值做容量预估vLLM 启动日志里其实已经把 KV cache 的可用容量暴露得很清楚了很多人没注意看。举个例子(vllm) Initializing a KV cache with 20480 blocks, block size 16.这说明 KV cache 池总共能存 20480×16 327680 个 token 的 KV 数据。如果这时你设置的max-model-len是 4096理论上单实例最多能并发 80 个请求但当你把max-num-seqs设为 128 时就有 48 个请求会排不进 KV cache 池要么排队等待要么直接报错。所以看到日志里的 cache block 数量心里要有个大概换算。如果你的场景是长对话平均每请求 8000 token那 20480 块的池子只能装 40 个并发请求。超了就得用--max-num-seqs把并发请求数控制在合理范围。4. 核心参数调优让 vLLM 学会“量入为出”定位到问题后下一件事就是把参数调到匹配你的硬件和工作负载。vLLM 提供了大量运行时参数很多人被参数表唬住了其实关键的就那几个掌握后足以应对绝大多数 OOM。4.1 gpu_memory_utilization显存池总预算最重要的旋钮gpu_memory_utilization控制 vLLM 最多使用多大比例的 GPU 显存。默认 0.9意味着它会为 KV cache 池预留 90% 的显存减去模型权重和 CUDA context 之后。这个值设太高留给临时张量的空间太少容易在前向计算时 OOM设太低KV cache 太小并发能力受限。我的经验值是如果是 24GB 显存的显卡如 3090/4090模型 7B 量化后权重约 6GBgpu_memory_utilization设在 0.85 左右比较稳妥如果是 80GB 的 A100/H100跑 70B 量化模型可以大胆点设 0.9如果是 16GB 以下的小卡建议从 0.7 起步慢慢试。有个细节值得注意vLLM 计算可用显存时是基于torch.cuda.mem_get_info()获得的当前空闲显存而不是物理显存总量。如果你机器上已经跑了别的进程占了显存vLLM 的 0.9 是“当前空闲的 90%”而不是“物理显存的 90%”。所以同一份配置在不同的运行环境下效果可能完全不同。4.2 max_model_len别让 KV cache 为用不上的长上下文买单max_model_len是很多生产环境 OOM 的隐形杀手。模型本身支持 32K 上下文不代表你的业务需要 32K。如果你的场景平均请求长度只有 2K但你给 vLLM 设了 32K 的max_model_len那 KV cache 池的容量就会被严重稀释能支撑的并发数直接少一个数量级。具体数值怎么定我建议先统计线上请求的实际长度分布。取 P99 值作为参考再留 10% 到 20% 的余量。比如线上 99% 的请求不到 4K那max_model_len设 4096 就够用了。要是设了 8192并发容量立刻减半这就是“隐性 OOM 源头”。4.3 max_num_seqs 和 max_num_batched_tokens控制瞬态显存峰值max_num_seqs限制 vLLM 一次最多并行处理多少个序列默认 256。max_num_batched_tokens限制一次前向传播里最多装多少 token这个值设太高会带来瞬时显存峰值。如果 OOM 是“跑着跑着突然崩”而且日志显示临时张量分配失败优先看这两个参数。把max_num_seqs从 256 降到 64或者把max_num_batched_tokens设成 4096、2048 这种较低值往往能立竿见影。缺点是吞吐量会下降但对稳定性要求高的场景宁可吞吐低一点也不要频繁 OOM。4.4 量化与精度选择从源头压缩权重如果模型权重本身太大参数的调优空间终究有限。量化是治本的手段之一。vLLM 原生支持 AWQ、GPTQ、FP8、INT8 等多种量化格式。以 7B 模型为例FP16 权重约 14GBINT8 权重约 7GBAWQ 4bit 权重约 4GB。量化后的权重占用直接减半甚至减到三分之一KV cache 池就能留出更多空间。代价是精度轻微下降在绝大多数生成式任务里几乎感知不到。生产环境如果显存紧张我的建议是优先上 AWQ 或 GPTQ 量化模型而不是硬扛 FP16。4.5 多卡方案单机多卡部署与张量并行当单卡放不下模型时vLLM 支持多卡张量并行tensor parallelism。通过在启动参数里加--tensor-parallel-size 2或者4把模型权重切分到多张卡上。这种模式下每张卡只承担一部分权重和一部分 KV cache显存压力被分散。但多卡并行有几个隐藏问题。一是通信开销卡间通信通过 NVLink 或 PCIe通信量大时会拖慢速度二是 KV cache 在多卡间是均分的显存小的卡会成为瓶颈导致整机明明总体容量够却因为某张卡爆显存而失败三是多实例并行时每张卡上可能有多个模型叠加后显存管理更加复杂。实操层面如果两张卡型号不同、显存不等最好避免张量并行改成数据并行或干脆单卡分模型部署。如果是同型号同显存的卡--tensor-parallel-size可以放心用72B 模型在 4×48GB 的卡上用 TP4 是一个常见且稳定的配置。5. 实战案例复盘从 OOM 到稳定运行的全过程光讲原理容易飘来一个真实的排查过程你就知道整套方法论怎么落地了。5.1 案例背景与初始配置上个月我在一台 4×A800 80GB 的机器上跑一个 72B 量化模型AWQ 4bit服务启动后能正常响应但跑了不到半天开始频繁出现CUDA out of memory崩溃后整个服务挂掉需要手动重启非常影响使用。当时启动命令大致是vllm serve /data/models/Qwen-72B-AWQ \ --tensor-parallel-size 4 \ --gpu-memory-utilization 0.9 \ --max-model-len 32768 \ --max-num-seqs 256 \ --max-num-batched-tokens 8192配置看起来问题不大每张卡 80GB4bit 权重约 40GB平均到每张卡才 10GB 权重显存应该绰绰有余。但实际跑起来就是崩。5.2 定位过程与关键发现第一步我看nvidia-smi发现每张卡的显存占用不一致。有的卡用了 71GB有的卡只有 55GB说明张量并行下负载分布并不均匀。再往细了看占用高的卡上 CUDA context 和临时缓冲区特别大。第二步我看了 vLLM 的启动日志发现 KV cache 显示有约 18GB 每卡的空间但并发量一上来KV cache 池消耗极快。原因在于max-model-len设了 32768——但我们的业务请求平均不到 3K token绝大多数 KV cache 都在“待命”完全浪费了。第三步我把业务侧的请求日志拉出来统计了 P99 长度大约是 5120 token。这意味着max-model-len完全没必要设 32768设 8192 或者 6144 足矣。5.3 调整方案与效果对比最终我做了三个改动--max-model-len从 32768 改成 8192--gpu-memory-utilization从 0.9 降到 0.85--max-num-seqs从 256 降到 128。调整后再看 KV cache 可用块数比原来多了将近 3 倍。高峰时段的显存占用稳定在 65GB 到 72GB 之间再也没出现 OOM。这个案例想说明的其实就是一点OOM 不完全等于硬件不够很多时候是参数和业务场景不匹配让显存被无效预留吃掉了。做容量规划时优先从业务实际需求反推参数而不是从模型理论上限往上顶。6. 多模型与多实例部署的显存分配实战现在很多团队都会同时部署多个模型比如一个通用对话模型、一个 embedding 模型、一个 RAG 用的 rerank 模型。vLLM 支持多模型服务但显存分配不好照样天天 OOM。6.1 同卡多模型的预算分配原则在同卡跑多个 vLLM 实例核心原则是每个实例的gpu_memory_utilization加起来不要超过 0.8 到 0.85剩下 15% 到 20% 留给 CUDA context、驱动、临时张量和系统开销。如果你给两个实例分别设 0.5 和 0.4加起来 0.9表面上看没问题但两个实例各自还要吃 CUDA context 和激活值的开销叠加后很容易超过物理限制。我自己的经验是同卡多实例时每个实例的gpu_memory_utilization之和控制在 0.7 比较稳。比如一张 80GB 的卡跑 7B 模型和 1.5B 模型7B 那个给 0.41.5B 那个给 0.3剩下 30% 作为安全余量。虽然看起来浪费但至少不会半夜突然 OOM。6.2 多模型服务时的显存隔离手段除了控制总预算多模型部署还建议在启动命令里加上--max-model-len的区别化配置。大模型可以给长上下文小模型给短上下文就行避免小模型也去抢一堆 KV cache。另外可以启用 vLLM 的--enable-prefix-caching功能对于共享前缀的请求比如 system prompt 固定的场景KV cache 可以跨请求复用。这样能显著降低长 prompt 场景下的 KV cache 消耗也是减少 OOM 的一个实用手段。6.3 纯 CPU 模式和 Lean Studio 的对比思考互联网上经常有人问 vLLM 的纯 CPU 模式以及 Lean Studio 和 vLLM 的区别。这里顺带说一句vLLM 确实支持 CPU 推理通过--device cpu启动好处是不依赖 GPU 就能跑但推理速度比 GPU 慢一到两个数量级KV cache 放在内存里也会吃大量 RAM仅适合开发测试和小规模内部使用生产环境基本不推荐。至于 Lean Studio它在某些方面更类似 vLLM 这类推理服务框架但侧重的是本地轻量化部署和模型管理对显存的调度策略不如 vLLM 精细。一旦上了规模显存优化的差距就会体现出来。这也是为什么 vLLM 在“高并发 大模型 长上下文”的组合里依然是首选。7. 常见问题与避坑速查表为了方便你排查我把实际工作中遇到最多的 OOM 相关问题整理成了一张速查表遇到问题对着找就行。场景表现根因方向首选解法启动即 OOM模型权重就占满了硬件显存不足 / 精度太高换大卡 / 量化模型 / 多卡 TP短请求正常长上下文或高并发 OOMmax_model_len 过大 / KV cache 池不够降低 max_model_len / 调大 gpu_memory_utilization谨慎服务跑一段时间后随机 OOM并发临时张量峰值 / 显存碎片调低 max_num_seqs / max_num_batched_tokens同卡多模型第二个实例起不来显存预算没有合理分配所有实例 gpu_memory_utilization 之和控制在 0.7 左右多卡 TP 时某张卡先爆显存卡间显存不均 / 负载不均衡改用同规格卡 / 减 TP size / 检查业务侧长短请求混合度日志里出现 NoAvailableMemoryForCacheKV cache 池被打满提高 gpu_memory_utilization / 降低 max_model_len / 降低并发想要 CPU 跑 vLLM无 GPU 或 GPU 显存不够--device cpu但接受慢速内存要够大7.1 几个容易被忽略的关键细节第一vLLM 升级版本后默认行为可能变化。比如 0.4.x 到 0.5.x、0.6.x 之间KV cache 的分配策略、显存对齐方式都有改动老配置换到新版本可能莫名其妙多占几百 MB 到 1GB 显存。生产环境升级前先在测试环境对比显存占用的变化。第二别用CUDA_VISIBLE_DEVICES乱指定 GPU。如果你设置CUDA_VISIBLE_DEVICES0后用 vLLM 启动显存计算是基于 0 号卡来的没问题但如果设置成CUDA_VISIBLE_DEVICES0,1而只用一张卡做 TP1vLLM 可能会在两张卡上都初始化 CUDA context白白吃掉多余的显存。建议要么物理隔离 GPU要么明确指定--tensor-parallel-size。第三OOM 后服务重启不是“重启就完事”。vLLM 异常退出后显存可能不会立刻完全释放有时候等几秒甚至几十秒才恢复。快速重启脚本里最好加上sleep 5或者提前用nvidia-smi --query-compute-appspid,used_memory --formatcsv检查是否有僵尸进程占用显存。第四长文本场景下prefix caching 的收益非常明显。我们线上把 system prompt 固定下来开启--enable-prefix-caching后KV cache 占用直接下降了 40% 多OOM 的发生频率也大幅降低。这个参数在很多中文教程里被忽略但真的非常实用。8. 最后分享一个我自己多次踩坑后的体会说实话vLLM 跑出CUDA out of memory这件事等你真的把显存分配机制搞懂了会发现 80% 的 OOM 都是“参数和环境不匹配”造成的而不是硬件不够。我自己最早也被这个问题折磨得不行后来养成一个习惯每次新部署一个模型先花十分钟做一次显存容量预测再动手写配置。具体做法很简单拿到一个模型先查三个数权重大小、精度位数、计划并发和上下文长度。然后用一个小脚本估算出各项占用的总和再对照显卡显存做加减法。这样配置参数时心里有底不会一遍遍试错试到怀疑人生。vLLM 的活跃社区和官方文档也在快速迭代如果你用的版本比较新多看看 release note 里的显存相关改动有时候一个小版本更新就能解决困扰你一周的问题。
返回列表