最近在调一个简单的图像分类模型,ResNet18跑CIFAR-10。我batch size已经降到16了,但显存还是稳定上升,跑十几个epoch就OOM了。代码里用了DataLoader,shuffle=True,没开pin_memory。我怀疑是不是自己写的自定义Dataset里存了太多中间变量,或者是在训练循环里把loss和输出都保留了计算图?我试过在每个batch后手动del loss和output,也调用了torch.cuda.empty_cache(),但好像没啥用。有没有老哥遇到过类似情况?或者有没有什么工具能实时监控哪个变量占着显存不释放?感谢!
PyTorch训练时显存一直涨,但batch size已经很小了,是哪里漏了?
全部回复
共 169 条我之前也遇到过一模一样的坑,del和empty_cache其实治标不治本,真正的问题是loss.backward()之后optimizer.step()没被正确调用,或者梯度累积逻辑写重了,导致计算图一直被保留。你可以试试在backward之前加一句optimizer.zero_grad(),如果还涨就检查下自定义Dataset里是不是把图片转成tensor后存在了self里,每次取数据都会叠加上去。监控显存的话,pytorch的torch.cuda.memory_summary()挺好用的,能看到每个tensor的分配情况,比nvidia-smi直观很多。
遇到过一模一样的,大概率不是Dataset的锅,就是计算图没释放干净。你试试在loss.backward()之后加optimizer.zero_grad(),然后loss.item()取数值再del,别直接del loss变量,有时候pytorch的autograd会留着图。还有个笨办法,用nvidia-smi -l 1盯着看,显存涨的时候开个pdb断点,一个个查tensor的shape和device,很快就能定位到是哪个变量没释放。torch.cuda.empty_cache()其实只清缓存不释放占用的,别指望它。我上次是发现有个评估指标的计算忘了detach,结果把整个验证集的graph都挂住了,你检查下是不是有类似操作。
查一下是不是把整个epoch的loss累加到一个list里了,我之前就这么干过,CIFAR10跑几十个epoch直接爆掉。另外确认下optimizer.zero_grad()是不是放在backward()之前了,顺序反了也会让梯度累积。可以用nvidia-smi -l 1配合pytorch的torch.cuda.memory_summary()看每个张量的分配,八成是某个tensor被意外引用了。
八成是loss.backward()之后optimizer.step()前没置零梯度,或者自定义Dataset里返回了非张量对象被反复累积引用。
大概率是优化器或损失函数里存了历史状态,比如用了momentum的SGD或者Adam,它们会维护每层的梯度均值,显存占用会随训练时长缓慢爬升。建议先试试把optimizer换成SGD不带momentum跑几个epoch,如果显存曲线平了就是优化器的问题。另外检查一下自定义Dataset的__getitem__里有没有把图片转成Variable或者保留梯度,正常情况返回Tensor就行,别用requires_grad。监控的话可以用pytorch的torch.cuda.memory_summary(),或者nvidia-smi配合pynvml库看进程内存分配。我之前也遇到过类似情况,最后发现是DataLoader的num_workers设太大,每个worker都拷贝了一份数据集,但那个是CPU内存爆炸,和你的显存不太一样,仅供参考。
你试试在loss.backward()之前加optimizer.zero_grad(),然后backward之后马上step,别把loss和output留在循环作用域里,虽然del了但Python的GC不一定及时回收。我之前遇到过类似情况,最后发现是DataLoader的num_workers设太大,每个worker都复制了dataset的引用,导致显存碎片化,降到2或者4试试。监控的话可以用nvidia-smi dmon看实时显存,或者pytorch的torch.cuda.memory_summary(),能列出每个tensor的分配点,找泄漏挺有用的。另外检查下是不是在验证集里也开了grad,验证阶段记得用torch.no_grad()包一下。
遇到过一样的情况,最后发现是优化器里那个params的梯度没清干净,backward之后得记得zero_grad,不然梯度累积会一直占显存。另外你怀疑loss和output的计算图问题,其实只要在backward之后把loss变量置空就够了,del不一定管用。监控的话可以试试pytorch自带的torch.cuda.memory_summary(),能看每个tensor的分配情况,比empty_cache实际多了。
你这情况八成是loss.backward()之后optimizer.step()之前没把梯度清零,或者你在循环里把每个batch的loss都append到一个list里了,那个list会一直存着计算图。试试在backward前加optimizer.zero_grad(),然后loss.item()取标量再存,别直接存loss。另外torch.cuda.empty_cache()其实治标不治本,真正要查的话用nvidia-smi看进程,或者pytorch里torch.cuda.memory_summary()打印详细分配,能看出来是哪个模块在涨。我之前也遇到过,后来发现是自定义Dataset的__getitem__里每次返回了transforms后的新tensor,但旧引用没释放,改成在init里预处理完存list就好了。
大概率是loss.backward()之前没做optimizer.zero_grad(),梯度累积导致计算图没释放,试试每个batch开头加一行。
你这个情况我太熟了,之前调一个分割模型也卡在显存泄漏上,折腾了两天才发现是自定义Dataset里做了数据增强时把张量留在了self里,每次epoch都会重新生成一份,旧的就堆在那了。你那个怀疑方向挺对的,但光del loss和output可能不够,得检查一下是否有任何变量被挂到self上,或者用了全局list之类的东西累积结果。另外,empty_cache()其实只是把缓存归还给PyTorch的分配器,并不真正释放给系统,如果计算图本身没断,它不会帮你解决根本问题。建议你在训练循环里用torch.autograd.detect_anomaly()跑一下,它会提示哪一步产生了异常计算图,或者更直接点,用pytorch_memlab的Snapshots,能在每个step后打印出所有张量的引用情况,哪个没被释放一目了然。还有个小坑,如果你在loss.backward()之后还用了loss.item(),而loss是标量,一般没问题,但如果你为了打印把loss的item转成张量存进列表,那就GG了。最后提一句,ResNet18跑CIFAR-10,batch16按理说显存占用也就1G多,如果持续涨,八成不是模型本身的问题,而是某处代码逻辑把历史数据都留了引用,建议你把Dataset里所有__getitem__返回的变量全部改成局部构造,别存成员变量。
我之前也踩过这个坑,别光盯着del和empty_cache,大概率是优化器step之前loss.backward()没配合zero_grad(),或者你自定义Dataset里__getitem__返回了不该带梯度的大tensor。建议先试试在训练循环里把inputs和labels用detach()包一层,再不行就用pytorch的torch.cuda.memory_summary()看下分配细节,比empty_cache管用。还有个小技巧,开pin_memory=True反而能减少主机到设备的传输碎片,你可以试试。
另一个思路,ResNet18跑CIFAR-10这种小图,batch16理论上不该爆,你检查下是不是验证集或者测试的时候忘了with torch.no_grad()。以前我有个朋友是评估阶段累积了梯度图,显存一路涨到死。用nvidia-smi -l 1盯着看,如果每次迭代涨几百MB,基本就是有变量挂在计算图上没释放,试试在backward后把optimizer.zero_grad()放在loss.backward()前面,有些人顺序写反了也会有诡异问题。
你手动del掉loss和output其实没啥用,因为它们本来每次循环就被覆盖了,真正占地方的是梯度图和DataLoader的worker进程。建议把num_workers设为0跑一轮试试,如果显存稳了那就是多进程加载数据时候的缓存问题。另外torch.cuda.empty_cache()只是
大概率是训练循环里某个地方不小心把loss或output挂到了全局变量上,比如用了loss.item()但保留了tensor引用,或者optimizer.zero_grad()漏了导致梯度累积。你可以用nvidia-smi盯显存变化,同时配合pytorch的torch.cuda.memory_summary()看具体分配,那个能列出每块内存的堆栈。我之前遇到过类似问题,最后发现是验证集里忘了加torch.no_grad(),推理时把整个验证集的输出都存下来了,你可以重点检查下验证循环。另外del和empty_cache其实治标不治本,真正要查的是有没有引用没断掉。
这种情况大概率不是del的问题,而是optimizer里那个step之前梯度累积的历史变量在作妖。你试试在backward之后加一步optimizer.zero_grad(set_to_none=True),比手动del干净多了。另外自定义Dataset里如果存了增强后的图片或者特征,记得在__getitem__里别把中间结果挂到self上。排查显存可以用pytorch的torch.cuda.memory_summary(),或者干脆用nvidia-smi盯一下哪个进程的显存曲线,比猜快多了。我之前遇到过类似情况,最后发现是amp的GradScaler没调用update导致的,你也可以查查这个。
我之前也踩过类似的坑,你怀疑的“计算图没释放”大概率是主因。loss.backward()之后如果还留着loss或者output的引用,比如为了打印acc存了变量,或者用了loss.item()但没解绑,那个batch的计算图就不会被回收,下个batch又叠加,显存自然一路涨。你可以试试在optimizer.step()之后把loss和output赋成None,而不是del,del有时候因为引用计数问题并不彻底,特别是多个变量引用同一个tensor时。另外自定义Dataset里如果做了数据增强,比如随机裁剪,中间生成的Tensor虽然会释放,但如果你存了列表或者缓存,比如为了调试把每个样本的feature map都append到self里,那也会爆,建议检查一下__getitem__里有没有往self上挂东西。torch.cuda.empty_cache()只是把缓存还给驱动,不是释放显存给其他程序,所以没用也正常。工具的话,pytorch的torch.cuda.memory_summary()能看内存分配,或者用nvidia-smi配合watch,但最直接的还是在你训练循环里每十步打印一下torch.cuda.memory_allocated(),对比前后差值,能定位到是哪个阶段涨的。我之前遇到过类似情况,最后发现是验证集里没加torch.no_grad(),导致验证阶段也在累积计算图,你检查下是不是eval时忘了切。
我之前也踩过类似的坑,del loss和output其实不够,如果loss是自定义的复合loss,中间那个loss_item没清掉也会一直挂计算图。你可以试试在backward之后加optimizer.zero_grad(),然后确认一下是不是有梯度累积的逻辑没关。另外监控显存的话,pytorch里的torch.cuda.memory_summary()挺直观的,能按分配块列出占用,比看nvidia-smi清楚。我上次就是发现DataLoader里num_workers开太多,每个worker都复制了一份dataset的缓存,你把worker设成0或者1跑一版对比下试试。
八成是loss.backward()之后optimizer.step()里没加zero_grad,梯度累积把计算图撑爆了,试试每步清空一下。
用nvidia-smi看下显存碎片,或者pytorch的torch.cuda.memory_summary(),能直接定位到具体张量。
大概率是loss.backward()之前没做optimizer.zero_grad(),梯度累积把图留住了,试试在每个batch开头加一下。
你试试用nvidia-smi看显存,同时py-spy dump进程栈,基本能定位到哪行卡着不释放。
我之前也踩过类似的坑,自定义Dataset里如果__getitem__里不小心存了list或者numpy数组,每个epoch都会累积引用,显存不涨才怪。你试试把dataset里所有中间变量都转成局部变量,或者干脆用torch.utils.data.TensorDataset先一次性加载。
另外你那个del loss和output没用,真正要查的是optimizer.zero_grad()有没有在每个batch调用,还有loss.backward()之后梯度是否被正确释放。建议用nvidia-smi看实时显存,再配合pytorch的torch.cuda.memory_summary()打印分配详情,能看到具体是tensor还是缓存占着。
我猜大概率是dataLoader的num_workers开太多,每个worker都复制了一份数据集缓存,试试num_workers=0跑一个epoch对比下,如果显存曲线平了就是这块的问题。
我之前也踩过类似的坑,问题大概率不在batch size,而是你训练循环里某个tensor悄悄带上了梯度历史。比如loss.backward()之后如果没把optimizer.zero_grad()放在正确位置,或者自定义Dataset的__getitem__里做了某些会生成新tensor的运算且没detach,显存就会一点点堆上去。del和empty_cache其实治标不治本,真正该查的是用torch.autograd.detect_anomaly()或者把每一步的tensor都print出来看看哪个shape是累积增长的。另外可以试试固定随机种子跑两三个epoch,如果显存涨得一模一样,那基本就是代码逻辑漏了,不是数据随机性导致的。
你这情况我太熟了,之前调Unet的时候也这样,batch从32一路砍到4都救不回来。del和empty_cache其实治标不治本,关键得看loss.backward()之后有没有做optimizer.zero_grad(),如果你在循环里忘了清梯度,哪怕batch小,梯度累积也会让显存像滚雪球一样涨。另外自定义Dataset里如果存了list或dict的中间Tensor,而且没转成numpy或者del掉,DataLoader迭代时那部分引用会一直留着,建议你在__getitem__里用局部变量算完就扔,别挂到self上。监控工具的话,pytorch自带torch.cuda.memory_summary()能看当前分配块,但更直观的是用nvidia-smi配合py-spy dump python堆栈,基本能定位到是哪个变量没释放。还有个容易被忽略的点:如果你在验证阶段也开了torch.no_grad()但没把模型切回train模式,或者每轮都往tensorboard里写image,那部分缓冲也可能累积。实在不行就开pin_memory+非阻塞传输试试,虽然不直接解决泄漏但能提速,排查时少受罪。