4、常见问题一:显存泄漏——现象、根因分析、排查工具
显存泄漏,说白了就是你的程序“借了不还”。
GPU显存是有限的,你申请了一块,用完得释放。如果只申请不释放,显存就会慢慢被吃光。我见过不少团队,模型推理跑着跑着就OOM了,重启一下又好一阵子,过一会儿又崩。嗯,十有八九就是显存泄漏。
4.1 现象:你该怎么判断?
显存泄漏的症状其实挺明显的。我个人习惯用nvidia-smi盯着看。如果你发现:
- 显存只增不减:每次推理请求进来,显存占用就往上跳一跳,但请求结束后,占用不回落。
- OOM(Out of Memory):跑着跑着,程序直接报错,说显存不够了。你想想看,如果一开始能跑,跑了几百次后崩了,那大概率是泄漏。
- 服务响应变慢:显存快满的时候,GPU可能会触发内存交换,或者频繁GC(垃圾回收),导致推理延迟飙升。
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字段。如果请求结束后这个值没有回到请求前的水平,那就有泄漏。
- 用
nvidia-smi -l 1确认显存持续增长。 - 用
py-spy top --pid <PID>找到最活跃的函数。 - 在可疑函数前后加
torch.cuda.memory_summary(),对比显存变化。 - 检查是否有全局变量、闭包或
list.append持有tensor引用。 - 确认是否忘了
with torch.no_grad()或.detach()。
4.4 避坑指南
我曾经踩过一个坑:用torch.cuda.empty_cache()来“解决”泄漏。这个函数只是清空PyTorch的缓存,但不会释放Python对象持有的显存。说白了,它治标不治本。真正的泄漏,必须从代码层面切断引用。
另外,如果你用了torch.jit.script或torch.nn.DataParallel,也要小心。这些模块有时会持有额外的显存引用,尤其是在多进程场景下。我建议在排查时,先简化模型,去掉这些包装,看看泄漏是否消失。
嗯,最后说一句:显存泄漏的排查,耐心比技术更重要。一步步缩小范围,总能找到那个“借了不还”的家伙。