资讯详情

显存显示有余量却报CUDA OOM?GPU服务器显存排查全指南

📅 2026/11/2 12:39:58 | 华诺云谱 👁 阅读
显存显示有余量却报CUDA OOM?GPU服务器显存排查全指南
租GPU服务器跑模型我隔三差五就会收到几乎一模一样的求助“日志里报CUDA OOM可nvidia-smi看显存明明还剩二十多G这机器是不是被坑了”这句话本身不算错但“显存有余量”和“这段显存能用”完全是两码事。nvidia-smi显示的是物理显存的当前占用情况而CUDA分配器在申请内存时看的是能不能拿到一段连续、可用、且符合当前上下文的显存块。这篇文章不写空理论就按我在租用GPU服务器、低显存跑模型时踩过的真实场景来讲那些“余量”到底被谁偷走了为什么2MB都分配不出来以及遇到OOM时应该按什么顺序排查。1. nvidia-smi 显示“显存够用”和 CUDA 能分到显存是两码事1.1 你在nvidia-smi里看到的“空闲”不一定是你能用的空闲先做一个最基础的区分。nvidia-smi里每一张卡的Memory-Usage表示当前这张卡上所有CUDA上下文一共占了多少显存。这个数字是物理视角的全局数字。但如果这台GPU服务器本身是多人共享的或者你跑在容器里那么“能看到的显存”和“你能申请到的显存”会存在偏差。最常见的情况是容器里看到宿主的整张卡你启动了一个Docker容器容器里执行nvidia-smi它依然会报告这张物理GPU的总显存比如80GB。但运行时真正允许你这个进程使用多少显存可能被容器runtime、调度平台或者MIG实例限制在某个配额以内。当你的进程申请超过配额时CUDA立刻返回out of memory而nvidia-smi从容器内部看过去可能还显示“空闲60GB”。类似的还有MIG模式。一张物理GPU被切成分区nvidia-smi会给你展示计算实例的名称和内存上限。此时“总显存”已经是切片后的数字但如果你没注意上下文依然会觉得“剩余量还很多怎么会OOM”。所以在服务器上排查这种问题第一步永远是问自己一个问题这个OOM是物理显存不足还是配额不足如果是配额不足那么即使物理显存空出一半你也一样拿不到。1.2 一个“2MB都分配不出来”的真实案例我在调试大模型推理时见过一条特别有代表性的报错CUDA out of memory. Tried to allocate 2.00 MiB当时的nvidia-smi显示GPU还有接近30GB空闲。很多人的第一反应是驱动坏了、显存虚标。但真正的原因不是“缺2MB”而是CUDA分配器在虚拟地址空间里找不到一段连续的2MB区域。这里有个容易混淆的概念显存不只是物理容量还有一个虚拟地址空间的维度。当程序反复分配、释放不同大小的显存块之后剩下的物理显存总量可能很大但这些物理页被映射到地址空间时被大量活动块切碎了。申请2MB听着很小可如果那30GB空闲都被打散成几百KB到1MB的小碎片那2MB的连续请求一样会失败。你可以把GPU显存想象成一个停车场。余量很大不等于有一个能停大巴车的空车位。如果整个停车场到处都是几米长的死角一辆12米长的客车就是进不去倒车也不行。显存OOM很多时候不是“地方不够”而是“连续的地方不够”。1.3 系统内存和GPU显存的OOM也不要混在一起还有一坑。很多人说“OOM”但日志里写的是“cannot allocate memory”这其实可能是宿主机系统内存不足而不是GPU显存不足。尤其是在低显存部署场景里你需要把一部分层offload到CPU或者使用pinned memory做异步拷贝。如果CPU内存不够CUDA在准备H2D拷贝缓冲区时同样会报类似“out of memory”的错误。看到的词都一样但解决方式完全不同。所以先别急着骂显卡。看到OOM第一件事是确认报错来自GPU驱动/CUDA层还是来自操作系统内存分配层。两者都在字面上叫“OOM”但一个是显存一个是内存。2. PyTorch 缓存分配器显存被“扣住”的常见原因2.1 PyTorch 不是按需向驱动申请显存而是提前“囤地”在PyTorch里用GPU绝大多数情况下你会用到它的缓存分配器。这个分配器的核心逻辑很务实cudaMalloc是昂贵操作不能每次创建张量都向CUDA申请一次。所以分配器会在第一次需要显存时一次性向驱动申请一大块然后在进程内部把这块内存切成小块给张量用。这样做的好处是速度快坏处是当一个张量被释放时它的显存并不是真正还给GPU而是留在PyTorch的缓存池里备用。所以nvidia-smi看到的使用量往往大于你当前“存活张量”的总大小。比如推理时前向传播过程里有一个巨大的临时张量占用了10GB。计算完成后这个临时张量被销毁但PyTorch没有把这10GB还给CUDA。此时nvidia-smi里依然显示这张卡用了10GB。如果之后你又要申请一个3GB的缓存块而当前空闲只有2GBCUDA就报OOM尽管这10GB里明明有8GB是可以被PyTorch“还回去”的。很多人在这里就懵了明明才刚释放过显存怎么还是OOM2.2 allocated和reserved是两个完全不同的数字排查PyTorch显存问题时我最先看的不是nvidia-smi而是PyTorch自带的显存统计。在Python里执行import torch print(torch.cuda.memory_summary(devicecuda:0)) print(torch.cuda.mem_get_info(devicecuda:0))memory_summary里会明确分开两个关键数字Allocated memory当前真正被张量占用的显存大小。Reserved memoryPyTorch分配器从CUDA手里申请到的显存大小其中一部分正躺缓存池里待复用。如果Allocated只有2GBReserved却有10GB那你的显存明显被缓存分配器“扣住”了8GB。nvidia-smi看到的占用大概率接近Reserved而不是Allocated。此时调用一次torch.cuda.empty_cache()能释放掉缓存池里所有“当前没在使用”的块让Reserved回落到接近Allocated。要注意这只对PyTorch内部缓存有效不能解决碎片问题。如果那8GB被切成几百块散落在地址空间里释放后能不能合成一个完整的连续区域还要看驱动和分配器的处理。2.3 峰值分配决定了PyTorch“敢不敢”预留显存还有一种更隐蔽的情况PyTorch分配器会根据历史峰值保留一个“够用”的缓存池。它不会傻到把所有释放的块全部还给驱动因为那样下次遇到同样峰值时会再次触发昂贵的cudaMalloc。所以哪怕你在两次推理之间手动做了empty_cache只要某个峰值分配出现过Reserved就不会一下归零。这也是为什么我不建议用“nvidia-smi当前空闲”来判断模型能不能跑。要看的是负载周期内的峰值。你的模型可能在某个瞬间需要15GB绝大部分时间只占5GB。如果只看平时快照会以为5GB就够但一到那个瞬间15GB申请失败OOM来了。2.4 两个环境变量能救回不少“假OOM”PyTorch从1.9左右开始提供了显存分配策略环境变量对碎片化问题特别有用的有两个PYTORCH_CUDA_ALLOC_CONFexpandable_segments:True这个配置会让分配器使用可扩展段而不是一次性申请一个大块。它更倾向于动态扩展已有段显著降低虚拟地址碎片。深度学习训练、长序列推理场景里这个参数是我第一个尝试的效果好过硬调batch size。另一个参数是PYTORCH_CUDA_ALLOC_CONFmax_split_size_mb:128它的作用是控制大块缓存被二次切分的阈值。主要场景是一个大块被反复切分成各种尺寸的小块导致很多块既不能完整满足新请求又因为太小无法归还给驱动。把它调大能减少这种“碎块踩踏”但不同模型表现不一样建议作为第二个试验项。如果你用的是TensorFlow也有对等选项import tensorflow as tf gpus tf.config.experimental.list_physical_devices(GPU) if gpus: tf.config.experimental.set_memory_growth(gpus[0], True)开启后TF不再一把梭哈把整张卡预占而是按需增长。副作用是运行中可能遇到更多碎片对常规推理没有大问题。3. 模型显存占用权重只是冰山一角KV Cache和激活值才是凶手3.1 从参数到权重的第一层显存计算很多人会拿“模型参数量 × 精度字节数”来估算显存这个思路没错但不完整。先看最简单的推理场景精度每参数字节数7B模型权重大致占显存FP32428GBFP16/BF16214GBINT817GBINT4 / NVFP40.53.5GB这张表只能告诉你“权重裸大小”。真正的显存需求还得加上KV Cache、激活值、框架预分配的计算缓冲区和CUDA上下文。把这四类一起算进去才是一个能落地的数字。训练场景更夸张。除了权重还要存梯度和优化器状态。Adam优化器在混合精度训练下每个参数至少要额外吃掉8到12字节最终总占用往往是“权重占显存”的三到四倍。如果你拿着推理的估算去开训练任务不OOM才怪。3.2 KV Cache为什么可以瞬间吃光“剩余显存”大语言模型推理时随着生成token数量的增长KV Cache是线性增长的。它的计算公式大致是KV Cache大小 ≈ 2 × 层数 × KV头数 × 头维度 × 精度字节数 × 当前序列长度 × batch这里面的“2”对应K和V两份缓存。举个例子一个32层、每个KV头128维、8个KV头的模型用FP16存KV Cache每个token大约需要2 × 32 × 8 × 128 × 2 / 1024 / 1024 ≈ 0.125MB看着不大但换算到不同上下文长度上下文长度KV Cache占用4K512MB32K4GB128K16GB很多低显存运行模型的任务权重通过INT4量化能塞进8G卡但一旦你想把上下文从8K拉到32KKV Cache直接多出几GB。这个多出来的部分就是运行中申请失败的常见火药桶。显存快照在生成前看还有4GB空闲跑到第40轮对话忽然OOM大多数时候就是KV Cache在作祟。3.3 临时计算缓冲区和“预占”的坑除了KV Cache推理框架一般还会为算子分配临时计算缓冲区。比如某些注意力算子和归一化步骤需要在运行时申请一块大小和batch、序列长度、head维度挂钩的scratch memory。这些缓冲区不一定全程存在但会在计算的那一瞬间申请最大块。更常见的是框架“预占”。像llama.cpp这类推理引擎启动时就会根据--ctx和--batch提前把KV Cache和compute buffer一次性分配好。你以为模型很小所以不会OOM但框架为了最大化吞吐直接把未来可能用到的显存全锁定了。此时nvidia-smi显示“还剩一点”可一旦有新的连续大块申请需求就立刻失败。所以模型显存规划不能只算权重。至少要按下面这个公式来预估总显存需求 权重占用 KV Cache上限 激活峰值 框架固定开销算完还要再留10%到20%余量因为CUDA上下文、驱动状态、多进程共享都会额外吃掉一点。4. MoE、量化与低显存部署为什么省了计算却没省显存4.1 MoE的“稀疏”省的是计算不是显存最近关于MoE架构的低显存讨论特别多有一个误区反复出现既然模型每次只激活Top-K个专家那是不是可以把其他专家不加载进显存答案是否定的。MoE模型的“稀疏激活”指的是推理计算量只作用于少数专家但权重加载量依然是全部参数。模型的总显存占用不会因为路由只挑了2个专家而变小。你可以把MoE想象成一家大食堂100个厨师都站在备菜区一桌客人最多同时让3位厨师做菜但你不能确定下一个客人会点哪个厨师的拿手菜所以这100个人都必须在岗位上。除非你做专家级offload把一部分专家放在CPU内存里需要时再流式换入否则显存占用和同参数量的Dense模型在一个量级。这也是为什么有些宣传“8G显存就能跑”的MoE模型实际用起来还是会OOM。它们可能压低了权重精度、缩短了上下文、控制了batch但只要你把其中一项放宽显存就会立刻炸掉。4.2 量化降的是权重不是所有中间产物量化模型能大幅降低权重占用这是好事。比如一个70B模型FP16权重要140GBINT4后大约35GB这差距非常可观。但要注意量化只压缩静态参数。KV Cache、中间激活值、算子临时缓冲区绝大多数情况下还是以FP16或BF16的精度计算不会因为权重变成INT4就自动减半。“NVFP4量化显存要求”这类说法如果只标了权重占用那是不完整的。你看到的“需要6G/8G显存”很可能只算了量化权重没算KV Cache和推理框架的compute buffer。实际部署时8G卡跑一个权重只占4G的模型依然可能因为上下文太长、batch太大而OOM。低显存部署第一步不是“调节多少层量化”而是先明确自己的上下文长度和batch。把KV Cache上限确定下来再反推能放得下多少权重。否则很容易陷入“量化好了但一加长上下文就崩”的循环。4.3 低显存跑模型的正确姿势算峰值然后给峰值留15%以上我自己在6G和8G卡上跑模型习惯按下面顺序估算确定目标上下文长度算出KV Cache上限。估算推理框架的临时缓冲区通常按1GB到2GB预留。用量化后权重大小往显存里套。三者相加后再除以0.85相当于至少留15%缓冲。如果算出来超过显卡容量优先砍上下文、砍batch而不是继续强行把权重压得更狠。权重压到4bit之后再往2bit压精度和稳定性都会明显受影响反而可能触发更多推理bug。在低显存卡上“够用但紧张”从来不是很好的状态。Model只要能跑步调还稳比硬塞进去天天OOM强得多。5. 一份可以直接抄的 OOM 排查链路5.1 先确认报错来自哪一层遇到OOM时立刻区分三种情况CUDA层报错CUDA out of memory但进程还在只是无法继续申请。系统内存层报错RuntimeError: Cannot allocate memory可能是宿主机CPU内存不够。进程被直接杀死Killed这种情况多半是系统OOM Killer动手了不一定是显存。如果是第三种去看看系统内存和swap。特别是在offload场景下几十层模型被塞进CPU内存CPU内存本身不够时GPU任务会直接连同进程一起被干掉。表现形式很像是“显存莫名其妙OOM”实际根因完全不在GPU上。5.2 查进程别放过“看不见的占用者”在共享租用的GPU服务器上OOM可能是别人的进程造成的。先看全局nvidia-smi如果有权限再查具体计算进程nvidia-smi --query-compute-appspid,process_name,used_memory --formatcsv这里有个细节容器内PID命名空间隔离你在容器里看到的PID未必是宿主机的真实PID。但至少能判断出“这张卡上有多少个计算进程”。如果发现一个不属于你的进程占着一半显存那OOM的原因就找着了。这时候不是优化代码的问题而是需要跟服务商确认这张卡是不是真的独占。5.3 用PyTorch内存报告区分“真不够”和“假不够”跑代码前先执行import torch print(torch.cuda.memory_summary(devicecuda:0))把输出里的Reserved和Allocated对比一下。如果Reserved明显大于Allocated优先试试empty_cache再配合PYTORCH_CUDA_ALLOC_CONFexpandable_segments:True跑一次。如果是“真不够”memory_summary里通常会显示当前申请的最大块大小然后告诉你分配失败。这个失败信息特别有用——注意它给出的数字比如“Tried to allocate 4.00 GiB”。如果你看到申请的是4GB而显存只满到80%那大概率不是总容量问题而是碎片或Reserved占用问题如果你申请的是一个接近剩余总量的数字那才是显存不足。5.4 用“减半法”锁定谁是凶手当模型各种参数混在一起时最快定位峰值来源的办法是逐个减半把batch size减半如果OOM消失说明激活值或batch相关缓冲区是主因。把上下文长度减半如果OOM消失KV Cache是主因。把并行度或者显存里的层数减半如果OOM消失权重加载是主因。把推理框架的n_batch调小如果OOM消失compute buffer是主因。每一步变化对应不同的显存消耗项。这个方法看起来笨但比盯着nvidia-smi猜要快得多。一次只改一个变量问题边界很快就清楚了。5.5 给PyTorch设一个显存上限“护栏”如果你在租用的共享卡上跑服务不想因为自己的进程吃满显存而影响别人也不想被别人影响可以用PyTorch的进程级显存比例import torch torch.cuda.set_per_process_memory_fraction(0.9, device0)意思是当前进程最多使用该GPU显存的90%。好处是让分配器更谨慎不会过度预留坏处是如果你原本正好需要95%那会有更多OOM。这个值更适合服务型任务而不是训练调试。6. 租用GPU服务器怎么提前把显存余量算明白6.1 租卡之前先按峰值算一次“最少显存”我不建议只看“这模型要20G显存所以我租24G卡”。正确的做法是把所有运行时峰值都算出来。下面这个简化估算可以当作模板params_b 7 # 参数量单位十亿 weight_gb params_b * 2 # FP16权重 kv_gb 4 # 目标上下文对应的KV Cache按你的场景估 act_gb 1.5 # 激活和临时Buffer推理一般留1~2GB peak weight_gb kv_gb act_gb safe peak / 0.8 # 留20%余量 print(f峰值约 {peak:.1f} GB建议租用显存 {safe:.1f} GB)如果是训练把优化器和梯度也加进peak。训练时的显存峰值往往比推理高好几倍千万不要拿推理的估算去卡训练。6.2 共享型租卡的三个提前问句租GPU服务器时我建议在下单前问服务商三个问题这张卡是独享还是共享nvidia-smi里能不能看到其他人的进程容器内能申请到的显存上限是多少有没有MIG/vGPU切片限制CPU内存给多少系统盘和swap是否够用于模型offload这三个问题能避免绝大多数“明明有显存却OOM”的诡异场景。很多人把锅甩给服务商最后发现是容器默认限制了显存或者卡本身被别人占了。6.3 多卡服务器上用CUDA_VISIBLE_DEVICES隔离你自己的进程如果你租到的是8卡机优先用环境变量把进程绑定到指定卡避免多个任务因为默认分配逻辑互相挤占export CUDA_VISIBLE_DEVICES0,1如果服务商支持计算模式设置也可以把卡设为独占模式防止其他租户强行分食nvidia-smi -c EXCLUSIVE_PROCESS需要root权限所以只有租用独立服务器时才用得着。容器的场景里容器runtime一般已经做了隔离不需要也不希望你直接改宿主机的GPU设置。6.4 监控显存时别信“瞬时空闲”要看时间曲线运行时可以用watch -n 1 nvidia-smi重点关注显存使用量的趋势。如果它在一个推理循环里反复上升到同一个峰值后回落那是正常波动如果它一步步爬升但不回落那就是显存泄漏。泄漏的典型来源是循环里随手创建的张量没释放、缓存列表越攒越多、或者某个框架的静态缓存没有及时清理。曾经排查过一个服务用户抱怨“跑久了必OOM”问题是每次推理逻辑里中途生成的中间结果被追加到了一个全局list里。看起来每个item很小但几千轮之后所有激活值全留在显存里。nvidia-smi每次空闲时看着都正常只有把时间线拉长才看出问题。这个教训总结成一句话显存余量是快照不是承诺。6.5 让我留在最后的一条个人经验我现在判断一台租用GPU服务器能不能跑某个任务早就不看“当前剩余多少显存”了。我会把任务分成三个数字权重需要多少、峰值需要多少、运行环境需要多少。前两个是模型决定的第三个是框架和容器决定的。只有三者都满足我才敢说“这台机器稳”。如果只是勉强塞进去那说明你离下一次OOM只差一个更长的上下文或者更大的batch。这也是为什么我强烈建议在低显存卡上跑模型时把余量当成“保险”而不是可压榨的空间。真等到只剩一两G才想起来优化上下文往往已经来不及了。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑