最近在调一个简单的图像分类模型,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已经很小了,是哪里漏了?
全部回复
共 12 条看到你说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和output的梯度计算图没彻底释放,光del不够,试试在backward()之后加一步optimizer.zero_grad(),或者用with torch.no_grad()包一下推理部分。另外自定义Dataset里如果存了图像增强的中间结果也容易累积,建议每次只返回当前batch的数据。监控显存的话可以用nvidia-smi -l 1实时看,或者试试torch.cuda.memory_summary(),能列出每块显存的分配情况。
这种显存一直涨的问题我调ResNet时也遇到过,大概率是计算图没释放。你提到怀疑loss和output保留计算图,可以试试在backward之后加个optimizer.zero_grad(),或者直接用loss.item()取标量再del。另外自定义Dataset里如果存了list累加图像或标签,也可能导致显存泄漏。推荐用nvidia-smi -l 1实时看显存占用,或者pytorch的torch.cuda.memory_summary()能打印详细分配信息,定位到具体哪一行代码在持续申请显存。