最近在跑一个图像分割的模型,用的DeepLabV3+,backbone是ResNet101。我把batch_size降到2了,输入图片也缩到256x256,但显存还是从开始的2G一直涨到12G,最后OOM。我查了网上说可能是梯度累积、或者变量没detach的问题,但我没开梯度累积,损失函数也是常用的CrossEntropy。想问问大家有没有什么成熟的debug思路?比如用torch.cuda.memory_summary()看哪里泄露,或者有没有工具能可视化每层的显存占用?另外,是不是我模型里有循环或者多次forward导致的?感谢各位大佬!
PyTorch训练时显存一直涨但batch_size已经很小了,咋排查?
全部回复
共 180 条这情况我也踩过坑,先别急着怀疑模型结构,大概率不是循环的问题。你试试在每个step末尾加torch.cuda.empty_cache()看能不能缓解,如果不行就盯一下loss.backward()之后optimizer.zero_grad()的顺序,有时候梯度没清干净也会这样。另外torch.cuda.memory_summary()确实能看分配细节,重点看有没有大量保留的中间激活值,建议配合torch.autograd.detect_anomaly()跑一遍,能定位到具体是哪一行爆的。我之前是输出了个没用的特征图没detach,改了之后显存曲线就平了。
我之前也遇到过类似情况,最后发现是DataLoader的num_workers开太多,每个worker的缓存没释放,叠加起来就把显存吃满了,你试试把workers降到0或者2看看。另外torch.cuda.memory_summary()里有个“allocated”和“reserved”的区别,如果reserved一直涨但allocated稳定,多半是碎片化,不是真泄露,可以用torch.cuda.empty_cache()配合监控一段loss曲线判断。还有DeepLabV3+的ASPP模块本身有多个并行分支,如果你在forward里意外保留了中间特征图,比如为了可视化存了list,那即使batch小也会累积,检查下有没有把feature append到全局变量里。
大概率是验证集里也开了梯度计算,或者每个step没清空optimizer的梯度,试试with torch.no_grad()包住验证流程。
用nvidia-smi看下是不是别的进程占着显存,我上次就是多卡没设环境变量导致涨到爆。
我之前也踩过这个坑,显存涨到OOM不一定是batch_size的问题,很可能是验证集或者评估阶段也算了梯度。你试试在验证循环里加torch.no_grad(),然后把optimizer.zero_grad()放在loss.backward()之前,顺便检查下有没有把中间变量存成self.xxx。
另外torch.cuda.memory_summary()确实能看缓存分配,但更快的办法是开一下pytorch的memory profiler,或者用nvidia-smi盯一下是不是有其他进程占着显存。如果模型里有类似自注意力或者特征金字塔的循环,建议把每层输出都detach一下,ResNet101的中间特征挺吃显存的。
我上次是发现DataLoader的num_workers太多,每个worker都复制了一份模型权重,显存直接翻倍。你可以先试着把workers设为0跑一跑,排除这个因素。
这问题我踩过坑,你先别急着怀疑梯度累积,多半是验证集或者测试集里的forward也开了grad,或者模型里有dropout/bn没切eval模式,导致推理时还在建图。另外torch.cuda.memory_summary()确实能看,但更直接的是用pytorch的profiler或者nvidia-smi -l 1盯着看,如果显存是阶梯状涨而不是直线,那八成是缓存了中间变量没释放。还有个野路子,把batch_size设成1跑一次,如果显存还涨,就逐模块二分法排查,比如把ASPP或者decoder单独拎出来测。你试过在loss.backward()之前手动del掉中间变量吗?有时候是计算图的钩子没清干净。
试试在每次迭代后print(torch.cuda.max_memory_allocated())对比一下,如果峰值持续走高大概率是优化器state或计算图没释放。
我遇到过类似情况,最后发现是dataloader的num_workers开太多,每个worker都缓存了CUDA上下文,把workers调成0或2就好了。
试试把验证集的forward也包在torch.no_grad()里,跑一个epoch看显存曲线稳不稳,能很快定位是不是反向传播的锅。
大概率是有变量在循环里被反复引用没释放,用torch.cuda.memory_summary看峰值分配点比瞎猜快。另外试试把梯度清零后手动跑一个step看显存是否回落。
先检查是不是验证集也开了grad,或者loss里有没detach的中间变量,我之前就是这么炸的。memory_summary里看allocated和reserved差值最直观。
老哥可以先试试固定随机种子复现一下,如果每次都涨到同一个数就八成是缓存问题,清下cudnn benchmark看看。
用pytorch的memory_snapshot或者nvidia-smi配合py-spy抓一下,大概率是backbone里某些层的计算图没释放,试试把中间变量用del删掉再gc.collect()。
我之前也遇到过类似情况,最后发现是backbone的BN层在训练模式下会持续更新running_mean/var,如果用了多卡同步BN或者数据加载时每个step的batchnorm统计量没释放,显存会慢慢累积。你可以试试在验证阶段用torch.no_grad()包一下,或者检查下有没有把验证集的loss也加进计算图里。另外DeepLabV3+的ASPP模块如果有空洞卷积并行,某些实现会隐式保留中间激活,建议用torch.cuda.memory._dump_snapshot()生成火焰图看看具体哪一层在涨。我那次最后是发现DataLoader的num_workers>0时,worker进程会缓存部分CUDA上下文,显存不降但会持续涨,把workers设为0试试。
试试关掉cudnn.benchmark,有些情况会缓存workspace,然后盯一下每个step的allocated和reserved差值。
用pytorch的memory_profile或snakeviz看下峰值在哪,大概率是backbone里重复计算没释放,先排除下模型里有没有隐式缓存。
torch.cuda.memory_summary()确实能看缓存分配,但更推荐用pytorch的memory_profiler或者给每个模块挂hook逐层打印显存,我上次就是这么定位到是中间特征图没释放。另外你确认下是不是dataloader的num_workers开太多,有时候数据加载线程也会占显存,我遇到过类似情况把workers降到2就好了。还有ResNet101的DeepLabV3+本身就不小,256输入2的batch撑死3G左右,涨到12G肯定是有变量被留在了计算图里,你试试在loss.backward()后加optimizer.zero_grad(set_to_none=True)再手动del掉中间变量。如果还涨,查一下是不是有eval模式下的batch norm统计量被错误更新了,我朋友之前就是这问题。
之前跑检测模型也遇到过一模一样的情况,batch size调成1都救不回来。你试试在训练循环里每隔几步打一下torch.cuda.max_memory_allocated(),如果这个值持续增长而当前allocated没变,基本就是有变量在计算图里被保留了。我上次是发现模型里有个辅助loss的中间变量被存进了list用于可视化,结果那个list没清空,每个step都挂着整张计算图,显存自然就线性涨上去了。另外DeepLabV3+的ASPP模块里如果用了空洞卷积,某些实现会隐式地创建多个不同rate的中间特征,这些张量在反向传播后应该被释放,但如果你在代码里不小心引用了outputs里的某个元素,比如为了写tensorboard而保存feature map,那也会把整条链路锁住。建议你把forward里所有非必要返回值都注释掉,只留最终输出,跑几个step看看显存曲线是否变平。还有个土办法,就是开一个子进程每50步跑一次gc.collect()和torch.cuda.empty_cache(),虽然不治本但能定位是不是纯碎Python对象引用的问题。如果还不行,用torch.autograd.detect_anomaly()配合设置torch.cuda.set_per_process_memory_fraction(0.5)限流,报错时能看到具体是哪个op保留了张量。最后检查一下你的DataLoader,如果num_workers>0而且pin_memory=True,有时候缓存队列也会吃显存,不过通常不会吃到12G这么夸张。
我之前也遇到过类似的坑,最后发现是dataloader的num_workers设太高,加载数据时额外开了很多缓存,加上模型里BN层的running_mean这些buffer也会悄悄占显存。你先用torch.cuda.memory_summary()看看是不是真的在稳步增长,还是只是峰值高,如果每次迭代后释放了但峰值累加,可能是优化器状态或者backbone里某些中间变量没被释放。另外检查下有没有在循环里把loss或输出append到list里,这种情况最常见,一累积就爆。最好把model.eval()和torch.no_grad()用在验证集上,别让验证阶段的图也参与训练。至于可视化每层占用,可以用pytorch的profiler,那个能看每个op的显存分配,比手动猜靠谱多了。
我上次也遇到过类似情况,最后发现是dataloader的num_workers开太多,每个worker都在缓存数据,显存就这么被吃掉了,你可以先把这个调成0试试。另外torch.cuda.memory_summary()确实有用,能看出是模型参数还是中间激活值占大头,但更推荐用pytorch的profiler看每个op的显存分配,比手动猜高效多了。还有个小坑,如果你在验证集上也跑了forward但没包在torch.no_grad()里,梯度图会一直累积,显存自然就线性涨了,你检查下训练循环里是不是漏了这一步。
遇到过类似的坑,最后发现是DataLoader的num_workers开太多,每个worker都预加载了一批数据在显存里,调成0或者4以下就好了很多。另外你可以跑一个batch然后等几轮,用nvidia-smi看显存是不是稳定,如果还在涨就试试把validation里的no_grad加上,有时候是评估阶段没关梯度导致的。memory_summary确实能看缓存分配,但那个峰值不一定准,更推荐用pytorch的profiler看每层tensor的存活时间,能定位到是不是某个模块在反向传播后没释放。
显存只涨不降大概率是计算图没释放,试试每个step结束加下optimizer.zero_grad()和torch.cuda.empty_cache(),先排除这个再查别的。
DeepLabV3+配ResNet101这个组合本身就挺吃显存的,但batch_size降到2还从2G涨到12G,那基本可以排除是模型本身参数占用的锅了,大概率是训练循环里有东西在累积。你可以先试试在训练循环里每个step打印一下torch.cuda.memory_allocated()和memory_reserved(),如果allocated一直在涨那说明有tensor被持有没释放,如果只是reserved涨而allocated稳定,那多半是碎片问题,调一下PYTORCH_CUDA_ALLOC_CONF就行。另外有个特别容易被忽略的点,就是你算loss或者metric的时候有没有把tensor存进list或者dict里,比如把每个batch的output攒起来最后统一算mIoU,这种写法显存不炸才怪。还有就是验证阶段有没有用torch.no_grad()包住,我见过不少人训练部分写对了但validation忘了加,每次验证都建计算图,显存蹭蹭往上跑。memory_summary确实好用,但建议配合torch.cuda.memory_snapshot()看具体是哪些张量还活着,定位起来更准。如果这些都排除了,可以看看是不是dataloader的worker里有什么全局缓存,或者用了什么预训练权重加载后没释放临时变量。
显存只涨不降最后OOM,八成是计算图被什么东西一直抓着没释放。你说没开梯度累积,但可以重点看看validation或者test那部分代码有没有包在torch.no_grad()里,这个坑太常见了,一不小心eval阶段就把整张图全建起来了。另外你提到模型里可能有循环或者多次forward,这个方向挺对的,比如有些分割头会在不同尺度上反复调用同一个模块,如果中间变量被list存着就很容易泄漏。排查的话torch.cuda.memory_summary()确实能看个大概,但更直接的是用torch.cuda.memory_allocated()在每个epoch或iteration打点,看看到底哪一步开始往上涨。还有个偏方是拿小得离谱的输入跑几个iteration,如果显存照样稳步上升那基本就是累积而非峰值问题。真要可视化每层占用,可以试试torchinfo或者用hooks记录每层输出的shape和显存,不过对定位泄漏来说不如直接二分注释代码来得快。
查下验证阶段有没有加no_grad,我之前就是忘了这个,显存一路飙到爆。