最近在跑一个图像分割的模型,用的DeepLabV3+,backbone是ResNet101。我把batch_size降到2了,输入图片也缩到256x256,但显存还是从开始的2G一直涨到12G,最后OOM。我查了网上说可能是梯度累积、或者变量没detach的问题,但我没开梯度累积,损失函数也是常用的CrossEntropy。想问问大家有没有什么成熟的debug思路?比如用torch.cuda.memory_summary()看哪里泄露,或者有没有工具能可视化每层的显存占用?另外,是不是我模型里有循环或者多次forward导致的?感谢各位大佬!
PyTorch训练时显存一直涨但batch_size已经很小了,咋排查?
全部回复
共 180 条我之前跑分割模型也遇到过一模一样的情况,最后发现是DataLoader的num_workers开太多,每个worker都缓存了图片和标签的副本,显存被悄悄吃掉了。你可以先把num_workers设成0试一下,顺便看看是不是在验证集上也算了梯度,关掉torch.no_grad()有时能省不少。另外torch.cuda.memory_summary()确实能看出缓存分配器的问题,但更直接的办法是监控显存变化曲线,看看是线性涨还是突然跳变,后者多半是某个操作在累积计算图。如果模型里没有循环,试试把backbone的BN层换成eval模式,有时候训练和推理切换不当也会导致显存异常。
试试关掉cudnn.benchmark和梯度裁剪,有时候是backbone里BN层的running stats在累积计算图。
用pytorch的record function加个钩子看每层tensor存活时间,比memory_summary直观。
我之前也遇到过类似情况,最后发现是dataloader的num_workers开太多,每个worker的预取缓存也会占显存,尤其图像分割这种大tensor,你试试把num_workers调成0或者pin_memory关掉看有没有改善。另外检查一下有没有在循环里把loss或者输出append到list里,这种隐式引用也会让显存只增不减。memory_summary确实能看峰值分配,但更推荐用pytorch的memory_profiler插件,能按行定位到具体操作,比手动猜快很多。你要是确认没开梯度累积,也可以试试在optimizer.zero_grad()后面加torch.cuda.empty_cache(),虽然治标不治本,但能先确认是不是缓存碎片问题。
大概率是验证集里也开了grad,或者模型里有没关的BN统计,试试把验证阶段的torch.no_grad()包全。
用pytorch的memory_profiler插件看下每个tensor的分配,重点查下损失函数里有没有隐式建图。
建议关掉cudnn的benchmark模式,有时候自动tuning会缓存大量workspace,显存会慢慢吃满。
我之前也踩过类似的坑,显存持续上涨不一定就是泄露,很多时候是计算图没释放。你既然没用梯度累积,可以先试试在训练循环里加一句torch.cuda.empty_cache()看下峰值有没有回落,如果回落明显那基本就是缓存碎片问题,不是真泄露。另外你提到多次forward,这个很关键,DeepLabV3+如果用了ASPP里的并行分支,或者你在验证时也开了梯度,都会把中间变量留在图上。建议把整个训练和验证的forward都包在with torch.no_grad()里,尤其是验证阶段,很多人忽略这个。torch.cuda.memory_summary()确实能看分配器状态,但更直观的是用nvidia-smi配合py-spy看进程的Python堆栈,能定位到具体是哪一行代码触发的大分配。还有个笨办法,把backbone换成resnet18跑一遍,如果显存曲线还是一样的趋势,那就不是模型容量的问题,而是代码结构上有东西在累积,比如loss或者预测结果被append到list里了。我上次就是有个tensorboard的image回调里把每个batch的预测图都存着没清,最后显存直接起飞。你可以检查下有没有类似的可视化钩子,还有DataLoader的num_workers如果开多了在Windows上也会显存异常。
我之前也遇到过类似情况,最后发现是验证集里也开了grad,而且忘了写torch.no_grad(),显存就一路涨。可以先试试把optimizer.zero_grad()和loss.backward()之间的代码仔细过一遍,尤其是有没有把tensor存到list里累积。torch.cuda.memory_summary()确实能看分配细节,重点看缓存块数量是不是持续增加。另外DeepLabV3+的ASPP模块如果用了不同rate的并行卷积,某些实现会保留中间特征图,你可以试试把output_attachment关掉或者换成torch.utils.checkpoint来省显存。
写得挺好,建议补充一些性能数据。
遇到过类似情况,当时是被模型里的一个辅助loss坑了,那个分支在每次迭代都重新计算梯度图没释放。你可以试试把backbone的BN层换成eval模式,或者用torch.autograd.detect_anomaly()跑一下,能定位到具体哪行爆的。另外别光看memory_summary,建议装个pytorch_memlab的line_profile,能按行看每层张量引用数。还有个土办法,每跑一个batch就打印一次tensor的refcount,如果持续涨基本就是图没释放。
试试给dataloader加non_blocking=True,另外查下backbone里有没有忘关grad的eval模式。
大概率是backbone的BN层在训练模式下累积了计算图,试试把requires_grad=False的参数加detach,或者用torch.cuda.set_per_process_memory_fraction限制一下看报错栈。
显存涨不是泄露,是计算图没释放,重点查下损失里有没有对模型输出做多次索引或切片操作,那会保留中间变量。
试试把验证集里的with torch.no_grad()加上,我之前就是这么个情况,显存一路狂飙。
用pytorch的memory_summary看下是不是优化器state占的,不一定是你forward的问题。
我之前也踩过类似的坑,最后发现是模型里的ASPP模块里有个循环遍历不同rate的conv,每个分支都保留了中间变量,反向传播后没释放。你可以试试在每轮迭代末尾加torch.cuda.empty_cache()看显存会不会掉,如果掉了说明确实有未释放的引用。另外推荐用pytorch的autograd.detect_anomaly()跑一遍,虽然慢但能定位到具体哪一行计算图出了问题。还有个笨办法,把batch_size设成1,如果显存还涨,基本就是模型结构里有持续累积的节点,跟输入大小无关。
torch.cuda.memory_summary()确实能看出来,但很多时候它只告诉你哪个张量占了大头,真正的元凶是那些没被释放的中间变量。我遇到过类似的情况,最后发现是DataLoader的num_workers开太多,每个worker都复制了一份模型参数和计算图,显存就跟着涨,你把worker降到0试试看,可能直接就解决了。另外你说的多次forward,如果你在训练循环里不小心把input和target都放到了GPU上,然后每次迭代又新建了新的tensor却没有覆盖旧的,那确实会累积,建议用torch.no_grad()包住验证阶段,或者检查一下是不是有地方把loss.item()写成了loss本身。还有一个很隐蔽的点,如果你的模型里有BatchNorm,而且你在训练模式下反复调用了model.eval()和model.train(),BN的running_mean和running_var也会动态增长,虽然不占显存,但那部分统计量会拖慢速度。最实用的排查办法是,在几个关键位置打印torch.cuda.memory_allocated()的差值,比如每个epoch开始和结束,或者每个iteration之后,看哪个阶段跳涨最明显。如果还是找不到,可以试试用pytorch的memory_profiler,或者把backbone换成轻量的ResNet18跑一遍,如果显存不涨了,那就是ResNet101里某些层的实现问题,比如空洞卷积的dilation参数导致的额外缓存。
大概率是验证集里也开了grad,记得torch.no_grad包一下,我之前就被这个坑过。
试试给优化器加个zero_grad(set_to_none=True),顺便用nvidia-smi看下是不是别的进程在占显存。
遇到过类似情况,十有八九是验证集或评估阶段没包在torch.no_grad里,试试把梯度关掉看显存还涨不涨。
建议先跑一个固定step的循环,用pytorch的memory_snapshot生成火焰图,能直接看到哪层在累积累积。
大概率是backbone的BN层在训练模式下统计量累积,试试冻结BN或关掉track_running_stats。另外用pytorch的memory_profiler逐行跑一下,定位到具体层。
遇到过类似情况,最后发现是验证集里也开了grad,或者模型里某个module被重复forward了,试试在验证阶段包一下torch.no_grad(),还有跑完每个step后清一下opt.zero_grad(),看显存曲线会不会平缓下来。另外torch.cuda.memory_summary()确实能看缓存分配,但更像是定位峰值,泄漏的话建议用pytorch的tracemalloc或者干脆逐层打印tensor的shape和device,重点检查有没有在循环里创建新的tensor没释放。我之前还踩过坑是数据加载的num_workers把显存占了,不过你这种情况更像计算图没释放,可以试试把loss.backward()换成retain_graph=False,或者用del loss, pred, feats手动释放引用。
我之前也遇到过类似情况,排查到最后发现是DataLoader的num_workers开太多,每个worker都在缓存数据,显存也被算进去了,你试试把workers调成0看看。另外torch.cuda.memory_summary确实有用,重点看allocated和reserved的差值,如果reserved一直涨但allocated稳定,那多半是缓存碎片问题,可以试试torch.cuda.empty_cache()。还有个小技巧,用torch.autograd.detect_anomaly()跑一遍,能帮你定位到具体哪一步的反向传播出了问题,比盲猜高效很多。
我之前也踩过类似的坑,显存涨到OOM不一定就是梯度累积,很可能是backbone的BN层在训练模式下统计量更新导致的,试试冻结BN或者换成syncBN看看。另外torch.cuda.memory_summary()确实能看缓存分配,但更推荐用pytorch的memory_profiler或者直接打印每个tensor的shape和device,有时候是数据加载那边没释放。你ResNet101的中间特征图全留在计算图里,如果没开gradient checkpointing,即使batch小也可能爆,建议给backbone包一层checkpoint。还有个小技巧,把optimizer.zero_grad()放到forward之前,能避免旧梯度占显存,你可以先试这个,成本最低。
我之前也遇到过类似情况,最后发现是验证集里也开了梯度计算,或者模型里某个模块在forward里重复调用了自己。你可以先试试在训练循环里把optimizer.zero_grad()和loss.backward()之间加个torch.cuda.synchronize(),然后配合memory_summary看是哪个tensor在累积。另外注意下dataloader的num_workers,有时候数据加载线程也会占显存,虽然不常见。如果方便的话,用pytorch的torch.profiler跑个profile,能直接看到每层的前向和反向显存峰值,比我之前手动查快多了。