3、PyTorch显存管理机制:缓存分配器、cudaMalloc与显存池的真相
聊到显存溢出,很多人第一反应就是“我模型太大了”。但说实话,很多时候问题出在PyTorch怎么管显存,而不是你用了多少。我见过不少项目,模型明明不大,显存却莫名其妙爆了。今天我们就来扒一扒PyTorch的显存管理机制,看看它到底在背后干了什么。
3.1 PyTorch的缓存分配器:一个“小气”的管家
PyTorch不会每次需要显存都去找CUDA要。它自己养了一个“缓存分配器”(Caching Allocator)。这个分配器的工作方式,说白了就是:先借一大块,用完了不还,留着下次用。
为什么会这样?因为cudaMalloc这个操作很贵。每次调用cudaMalloc,都要和驱动层打交道,开销不小。PyTorch为了省事,干脆一次性多申请一些,放到自己的“显存池”里。你每次创建Tensor,它就从池子里划一块给你。你释放Tensor,它也不真还给系统,而是放回池子。
核心逻辑:PyTorch的显存管理 = 预分配 + 缓存复用 + 延迟释放
我个人习惯用torch.cuda.memory_summary()来查看当前分配器的状态。这个命令能告诉你:当前池子里有多少块显存、每块多大、哪些是活跃的、哪些是缓存的。
import torch
# 查看当前显存分配器的详细状态
print(torch.cuda.memory_summary())
# 输出示例(简化):
# Allocated: 2.5 GB
# Cached: 4.0 GB
# 这里 cached 就是池子里还没还给系统的部分
你看,Allocated是你真正在用的,Cached是池子里躺着的。两者差的那1.5GB,就是PyTorch“扣着不还”的部分。很多新手看到nvidia-smi里显存占用很高,就以为模型太大,其实可能只是缓存没清。
3.2 cudaMalloc与cudaFree:底层的那点事
PyTorch的缓存分配器底层,最终还是调用了CUDA的cudaMalloc和cudaFree。但调用时机和你想的不太一样。
- cudaMalloc:只在分配器“第一次”需要扩大池子时调用。之后池子够用,就不再调用了。
- cudaFree:几乎不主动调用。除非你显式调用
torch.cuda.empty_cache(),或者池子实在太大需要收缩。
我在项目中遇到过一个问题:训练循环里每次迭代都创建了一些临时Tensor,按理说迭代结束就释放了。但显存占用却一路攀升,直到OOM。排查后发现,是因为这些Tensor大小不一,分配器找不到合适的缓存块,只好不断cudaMalloc新块。旧的块又因为大小不匹配,一直躺在池子里不被复用。
避坑指南:我曾经因为Tensor形状频繁变化,导致分配器碎片化严重。后来统一了输入尺寸,显存占用直接降了30%。
3.3 显存池机制:大池子与小池子
PyTorch的缓存分配器内部维护了多个“池子”。每个池子负责管理不同大小的显存块。分配器会尽量从最匹配的池子里找块给你。如果找不到,就分裂大块或者合并小块。
这个机制有点像内存池的伙伴算法。但有个坑:池子里的块一旦被分配出去,即使你释放了Tensor,这个块也不会立即合并回大块。它只是被标记为“空闲”,但位置和大小不变。这就导致了碎片化。
你想想看,如果你反复分配不同大小的Tensor,池子里就会充满各种大小的“空洞”。下次要一个大块时,虽然总空闲空间够,但没有连续的大块,分配器就只能再去cudaMalloc。这就是显存占用虚高的常见原因。
注意:显存池的碎片化是隐形的杀手。它不会直接报错,但会让你的显存利用率越来越低。最终OOM时,你甚至不知道为什么。
3.4 torch.cuda.empty_cache()的真相
很多人把torch.cuda.empty_cache()当成“救命稻草”。每次显存不够就调一下。但它的真实作用,可能和你想的不一样。
它不会释放你正在使用的Tensor。它只释放缓存分配器池子里那些“空闲”的块。说白了,就是把PyTorch扣着不还的那部分,还给CUDA。
import torch
# 创建一个Tensor
x = torch.randn(1000, 1000).cuda()
# 释放Tensor
del x
# 此时显存占用可能没变,因为池子里还缓存着这块空间
print(torch.cuda.memory_allocated()) # 0
print(torch.cuda.memory_cached()) # 4 MB (缓存还在)
# 调用empty_cache
torch.cuda.empty_cache()
# 现在缓存也被清掉了
print(torch.cuda.memory_cached()) # 0
嗯,这里要注意:empty_cache()不是银弹。频繁调用它反而会降低性能,因为每次清空后,下次分配又要重新cudaMalloc。我建议只在以下场景使用:
- 训练结束后,释放显存给其他进程用
- 调试时,想确认当前真正占用的显存
- 切换模型或任务时,清理环境
真相总结:torch.cuda.empty_cache() 清的是“缓存”,不是“分配”。它解决的是显存占用虚高的问题,不是显存不够用的问题。
3.5 一张图看懂PyTorch显存管理
下面我用一张流程图,把整个机制串起来。你一看就明白。
这张图把整个流程说清楚了。你创建Tensor时,分配器先看池子里有没有合适的空闲块。有就直接用,没有就去cudaMalloc。用完释放后,Tensor回到池子,但没还给系统。只有你调empty_cache(),池子里的空闲块才会被真正释放。
3.6 实战建议:怎么用好这个机制
知道了这些,你就能有针对性地优化显存了。我总结几条实战经验:
- 尽量复用Tensor:不要频繁创建和销毁。用
torch.zeros_like()或者原地操作(inplace=True)来复用显存。 - 统一Tensor尺寸:避免形状频繁变化,减少碎片化。我习惯在数据加载时就pad到固定尺寸。
- 慎用empty_cache():只在需要释放显存给其他进程时调用。训练循环里不要加,否则会拖慢速度。
- 监控分配器状态:用
torch.cuda.memory_summary()定期检查,看cached和allocated的差距。差距过大说明缓存太多。
一个小技巧:我习惯在训练脚本开头设置torch.cuda.set_per_process_memory_fraction(0.9),限制PyTorch最多用90%的显存。这样即使有缓存,也不会把显存占满导致其他进程OOM。
好了,PyTorch的显存管理机制就讲到这里。你理解了缓存分配器、cudaMalloc、显存池和empty_cache的真相,以后遇到显存问题,至少知道该从哪里下手排查了。
想要更多资料,联系可可