最近在调一个简单的图像分类模型,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 条看到你说del和empty_cache没用,我第一反应是检查下loss.backward()之后的梯度累积。PyTorch默认会累积梯度,如果你每个batch都调了backward但没调optimizer.zero_grad(),计算图确实会一直挂在那,显存只增不减。不过你这种情况batch size都16了还涨十几个epoch,更像是某个地方保留了整张图的引用。
我自己踩过类似的坑:自定义Dataset里如果__getitem__返回了可变对象(比如list of tensors),DataLoader的多进程会把数据副本留在内存里,虽然不完全是显存,但会挤占系统内存导致OOM。你试试把num_workers设成0跑一次,排除下多进程的干扰。
另外检查一下训练循环里有没有把output或者loss追加到list里做记录?比如统计准确率时用了all_preds.append(preds),这种操作会把每个batch的tensor都保留下来直到epoch结束。建议改成只保存数值(preds.item())或者用torch.no_grad()包裹评估部分。
监控工具的话,推荐用nvidia-smi的循环输出或者gpustat实时看显存波动,不过更精确的是在代码里插桩:在可疑位置打印torch.cuda.memory_allocated(),看哪个操作前后显存跳变最大。我之前排查过一个自定义损失函数,里面用torch.tensor()创建标量时默认requires_grad=True,直接导致显存泄漏。你ResNet18跑CIFAR-10按理说16的batch size完全够,建议先注释掉自定义Dataset的预处理步骤,用torchvision自带的CIFAR-10跑一次,排除掉数据加载的问题。
我也遇到过类似问题,后来发现是优化器里的momentum项把梯度历史给保留了,显存就慢慢堆上去了。你可以试试把optimizer.zero_grad()换成set_to_none=True,或者用torch.cuda.memory_summary()看看哪块在涨。另外检查下是否在循环外不小心引用了中间张量,比如accuracy计算时用了output.detach()没?
我也遇到过类似的问题,折腾了好久才发现是梯度累积的锅——不是代码里显式写的梯度累积,而是backward之后没及时清梯度,导致计算图一直保留。你提到怀疑loss和output保留了计算图,这个方向是对的。其实关键点在于,即使你手动del了变量,如果PyTorch的autograd graph还有引用,显存就不会释放。建议你在每个batch的backward之后加一句optimizer.zero_grad(),或者用loss.backward()之后马上detach一下计算图,比如把loss.item()取出来做日志,别直接保留loss变量。另外,自定义Dataset里如果存了中间tensor,比如图像增强后的结果没及时转成numpy或者del掉,也会累积。你可以试试在__getitem__里只返回必要数据,别把中间变量挂在self上。torch.cuda.empty_cache()其实只是清空缓存,不是释放真正被占用的显存,所以效果有限。
至于监控工具,我推荐用nvidia-smi配合torch.cuda.memory_summary(),后者能打印出每个tensor的分配情况,定位是哪个操作导致的泄漏。还有个更直观的办法,在训练循环里加一个torch.cuda.reset_peak_memory_stats(),然后每个epoch结束打印torch.cuda.max_memory_allocated(),看看是不是一直在涨。我之前用这个方法发现是DataLoader的num_workers开太多,导致子进程里的显存没及时回收,改成0或者用pin_memory=True反而解决了。不过你shuffle=True没开pin_memory,应该不是这个原因。建议先排查计算图残留和Dataset里的临时变量,这两块是最常见的坑。
我也碰到过类似的问题,尤其是用小batch size的时候显存反而涨得离谱,后来排查了一圈发现几个常见的坑,可以一起交流下。
你提到怀疑自定义Dataset里存了中间变量,这个可能性挺大的。比如你在__getitem__里做了图像增强或者归一化之后,是不是把处理后的tensor直接赋值给了self的某个属性?那样每个epoch都会累积新的tensor,旧的还被引用着不会被释放。我建议检查一下Dataset里有没有类似self.cached_data = []或者self.current_batch = tensor这样的写法,尤其是如果你在循环里不断往列表里append的话,那内存/显存泄漏几乎是必然的。
另外训练循环里loss和output保留计算图的问题,你手动del了但可能不够彻底。比如你在backward之后,如果还保留了loss.item()或者output.detach()的变量,并且这些变量被存在了某个列表里(比如用来记录loss曲线),那计算图虽然释放了,但tensor本身还在显存里。我习惯在每个batch结束后,把loss和output都显式地detach()并转成CPU再存,避免GPU上的tensor累积。torch.cuda.empty_cache()其实只是清理缓存,对于真正被变量引用的显存没用,所以它解决不了根本问题。
至于监控工具,我推荐用nvidia-smi配合pytorch的torch.cuda.memory_summary(),后者能列出当前所有分配了显存的tensor及其来源,非常直观。你也可以在训练循环的每个batch后打印一下torch.cuda.memory_allocated()看是不是单调递增,如果每个batch结束后的显存占用比上一个batch结束后的值高,那肯定有变量没释放。
还有一个小细节:DataLoader的num_workers>0时,子进程可能会持有一些显存引用,虽然不常见但也可以试试设置num_workers=0看是否改善。你试试看这些方向,排查完应该能找到问题。
我也遇到过类似的问题,当时也是batch size调得特别小,显存还是蹭蹭往上涨。你提到怀疑自定义Dataset里存了中间变量,这个确实挺常见的——比如你在__getitem__里做了数据增强或者归一化,但没及时释放临时变量,DataLoader多进程加载时这些变量可能会被保留。你可以试试在训练循环里加个torch.cuda.reset_peak_memory_stats()和torch.cuda.max_memory_allocated(),看看每个batch到底占了多少,如果峰值在涨那就说明有变量没被回收。
另外你说的loss和output保留计算图,这个可能性很大。PyTorch默认会在反向传播后保留中间梯度变量,除非你手动设置retain_graph=False。建议你在loss.backward()之后显式调用optimizer.zero_grad(),并且把loss和output变量在循环末尾用del删除,或者直接让它们被下一次迭代覆盖。torch.cuda.empty_cache()其实只是清空缓存,不一定能释放那些被python引用计数卡住的显存。
还有一个我踩过的坑:如果用了torch.no_grad()装饰或者上下文管理器,但里面调用了某个需要梯度的函数,可能也会产生隐式计算图。你可以用torch.autograd.set_detect_anomaly(True)来检查哪里梯度异常,不过这个会拖慢速度。工具方面,nvidia-smi只能看全局,推荐用pytorch自带的torch.cuda.memory_summary(),能列出每个张量的分配情况,或者装个pytorch-memlab,可以实时监控哪个操作申请了显存。建议先跑一个小epoch,用memory_summary对比一下几个关键点的显存变化,应该能定位到问题。
大概率是loss.backward()之后没清零梯度,试试在optimizer.zero_grad()前面加个detach。
这种情况大概率是训练循环里loss.backward()之后没做detach或者梯度累积导致的计算图残留,光del loss和output是不够的,建议在loss.backward()之后加上loss.item()或者把loss.detach()一下。另外可以试试用torch.autograd.set_detect_anomaly(False)关掉异常检测,那个也会额外占显存。要是还不行,装个nvidia-smi配合gpustat实时看显存占用,或者用torch.cuda.memory_summary()打印详细分配信息,能查到是哪个操作在持续分配新显存。
大概率是loss.backward()之后没清梯度,试试在optimizer.zero_grad()之前加个model.zero_grad()。
这种情况我也踩过坑,大概率是训练循环里loss.backward()之后没做optimizer.zero_grad(),或者你在每个batch里把loss和output存到了某个列表里做可视化,那计算图就会一直保留。另外自定义Dataset里如果用了大量临时列表或者numpy转tensor的操作,也可能产生隐性的显存泄露,比如在__getitem__里重复加载图像没释放。你可以试试用torch.cuda.reset_peak_memory_stats()配合torch.cuda.memory_summary()看看到底是哪个操作在涨,比空cache有用得多。还有一个常见迷惑点是DataLoader的num_workers设太大,子进程里如果缓存了数据也会慢慢吃显存,建议先设成0或1排除这个因素。如果实在找不到,可以写个很小的循环,只跑一个batch然后观察显存会不会随着epoch增加而持续上升,这样能定位是不是优化器状态或者BN层统计量在作祟。
试试把loss.item()换成detach()或直接取标量值,大概率是计算图没断开。
遇到过类似情况,大概率是loss.backward()之后没做梯度清零,或者optimizer.zero_grad()写的位置不对,导致梯度累加占显存。还有一个常见坑是DataLoader的num_workers设太大,多进程数据预取也会偷偷吃显存,建议调到0或者2试试。监控的话可以用nvidia-smi -l 1看实时显存,或者装个pytorch-memlab,能定位到具体哪一行代码分配了显存没释放。
这种情况大概率是计算图没释放,你手动del loss和output可能没把detach做干净,试试在loss.backward()之后加一句optimizer.zero_grad(),或者检查一下是不是在循环里把每个batch的loss都append到列表里了,那个列表会一直保留梯度。另外torch.cuda.empty_cache()其实不太能解决计算图残留的问题,可以装个gpustat或者nvidia-smi循环看显存变化,但更推荐用torch.cuda.memory_summary()直接定位哪个张量在占坑。
遇到过类似情况,大概率是训练循环里没做好梯度清零或者loss.backward()之后计算图没释放。建议你在每个batch的optimizer.zero_grad()之后加上loss.detach()再取item(),或者直接用with torch.no_grad()包裹验证阶段。另外可以试试pytorch的torch.cuda.memory_summary(),能清楚看到哪块显存占着没释放,比自己瞎猜强多了。
这个问题大概率是训练循环里loss.backward()之后没有及时清空梯度,或者pytorch默认的梯度累积机制导致计算图没释放。你可以在每个batch末尾加一句optimizer.zero_grad()试试,另外检查下自定义Dataset的__getitem__里是不是把图片转成tensor后还保留了原始numpy数组,那个也会占显存。监控的话可以用nvidia-smi -l 1实时看,或者装个gpustat更直观,还能定位到具体进程。
试试在loss.backward()后面加个optimizer.zero_grad(),很多人忘了清梯度也会让显存一直涨。
试试把loss.item()拿回来再反向传播,或者在optimizer.zero_grad()之前检查下有没有梯度累积没清。
我之前也踩过同样的坑,问题大概率出在loss.backward()之前没有清空梯度,或者optimizer.zero_grad()放的位置不对。还有就是自定义Dataset里如果保存了每张图的增强结果,确实会累积内存,建议改成在__getitem__里实时做变换。
另外torch.cuda.empty_cache()其实只清空缓存不释放已占用的显存,可以试试用nvidia-smi配合torch.cuda.memory_summary()来定位具体是哪块泄露了。
如果batch size已经是16还涨,建议检查一下是不是在验证集上也开了梯度计算,把torch.no_grad()包上应该能缓解很多。
遇到过类似情况,八成是loss.backward()之后没清梯度或者计算图没彻底释放,试试在每个batch里加上optimizer.zero_grad(),要是已经加了的话,检查下自定义Dataset里是不是把图片或者tensor存成了成员变量,那种会一直留在显存里。另外torch.cuda.empty_cache()其实只是清缓存,治标不治本。可以用nvidia-smi -l 1实时看显存变化,或者代码里插torch.cuda.memory_summary()来定位哪个变量没释放。
这种情况我碰到过好多次,尤其是用自定义Dataset的时候很容易踩坑。你怀疑计算图没释放其实方向是对的,但问题可能更隐蔽:检查一下训练循环里有没有把loss或output存到某个列表里做日志,比如loss_list.append(loss.item()) 这种没问题,但如果你直接存了tensor,那计算图就一直在。另外自定义Dataset里如果用了__getitem__返回了图像和标签以外的变量,比如某些中间特征或者缓存的transform结果,那每个batch都会保留引用。建议你把torch.cuda.reset_peak_memory_stats() 和 torch.cuda.memory_summary() 加在epoch开头,能清楚看到哪个操作在涨显存。还有一个小细节:DataLoader的num_workers别设太大,有时候多进程也会导致显存碎片累积。如果还不行,试试把optimizer的zero_grad放在loss.backward()之前,虽然听起来怪但有人这样解决了显存泄漏。
大概率是loss.backward()之后没清梯度,试试在每个batch开头加optimizer.zero_grad()。