第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?

我个人习惯看两点:

  1. 是否出现在特定batch size上:换个更小的batch size就不崩了,那大概率是显存问题。
  2. 是否复现稳定:每次都在同一个地方崩溃,可能是代码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图帮你理清诊断逻辑

下面这张图总结了从观察现象到定位根因的流程。我自己调试显存问题时,脑子里跑的就是这个判断树:

显存不足的三种表现与诊断路径 观察现象 OOM报错 看日志:allocated & reserved 区别是否缓存碎片导致 程序崩溃 / Segfault 检查Killed / Segfault日志 排查OOM Killer / 温度 NaN / 训练中断 检查loss是否突变NaN 排查NCCL超时日志 确认根因:实际占用 vs 碎片 vs 临界态 采取行动:调batch / 清缓存 / 换硬件

2.5 一张表总结三种表现的识别要点

表现类型 日志特征 关键排查命令/方法 典型根因
OOM报错 RuntimeError: CUDA out of memory
ResourceExhaustedError
nvidia-smi 查看显存占用
对比allocated和reserved差异
缓存碎片 / 实际超标
程序崩溃 Killed
Segmentation fault
无任何CUDA错误堆栈
dmesg | grep -i oom
nvidia-smi -q -d TEMPERATURE
系统OOM Killer / GPU驱动重置
训练中断/NaN loss变为NaN或Inf
NCCL timed out
在训练脚本中加 torch.isnan(loss).any() 检查
查看TF日志中的BFC分配器报错
软OOM / 梯度范数爆炸 / 通信超时

嗯,到这里你应该能清楚区分三种显存不足的「死法」了。

个人建议:调试时先定位是哪种表现,然后再去针对性看日志。不要上来就调batch size,可能根本就不是实际量的问题。

记住一句话:OOM报错是显存满了,崩溃是驱动崩了,NaN是临界态爆了。三种情况对应完全不同的解决方案,下几章我会挨个讲怎么动手修。