4、常见问题一:显存泄漏——现象、根因分析、排查工具

显存泄漏,说白了就是你的程序“借了不还”。

GPU显存是有限的,你申请了一块,用完得释放。如果只申请不释放,显存就会慢慢被吃光。我见过不少团队,模型推理跑着跑着就OOM了,重启一下又好一阵子,过一会儿又崩。嗯,十有八九就是显存泄漏。

4.1 现象:你该怎么判断?

显存泄漏的症状其实挺明显的。我个人习惯用nvidia-smi盯着看。如果你发现:

  • 显存只增不减:每次推理请求进来,显存占用就往上跳一跳,但请求结束后,占用不回落。
  • OOM(Out of Memory):跑着跑着,程序直接报错,说显存不够了。你想想看,如果一开始能跑,跑了几百次后崩了,那大概率是泄漏。
  • 服务响应变慢:显存快满的时候,GPU可能会触发内存交换,或者频繁GC(垃圾回收),导致推理延迟飙升。
⚠️ 注意: 有一种情况容易混淆——显存碎片化。碎片化也会导致OOM,但显存总量没变。泄漏是总量在涨,碎片化是总量不变但分配不出连续块。用nvidia-smi看总占用就能区分。

4.2 根因分析:为什么会泄漏?

我排查过很多次,总结下来,显存泄漏的根因主要有这几类:

根因类别 具体表现 常见场景
Python对象引用未释放 Tensor、Variable等对象被全局变量或闭包持有,无法被GC回收 在循环中不断创建新tensor,却赋值给全局list
PyTorch计算图累积 开启了requires_grad=True,但没有detach()或zero_grad() 推理时误用了训练模式,或者忘记with torch.no_grad()
CUDA缓存未清空 PyTorch的CUDA缓存分配器会缓存已释放的显存块,但不会立即归还给OS 频繁创建和销毁tensor,缓存越积越多
第三方库/自定义算子泄漏 C++扩展或自定义CUDA kernel中,手动分配的显存没有释放 使用了cudaMalloc但忘了cudaFree

我曾经遇到过一个案例:一个同事在推理服务里用了torch.no_grad(),但他在循环里把每个batch的中间结果append到了一个全局列表里,说是为了“后续分析”。结果这个列表越来越大,显存直接爆了。嗯,这就是典型的Python对象引用泄漏。

4.3 排查工具:怎么找到泄漏点?

排查显存泄漏,我一般按这个顺序来:先用nvidia-smi看现象,再用py-spy或torch.cuda.memory_summary()定位代码。

4.3.1 nvidia-smi:第一道防线

这个命令大家应该很熟了。但排查泄漏时,我建议你加上-l参数持续监控:

# 每1秒刷新一次,显示显存使用
nvidia-smi -l 1

# 只看某个进程的显存
nvidia-smi --query-compute-apps=pid,used_memory --format=csv -l 1

我个人习惯开两个终端:一个跑nvidia-smi -l 1,另一个跑推理请求。如果看到Used Memory列在请求结束后不下降,或者持续上升,那基本实锤了。

💡 小技巧: 用watch -n 1 nvidia-smi也可以,但-l更直观。另外,注意看Volatile GPU-Util,如果显存涨但利用率低,可能是泄漏而非计算密集。

4.3.2 py-spy:定位Python代码中的泄漏

nvidia-smi只能告诉你“泄漏了”,但不知道是哪行代码。这时候py-spy就派上用场了。它是一个采样分析器,可以查看Python进程的调用栈和内存分配。

安装很简单:

pip install py-spy

排查泄漏时,我通常这样用:

# 查看进程的调用栈,找出显存分配热点
py-spy record -o profile.svg --pid <你的进程PID> --duration 30

# 或者实时查看
py-spy top --pid <你的进程PID>

它会生成一个火焰图(SVG文件),你可以在浏览器里打开。火焰图里越宽的条,表示那个函数占用的时间或内存越多。如果看到某个函数反复出现,而且宽度在增长,那很可能就是泄漏点。

我记得有一次,我用py-spy发现一个preprocess()函数里,每次请求都创建了一个torch.Tensor并赋值给self.cache,但旧对象没有被释放。嗯,找到问题后,改成局部变量就解决了。

4.3.3 PyTorch内置工具:torch.cuda.memory_summary()

如果你用的是PyTorch,它自带一个很强大的显存分析工具:

import torch

# 在怀疑泄漏的地方打印显存快照
print(torch.cuda.memory_summary())

# 或者更细粒度地查看
print(torch.cuda.memory_stats())

这个函数会输出当前进程的显存分配详情,包括:

  • 当前分配的总显存
  • 峰值显存
  • 缓存大小(PyTorch内部缓存)
  • 每个tensor的大小和地址

我一般会在推理请求的前后各打一次memory_summary(),对比current_allocated_bytes字段。如果请求结束后这个值没有回到请求前的水平,那就有泄漏。

🔍 实战排查步骤:
  1. 用nvidia-smi -l 1确认显存持续增长。
  2. 用py-spy top --pid <PID>找到最活跃的函数。
  3. 在可疑函数前后加torch.cuda.memory_summary(),对比显存变化。
  4. 检查是否有全局变量、闭包或list.append持有tensor引用。
  5. 确认是否忘了with torch.no_grad()或.detach()。

4.4 避坑指南

我曾经踩过一个坑:用torch.cuda.empty_cache()来“解决”泄漏。这个函数只是清空PyTorch的缓存,但不会释放Python对象持有的显存。说白了,它治标不治本。真正的泄漏,必须从代码层面切断引用。

另外,如果你用了torch.jit.script或torch.nn.DataParallel,也要小心。这些模块有时会持有额外的显存引用,尤其是在多进程场景下。我建议在排查时,先简化模型,去掉这些包装,看看泄漏是否消失。

嗯,最后说一句:显存泄漏的排查,耐心比技术更重要。一步步缩小范围,总能找到那个“借了不还”的家伙。

显存泄漏排查流程 1. 现象确认 nvidia-smi -l 1 2. 定位代码 py-spy top --pid 3. 细粒度分析 memory_summary() 根因类型 Python对象引用 全局变量/闭包持有 计算图累积 忘用no_grad/detach CUDA缓存未清 PyTorch缓存分配器 自定义算子泄漏 cudaMalloc未free ✅ 修复:切断引用 / 释放显存
📌 总结: 显存泄漏的排查,核心就是“观察现象 → 定位代码 → 分析根因 → 修复”。工具只是辅助,关键还是理解Python和PyTorch的内存管理机制。多练几次,你也能一眼看出问题。
可可精品资料库