第2章:显存不足的典型表现——OOM错误、程序崩溃、训练中断的日志特征
显存不够用了,模型到底会怎么「死」?
很多人以为就是弹个红框报错。其实没那么简单。
我见过的情况大致分三类:直接OOM报错、程序默默崩溃、训练诡异中断。每一种的日志特征都不一样,排查思路也不同。
2.1 最常见的OOM报错
先聊最具代表性的——PyTorch的OOM日志。这是GPU显存耗尽时CUDA驱动主动抛出的异常。
2.1.1 PyTorch OOM日志长什么样?
RuntimeError: CUDA out of memory. Tried to allocate 2.00 GiB (GPU 0; 24.00 GiB total capacity; 21.32 GiB already allocated; 1.56 GiB free; 22.14 GiB reserved in total by PyTorch)
这段信息量很大。别光看第一行,后面括号里全是线索。
我拆开给你讲:
- Tried to allocate 2.00 GiB —— 这次分配尝试要2GB
- 24.00 GiB total capacity —— 你显卡总共就24GB
- 21.32 GiB already allocated —— 已经用了21.32GB
- 1.56 GiB free —— 看起来还有1.56GB空闲
- 22.14 GiB reserved in total —— 但PyTorch的缓存分配器占用了22.14GB
注意:already allocated和free是当前实际用量,reserved in total包含了缓存。这个「缓存」是PyTorch为了加速后续分配提前占着的地盘,但CUDA `cudaMalloc` 认为它已经被占用了。
关键判断点:
如果 reserved in total 远大于 already allocated,说明缓存碎片化严重。这时候即使总量 free 看起来不少,也会报OOM。
TensorFlow这边,日志风格不太一样:
ResourceExhaustedError: OOM when allocating tensor of shape [64, 3, 224, 224] and type float on /job:localhost/replica:0/task:0/device:GPU:0 by allocator GPU_0_bfc
它直接告诉你在分配哪个shape的tensor时挂了。形状 [64, 3, 224, 224] 就是你的batch大小、通道、高、宽。计算一下:64 × 3 × 224 × 224 × 4字节 ≈ 38.5MB。一个tensor而已,为什么会撑爆?说明当时显存已经被别的东西塞满了。
2.1.2 这些数字怎么读?
我建议你养成习惯:每次OOM都认真看报错里的第二个数字——total capacity和already allocated的差距。
举个例子:如果total是80GB,already allocated已经78GB,你是真满了。但如果只有40GB,那还有将近一半的空间,问题出在缓存和碎片上。
我的一个小技巧:
遇到OOM先跑一次 nvidia-smi 看看进程占了多少。然后设个环境变量 PYTORCH_CUDA_ALLOC_CONF=max_split_size_mb:128 试试。这个参数控制PyTorch缓存分配器切分内存块的大小,调小能缓解碎片。
2.2 程序直接崩溃,连报错都没有
比OOM更恶心的情况:程序突然没了,终端直接回到shell提示符,日志最后一行是某个正常的loss打印。
为什么会这样?
2.2.1 CUDA driver被干掉
Linux的OOM Killer机制会监控系统内存。一旦系统RAM不够,它会杀掉占用最高的进程。但如果是GPU显存爆了而系统RAM还好,驱动可能直接挂掉。
# 典型日志结尾:
Epoch 10, Batch 240, Loss: 2.354
Killed
# 或者:
Segmentation fault (core dumped)
Killed 和 Segfault 是最常见的「暴毙」信号。没有任何CUDA错误堆栈,因为CUDA驱动已经脱离控制了。
我曾经踩过的坑:
有一次模型训练到第17个epoch准时崩溃,但显存占用看起来没满。查了两天才发现是GPU散热不够,温度超过85°C后驱动自动重置了。所以遇到无征兆崩溃,先跑 nvidia-smi -q -d TEMPERATURE 看看温度。
2.2.2 怎么区分是显存问题还是代码bug?
我个人习惯看两点:
- 是否出现在特定batch size上:换个更小的batch size就不崩了,那大概率是显存问题。
- 是否复现稳定:每次都在同一个地方崩溃,可能是代码bug;随机在不同的batch崩溃,多半是显存波动或碎片导致的临界状态。
2.3 训练中断——最隐晦的表现
你想想看,训练跑了一天,loss曲线正常往下走,突然某一轮batch之后loss变成NaN,然后一直NaN下去……这算不算显存不足?算。
很多时候显存没到OOM阈值,但tensor的值已经溢出了。原因可能是某次计算用了过大的中间结果,超出了float16的范围。
2.3.1 NaN / Inf 的日志特征
# PyTorch NaN检测日志(手动添加hook)
[WARN] Batch 320: loss is NaN after loss.backward()
[WARN] Batch 321: loss is Inf after optimizer.step()
# TensorFlow默认检测到的NaN
2024-07-20 14:32:01.123456: W tensorflow/core/common_runtime/gpu/gpu_bfc_allocator.cc:42]
Allocator (GPU_0_bfc) ran out of memory trying to allocate 2.10GiB with freed_by_count=0.
The allocator had allocated 18.21GiB, 0B freed by the allocator, 0B free.
注意上面TF日志:虽然报了OOM,但训练不一定直接崩掉。TensorFlow的BFC分配器会在分配失败时尝试回收缓存,如果回收失败就报NaN,但进程可能继续跑。这就是所谓的「软OOM」——loss变成NaN,程序死不了,但训练废了。
如何识别这种软OOM?
- loss突然从0.3跳到NaN,且重启后不同步数才出现。
- 同一份代码、同一份数据,换张更大显存的显卡就正常了。
nvidia-smi显示GPU利用率降到0%,但进程还在。
2.3.2 另一种中断:通信超时
多卡训练时,显存不足可能导致某张卡计算太慢,触发NCCL超时。
RuntimeError: NCCL timed out.
This error is typically caused by:
- Not all ranks called into collective operation
- GPU memory pressure causing slow compute/node
你看,NCCL自己都说了:GPU memory pressure引起计算变慢。一张卡的显存快满了,计算速度拖后腿,其他卡等它完成allreduce,等到超时。
所以显存不足不仅影响单卡,还拖累整个分布式集群。我在项目中遇到过,一块被别的进程占掉2GB显存的卡,导致8卡训练每5分钟就断一次。
2.4 一只SVG图帮你理清诊断逻辑
下面这张图总结了从观察现象到定位根因的流程。我自己调试显存问题时,脑子里跑的就是这个判断树:
2.5 一张表总结三种表现的识别要点
| 表现类型 | 日志特征 | 关键排查命令/方法 | 典型根因 |
|---|---|---|---|
| OOM报错 | RuntimeError: CUDA out of memoryResourceExhaustedError |
nvidia-smi 查看显存占用对比allocated和reserved差异 |
缓存碎片 / 实际超标 |
| 程序崩溃 | KilledSegmentation fault无任何CUDA错误堆栈 |
dmesg | grep -i oomnvidia-smi -q -d TEMPERATURE |
系统OOM Killer / GPU驱动重置 |
| 训练中断/NaN | loss变为NaN或InfNCCL timed out |
在训练脚本中加 torch.isnan(loss).any() 检查查看TF日志中的BFC分配器报错 |
软OOM / 梯度范数爆炸 / 通信超时 |
嗯,到这里你应该能清楚区分三种显存不足的「死法」了。
个人建议:调试时先定位是哪种表现,然后再去针对性看日志。不要上来就调batch size,可能根本就不是实际量的问题。
记住一句话:OOM报错是显存满了,崩溃是驱动崩了,NaN是临界态爆了。三种情况对应完全不同的解决方案,下几章我会挨个讲怎么动手修。