最近在调一个简单的图像分类模型,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 条这个问题我也踩过坑,核心大概率不是batch size的问题,而是你怀疑的计算图没释放。PyTorch的loss.backward()之后如果没做detach或者用.item()取loss值,整个计算图会一直保留到下一个backward,尤其是你如果还打印loss或者把loss存进列表里做可视化,那显存就彻底炸了。建议你检查一下训练循环里有没有类似losses.append(loss)这种操作,改成losses.append(loss.item())就行。另外自定义Dataset里如果存了图片的增强版本或者中间特征,也容易变成隐形的显存泄漏,最好在__getitem__里只返回原始数据,所有变换写到transforms里。至于监控工具,pytorch自带的torch.cuda.memory_summary()能打印所有CUDA tensor的分配情况,比empty_cache这种玄学操作靠谱多了,可以跑一个epoch后打出来看看。还有个小技巧,用torch.no_grad()包裹推理阶段或者不用的变量,也能阻止自动求图积累。
这问题我太熟了,之前调一个分割模型也折腾了好久。你猜得没错,自定义Dataset里存中间变量确实是个常见坑,比如在__getitem__里把图像增强后的tensor挂到self上,或者把loss和output的detach()漏了,导致计算图一直累积。但更隐蔽的是,如果你的训练循环里用了类似loss.backward()之后没及时optimizer.zero_grad(),或者把多个batch的output拼接起来做可视化,都可能让显存像漏了一样涨。建议你试试用torch.cuda.memory_summary()打印详细分配记录,或者在代码里插桩,每跑一步就输出当前显存占用,定位到具体哪一行开始暴涨。另外可以检查下DataLoader的num_workers,如果开太多有时也会因为子进程缓存导致显存缓慢泄漏。别太依赖empty_cache(),它只能释放缓存区,留不住真正的引用。
遇到过类似的问题,建议检查一下训练循环里是不是把每个batch的loss都累加到了某个list里做可视化,或者有梯度回传后没及时zero_grad。另外torch.cuda.empty_cache()其实只是清空缓存池,不解决真正占用的显存,可以试试用torch.cuda.max_memory_allocated()看峰值出现在哪,或者跑一小段代码后打印当前显存占用的tensor列表。我之前就是被自己写的计算loss时的中间变量坑了,加了个.detach()就稳了。
八成是loss.backward()之后没清梯度,试试在optimizer.zero_grad()前加个model.zero_grad()看看。
试试把loss.item()拿过来用,别直接保留loss,或者优化器里加个zero_grad看看是不是梯度累积的问题。
这种情况我遇到过,多半是训练循环里某个变量没及时释放计算图。比如loss.backward()之后如果没把optimizer.zero_grad()放在合适位置,或者把loss.item()和output.detach()混用了,都可能导致显存累积。建议你在每个batch最后加个torch.cuda.synchronize()试试,或者用nvidia-smi -l 1实时看显存变化,同时检查下自定义Dataset里是不是把整个图像列表都缓存了。
我自己也踩过类似的坑,感觉你说的“保留计算图”可能是元凶。虽然你手动del了loss和output,但如果loss.backward()之前没有把优化器的梯度清零(比如忘了optimizer.zero_grad()),或者用了类似loss.item()而不是直接丢掉变量,计算图仍然可能被保留。可以试试在每个batch开头显式调用optimizer.zero_grad(),或者在backward之后加上detach()操作。另外,自定义Dataset里如果存了图片的Tensor而不是路径,或者DataLoader的num_workers设得太大,也可能导致显存泄漏——每张卡上的缓存不会因为进程结束就自动释放。想排查的话,可以用torch.cuda.memory_summary()打印当前所有分配块,或者装个pytorch-memlab在训练循环里标记可疑位置。顺便问一下,你用的ResNet18是预训练模型吗?有时候BatchNorm层的状态也会偷偷累积。
我之前也踩过类似的坑,后来发现最隐蔽的问题是训练循环里把loss.backward()之后忘了把optimizer.zero_grad()放在合适的位置。你提到怀疑计算图没释放,这个可能性很大,尤其是如果你在loss.backward()之后没有及时把loss或output的引用删干净,PyTorch会保留整个计算图直到反向传播结束。其实手动del和empty_cache很多时候治标不治本,因为显存碎片化或者Python的垃圾回收延迟会导致释放不彻底。建议你用torch.cuda.memory_summary()打印一下显存分配情况,它能告诉你哪些tensor还活着。另外检查一下自定义Dataset里是不是存了所有样本的预处理结果,比如把图片转成tensor后放在列表里,这种跨epoch的引用也会让显存持续累积。还有个偏方,用torch.utils.checkpoint把部分层做梯度检查点,虽然慢一点但能切断中间变量的依赖链。
遇到过类似问题,大概率是loss.backward()之后没清零梯度,或者优化器里没设zero_grad()?还有自定义Dataset里如果存了所有图像路径或者中间特征,显存也会慢慢堆积。建议用torch.cuda.memory_summary()看看到底哪个操作占着显存,或者试试在epoch结束时加个gc.collect()。另外torch.no_grad()在验证阶段也得加上,不然推理时计算图不释放也是个坑。
这种情况大概率是训练循环里没有正确清除梯度或计算图,试试在loss.backward()后面加上optimizer.zero_grad(),并且确保loss和output在每次迭代后都不再被引用。另外自定义Dataset里如果存了所有图像的中间特征,也会导致显存累积,建议检查一下__getitem__里有没有把数据挂在self上。工具的话可以用nvidia-smi -l 1实时看显存变化,或者pytorch的torch.cuda.memory_summary()打印详细分配情况。
我遇到过一模一样的坑,问题大概率出在loss.backward()之前没有清空梯度,或者自定义Dataset里把图片做了transform后没释放临时变量。你试试在optimizer.zero_grad()之前加个model.zero_grad(),或者检查下loss和output有没有被赋值给类属性。另外torch.cuda.empty_cache()其实只是清缓存,真正占着显存的是没释放的计算图,可以试试用torch.no_grad()包装验证集,或者用gc.collect()强制回收。
检查下优化器里是不是把梯度清零了,或者试试with torch.no_grad()处理验证集。
我也遇到过类似的情况,当时折腾了好久才发现是训练循环里没加optimizer.zero_grad(),或者加了但位置不对——如果梯度没清干净,计算图就会一直累积,显存自然稳涨。你试了手动del和empty_cache,但这两个对计算图残留其实没太大作用,尤其是如果loss.backward()之后还有变量挂着梯度。建议你在每个batch里检查一下:loss.backward()之后有没有把loss和output的引用彻底断开,比如用loss.item()取值而不是直接保留loss变量。另外,自定义Dataset里如果存了太多中间tensor,比如在__getitem__里生成了一些没释放的临时变量,也会导致显存泄漏,最好确认一下那些变量是不是被改成了局部作用域。还有一个比较省事的办法是用torch.cuda.memory_summary()打印当前显存分配,看看是哪个张量占着不释放,或者用nvidia-smi配合watch -n 1实时盯一下显存变化,比光靠感觉靠谱。如果这些都不行,可以试试把CIFAR-10的DataLoader换成torchvision.datasets自带的,排除一下自定义Dataset的问题。
大概率是loss.backward()之后没清梯度,试试在每个batch加个optimizer.zero_grad()。
我之前也遇到过类似问题,后来发现是训练循环里忘了加optimizer.zero_grad(),导致梯度一直累积,计算图没法释放。你可以检查下这一步,另外DataLoader里num_workers设得高也可能让显存缓慢上涨,可以调成0试下。建议用torch.cuda.memory_summary()打印出来看看,哪个Tensor占着坑一目了然。
试试把loss.item()再backward,或者用with torch.no_grad()包一下验证阶段,大概率是计算图没释放。
试试把loss.item()换成loss直接存,或者检查下优化器里有没有 retain_graph=True 的骚操作。
确实遇到过一模一样的问题,尤其是用ResNet这种标准模型时,显存上涨往往不是模型本身的问题,而是训练循环里某个细节没处理好。你说怀疑loss和输出保留了计算图,这个方向很可能对了——PyTorch的loss.backward()之后,如果没把梯度清零或者变量没及时释放,计算图确实会累积。我建议你检查一下训练循环里有没有类似loss = criterion(output, target)这种赋值后,后面又用到了output做其他操作,比如统计准确率时重复调用了output.argmax(),这会让中间变量一直留在显存里。另外,即使手动del和empty_cache,如果Python的垃圾回收机制没触发,显存也不会立刻释放,可以试试在每个epoch结束后显式调用gc.collect()。监控工具方面,我一般用nvidia-smi结合torch.cuda.memory_summary(),后者能列出每个张量的显存占用,挺直观的。还有个常见坑是DataLoader的num_workers设太大,子进程会预加载数据导致显存碎片化,你可以先设成0试试。最后,如果batch size已经16了还涨,也可以考虑把验证集的梯度计算关掉——用with torch.no_grad()包裹验证阶段,很多人会漏掉这个。
这个问题我之前也踩过坑,大概率不是DataLoader的问题,而是你训练循环里那个loss.backward()之后没做梯度清零吧?很多新手容易忘掉optimizer.zero_grad(),这样梯度会不断累积,计算图也会一直保留,显存自然就炸了。另外你说的自定义Dataset里存中间变量确实是个疑点,比如__getitem__里如果用了列表append或者把图像数据重复赋值给类属性,这些对象在epoch之间不会被释放,会越堆越多。你可以试试在每次迭代结束后用torch.cuda.reset_peak_memory_stats()打一下峰值,或者装个nvidia-ml-py3实时看显存占用曲线,比靠感觉准得多。还有一个冷门点:DataLoader的num_workers设太高时,子进程会复制一些缓存,也会导致显存缓慢上涨,建议先设成0排除这个因素。如果这些都没问题,那可能得检查下模型里有没有意外的requires_grad=True的buffer,比如某些BN层的参数被错误地当成了可学习变量。总之先调zero_grad,再排查自定义Dataset,最后用工具监控,大概率能定位到。
这个我熟,之前调RegNet的时候也踩过类似的坑。你提到怀疑是自定义Dataset里存了中间变量,这个可能性很大,尤其是如果你在__getitem__里做了图像增强或者往tensor列表里append东西,Python的引用会一直保留到epoch结束才释放。另外你试过del loss和output但没效果,一个常见原因是optimizer.zero_grad()没调对位置——如果梯度累积或者忘记在backward之前清空,计算图会一直累积。建议你在每个batch里检查一下loss.item()是不是被赋值给一个外部变量,或者有没有把output存到某个list里做统计,这些都会让计算图不释放。至于监控工具,我习惯用nvidia-smi配合torch.cuda.memory_summary(),后者能直接打印出当前所有tensor的分配情况,哪个变量占了多少一目了然。还有一个冷门点:DataLoader的num_workers设太大会导致子进程缓存数据,显存也会慢慢涨,试试workers=0看有没有改善。