最近在调一个简单的图像分类模型,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.zero_grad()放在正确位置,或者loss项里有没有累积到非leaf变量上。另外自定义Dataset里如果做了数据增强还存了tensor,建议转成numpy再返回,不然每个epoch都会重新挂载到计算图里。监控的话可以试下nvidia-smi的循环看PID,但更靠谱的是用pytorch的torch.cuda.memory_summary(),能直接看到哪个op分配了最多显存。
我之前也踩过这个坑,大概率不是Dataset的问题,而是训练循环里用了类似loss.backward()之后没加optimizer.zero_grad(),梯度累积导致计算图一直挂着。你可以试试在每个batch开头直接optimizer.zero_grad(),顺便把loss.item()取出来用,别留着loss变量。另外torch.cuda.empty_cache()其实只是清缓存,不是释放显存,真正要查的话用nvidia-smi看进程PID对应的内存,或者代码里加torch.cuda.memory_summary(),能明确看到是哪些tensor占着。我之前还发现是DataLoader的num_workers开太多,每个worker会复制一份数据,但你这batch小应该不至于,先查查梯度那块。
遇到过类似的,最后发现是优化器state和BN层的running stats在作祟,尤其是你把batch size调小之后,BN的动量更新会更频繁,显存碎片也会累积。你手动del loss和output确实能释放计算图,但torch.cuda.empty_cache()其实只是把缓存还给pytorch的allocator,不是真正还给驱动,所以看起来没用。建议你试试在训练循环里加一个torch.cuda.reset_peak_memory_stats()然后每隔几个batch打印一下torch.cuda.memory_allocated(),对比一下是不是真的一直涨,有时候是显存碎片化导致的假性上涨。另外自定义Dataset里如果存了list或者numpy数组,在__getitem__里做transform时很容易因为引用计数问题导致内存不释放,可以检查一下是不是把数据都转成tensor再存。还有个隐蔽的点,你用了shuffle=True,每个epoch都会重新生成索引,如果DataLoader的num_workers>0,worker进程会复制一部分数据到共享内存,那个不占显存但会占内存,可能影响整体资源分配。我之前用pytorch自带那个torch.cuda.memory._dump_snapshot()生成了snapshot文件,用chrome打开真的能看到每个tensor的分配位置,你可以试试。如果还不行,把loss.backward()换成loss.backward(retain_graph=False)确保默认不保留,再检查一下optimizer.zero_grad()是不是在每个batch开头都调用了。
我赌五毛是loss.backward()之后optimizer.step()之前少了zero_grad(),或者是自定义Dataset的__getitem__里每次返回了新的tensor没释放。你可以用nvidia-smi -l 1配合pytorch的torch.cuda.memory_summary()看看,哪个变量分配了内存一目了然。另外empty_cache()只是清缓存,不解决计算图累积的问题,重点检查一下有没有把每个batch的loss塞进一个list里做可视化。
试试在训练循环外面把criterion和optimizer的梯度都显式设成None,有时候del不彻底。我之前遇到过类似问题,最后发现是DataLoader的num_workers开太多,每个worker都保留了上一轮的缓存,调成0或者4以下就好了。
说到PyTorch显存泄漏,我第一反应就是loss.backward()之后优化器step()之前,你如果手动算了loss.item()或者为了打印acc把output也拿去算东西,计算图确实会一直挂着。你del了loss和output,但可能忽略了optimizer.zero_grad()的时机,如果梯度累积或者某些参数没清干净,图还是会留在显存里。我之前遇到过类似情况,最后发现是自定义Dataset里对图像做了随机裁剪,每次__getitem__都生成新的tensor并且没转成numpy,导致缓存里堆了一堆中间结果。你可以试试在训练循环里用torch.cuda.memory_summary(),它能打印出每个tensor的分配情况,或者直接用nvidia-smi配合pytorch的memory_allocated对比看是不是缓存碎片问题。另外,ResNet18跑CIFAR-10,batch size 16按理说显存占用应该很稳,我怀疑是不是你用了什么第三方库或者数据增强,比如autoaugment,它内部可能有静态变量在累积。建议先把DataLoader的num_workers调成0,排除多进程共享显存没释放的可能,然后每个epoch结束打印一次memory_reserved,看是不是线性增长。如果确认是计算图的问题,可以试试在backward之后加上optimizer.zero_grad(set_to_none=True),这个比默认的zero_grad更彻底。
我之前也踩过这个坑,大概率是loss.backward()之后没写optimizer.zero_grad(),或者写了但位置不对,梯度累积在计算图里就会让显存一路涨。你手动del只是删了引用,计算图本身可能还被梯度持有。建议在backward和step之间加一行zero_grad,然后看看是不是把整个batch的loss都append到一个list里了,那个list会一直存着所有历史loss的tensor。监控的话可以用nvidia-smi看整体,但定位具体变量最土的办法是每个epoch打印一下torch.cuda.memory_allocated(),分段对比涨在哪。我之前还遇到过DataLoader的num_workers设太大导致显存碎片化,不过你batch16应该不至于,先查梯度吧。
我之前也踩过类似的坑,最后发现根本不是batch size的问题,而是优化器里的动量项在偷偷累积梯度历史。你用SGD+momentum的话,每个step的梯度都会保留在优化器状态里,如果中间变量没有及时释放,显存占用就会像滚雪球一样涨。del和empty_cache其实只清了解除引用后的碎片,但只要你训练循环里有任何一行代码不小心把tensor挂到了全局变量或者loss的backward钩子上,那这招就没用。建议你先把loss.item()拿出来做反向传播,别让loss本身留在计算图里,同时把optimizer.zero_grad()放到forward之前试试,很多人习惯放在backward之后其实顺序会影响中间缓冲区的生命周期。另外可以装个pytorch_memlab,它能按行告诉你每个tensor的分配点,比nvidia-smi直观多了。我之前排查过一个自定义Dataset,里面存了归一化后的图像副本,每次__getitem__都新建一个tensor还忘了释放,那个才是真凶,你检查下是不是在dataset里做了太多预处理没回收。最后说个冷门的,DataLoader的num_workers如果设得比CPU核数多,子进程会复制内存快照,也会造成显存虚高,虽然你关了pin_memory但多进程的shared memory有时候也会被误算进显存统计里。
大概率是loss.backward()之后optimizer.step()没包在with torch.no_grad()里,试试梯度累积或者检查下有没有变量被意外retain_graph。
我遇到过一模一样的坑,大概率不是Dataset的问题,而是优化器或者loss.backward()之后梯度没清干净——你试试在backward后面加optimizer.zero_grad(),如果已经加了的话,检查下是不是有BN层或者评估模式下忘了切model.eval()导致统计量在累积。另外del和empty_cache确实没用,显存是PyTorch缓存池在管,建议装个nvidia-smi配合pytorch的torch.cuda.memory_summary()看下每个tensor的分配情况,我之前就是靠这个发现有个tensor在循环里被反复创建但没释放。
八成是loss.backward()之后optimizer.step()没置零梯度吧,试试optimizer.zero_grad()放对位置没。
试试在loss.backward()前加optimizer.zero_grad(),然后loss backward后记得detach输出,八成是梯度累加没清干净。
用nvidia-smi dmon看实时显存,或者pytorch的torch.cuda.memory_summary()定位最准,比瞎猜强多了。
试试点detect_anomaly定位一下,另外看看是不是优化器step里梯度没清零导致计算图累积了。
大概率还是计算图没释放,del loss和output只是删了引用,但如果你代码里用了loss.item()以外的操作,比如loss.backward()之后还拿着loss算指标,或者把output存进了list做可视化,图就会一直挂着。建议在backward后面加optimizer.zero_grad()的同时,把loss和output的引用彻底清掉,或者用with torch.no_grad()包住验证阶段。监控的话试试pytorch的torch.cuda.memory_summary(),能看每个张量占用,我上次就是靠它抓到一个藏在自定义loss里的中间变量。另外CIFAR-10用ResNet18,batch16按理说很轻松,也可能跟你装的CUDA版本和cuDNN的缓存策略有关,偶尔开一下cudnn.benchmark=False对比下。
大概率是loss.backward()之后optimizer.step()前忘了挂zero_grad,或者自定义Dataset里把每张图都做了repeat增广存下来了,查查这两处。
试试nvidia-smi看下是哪个进程在涨,或者用pytorch的torch.cuda.memory_summary()打印下分配明细,定位很快。
试试把loss.item()再backward,或者用torch.autograd.set_detect_anomaly排查下,大概率是优化器step没清梯度。
用nvidia-smi看下进程占用,或者pytorch的memory_summary()定位,基本都是梯度累积或钩子没释放。
十有八九是loss.backward()之后optimizer.step()之前没加optimizer.zero_grad(),梯度累积把图撑爆了。
试试在训练循环里把inputs和labels也detach一下,或者用torch.autograd.detect_anomaly()定位下。
del loss和output没啥用,真正要查的是optimizer.zero_grad()有没有放在loss.backward()之前,还有forward里有没有把中间feature map存成self.xxx,ResNet18按理说16的batch不该爆。建议用pytorch的autograd检测工具,或者直接注释掉自定义Dataset里可疑的赋值语句跑个几轮看显存曲线。我之前遇到过类似情况,最后发现是apex的f16混精没开对,梯度累加导致显存碎片化。
你试试把DataLoader的num_workers设成0跑一下,有时候多进程的数据预处理会在主显存里残留tensor。另外torch.cuda.empty_cache()只能清缓存块,救不了真正被变量占用的显存,建议用nvidia-smi -l 1实时盯,同时用pdb在训练循环里逐步排查。我怀疑你那个自定义Dataset里可能有__getitem__里不小心把整张图或tensor存到了list里,这种累积就很隐蔽。
其实最靠谱的办法是开个hook,把每个tensor的shape和device打出来,或者用torch.profiler看每个op的显存分配。你试过把loss.item()再backward吗?如果loss是标量但还带着计算图,backward后图应该释放,但如果你在循环里额外记录了loss列表,那个列表会一直累积。
八成是loss.backward()之后optimizer.step()没包在with torch.no_grad()里,或者自定义Dataset的__getitem__返回了多余引用。试试nvidia-smi看进程,或者pytorch的torch.cuda.memory_summary(),一查一个准。
八成是loss.backward()之后optimizer.step()没包在with torch.no_grad()里,或者自定义Dataset的__getitem__里返回了非必要tensor。
试试给loss.backward()加retain_graph=False,另外检查下优化器step是不是包在no_grad里了,多半是acc梯度累着了。