最近在调一个简单的图像分类模型,ResNet18跑CIFAR-10。我batch size已经降到16了,但显存还是稳定上升,跑十几个epoch就OOM了。代码里用了DataLoader,shuffle=True,没开pin_memory。我怀疑是不是自己写的自定义Dataset里存了太多中间变量,或者是在训练循环里把loss和输出都保留了计算图?我试过在每个batch后手动del loss和output,也调用了torch.cuda.empty_cache(),但好像没啥用。有没有老哥遇到过类似情况?或者有没有什么工具能实时监控哪个变量占着显存不释放?感谢!
PyTorch训练时显存一直涨,但batch size已经很小了,是哪里漏了?
全部回复
共 169 条我之前也踩过这个坑,ResNet18按理说16的batch根本不该爆。你查下loss.backward()之后有没有optimizer.zero_grad(),如果梯度没清干净,计算图会一直累积,显存就线性涨。另外自定义Dataset里如果存了tensor而不是转成numpy,DataLoader每次取数据都会把引用留在内存里,试试在__getitem__里return之前加个.clone()或者直接转成float。监控的话可以用nvidia-smi -l 1看趋势,但定位具体变量还是得靠pytorch的torch.cuda.memory_summary(),那个能列出每个tensor的分配情况。你del和empty_cache没用大概率是因为计算图根本没释放,检查一下是不是有loss.item()被存到list里了。
说到这个我第一反应就是loss.backward()之后optimizer.step()之前,你的梯度累积是不是没清?虽然你手动del了loss和output,但backward算出来的梯度只要不清零,计算图确实会释放,可显存碎片和缓存不会完全回去。torch.cuda.empty_cache()只是把缓存还给驱动,不是还给系统,实际占用看着可能还是涨。
另外你怀疑自定义Dataset存中间变量,这个思路对,但更常见的是在__getitem__里做了太多transform或者把整个图片tensor都保存在了self.data里,加上shuffle=True每次打乱索引,DataLoader会预取下一批,如果worker数默认0还好,要是设了num_workers>0,每个worker都会复制一份数据集引用,显存占用会翻倍涨。
我建议你先用nvidia-smi看是每个epoch增量一致还是突然跳变,如果是线性涨,大概率是每个step没释放某个临时tensor。可以试试在训练循环里把inputs和labels也del掉,并且用detach()隔离输出。最直接的工具是pytorch的torch.cuda.memory_summary(),能打印每个张量的分配详情,或者用nvidia的Nsight Systems看时间线上的分配峰值。
我上次遇到类似情况,最后发现是自己在计算accuracy时把preds和labels都留在GPU上没转CPU,每个batch都堆一点,十几个epoch自然爆炸。你检查下是不是验证集的循环里也累积了tensor,或者有没有把每个batch的loss存进list没清空。
我之前也踩过类似的坑,ResNet18按理说16的batch不该爆,你不如先排查一下是不是验证阶段忘了写torch.no_grad(),验证集forward也会累积计算图,尤其是你每个epoch都跑验证的话,显存就会一点点被吃掉。另外自定义Dataset里如果存了图像增强后的Tensor而不是路径,那每个worker都会复制一份,数据加载阶段就会占掉一部分显存,但这部分通常不会持续涨,更像是峰值高。你那个del loss和output其实没啥用,因为只要loss.backward()之后optimizer.step()正常,计算图应该会被释放,真正要查的是有没有把每个batch的loss或输出append到一个list里保留引用,哪怕只保留标量也会让显存碎片越堆越多。建议你用pytorch的autograd.detect_anomaly()跑几个batch看能不能定位到具体操作,或者干脆用nvidia-smi dmon实时看显存占用,再配合代码里打印每个tensor的device和shape,基本能锁定问题。还有个冷门可能,DataLoader的num_workers设太大,每个worker都会缓存一部分数据在显存里,试试调成0或者2看看涨速有没有变化。
显存慢慢涨大概率不是del能解决的,重点查一下是不是在训练循环里把loss累加到了某个list或者tensor里,比如total_loss += loss这种,计算图会一直挂着。另外自定义Dataset如果__getitem__里返回了带梯度的tensor也会出问题。可以试试torch.cuda.memory_summary()看看到底是哪块在涨,比empty_cache管用多了。
你del loss和output其实没啥用,因为真正占显存的是optimizer里累积的梯度或者计算图没被释放,得看是不是在训练循环里不小心把带grad的张量存到list或dict里了,比如记录每个batch的loss做可视化那种,时间一长显存就爆了。另外empty_cache只是回收缓存,不解决引用没释放的问题。建议先试试torch.cuda.memory_summary()看看到底是哪块在涨,或者用memory_profiler跑几个step对比一下。我遇到过类似的是在验证阶段忘了加torch.no_grad(),结果验证集也建了图,加上就好了。还有个小坑是DataLoader的num_workers如果设得高,每个worker里也可能有缓存,不过那个一般不会持续涨。你也可以在训练循环里打印torch.cuda.memory_allocated(),看每个epoch后是不是单调上升,这样能确认是泄漏还是碎片。
我遇到过类似情况,最后发现是验证/测试阶段没加torch.no_grad(),虽然不是训练循环但每个epoch都在悄悄建图。你检查一下eval那段是不是也漏了。另外手动del加empty_cache基本没用,显存泄漏通常是计算图引用没断,不是碎片问题。可以试试torch.cuda.memory_summary()看看到底哪块在涨。
检查一下有没有在训练循环里累加loss或者把每个batch的output存进list里,这种写法特别容易让计算图越堆越多。另外自定义Dataset里如果每次__getitem__都创建新tensor并挂到某个全局变量上,也会涨。可以装个torch.cuda.memory_summary()或者用nvidia-smi -l 1盯着看,一般能定位到是缓存还是真实占用。还有别忘了验证阶段加torch.no_grad(),这个漏了显存也会慢慢上去。
显存缓慢上涨大概率不是del没生效,而是你训练循环里某个地方还在累积引用,比如把每步的loss写进list里准备画曲线,或者metrics里存了tensor没detach。你既然自定义了Dataset,也确认下__getitem__返回的是不是numpy转tensor,有没有返回整个文件句柄之类的东西。监控的话可以用torch.cuda.memory_summary()每个epoch打一次,配合gc.collect()看看对象数变化。empty_cache只回收缓存分配器里没用的块,救不了还活着的引用,所以别指望它。
你del loss和output其实没啥用,Python的引用计数在循环里只要变量被重新赋值,旧的计算图该释放就释放了,问题大概率不在这。我比较怀疑你自定义Dataset里是不是把整个数据集的tensor或者numpy数组缓存在了__init__里,或者__getitem__返回的东西被某个全局list悄悄存下来了,这种才是真正的泄漏点。另外可以试试torch.cuda.memory_summary(),每个epoch打一次,看allocated和reserved的差值变化,比empty_cache有用多了,empty_cache只是把缓存还给驱动,不解决根因。还有个容易被忽略的地方是metric或者日志里用了loss.item()之外的累积方式,比如把每步的output append进list等着最后算acc,那显存不炸才怪。你可以在训练循环里print(torch.cuda.memory_allocated()/1e9),看是每步都在涨还是只在特定操作后跳变,基本就能定位了。如果实在找不到,用tracemalloc配合gc看看Python对象数量是不是也在涨,能排除是不是纯Python层面的问题。