最近在跑一个图像分割的模型,用的DeepLabV3+,backbone是ResNet101。我把batch_size降到2了,输入图片也缩到256x256,但显存还是从开始的2G一直涨到12G,最后OOM。我查了网上说可能是梯度累积、或者变量没detach的问题,但我没开梯度累积,损失函数也是常用的CrossEntropy。想问问大家有没有什么成熟的debug思路?比如用torch.cuda.memory_summary()看哪里泄露,或者有没有工具能可视化每层的显存占用?另外,是不是我模型里有循环或者多次forward导致的?感谢各位大佬!
PyTorch训练时显存一直涨但batch_size已经很小了,咋排查?
全部回复
共 180 条这问题我上周刚踩过类似的坑,你试试在训练循环里加torch.cuda.empty_cache()看能不能缓解,如果有效基本就是缓存碎片而不是真泄露。另外用pytorch的autograd.detect_anomaly()跑一下,能定位到具体哪一步的梯度计算出了问题,我之前就是有个自定义op没写backward导致图一直挂着。还有个小技巧,把dataloader的num_workers设成0跑几个step对比下,排除数据加载线程占用显存的可能性。你模型里如果有用aspp模块的话,检查一下空洞卷积的padding设置,那个很容易在深层特征图上有隐性的shape广播。
这问题我太有共鸣了,之前跑分割模型也遇到过一模一样的症状,batch_size调到1都救不回来。你说没开梯度累积,但有个坑经常被忽略——如果模型里有BN层,而且你用了自定义的训练循环,可能忘了在eval模式下跑验证集,导致验证时也更新running stats,显存就会悄悄累积。另外建议你直接看torch.cuda.memory_summary()的输出,重点看“allocated”和“reserved”的差值,如果reserved远大于allocated,说明是缓存碎片问题,可以试试torch.cuda.empty_cache()在每轮迭代后手动清一下,虽然治标不治本但能确认方向。还有个办法是用torch.autograd.detect_anomaly()跑几个step,它会直接告诉你loss反传时哪一步产生了梯度异常,很多时候是某个自定义操作符没有实现好,比如你用了F.interpolate但没指定align_corners,某些版本会隐式创建临时变量。至于多次forward,你得检查一下有没有在loss.backward()之后还继续调用模型,比如在tensorboard里记录特征图时不小心又跑了一次forward,这种最容易漏。我那次最后发现是dataloader的num_workers设太大,每个worker都预加载了图片到GPU缓存,把persistent_workers=True关掉就好了,你可以先看看这个。实在不行就一步步缩小模型,先换成ResNet18跑,如果显存还涨,就基本锁定是训练循环或数据管道的锅了。
大概率不是显存泄漏,是计算图没释放,试试把loss.backward()换成loss.backward(retain_graph=False)看看。
用nvidia-smi盯显存曲线,如果只涨不降就断点检查每个batch的loss和梯度,别光看summary。
torch.cuda.memory_summary()确实能看缓存分配,但更建议你监控每个epoch的峰值显存,如果峰值稳定但整体趋势上涨,大概率是计算图没释放。DeepLabV3+本身没有循环,但你试试把optimizer.zero_grad()放到loss.backward()之后执行,有时候梯度累积是隐式的。另外检查下dataloader有没有把图像和label都放到cuda,label如果是LongTensor且没detach也可能被误存。我之前遇到过类似问题,最后发现是验证集里有个自定义metric把整个batch的预测结果存成了list,导致显存持续增长。你可以用pytorch的torch.cuda.set_per_process_memory_fraction限制最大显存,让OOM早点触发,方便定位是哪一步涨的。
试试关掉cudnn的benchmark,有时候卷积搜索缓存也会吃显存,我上次就是这么解决的。
显存涨不一定是泄露,看看是不是验证集也跑了,把torch.no_grad加上试试。
遇到过类似的,建议先用torch.cuda.memory_summary()看下是不是真的在累积,很多时候是优化器里的动量项在作祟,尤其Adam类的会缓存梯度均值。另外如果你在训练循环里写了loss.backward()但没清空梯度,哪怕batch小也会涨,试试每步optimizer.zero_grad(set_to_none=True)。还有个笨办法,用nvidia-smi盯着看涨的节奏,如果每个step涨一点,基本就是计算图没释放,检查下有没有把tensor存到list里。可视化每层占用可以用pytorch的memory_profiler,不过我感觉先排查数据流比逐层看更高效。
我之前也遇到过类似情况,最后发现是backbone的BN层在训练模式下会持续累积running stats,虽然不占显存但会拖慢释放节奏,你试试用torch.no_grad()包住验证阶段的forward,或者把optimizer.zero_grad()改成set_to_none=True,能省不少缓存。另外强烈建议用torch.cuda.memory_summary()看下是tensor还是cache占的,如果全是cache的话可能是PyTorch的缓存分配器没及时回收,可以设PYTORCH_CUDA_ALLOC_CONF=max_split_size_mb=128强制碎片化释放。还有个小技巧,在循环里print一下每步的allocated和reserved,如果reserved一直涨而allocated稳定,那就是缓存碎片问题,不是泄露。
试试把验证集里的with torch.no_grad()加上,我之前就是这么漏的,显存直接起飞。
我之前也踩过这个坑,图像分割模型很容易在decoder部分有隐式的上采样梯度保留。你先用torch.cuda.memory_summary()看下是不是“saved tensors”占了大头,如果是的话,八成是中间feature map被保留了。另外检查下代码里有没有在循环里重复调用model,或者对同一个tensor反复做backward,这会让计算图累积。还有个笨办法,把batch_size设成1,显存还涨就基本确定是模型结构或数据加载的问题,跟batch无关了。
大概率是验证集或可视化里也算了梯度,试试把torch.no_grad()包全再观察显存曲线。
我之前也踩过类似的坑,你试试把验证集里的with torch.no_grad()加上,很多时候是验证阶段忘了关梯度才涨的。另外DeepLabV3+的ASPP里有并行的空洞卷积,如果用了多尺度或者辅助loss,也可能隐式保留计算图,建议把每个forward的输出都检查一下requires_grad。最直接的办法就是设个torch.cuda.set_per_process_memory_fraction(0.5)把显存锁死,然后跑一个step看报错堆栈,会指向具体是哪一行分配了新块。还有个小技巧,用pytorch的profiler导出每个tensor的size和device,基本能定位到是backbone还是head在涨。
torch.cuda.memory_summary()确实得先看一眼,重点看是不是有大量的缓存碎片或者某个tensor一直没释放。我之前遇到过类似情况,最后发现是dataloader里写了太多预处理操作,每个step都会在GPU上生成中间变量没回收。另外你可以试试在训练循环里每隔几步打印一下memory_allocated和memory_reserved的差值,如果reserved一直涨但allocated稳定,那就是缓存策略的问题,可以设PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True。至于多次forward,DeepLabV3+本身没这个问题,除非你在aux_loss或者评估模式里额外调了模型,检查下有没有把验证集的forward也留在计算图里。
八成是验证集或评估阶段也开了梯度,试试with torch.no_grad()包住,或者查下有没有把loss累加到tensor上。
跑一下nvidia-smi看是不是别的进程占的,我上次就是被另一个实验的显存坑了,白排查半天。
显存持续涨到OOM大概率不是单次forward的问题,更像是计算图没释放或者优化器状态在累积。你可以先试下在每次backward后加torch.cuda.empty_cache()看能不能缓解,如果没用就排查下模型里有没有把tensor存到list或者self里。另外torch.cuda.memory_summary()确实能看分配细节,但更推荐用pytorch_memlab的LineProfiler,能精确到哪一行代码触发了分配。还有个坑是DeepLabV3+的ASPP模块如果用了空洞卷积,多尺度特征拼接时可能产生隐式中间变量,建议用torch.autograd.detect_anomaly()跑一遍,能直接定位到梯度异常的位置。
你试试开torch.cuda.memory_summary()看下是不是有大量activation被保留了,我遇到过类似情况是模型里用了self.training=True导致的,比如BN层在eval模式没切回来。另外DeepLabV3+的ASPP模块里如果有多尺度分支,可能每个分支都缓存了中间特征,建议检查下有没有把torch.no_grad()包在验证逻辑外面,或者用torch.cuda.set_per_process_memory_fraction先限流看涨到哪一步崩的。我之前有个项目是backbone里有个残差连接用了+=没写对,导致计算图没释放,你可以打印每层tensor.grad_fn看看有没有异常的累积路径。
这个现象我遇到过,多半不是显存泄漏,而是PyTorch的缓存分配器在搞鬼——它不会主动把释放的显存还给驱动,所以memory_summary里看到的可能都是虚拟占用。你先试试在验证集上关掉梯度(with torch.no_grad()),如果显存稳定了那就是训练时反向传播的临时变量在累积。另外检查下有没有把loss.item()结果存进list,或者每个step调了tensor.detach()但没清引用。实在不行就用torch.cuda.reset_peak_memory_stats()看下峰值到底涨在哪,配合nvidia-smi的实时监控能更直观。
大概率是验证集或评估阶段也开了梯度,试试with torch.no_grad()包住那部分。
试试关掉cudnn的benchmark,有时会缓存workspace把显存撑爆,之前我这么解决的。
用pytorch的memory_summary抓个快照,看是不是优化器状态或中间变量没释放,比猜快多了。
这个现象我遇到过,基本可以排除batch_size的问题,更像是有个hidden state或者中间变量被意外保留了。你可以试试在每次迭代结束加torch.cuda.empty_cache(),然后再用nvidia-smi看显存曲线,如果还是涨,重点查一下forward里有没有把某个tensor append到list或者self.xxx上。另外PyTorch的autograd会保留计算图,如果loss.backward()之后没有清空optimizer的梯度,某些环境下也会异常,虽然你用的CrossEntropy不太可能,但可以打印一下每层grad_fn看看有没有累积的buffer。说实话memory_summary有时候信息太杂,我更喜欢用pytorch_memlab的检测器,能直接标出泄漏点,你试试看。
换个思路,你可以用torch.autograd.detect_anomaly()跑一下,虽然它主要查NaN,但有时候能间接暴露反向传播里的问题。我之前遇到过类似情况,最后发现是模型里的ASPP模块有个自适应池化层,输入尺寸不固定导致每次forward都重新分配缓存,你DeepLabV3+也有这个结构,建议检查下是否有类似动态shape的层。另外如果你在验证集上也跑,记得用torch.no_grad()包住,不然梯度图会一直累积。实在不行就把resnet101换成resnet50对比一下,排除backbone本身的特性。
我猜你可能是没开c
这种持续上涨到OOM的情况,大概率不是单层显存峰值问题,而是有变量在计算图里被保留了。你试试把每个step的loss加上detach()再打印,或者检查一下有没有把tensor存进list里做可视化。另外torch.cuda.memory_summary()确实有用,重点看allocated和reserved的差值,如果reserved一直涨说明缓存没释放。我之前遇到过类似问题,是数据加载时对同一张图做了多次增强但没释放中间变量,你可以查查数据管线。