最近在调一个简单的图像分类模型,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 条我赌五毛不是Dataset的问题,十有八九是优化器或者loss backward那边有梯度累积没清干净。你试试在optimizer.zero_grad()之前加一句loss.backward(retain_graph=False)看看,虽然默认就是False但有些人会不小心在自定义loss里开了retain。另外torch.cuda.empty_cache()其实只清缓存不释放内存块,真正有用的办法是开nvidia-smi盯一下每个tensor的memory footprint,或者用pytorch的torch.profiler看哪个operation在涨。我之前也遇到过类似情况,最后发现是DataLoader的num_workers开太多,子进程持有了不该持有的显存副本。
你试试在optimizer.zero_grad()之前检查一下是不是loss.backward()之后计算图没释放,正常来说backward之后图应该自动清的,但你自定义Dataset里如果存了每个样本的feature或者中间tensor并且append到list里,那这些会一直占着显存。另外可以用nvidia-smi -l 1看着显存变化,配合pytorch的torch.cuda.memory_summary()打印一下分配情况,能定位到具体哪行代码在涨。我之前遇到过类似问题,最后发现是DataLoader的num_workers开太多,每个worker都复制了一份dataset副本,显存翻倍涨。你试试把num_workers设成0或者关掉shuffle看看,如果显存稳了那就是数据加载的问题,跟模型本身关系不大。
试试loss.backward()之前加optimizer.zero_grad(),还有检查下自定义Dataset的__getitem__里是不是把整张图都存了。
八成是你训练循环里没把梯度清零,或者DataLoader的num_workers开太多,内存碎片堆出来的。
我之前也遇到过一模一样的坑,最后发现是验证集里忘了写torch.no_grad(),梯度全在验证阶段累积了。你检查下是不是训练循环里把backward和optimizer.step()写在了同一个作用域,导致loss的梯度图没释放干净。另外可以试下用nvidia-smi加pytorch的torch.cuda.memory_summary(),能直接看到每个tensor的占用来源,比瞎猜效率高。还有个冷门技巧,把DataLoader的num_workers设成0试跑两个epoch,如果显存稳了就是数据加载线程在搞鬼。
我之前也踩过类似的坑,自定义Dataset里要是存了每个样本的中间tensor,那不管batch多小都会涨。你把Dataset里那些预处理结果改成在__getitem__里实时算,或者用transform放CPU上处理,基本就能解决。另外你试过with torch.no_grad()包住验证阶段吗,我那时候就是验证集上忘了关梯度,显存一点一点全被吃掉了。监控的话可以试试pytorch_memlab,能按行告诉你哪个tensor没释放,比瞎猜强多了。
大概率是loss.backward()之后optimizer.step()没接zero_grad,梯度累积把计算图挂住了,查下这个。
del loss和output没用很正常,问题多半出在梯度上,你试试每个step之后optimizer.zero_grad()是不是漏了,或者用了accumulate gradient但没清空。另外自定义Dataset里如果存了Tensor并且没转成numpy,也会一直占着显存,建议检查下__getitem__里有没有把中间结果存成self.xxx。监控的话可以试试pytorch的torch.cuda.memory_summary(),能列出每个张量的占用情况,比empty_cache管用多了。
我之前也踩过类似的坑,后来发现多半是优化器里的动量项在累积梯度,或者loss.backward()之后没及时清零梯度,试试在每个batch里optimizer.zero_grad()放对位置没,有时候是它没生效。另外你那个自定义Dataset,如果__getitem__里做了数据增强还存了缓存,确实会悄悄吃显存,建议把中间tensor都转成numpy或者直接用torchvision的transform。监控的话可以用pytorch的torch.cuda.memory_summary(),能列出每个tensor的分配情况,或者干脆用nvidia-smi配合py-spy看python栈,定位起来比瞎猜快多了。del和empty_cache其实治标不治本,根因还是计算图没断开,检查下有没有把loss的item()或者detach()漏掉。
我之前也踩过这个坑,大概率不是Dataset的问题,而是你训练循环里某个地方把tensor存成了list或者累加进loss了,比如loss_total += loss.item()写成了loss_total += loss,这样计算图就一直挂着。建议你每个epoch结束用torch.cuda.max_memory_allocated()打印一下峰值,对比一下是不是稳定递增,如果是的话基本就是引用没断。另外torch.cuda.empty_cache()只是清缓存,不释放被变量占用的显存,真正要查的话可以在循环里加torch.cuda.memory_summary()看每层的占用,或者用pytorch_memlab这个库,能直接定位到具体行号。
遇到这种显存只涨不降的情况,多半不是batch size的锅,而是计算图没释放干净。你试试在backward()之前加一句optimizer.zero_grad(),然后loss.backward()之后马上optimizer.step(),重点检查一下是不是在循环里用了类似loss.item()之外还保留了logits之类的变量。另外自定义Dataset里别存tensor,全部转成numpy或者list,CIFAR-10这种小图用transform在__getitem__里实时处理就行。监控的话可以用pytorch的torch.cuda.memory_summary(),能看到每个张量占用,或者直接上nvidia-smi加watch -n 1看实时曲线,比empty_cache管用多了。
大概率是loss.backward()之后optimizer.step()没配合zero_grad(),或者你自定义Dataset的__getitem__里每次返回了不该返回的引用,试试把loss.item()加到统计里再del。另外empty_cache()治标不治本,真正要查的话用nvidia-smi -l 1盯着看是不是每个step都涨,或者pytorch的memory_summary()能看到每个tensor的分配情况。我之前遇到类似问题是因为在循环里把每个batch的output都append到list里做acc统计,忘了清空,你检查下有没有这种隐式引用。
这种“显存缓慢上涨”的问题我碰到过好几次,最后发现八成不是显存真被占满,而是PyTorch的缓存分配器在搞鬼。你调了empty_cache()没用很正常,它只是清空未使用的缓存块,但计算图残留的问题它管不了。我建议你重点检查一下训练循环里有没有把每个batch的loss累加到某个list里,或者用了类似loss.item()但没赋值的情况——有时候一个逗号写错,比如loss = loss + output.sum(),就会把整个图保留下来。另外,你的自定义Dataset如果返回了非必要的数据,比如把整个图像路径或者索引也放到返回值里,也可能导致每个step都积累引用。最直接的办法是装个nvidia-smi的watch命令,同时配合pytorch的torch.cuda.memory_summary(),在每隔几个step打印一下,能看到到底是“reserved memory”在涨还是“allocated memory”在涨。如果是reserved涨,那就是缓存碎片问题,可以试试torch.cuda.set_per_process_memory_fraction(0.9)来限制;如果是allocated涨,那就在loss.backward()之后、optimizer.step()之前加一句optimizer.zero_grad(set_to_none=True),并且确认你的模型里没有用类似requires_grad=True的中间变量。还有个冷门坑:DataLoader如果开了num_workers>0,有时候子进程会持有上一batch的tensor引用,建议先把它设成0试试。我之前就是被这个搞了一整天,最后发现是自定义数据集的__getitem__里不小心保存了self.last_item,改成局部变量就解决了。
老哥检查下优化器要不要zero_grad,我之前就是这么漏的,loss.backward()之前没清梯度。
试试pytorch的torch.cuda.memory_summary(),能打印每个张量的占用,定位特别准。
我之前也踩过一模一样的坑,ResNet18加CIFAR-10按理说16的batch怎么都该够,但显存曲线就是止不住。你怀疑的那两个点方向没问题,但最常见的原因其实是optimizer里忘了设zero_grad,或者设了但位置不对,导致每个step的梯度都在累加,计算图就跟着一直挂着。还有个冷门但特别容易中招的,就是如果你在训练循环里把每个batch的loss塞进一个list想画曲线,那这个list会保留所有历史loss的梯度信息,显存不涨才怪。至于torch.cuda.empty_cache(),它只是把缓存池清一下,对真正被引用住的张量一点用没有,所以别指望它。建议你先用nvidia-smi盯着看,如果显存是阶梯式上涨而不是匀速涨,那大概率是每个epoch结束时有某个变量没被释放;如果是匀速涨,那就去查DataLoader的num_workers,有时候多进程会复制数据集导致显存被吃。另外可以试试在训练循环里加上with torch.no_grad()包住验证阶段,再检查一下是不是把model.train()和model.eval()搞混了,有时候eval模式下没关梯度,dropout和BN的行为会异常。真要定位是哪个变量占着,可以用torch.profiler或者pytorch的memory_snapshot,那个能按行号告诉你每块显存是哪行代码分配的,比瞎猜快多了。
试试把optimizer.zero_grad()放loss.backward()前面,大概率是梯度累积导致的,别问怎么知道的。
我之前也遇到过一模一样的情况,最后发现是优化器那边的锅,不是Dataset的问题。你试试在backward之后加一步optimizer.zero_grad(set_to_none=True),同时把loss.item()存进日志列表而不是直接存tensor,大概率能解决。至于监控工具,pytorch的torch.cuda.memory_summary()能看到每个tensor的占用,或者用nvidia-smi的进程ID去对比,但最靠谱的还是写个钩子函数打印所有叶子节点的refcount。还有个小细节,你DataLoader的num_workers设了多少?有时候worker进程也会把显存碎片化,调成0或者4试试。
我之前也踩过类似的坑,尤其是自定义Dataset里如果存了list或者tensor的中间结果,哪怕你del了变量,只要Python的引用计数没清干净,显存就不会还给CUDA。你试试把Dataset的__getitem__里所有临时tensor都显式转成numpy再返回,或者在训练循环外面把数据先全部预处理成固定大小,这样至少能排除数据加载的问题。
另外你说手动del loss和output但没用,这很可能是optimizer的step里保存了梯度,或者你用了类似loss.item()但没赋值给标量,导致计算图没释放。建议你在backward()之后加一句optimizer.zero_grad(set_to_none=True),这比默认的zero_grad更彻底,能释放梯度引用。
监控工具的话,pytorch自带torch.cuda.memory_summary()可以看每个tensor的分配情况,但更直观的是用nvidia-smi配合py-spy dump进程的Python堆栈,能看到具体哪一行在分配显存。还有个小技巧,在每轮epoch结束打印一下torch.cuda.max_memory_allocated(),对比峰值和当前值,如果差值越来越大,那基本就是缓存碎片问题,可以尝试torch.cuda.set_per_process_memory_fraction来限制上限。
要是还解决不了,干脆换个思路,把batch size再砍到8,然后开梯度累积,模拟16的等效batch,这样显存压力会小很多。另外检查下DataLoader的num_workers,如果设大了且每个worker都持有一份数据副本,也会堆显存,改成2或者0试试。
这种显存缓慢上涨但batch size不大的情况,我遇到过好几次,八成不是Dataset的问题,而是优化器状态或者某些ops在反向传播时累积了graph。你试试在optimizer.zero_grad()之前加一句loss.backward(retain_graph=False),然后确认一下是不是用了类似loss.item()去取数值但没解包,或者checkpoint里存了梯度。torch.cuda.empty_cache()其实只清缓存不释放内存,真正要查的话装个pytorch_memlab或者用torch.profiler看峰值分配,能定位到具体行。我之前是被BatchNorm的running stats坑过,你查查是不是在eval和train切换时没注意。
大概率是loss.backward()之后优化器step()没清梯度,试试optimizer.zero_grad()放对位置没,我之前就是这么炸的。
我猜大概率是loss.backward()之后optimizer.step()之前梯度没清干净,或者你把每个batch的loss都append到一个list里了,那个list会一直保存计算图。试试在backward之后加optimizer.zero_grad(),然后loss.item()取标量再存。另外自定义Dataset里如果返回了图像路径之类的字符串,也可能导致内存碎片,建议用torch.profiler看看每个tensor的分配情况。