最近在跑一个图像分割的模型,用的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 mean和running var,如果你在验证阶段忘了切model.eval(),显存也可能异常,但你这个是纯训练,所以先排除这个。我建议你先用torch.cuda.memory_summary()看下allocated和reserved的差值,如果reserved特别大但allocated不大,那就是PyTorch的缓存分配器没释放,不是真泄露,可以试试torch.cuda.empty_cache()看能不能降下来。另外DeepLabV3+的ASPP模块里有几个并行空洞卷积,如果代码里不小心把中间结果append到list里存着了,每个step都会累积,显存必然线性涨,你查查有没有这种容器累积操作。至于循环和多次forward,如果你是单卡训练,正常迭代不会导致持续上涨,除非你用了类似冻结某些层然后手动多次forward求loss的写法,每个step的计算图没被释放。还有个很隐蔽的点,检查一下你是否在loss.backward()之后还保留了loss或者中间feature的引用,比如为了可视化存了某层的输出,这会让计算图一直挂在显存里。工具方面除了memory_summary,可以试试pytorch_memlab的line_profile或者torch.profiler,能精确定位到哪一行分配了显存。最后如果排查不出来,可以试试把batch_size设成1,如果还是涨,那就是模型结构里有个别层在迭代中动态创建参数,比如自定义的op里用了torch.where之类的,这种问题最头疼。
这种持续上涨到OOM基本可以排除模型本身占用量的问题,更像是计算图没释放。你可以试试在训练循环里加torch.cuda.empty_cache()看下峰值有没有回落,但更关键的是检查一下有没有把loss.item()或者梯度值不小心存进list里,那种累积也会让显存越占越多。
另外DeepLabV3+的ASPP模块里如果用了不同的空洞率并行,不会有循环,但如果你在forward里对特征图做了多次原地操作或者重用了某个中间变量,也可能导致缓存累积。我上次遇到过类似问题,最后发现是数据加载时num_workers没设对,每个epoch结束后的临时tensor没被回收。
你可以用torch.autograd.detect_anomaly()跑一个step,它会直接报出来哪一步的backward出了问题。另外torch.cuda.memory_summary()确实能看分配器的状态,重点看allocated和cached的区别,如果cached一直涨而allocated没涨,那就是缓存没清,不是真泄漏。
torch.cuda.memory_summary()确实能看出来,重点看allocated和reserved的区别,如果reserved一直涨但allocated稳定,多半是缓存碎片问题,可以试试torch.cuda.empty_cache()放在每个epoch后。另外DeepLabV3+的ASPP模块里有空洞卷积,如果用了多尺度特征融合,某些中间变量可能被意外保留,建议在forward里检查下有没有把feature map存成self.xxx。我之前遇到过类似情况,最后发现是dataloader的num_workers开太多,每个worker都预加载了一批图,显存是共享的但内存会爆,间接拖垮显存。你还可以试试用torch.autograd.detect_anomaly()跑一遍,看看有没有梯度异常导致的计算图保留。
试试关掉cudnn.benchmark再看,有时候卷积搜索缓存也会占显存,另外确认下每个epoch有没有清空outputs。
这问题我踩过坑,八成不是真泄露,是计算图没释放。你试试在每次迭代后把optimizer.zero_grad()和loss.backward()的顺序确认下,或者干脆在循环里加一句torch.cuda.empty_cache()看显存会不会回落。如果还涨,就检查下是不是有tensor被保存在list里做可视化或日志统计了,尤其是那些带requires_grad的。用torch.cuda.memory_summary()能看缓存分配器状态,但更直接的办法是hook每个模块的forward输出,打印shape和显存增量。还有,DeepLabV3+的ASPP里如果有并行分支且用了权重共享,多次调用同一个模块也会累积图,可以试着把中间结果detach一下。
遇到过类似情况,最后发现是dataloader的num_workers开太多,每个worker都会预加载一批数据到显存里,虽然batch_size小但workers一多累积起来也吓人,你可以先试试把num_workers设为0跑一下。另外torch.cuda.memory_summary()确实能看缓存分配,但更推荐用pytorch的memory_profiler或者给每层挂hook打tensor的shape和device,能直观看到是不是某层输出异常膨胀。关于重复forward,DeepLabV3+本身没有循环,但如果你在验证集上也开grad,或者用了BN的running_stats更新,可能间接保留中间变量,建议训练和eval模式切换时明确用with torch.no_grad()。还有个隐蔽点:如果用了数据增强里的随机缩放,图片尺寸每次不一样,某些op会按最大尺寸缓存workspace,显存会波动上涨,可以固定输入尺寸试试。最后检查下优化器是不是SGD+momentum,有些实现会把历史梯度也缓存,虽然一般不会涨这么多,但保险起见看下parameters的requires_grad有没有意外开着的。
试过用torch.cuda.memory_summary()看峰值分配在哪一层了吗?我之前遇到类似问题,最后发现是backbone里有个没用的中间变量被存下来做backward,用del加torch.cuda.empty_cache()能缓解一部分。另外你确认下是不是dataloader的num_workers>0时,每个worker都保留了一份模型副本,显存会累加。还有个笨办法,把loss.backward()改成retain_graph=False,然后每隔几步print一下torch.cuda.memory_allocated(),看是哪个step开始跳变的,能定位是不是某个batch特别大。
这情况我遇到过,先别急着怀疑代码逻辑,多半是训练循环里某个地方把计算图给保留了。你可以试试在每次反向传播后加一行torch.cuda.empty_cache(),同时检查下有没有把loss或者中间变量append到list里没释放。另外用torch.cuda.memory_summary()看峰值分配点挺管用的,我之前就是靠它发现是验证阶段忘了关梯度,但你这如果是纯训练就再查查数据加载那边,是不是num_workers开太多导致缓存堆积。还有个笨办法,把batch_size设成1跑几个step,如果显存还涨那就基本确定是图没释放,逐段注释代码二分定位吧。
试试关掉cudnn.benchmark,有时cudnn自动调优会缓存爆显存,我上次就是这么解决的。
试试关掉cudnn的benchmark,有时候卷积搜索缓存会吃显存,我遇到过类似情况。
建议直接跑个小batch看每步显存增量,用pytorch的memory_profiler定位哪层爆的。
这题我踩过,八成不是显存泄露,是PyTorch的缓存机制在作祟,显存涨到OOM不代表真的全占用了。你先跑个torch.cuda.memory_summary()看看,重点看reserved和allocated的差值,如果reserved巨大但allocated平稳,那就是缓存碎片问题,直接调PYTORCH_CUDA_ALLOC_CONF=max_split_size_mb:32能缓解。另外DeepLabV3+的ASPP模块如果用了不同膨胀率的并行分支,反向传播时梯度会暂存很多中间变量,你可以试试把torch.no_grad()包住验证阶段的forward,或者检查一下是不是dataloader的num_workers把缓存也吃进去了。我上次是换了个优化器就解决了,挺玄学的。
显存涨不停基本就是计算图没释放,试试每个step结束把optimizer.zero_grad()和loss.backward()之间加个torch.cuda.empty_cache()看看峰值降不降。
你这种小batch还涨八成是backbone里用了BatchNorm的running stats在累积,关掉BN的track_running_stats或者换SyncBN试试。
遇到过类似的,先别急着怀疑模型结构,最可能是优化器状态或者中间变量没释放。你试试在每步迭代后打印torch.cuda.memory_allocated(),看是不是线性增长,如果是,大概率是计算图没被正确释放,比如某个loss或输出被额外引用了。另外,DeepLabV3+的ASPP模块里如果有并行分支,用torch.cuda.memory_summary()能看出具体哪一层在涨,我上次就是发现辅助损失头没detach导致累积。也可以开一下torch.autograd.detect_anomaly(),虽然慢但能定位到具体操作。
遇到过类似的情况,最后发现是backbone的BN层在训练模式下会持续更新running stats,但显存暴涨往往不是这个引起的。你试试把torch.no_grad()包住validation阶段,我猜你可能是train和eval交替时忘了切model.eval(),导致eval的forward也建了完整计算图。另外DeepLabV3+的ASPP模块里有个并行的空洞卷积,如果用了不同的rate,某些实现会把多个feature map concat后再过后续层,这时候中间变量会一直驻留到backward结束,显存曲线就会像台阶一样涨。建议你用torch.cuda.memory._record_memory_history()抓一下allocations,配合snapshot工具看哪个tensor占着不释放,比memory_summary直观得多。还有个偏方,把optimizer.zero_grad()换成set_to_none=True,能省一点碎片显存,但解决不了根本问题。你提到没有循环,但我怀疑是不是数据加载时每个batch都调用了模型一次,而某个自定义Dataset的__getitem__里不小心把tensor存到了self上,那样的话每个step都会残留引用。最后检查一下损失函数里有没有类似torch.norm这种会隐式创建中间变量的操作,虽然CrossEntropy本身没事,但有些实现会先做softmax再取log,反而多存一份概率分布。
这问题我太熟了,之前跑分割模型也被坑过。你这种情况大概率不是显存“泄露”,而是计算图没释放,检查一下训练循环里有没有把loss或output保留在列表里,比如为了画曲线存了每个step的loss,这玩意儿会一直累积计算图直到 backward 才清。另外DeepLabV3+的ASPP模块里有多尺度空洞卷积,如果用了不同rate的并行分支,反向传播时中间激活值会占不少显存,你可以试试把torch.no_grad()包住验证集的前向,或者确认下是不是在验证阶段也开了梯度。还有个冷门点:如果用了BN,训练模式下的running_mean和running_var虽然不占显存,但每个batch的中间统计量会保留,偶尔也会造成显存缓慢上升。建议你直接在训练循环里每10个step打印一下torch.cuda.memory_allocated()和max_memory_allocated,如果max一直涨而allocated能降下来,说明是碎片化;如果allocated稳定但max涨,那可能是PyTorch缓存分配器没还给驱动,试试设置PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True。最后,可以用torch.profiler看每个op的内存增量,比memory_summary直观多了,能找到具体是哪一层在偷偷涨。
试试关掉cudnn的benchmark,有时候卷积搜索会缓存一堆workspace,我上次就这么解决的。
你这个情况我上周刚遇到过,最后发现是backbone里的BN层在训练模式下会持续累积running stats,而且ResNet101的中间特征图如果没释放引用,就算batch小也会越堆越高。建议先用torch.cuda.memory_snapshot()抓一下当前张量的分配栈,看看是不是有某个tensor被外部变量长期持有。另外可以试试把模型切到eval模式跑几个step,如果显存不涨了那基本就是训练逻辑里哪一步把中间结果存下来了,比如hook或者自定义forward里不小心append了list。顺便问下,你的dataloader有没有开num_workers>0?有时候数据预取的缓存也会占显存,虽然不常见但值得排除。
我之前跑分割模型也遇到过一模一样的坑,显存曲线跟心跳似的往上跳。你提到batch已经降到2了还这样,那基本可以排除数据并行和输入尺寸的问题,大概率是计算图没释放。一个很隐蔽的坑是,如果你在训练循环里把loss.item()或者一些中间变量存到list里,比如为了可视化或者计算指标,这些tensor虽然标量但会保留整个反向图的引用,显存就一点一点吃满了。你可以试试在每次反向之后加一句optimizer.zero_grad(set_to_none=True),这比手动detach更彻底,能清掉梯度相关的buffer。另外,torch.cuda.memory_summary()确实有用,但信息太杂,我更推荐用pytorch的profiler,它能看到每个op分配的内存峰值,配合tracemalloc能定位到具体是哪一行代码分配的。还有种情况是,你的模型里如果有类似自注意力或者特征金字塔的结构,即使没有显式循环,某些操作比如F.interpolate也会在反向时缓存中间结果,你可以检查一下是不是有重复调用的模块。最后,建议你把验证集也跑一遍,如果验证集显存也涨,那可能是模型前向里有动态图分支,比如依赖输入shape的条件计算,这种要检查下有没有在forward里创建新的参数。我上次最后发现是数据加载器的num_workers开太多,每个worker的cuda context没释放,换了persistent_workers=False就好了,你也可以试试这个冷门原因。
这个涨法明显不是正常波动,我赌你代码里肯定有某个地方把中间变量存成了list或者tensor的引用,比如在循环里append了loss或者feature map。你先用torch.cuda.memory_summary()看下allocated和reserved的差值,如果allocated一直涨而reserved稳定,那就是Python侧持有引用没释放。另外DeepLabV3+的ASPP里如果有并行分支,某些实现会在每个step重新构建计算图,建议检查一下是不是把forward写在循环里了,或者用了torch.no_grad()包住验证阶段。我上次遇到类似情况是logits忘了detach就丢进TensorBoard,一跑test就涨,你可以翻翻训练流程里有没有往summary writer里塞张量的操作。
显存只涨不降基本不是模型结构问题,更像是计算图没释放或者优化器状态在累积。你用torch.cuda.memory_summary()看下分配峰值出现在哪个环节,重点检查有没有把tensor存进list或者dict里没清,尤其是每个step都append的中间变量。另外DeepLabV3+的ASPP模块如果用了不同dilation的并行分支,某些实现会隐式保存所有中间激活,你可以试试用torch.autograd.detect_anomaly()定位,或者干脆把backbone的gradient checkpointing打开,能省不少显存,代价就是慢一点。我之前遇到过类似情况,最后发现是dataloader的num_workers>0时pin_memory=True没配好,导致每轮迭代都有隐藏的缓存没释放,你可以也查下这块。