2、PyTorch显存分配器探秘:深入理解PyTorch的缓存分配器工作原理,学会使用torch.cuda.memory_summary()诊断工具。

说实话,很多同学跑深度学习模型时遇到OOM(显存溢出),第一反应就是“显存不够,换卡”。但我在实际项目中排查过上百次OOM问题,发现至少有一半的情况,并不是真的显存不够,而是不会用显存。

PyTorch的显存分配器,就是那个在幕后帮你管理显存的小管家。你每次调用.cuda()或者把张量放到GPU上,都是它在干活。但这个小管家有个“小毛病”——它喜欢囤货。

2.1 缓存分配器:为什么显存只增不减?

先看一个现象。你写个简单的代码:

import torch
a = torch.randn(1000, 1000).cuda()
print(torch.cuda.memory_allocated())  # 当前实际使用的显存
print(torch.cuda.memory_reserved())   # 分配器预留的显存

你可能会发现,memory_reserved比memory_allocated大得多。为什么会这样?

PyTorch的分配器,说白了就是一个“缓存池”。它从CUDA驱动那里申请一大块显存,然后自己切成小块分给你用。当你释放张量时,它并不把显存还给CUDA,而是留在自己的池子里,等下次再用。

我个人习惯把这个机制叫做“显存囤积症”。它这么做是有道理的——每次向CUDA申请显存都要经过内核态切换,开销很大。缓存起来可以加速后续分配。但代价就是,你看到的nvidia-smi显存占用,往往比实际需要的多。

核心结论:PyTorch的memory_allocated才是你模型真正占用的显存。memory_reserved是分配器预留的缓存,两者之差就是“浪费”的部分。

2.2 分配器的工作流程:一张图看懂

我画了一张流程图,帮你理清分配器的内部逻辑。你看完就明白为什么显存会“只增不减”了。

PyTorch显存分配器工作流程 用户请求分配显存 检查缓存池是否有空闲块 有 没有 从缓存池中取出空闲块 向CUDA驱动申请新显存 返回显存地址给用户

你看,当用户释放张量时,分配器并不会把显存还回去,而是放回缓存池。这就是为什么你明明释放了张量,nvidia-smi显示的显存占用却没变。

2.3 诊断工具:torch.cuda.memory_summary()

好了,理论讲完了。现在说说怎么诊断。我最常用的工具就是torch.cuda.memory_summary()。这玩意儿比nvidia-smi详细得多。

用法很简单:

import torch

# 在训练循环中插入
model.train()
for batch in dataloader:
    outputs = model(batch)
    loss = criterion(outputs, targets)
    loss.backward()
    optimizer.step()
    
    # 打印显存快照
    if batch_idx % 100 == 0:
        print(torch.cuda.memory_summary())

输出会包含这些关键信息:

字段 含义 我的经验
Allocated 当前实际使用的显存 这个值才是模型真正占用的
Reserved 分配器预留的缓存 如果这个值远大于Allocated,说明缓存浪费严重
Free 设备上剩余的空闲显存 注意,这个值可能为0,但实际还有缓存可释放
Segment 分配器向CUDA申请的显存段 每个段默认2MB,大张量会申请大段

小技巧:如果你发现Reserved比Allocated大很多,可以调用torch.cuda.empty_cache()手动释放缓存。但注意,这会影响后续分配速度。我个人习惯在验证阶段调用一次,训练阶段尽量不碰。

2.4 实战:用memory_summary()定位OOM

我曾经接手过一个项目,模型在训练到第3个epoch时总是OOM。代码看起来没问题,没有显式泄漏。我加了memory_summary()后,发现了一个有趣的现象:

# 第1个epoch结束时的显存状态
Allocated: 4.2GB
Reserved:  6.8GB
Free:      1.2GB

# 第2个epoch结束时的显存状态
Allocated: 4.3GB
Reserved:  8.5GB
Free:      0.0GB

看到没?Allocated几乎没变,但Reserved一直在涨。这说明分配器在不断申请新缓存,但旧的缓存没被回收。最终Free变成0,新请求触发OOM。

问题出在哪里?我检查了数据加载部分,发现每个epoch都会创建新的DataLoader实例,里面有一些临时张量没有被正确释放。修复后,显存就稳定了。

避坑指南:我曾经遇到过一种情况——memory_summary()显示一切正常,但程序还是OOM。后来发现是CUDA版本和PyTorch版本不匹配导致的分配器bug。如果你遇到类似问题,先试试升级PyTorch到最新版。

2.5 分配器的高级配置

PyTorch的分配器其实提供了几个环境变量,可以调整它的行为。我整理了一下:

环境变量 作用 建议值
PYTORCH_CUDA_ALLOC_CONF 配置分配器策略 max_split_size_mb:128
CUDA_LAUNCH_BLOCKING 同步CUDA操作(调试用) 1(仅调试时开启)

举个例子,如果你发现分配器频繁进行小块显存的分配和释放,可以限制最大分割块大小:

import os
os.environ['PYTORCH_CUDA_ALLOC_CONF'] = 'max_split_size_mb:128'

这样分配器就不会把大块显存切得太碎,减少了碎片化。嗯,这里要注意,这个配置要在导入torch之前设置才有效。

2.6 总结一下

PyTorch的显存分配器,说白了就是一个“缓存池”机制。它帮你省去了频繁向CUDA申请显存的开销,但也带来了显存“虚高”的问题。

  • 记住两个指标:memory_allocated是真实占用,memory_reserved是缓存占用。
  • 用好诊断工具:memory_summary()能帮你看到分配器内部的细节。
  • 遇到OOM别慌:先看看是不是缓存没释放,再考虑换卡。

你想想看,如果你能看懂分配器在干什么,很多显存问题其实都能自己解决。下一章我会讲如何用torch.cuda.set_per_process_memory_fraction限制显存使用上限,这个在生产环境中特别有用。

可可精品资料库