最近在跑一个图像分割的模型,用的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都缓存了batch数据,显存直接被吃满。你可以先试试把num_workers设成0,或者用torch.cuda.memory_allocated()和torch.cuda.memory_cached()打点看看是不是缓存没释放。另外DeepLabV3+的ASPP模块里如果用了空洞卷积,某些实现会隐式创建计算图,建议检查一下有没有把中间变量存在了self里,那种累计引用也会导致显存涨。
顺便说一句,torch.cuda.memory_summary()确实能看出是tensor还是cache占大头,但更直接的是在训练循环里每隔几步打印一下max_memory_allocated,配合nvidia-smi看时间点。你要是方便的话,可以试试用torch.autograd.detect_anomaly()跑几个step,虽然慢但能定位到具体哪行反向传播爆的。我印象里多次forward但没累积梯度的话,显存应该是平的,除非你保存了每个step的输出列表。
八成是验证集里也开了grad,或者模型forward里缓存了中间变量,试试关掉grad再跑一轮看曲线平不平。
遇到过类似问题,八成是验证集/测试集里也开了梯度计算,或者loss反向传播后没清空计算图,试试加个with torch.no_grad()看看。
建议用pytorch的memory_summary看下缓存分配,再查下dataloader里有没有把图片或mask留在显存上,之前我就是多加载了变量没释放。
试试关掉cudnn.benchmark,加上torch.cuda.empty_cache()看曲线,大概率是验证集forward没包no_grad。
用pytorch的memory profiler逐行跑一下,或者直接看nvidia-smi的时间点,一般这种涨法都是优化器state或中间变量没释放。
我之前也踩过类似的坑,排查一圈发现根本不是模型的问题,是DataLoader的num_workers开太多,每个worker都会预加载一批数据到显存里,你batch_size调小但worker数没变,照样会涨。你可以先把num_workers设成0试试,或者用pin_memory=False对比一下。
另外你说loss是CrossEntropy,但DeepLabV3+的输出和mask的shape对不上时,有些实现会隐式地做one-hot或者广播,这种操作会在计算图里保留中间变量,虽然不大但架不住每个step都累积。建议你在训练循环里手动加一行torch.cuda.empty_cache(),虽然治标不治本,但能帮你确认是不是真的在增长。
还有个思路是监控每个tensor的refcount,用gc模块配合torch.cuda.memory_snapshot()看看是哪些分配块在持续增加。我之前遇到过类似情况,最后发现是自定义的损失函数里用了两次forward,第二次的梯度被隐式保留了下来,你检查下模型里有没有分支结构或者auxiliary loss。
如果你用的是DistributedDataParallel,那梯度同步的buffer也会占显存,试试单卡跑几个epoch对比一下。另外pytorch的caching allocator不会立刻释放显存,所以短期波动是正常的,但一直涨到12G肯定有持有引用的对象,建议在每次迭代后打印memory_reserved和memory_allocated的差值,如果差越来越大就是碎片化,如果allocated本身在涨就是真有泄露。
这种情况我遇到过,多半不是loss的问题,而是模型里有东西在反向传播时被保留了。你试试在optimizer.zero_grad()之前加一句torch.cuda.empty_cache(),再跑几个step看曲线,如果还是涨,基本就是计算图没释放。另外DeepLabV3+的ASPP模块里如果有并行分支,某些实现会重复调用同一个卷积模块,导致中间变量被缓存,你可以把forward里的中间结果用del手动删一下。memory_summary确实有用,但更推荐用pytorch的profiler或者nvidia-smi的每进程显存监控,能直接看到是哪个op在涨。还有个土办法,把batch_size设成1,如果还涨,那肯定不是batch的问题,是代码结构里有循环或者多次调用backward没清梯度。
试试关掉cudnn.benchmark,有时候自动tuning会吃满显存,还有用torch.autograd.detect_anomaly()看看有没有梯度爆炸。
建议先跑个固定step用nvidia-smi盯显存,再开memory_summary,多半是验证集forward没包no_grad,或者模型里有个隐藏的累积操作。
这个涨法明显不是正常波动,更像是计算图没释放或者有隐藏的引用。你可以先在每个step结束加torch.cuda.empty_cache()看能不能缓解,如果没用就试试把optimizer.zero_grad()放到loss.backward()之前,排除梯度累加的问题。另外DeepLabV3+的ASPP模块里有并行分支,如果代码里不小心把中间特征append到list里保存了,也会导致显存只增不减,检查下有没有这种隐式引用。工具的话,pytorch自带的memory_summary能看到缓存分配,但定位泄漏还是得靠hook,建议用torch.autograd.detect_anomaly()跑一个step试试。
你这个问题我之前也踩过坑,显存涨到OOM很多时候不是batch_size的锅,而是backbone里BN层的running stats在反向传播时累积了计算图。建议先用torch.cuda.memory_summary()看是不是tensor的缓存碎片太多,同时检查下每个epoch有没有把optimizer.zero_grad()放在合适位置。另外如果模型里有类似ASPOC或可变形卷积这种动态结构,也可能导致隐式建图,可以试试用torch.profiler看看哪一层的内存峰值最高。我之前遇到过类似情况,最后发现是dataloader的num_workers开太多,每个worker都保留了部分图,把workers调到0或者4以下就正常了。
你这情况八成不是显存泄露,而是计算图堆积。DeepLabV3+的ASPP模块里并行分支多,加上ResNet101中间特征图数量大,即使batch=2,每次backward后计算图没释放干净也会涨。先试试在optimizer.zero_grad()后面加torch.cuda.empty_cache(),然后每个step打印一下torch.cuda.memory_allocated()看是不是线性增长。如果还是涨,就检查下dataloader里是不是有对tensor的持久化引用,比如把样本存到list里了。我上次就是num_workers=0但数据集里有个缓存list没清,搞了半天。
跑一下torch.cuda.memory_summary()看下是哪个环节的tensor在累积,重点盯一下backbone的中间变量是不是被存下来了,ResNet101挺容易踩这个坑。另外你试试把batch_size设成1然后固定随机种子复现,如果还会涨基本就是代码里有个地方不小心把计算图挂在某个list或者dict上了。我之前遇到过类似情况,最后发现是验证集里忘了with torch.no_grad(),导致每轮验证都在往旧图里加节点。你那个多次forward的猜测也有可能,特别是如果用了类似feature pyramid的结构,得确认每次forward的中间结果有没有被外部引用。
这个涨法确实不像正常波动,我怀疑不是显存泄露,而是计算图没释放。你可以试试在训练循环里加个torch.cuda.empty_cache()看能不能手动清掉一部分,如果清完能掉下来,那大概率是某个地方把中间变量存到list里了。另外DeepLabV3+的ASPP模块如果用了不同dilation的并行分支,有没有可能你代码里不小心把特征图append到某个容器里了?我之前遇到过类似问题,最后发现是评估模式里没关grad,导致每次迭代都累积了历史图。建议你先用torch.cuda.memory_summary()看下是哪个op分配的峰值,然后重点检查forward里有没有在循环外定义可变长度的tensor操作。
显存从2G一路涨到12G这个特征其实不太像单次forward的静态分配,更像是有东西在累积计算图,你可以重点查一下训练循环里有没有把loss或者中间变量append到list里,或者用了tensorboard的add_scalar但没转成float。torch.cuda.memory_summary()确实能看缓存池分配,但更直接的办法是开一下detect_anomaly(),它会报出第一次产生nan或者梯度爆掉的op,通常这种异常会伴随显存异常。另外DeepLabV3+如果用了ASPP里的空洞卷积,理论上不会有循环,但你得确认一下有没有在forward里多次调用同一个模块比如对同一特征图做多次上采样,这种情况下用torch.autograd.graph的save_for_backward也能看到多余保存的激活值。我之前遇到过类似问题,最后发现是数据加载时每个batch都重新创建了Tensor但没有释放,换成pin_memory=False就好了,你可以先试试这个。
你试试在训练循环里每隔几个step打一下torch.cuda.max_memory_allocated(),如果峰值是在backward之后才涨,那多半是计算图没释放,重点检查一下有没有在loss.backward()之后还保留了中间变量。我之前遇到过类似情况,最后发现是模型里有个辅助loss的feature没detach,导致反向传播时把整张图都挂着。另外你那个DeepLabV3+如果是官方实现,ASPP模块里的空洞卷积在低分辨率输入下反而可能更吃显存,可以先换成mobile backbone排除一下模型本身的问题。
这种显存持续上涨最后OOM的情况,我强烈怀疑不是“泄露”而是真·缓存没释放。PyTorch的缓存分配器默认会保留显存块,哪怕你删了tensor,显存也不会立刻还给驱动,但如果你观察的是nvidia-smi的占用,它可能一直显示涨到峰值,而实际可用显存其实是够的。你试试在训练循环里每隔几步加一句torch.cuda.empty_cache(),如果显存能回落,那说明是缓存碎片问题;如果完全不回落,才是真的有人持有了引用。
另外你说的“多次forward”,我遇到过类似坑:如果你在验证集上也跑了forward,而且没有包在torch.no_grad()里,那么验证图的中间变量会一直留在计算图里,直到下一个epoch开始才释放,累积起来就很可观。DeepLabV3+的ASPP和decoder部分其实挺吃显存的,尤其ResNet101的中间特征图,256x256输入下每层也有几十MB,建议你用torch.profiler看看每个op的memory allocated,或者直接打印model的hooks统计每层输出tensor的shape和device,比memory_summary更直观。
还有个隐蔽点:你用的CrossEntropyLoss如果内部带one-hot或ignore_index处理,在分割任务里它会生成一个和输入同尺寸的float mask,这个也会占显存。我建议你检查一下数据加载部分,比如num_workers>0时,pin_memory为True有时会导致显存被额外占用,尤其在Windows上。最后,如果实在排查不出,可以试试把backbone换成ResNet50跑同样的数据,如果显存涨得慢很多,那基本就是模型结构本身在极限边缘了,可能需要梯度检查点(checkpoint)来换空间。
这种持续上涨到OOM的情况,大概率不是单次forward的问题,而是有东西在反向传播后没被释放。建议你先试试在step之后手动调一下torch.cuda.empty_cache(),如果显存能掉下来,那就是缓存碎片化;如果还是涨,就得怀疑是不是有loss或者中间变量被存进了list里。另外你用的DeepLabV3+的ASPP模块里如果有并行分支,也可能因为梯度图保存导致累积,可以试着用torch.autograd.detect_anomaly()跑一遍看具体卡在哪一步。我之前遇到过类似情况,最后发现是评估模式里忘了with torch.no_grad(),验证集forward也参与了梯度计算,你可以重点查下这个。
查一下是不是backbone的BN层在训练模式下累积了计算图,试试用torch.cuda.memory._dump_snapshot()看下哪步分配没释放。
我遇到过类似情况,最后发现是Dice Loss里对mask做了one-hot编码后没及时del,加个gc.collect()就稳了。
我之前也遇到过类似情况,最后发现不是泄漏而是计算图没释放。你试试把每个step的loss.backward()和optimizer.step()包在with torch.no_grad()外面,然后单独测一下forward不backward会不会涨,这样能快速定位是前向还是反向的问题。torch.cuda.memory_summary()确实有用,但更推荐用pytorch的profiler,它能按操作类型统计内存分配,比如看到是卷积的workspace还是激活值占大头。另外DeepLabV3+的ASPP模块里有空洞卷积,不同rate的并行分支在显存上是叠加的,虽然你输入256但ResNet101的stage4特征图依然不小,建议用torchsummary打印一下各层输出尺寸,可能某个中间tensor意外被保留到了下一轮。还有个坑是DataLoader的num_workers>0时,如果开了pin_memory,每轮迭代都会缓存一部分显存,虽然不参与梯度但累积起来也很可观,可以试着关掉pin_memory或者用torch.cuda.empty_cache()在每个epoch后清一下。你提到没有循环,但DeepLabV3+有些实现里会重复使用同一个卷积模块,如果写了类似self.aspp(x)两次而没加register_buffer,就会把中间结果留在显存里。最后建议直接跑一个最小复现,把backbone换成ResNet18,输入改成64x64,逐步增加复杂度,看哪一步开始指数级上涨,这样比盲猜工具高效得多。
遇到过类似情况,八成是验证集里也开了梯度或者有没清理的中间变量,试试把optimizer.zero_grad()和loss.backward()之间加个torch.cuda.empty_cache()看下曲线。
这种情况我踩过坑,大概率不是模型本身的问题,而是数据加载或者计算图没释放。你试试把验证集的forward也包在torch.no_grad()里,有时候验证阶段忘了关梯度,显存就会一点点累积。另外,用torch.cuda.memory_summary()看下是不是有大量的“allocated”但没释放的块,如果集中在某几个层,可能是中间变量被保留了。
还有个思路,你可以在每个epoch结束后打印一下memory.allocated和memory.reserved,如果allocated在涨而reserved不变,那就是有变量被引用没释放。我之前查出来是因为在循环里把输出append到list然后没清空,导致计算图一直被挂住。你可以搜一下pytorch的mem_tracker,或者干脆用nvidia-smi的定时采样,看是不是只有训练时涨,验证时不涨,这样能缩小范围。