最近在跑一个图像分割的模型,用的DeepLabV3+,backbone是ResNet101。我把batch_size降到2了,输入图片也缩到256x256,但显存还是从开始的2G一直涨到12G,最后OOM。我查了网上说可能是梯度累积、或者变量没detach的问题,但我没开梯度累积,损失函数也是常用的CrossEntropy。想问问大家有没有什么成熟的debug思路?比如用torch.cuda.memory_summary()看哪里泄露,或者有没有工具能可视化每层的显存占用?另外,是不是我模型里有循环或者多次forward导致的?感谢各位大佬!
PyTorch训练时显存一直涨但batch_size已经很小了,咋排查?
全部回复
共 180 条大概率是backbone里BN的running stats在反向时累积了计算图,试试冻结BN或者把图片尺寸再砍半看曲线斜率。
用pytorch的torch.cuda.memory._record_memory_history()能看分配栈,重点盯一下是不是有tensor没释放,我上次就是U-Net里skip connection反复拼接搞出来的。
我之前也遇到过类似的坑,最后发现是backbone里BatchNorm的running_mean在反向传播时被重复计算了,试试把模型切成eval模式或者冻结BN层看看显存曲线有没有变化。另外torch.cuda.memory_summary()确实有用,但更推荐用pytorch的memory_profiler插件,能按时间线看到每个tensor的分配和释放,比单看summary直观得多。还有个小技巧,如果代码里用了多尺度训练或者随机resize,每次forward都会重新构建计算图,也会导致显存虚高,可以先把输入尺寸固定死试试。你要是方便的话,可以贴一下模型forward里有没有循环或者切片操作,那种动态图最容易出问题。
我之前也踩过类似的坑,最后发现是backbone里用了可变形卷积,它的offset在反向传播时会累积计算图,导致显存持续增长。你可以先试试把backbone换成普通卷积,看显存曲线是不是就平了。另外torch.cuda.memory_summary()很有用,能看出是哪个tensor占着内存不释放,但要在OOM前抓住那一刻。还有个笨办法,就是固定iteration数,每步打印显存,如果稳步上涨而不是阶梯式跳变,多半是计算图没释放,查查有没有变量被意外保存了。
还有个小技巧,把输入改成随机噪声跑一次,如果显存还涨,说明跟数据无关,纯粹是模型结构或代码逻辑的问题。你可以试试在每次backward后加torch.cuda.empty_cache(),虽然不能根治,但能帮你确认是不是缓存碎片的问题。如果这样显存能稳住,那大概率是频繁创建小tensor导致的。
这情况我遇到过,八成是backbone里某些层保存了所有中间激活没释放,用torch.cuda.memory_snapshot看下哪个tensor占大头。
试试在每次迭代后加个torch.cuda.empty_cache(),要是还涨就检查下dataloader的num_workers是不是太高了。
大概率是backbone里没用no_grad的BN统计量在累积,试试冻结BN或者开eval模式看还涨不涨。
显存一直涨到OOM基本不是单次占用问题,多半是计算图没释放。试试把每个batch的loss.backward()换成retain_graph=False,或者查查代码里有没有把tensor存进list累积。
用torch.cuda.memory_summary()看下是tensor还是缓存占的,如果是缓存的话设下PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True能缓解碎片化。
试试把optimizer.zero_grad()挪到backward之前,我上次这么搞直接解决了,显存曲线平得跟心电图似的。
你这个问题我太熟了,之前跑分割模型也卡在这。显存持续上涨但batch很小,大概率不是数据或者损失函数的问题,而是计算图没被释放。你试试把每个step的loss.backward()之后加个optimizer.zero_grad(set_to_none=True),有时候梯度累积不是显式的,但某些自定义loss或者辅助loss会隐式保留中间变量。另外我强烈建议你用torch.cuda.memory_summary()配合torch.profiler,那个能按操作符看分配,比只看总显存直观多了。还有一个坑是ResNet101的BatchNorm在训练模式下会保存每batch的统计量,如果你有多个forward(比如验证时也开着grad),显存会线性叠加。你可以检查下是否在训练循环里不小心调用了model.eval()但没包torch.no_grad(),这个特别常见。如果确认没有循环,那试试在每步训练后手动del loss和output,再torch.cuda.empty_cache(),虽然治标不治本但能定位是不是缓存碎片。最靠谱的排查方法还是把batch_size设为1,如果还涨,就逐层打印中间变量的requires_grad和存储位置,大概率是某个激活被重复计算了。我之前还遇到过因为使用F.interpolate时align_corners参数不同导致内部缓存不释放的情况,你可以顺便看下上采样那部分。
我之前也遇到过类似情况,把batch_size调小后显存还是缓慢上涨,最后发现是网络里有个自定义的feature map在每次迭代时都做了切片操作,没释放旧引用。你可以先试试在训练循环里每步都打印torch.cuda.memory_allocated(),看是不是线性增长,如果是那就排查前向里有没有动态创建节点。另外torch.cuda.memory_summary()确实能看出tensor的分配位置,但更直观的是用pytorch_memlab的LineProfiler标注每一行,直接定位到具体代码。还有一种可能是backbone的BN层在训练模式下统计量更新,但一般不会涨这么猛,你先确认下是不是有多个计算图没清,比如loss.backward()后没手动del中间变量。
试试跑几个step后打印memory_summary,重点看缓存块和活跃块差值,大概率是计算图没释放。
用pytorch的torch.autograd.detect_anomaly开一下,能定位到具体哪行梯度爆的。
我之前也踩过类似的坑,最后发现是DataLoader的num_workers开太多,每个worker的显存副本没释放,加上pin_memory=True也会额外吃显存,你可以先关掉pin_memory试试。另外torch.cuda.memory_summary()确实能看缓存分配,但更直接的办法是用nvidia-smi监控每个进程的显存曲线,如果持续涨但没到OOM,多半是代码里某个tensor被意外保留,比如在循环里用loss.backward()却没清空optimizer的梯度,或者有tensor被全局变量引用。你提到没开梯度累积,但可以检查一下是否有类似loss.item()被误用成loss本身,导致计算图没释放。建议在每次迭代后打印torch.cuda.memory_allocated(),看是不是每个step都净增长,如果是,就在关键位置加del和torch.cuda.empty_cache()逐步定位。模型里的多尺度输入或辅助loss分支也可能造成隐式重复forward,你可以把forward里所有中间变量都显式赋给局部变量,避免意外持有。
大概率是验证阶段没关梯度,eval模式里忘写torch.no_grad了吧,试试加上再看看显存曲线。
显存持续上涨但batch很小,大概率是计算图没释放,试试在optimizer.zero_grad()前加torch.cuda.empty_cache()看看。
我上次遇到类似问题,用torch.autograd.detect_anomaly()定位到是某个自定义模块里有隐藏的for循环累积了梯度。
我之前也踩过类似的坑,最后发现是backbone里BatchNorm的running stats在作怪,不是显存泄露,而是PyTorch的autograd把整个计算图都保留了。你试过用torch.cuda.memory_summary()看分配峰值在哪一层吗?如果看到大量“allocated”集中在某个block,那基本就是计算图没释放。还有个很实用的技巧:在训练循环里每步清空一下缓存,torch.cuda.empty_cache(),但注意这只能缓解碎片化,不能根治。另外你说的多次forward,我怀疑是DeepLabV3+里的ASPP模块有并行分支,每个分支都保留中间激活,显存会成倍涨,你可以试试用checkpointing,就是torch.utils.checkpoint,把梯度重计算打开,能省不少。不过最诡异的还是从2G涨到12G这个过程,如果是计算图累积,应该一开始就高,而不是慢慢涨——你确认下是不是dataloader里num_workers太多,每个worker持有的临时缓存也会占显存?我之前还遇到过label smoothing在损失函数里隐式创建了额外变量,导致每个step都新开图,你换个简单的loss试试。如果还不行,就写个最小复现例子,把backbone换成ResNet18跑一下,看涨不涨,这样能二分定位是模型结构还是训练逻辑的问题。
这个问题太典型了,我前阵子也被类似情况折磨过。你先把batch降到2还涨,基本可以排除数据并行和batch本身的问题了,大概率是计算图没释放。我建议你直接跑一个固定步数的循环,在每步之后打一下torch.cuda.memory_reserved()和memory_allocated(),如果allocated在涨但reserved稳定,那可能是缓存碎片,如果reserved也跟着涨,那就真有节点在累积。你提到多次forward,这个很关键——DeepLabV3+如果用了ASPP里的空洞卷积,虽然本身没有循环,但有些实现会在forward里重复调用同一个模块,如果那个模块内部有变量被保存到self上,就会越积越多。另外,你检查一下有没有把loss或者中间feature map存到list里做可视化,哪怕只是调试用的,也会把整个计算图钉住。还有个冷门坑:如果用了BatchNorm,并且在训练模式下对同一batch做了多次forward(比如为了计算auxiliary loss),BN的running stats虽然不占显存,但每次forward的激活值如果没有被正确丢弃,就会挂在图上。工具方面,torch.cuda.memory_summary()确实能看分配器状态,但更直观的是用pytorch的autograd.profiler,它能把每个op的临时显存峰值列出来,你重点看那些输出shape很大的卷积或者上采样层,是不是有异常高的临时buffer。最后,如果实在找不到,可以在每次backward之后手动调torch.cuda.empty_cache(),虽然不治本,但能确认是不是缓存没清理的问题,如果加了立刻稳定,那就基本锁定是某个op的临时变量在作祟。
显存涨到OOM多半不是单次forward的问题,先试试固定随机种子复现,再逐步注释掉backbone和aspp模块定位哪块在涨。
大概率是验证集或保存logits时没关梯度,试试inference阶段包个torch.no_grad(),顺便看下dataloader有没有泄漏。
显存持续涨基本是计算图没释放,除了memory_summary,可以每步打印allocated和cached差值定位。
我遇到过类似情况,最后发现是DataLoader的num_workers开太多,每个worker都缓存了一堆数据在显存里,你试试把workers降到0或者2看看。另外DeepLabV3+的ASPP模块里有个并行的空洞卷积,虽然不算循环但梯度回传时会累积中间变量,建议用torch.autograd.detect_anomaly()跑一下,能直接定位到爆显存的具体操作。memory_summary确实有用,但更推荐用pytorch的memory_profiler,能按行输出每步分配和释放的tensor大小。还有一个坑是验证集评估时没包torch.no_grad(),如果每轮都跑验证,显存会慢慢堆上去。
先关掉CUDA缓存清一下再跑,大概率是验证集或日志里存了graph没释放,用torch.cuda.reset_peak_memory_stats()盯一下峰值。
我之前跑分割模型也遇到过一模一样的情况,最后发现是backbone的BN层在训练模式下不断更新running stats导致计算图被保留,你试试冻结BN或者用sync_bn试试。另外可以每隔几个step打印一次torch.cuda.memory_allocated()看是不是阶梯式上涨,如果是的话大概率是优化器step后没清空梯度,但你说没开梯度累积就有点奇怪了。还有个笨办法,把validation的forward也包在torch.no_grad()里,有时候eval模式下忘了关梯度也会涨显存。