最近在调一个简单的图像分类模型,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 条试试在backward()之后加一步optimizer.zero_grad()再loss.backward(),顺序反了的话梯度会累积占显存。另外自定义Dataset里如果对图片做了多次transform并且没及时释放中间tensor,确实会堆在显存里,建议把预处理挪到__getitem__外面用CPU做。监控的话可以看下nvidia-smi的PID占用,或者用pytorch的torch.cuda.memory_snapshot(),能具体到每个tensor的分配情况。我之前遇到过类似问题,最后发现是验证集循环里忘了with torch.no_grad(),你检查下是不是也漏了这个。
我之前也遇到过一模一样的情况,最后发现是DataLoader的num_workers设太大,每个worker都会复制一份数据集缓存,显存就被悄悄吃掉了。你可以试试把num_workers调成0或者2,然后观察下趋势。另外自定义Dataset里如果存了list或者numpy数组,记得在__getitem__里做切片而不是引用整个tensor,不然每个batch都可能带着整份数据进显存。至于监控工具,pytorch的torch.cuda.memory_summary()挺好用的,能看每块内存分配,或者用nvidia-smi配合watch命令盯实时变化。你那个手动del和empty_cache确实没大用,因为计算图只要没断,显存就不会真释放,检查下loss.backward()后有没有把optimizer.zero_grad()放在正确位置。
我之前也踩过类似的坑,尤其是用自定义Dataset的时候,很容易在__getitem__里把图像做了一堆变换还存了中间tensor,这些其实都会在计算图里挂着。你那个del和empty_cache没效果太正常了,因为这两个操作根本不会回收还在计算图里的显存,只是清缓存而已。建议你在训练循环里把loss.backward()之后、optimizer.step()之前,把optimizer.zero_grad()放到最前面,然后确保loss和output在反向传播后就被覆盖,而不是del,因为del只是引用计数减一,如果其他地方还引用着就白搭。真想排查的话,可以用torch.cuda.memory_summary()或者nvidia-smi配合py-spy看哪一行代码分配了内存,我上次就是这么找到的——结果发现是DataLoader的num_workers设成了0,反而导致每个batch的pin_memory默认行为异常,开了pin_memory和num_workers=4之后显存曲线就平了。还有个小技巧,把torch.no_grad()包住验证阶段的forward,不然验证集也会累积计算图。另外你ResNet18跑CIFAR-10,batch16真不至于OOM,八成是哪里有个list在append每个batch的loss或者输出,这个最隐蔽。
老哥这情况我遇到过,多半不是Dataset的锅,是你训练循环里某个地方把tensor留在图里了。比如loss.backward()之后如果没把optimizer.zero_grad()放在合适位置,或者用了类似loss.item()但又把output保存下来,计算图就会一直累积。建议你试试在backward后加一句optimizer.zero_grad(),然后看看是不是自定义loss里用了什么需要梯度的中间量没detach。监控工具的话,pytorch自带torch.cuda.memory_summary(),每步打印一下能看到哪些tensor占着显存,比empty_cache实在多了,那个函数本来就治标不治本。
大概率是验证阶段或者每个epoch结束时的评估逻辑漏了with torch.no_grad(),导致验证集的forward也建了图。你CIFAR-10训练时如果每个epoch跑一次测试,没包no_grad的话,那点显存累积起来十几个epoch肯定爆。建议检查一下验证循环,还有DataLoader的num_workers设大点,有时候数据加载本身也会留缓存,跟batch size关系不大。
我猜你是把output和loss都append到列表里用于记录日志了?比如losses.append(loss)这种,即使你del了当前变量,列表里还存着引用,计算图根本释放不掉。要么改成存loss.item(),要么隔几个batch清一次列表
大概率是loss.backward()之后没加optimizer.zero_grad(),梯度累积导致计算图没释放。
可以试试在日志里打印每步显存,或者用nvidia-smi -l 1看趋势,八成是优化器那步漏了。
遇到过一模一样的坑,大概率不是Dataset的锅,是你训练循环里某个tensor被意外保留了。比如loss.backward()之后optimizer.step()之前,如果用了loss.item()但没把loss本身置空,或者output在计算metric时被存进list,计算图就会一直挂着。建议在epoch结束或每个batch后加个torch.cuda.reset_peak_memory_stats(),再配合nvidia-smi的PID对照看,能定位到具体是哪一行涨的。另外检查下是不是用了梯度累积但忘了zero_grad,这个最隐蔽,显存会线性涨到爆。我之前就是被一个用于可视化的feature map变量坑了,删掉就稳了。
大概率是loss.backward()之后optimizer.step()没配zero_grad(),梯度累加把计算图留住了。
跟你讲,del loss和output其实很多时候只是心理安慰,问题大概率出在loss.backward()之后的optimizer.step()里,如果用了动量或者Adam,梯度缓存会一直累积。而且你那个自定义Dataset里如果存了list的tensor,每个epoch的shuffle会重新索引,但缓存不会自动清,建议把数据预处理全放到__getitem__里,别在初始化时一次性加载。还有个隐蔽的点,DataLoader的num_workers如果设了多进程,每个worker会复制一份数据,显存占用会翻倍,你试试num_workers=0跑几个epoch对比下。监控工具的话,pytorch的torch.cuda.memory_summary()能看当前分配块,但定位是哪个变量比较麻烦,我一般用nvidia-smi看进程PID,然后配合py-spy去dump栈。另外torch.cuda.empty_cache()只是把碎片还给缓存池,不是真释放给系统,所以显存数字看起来没降很正常。最后建议你关掉cudnn的benchmark模式,有时候自动调优会额外占显存,简单网络其实没必要。
我之前也遇到过类似问题,后来换了方案。
大概率是计算图没释放,loss.backward()之后记得用optimizer.zero_grad(),另外如果你在循环里把output存到list里做统计,那也会一直挂着图。建议用torch.cuda.memory_summary()看下分配峰值,或者干脆把loss.item()和output.detach()后再存,我之前就是这么排查出来的。还有个小技巧,把DataLoader的num_workers调大点,有时候数据加载线程里也有隐性占用,虽然不是主因但能缓解一点。
把loss和output的detach加上,或者用with torch.no_grad()包一下验证阶段,大概率是梯度图没释放。
这问题我之前也踩过坑,del loss和output其实没用的,关键是要看是不是每个batch的loss都往tensorboard或者list里存了,那玩意才是真占显存。你检查下代码里有没有把每个step的输出append到某个列表里,哪怕只是存了loss值,几百个epoch下来也会爆。另外自定义Dataset里别在__getitem__里做太多CPU上的预处理,特别是别把图片的numpy数组存成self的成员变量,那个会一直累积。实在找不出来就装个pytorch_memlab,能按行号显示每行代码分配的显存,查起来一目了然。
你查一下是不是在算loss的时候把logits传进loss函数后又对loss做了什么操作,比如打印或者记录到列表里,这会让计算图一直挂着。我之前就栽在把loss.item()误写成loss,结果每个batch都保留一整张图。另外看看有没有在循环里累加tensor,比如total_loss += loss这种,累积梯度也会导致显存线性增长。empty_cache()只是清缓存,不释放计算图,关键还是得确保每个batch的loss和output在下一轮迭代前被覆盖,可以试试在backward()之后加optimizer.zero_grad(set_to_none=True),这个对释放显存效果比del明显。监控的话用nvidia-smi看不太准,可以试试pytorch的torch.cuda.memory_summary(),能列出每个tensor的分配情况。
我之前也遇到过这种诡异情况,最后发现是优化器里设了weight_decay,但忘了把param_groups里的‘foreach’关掉,导致每次更新都额外保留了一份梯度。你试试在optimizer.step()之前加个with torch.no_grad(),或者检查下是不是BatchNorm的running stats在累积,有时候eval模式没切回来也会这样。监控工具的话,可以试试pytorch的torch.cuda.memory._record_memory_history(),能按行号打印分配栈,比empty_cache直观多了。另外你del完之后,看看是不是dataloader的worker进程没释放,多进程加载有时候会卡住旧缓存。
大概率是计算图没释放,检查下loss.backward()后面有没有 optimizer.zero_grad()。
用nvidia-smi看显存没用,得用torch.cuda.memory_summary()盯着分配器。
八成是loss.backward()之后optimizer.step()前忘了zero_grad,梯度累积把图撑爆了,查下这个。
试试pytorch的torch.cuda.memory_summary(),能看每行分配,比empty_cache管用。
八成是loss.backward()之后optimizer.step()前忘了把梯度清零,或者自定义Dataset里返回了非必要tensor,试试在batch里只返回图像和标签。
我之前也碰到过一模一样的情况,最后发现是DataLoader的num_workers设太大,每个worker的预取机制会在后台攒一堆batch,显存就被悄悄吃掉了。你试试把num_workers降到0或者2,然后pin_memory开起来,说不定就好了。另外你那个自定义Dataset里如果存了所有图像的tensor,建议检查下是不是在__getitem__里反复做了归一化或增广,生成了新对象没释放。empty_cache只是把缓存还给CUDA,但已经分配出去的显存它管不了,真想定位的话可以用pytorch的memory_snapshot或者nvidia-smi配合dmesg看进程峰值。
有个很隐蔽的坑是DataLoader的num_workers开太高了,子进程会复制一部分显存状态,虽然不直接占用但会干扰显存回收,我建议先把它设成0试试。另外你del loss和output之后,如果还在用optimizer.zero_grad()之前就调empty_cache,那确实没用,因为计算图还没释放干净。可以试试在backward之后马上加一步optimizer.zero_grad(set_to_none=True),这个能彻底断开梯度引用。监控的话我习惯用nvidia-smi加pytorch的memory_summary(),能看每个tensor的分配情况,比瞎猜快多了。
大概率是loss.backward()之后optimizer.step()前没做detach或者梯度累积的问题,你试试在loss.backward()后面加一句optimizer.zero_grad(),并且把loss.item()存进日志而不是直接存loss变量。显存监控的话用nvidia-smi -l 1配合pytorch的torch.cuda.memory_summary(),能直接看到每个tensor的分配情况。另外你自定义Dataset里如果返回了图像增强的中间变量,记得在__getitem__里用torch.no_grad()包一下,不然每次取数据都会挂到计算图上。还有empty_cache()其实只是清缓存池,真正占住的是计算图节点,这招基本没用。