最近在调一个简单的图像分类模型,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 条大概率是loss.backward()之后优化器step()前没清空梯度,试试optimizer.zero_grad(set_to_none=True)能省不少显存。
我遇到过一模一样的,del其实只减了引用计数,计算图还在,关键要看loss.backward()之后有没有把optimizer.zero_grad()放在正确位置,不然梯度累积也会让显存涨。另外检查下是不是在循环里把每个batch的loss append到list里了,那个list会一直存着所有loss的tensor,不会自动释放。想监控的话可以用pytorch的torch.cuda.memory_snapshot(),能查到具体是哪个tensor占的内存。
我之前也踩过类似的坑,问题多半不在batch size,而是optimizer的state_dict在累积,或者是你自定义Dataset里__getitem__每次返回的图片没做copy,导致DataLoader的worker缓存了太多历史样本。建议先试试把loss.backward()换成loss.backward(retain_graph=False),然后确认下是不是用了梯度累积但忘了每步清零。监控显存的话,可以用pytorch的torch.cuda.memory_summary(),或者直接看nvidia-smi的进程PID,再用pdb打断点看具体哪一行涨得最凶。我之前是发现torchvision的transform里有个随机裁剪会保留中间tensor,换成functional版本就好了。
这种显存缓慢上涨但batch size不大的情况,我猜大概率不是某个变量没释放,而是计算图累积或者DataLoader的worker在偷偷缓存东西。你试过手动del和empty_cache没效果挺正常的,因为PyTorch的显存分配器本身就会预留内存,empty_cache只是把空闲块还给驱动,不是真正清干净。建议你先用nvidia-smi看显存曲线,如果涨得很有规律,那多半是每个step里有些操作没包在torch.no_grad()里,比如loss.backward()之后还有个什么指标计算在构建图。另外你自定义Dataset里如果对图片做了transform,并且把结果存成list或者numpy数组,那每个epoch重新shuffle时可能旧数据没被垃圾回收,这也会让内存缓慢爬升。最直接的办法是装个pytorch_memlab,它能把每个tensor的分配点打出来,或者你用torch.profiler看下每个step的memory timeline,基本能定位到是哪一行在持续分配。还有个笨办法,把batch size调到1跑几个epoch看还涨不涨,如果涨那就是数据流的问题,不涨就是反向传播里有什么东西在累积。
我之前也踩过这个坑,大概率不是Dataset的问题,而是优化器状态或者BN层的running stats在累积。你可以试试在epoch结束的地方打一下torch.cuda.memory_summary(),看看是不是某个tensor的碎片化太严重。另外del和empty_cache其实治标不治本,真正要查的是有没有把整个batch的loss累加到一个变量上,比如total_loss += loss.item()写成了total_loss += loss,这个会保留计算图。也可以开一下CUDA的PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True,有时候能缓解碎片化导致的显存虚高。
试试把optimizer.zero_grad()放在loss.backward()之后而不是之前?虽然常规写法是前清梯度,但如果你手滑在循环里把梯度累加了,显存也会涨。另外检查下有没有在eval模式下还开着grad,比如model.eval()之后忘了包torch.no_grad(),这会让验证集也建图。实在不行就用pytorch的memory_profiler,每步打印一下tensor的size和device,很快就能定位到是谁在涨。
试试在backward前加optimizer.zero_grad(),还有loss.backward()后别忘detach输出,大概率是梯度累积了。
跟你讲,我上次也碰到过一模一样的情况,折腾了两天才发现是优化器那一步的问题。你检查下是不是每个step里都调用了loss.backward(),但是optimizer.zero_grad()的位置放错了,或者压根没在循环里调用?PyTorch默认会累积梯度,如果不清零,计算图虽然释放了,但梯度历史会一直堆在显存里,表现就是你说的这种“缓慢上涨”。另外,你自定义Dataset里如果存了GPU张量而不是CPU张量,那每个epoch加载数据时都会在显存里叠加副本,这个也很隐蔽,建议打印一下Dataset里每个元素的device和size看看。至于监控工具,别用torch.cuda.empty_cache()了,那个只清缓存不解决根本问题,试试nvidia-smi配合pytorch的memory_summary(),或者直接跑一段代码,在每个batch后打印torch.cuda.memory_allocated()和memory_reserved()的差值,能看出是模型参数、激活值还是数据加载在涨。还有个笨办法,把batch size降到4跑几个step,如果显存还在涨,基本就排除数据batch的问题了,重点查模型内部或者loss函数里是不是有变量被意外存成了self.xxx。
我之前也踩过类似的坑,最后发现根本不是变量没删的问题,而是DataLoader的num_workers设太高了,每个worker都有自己的显存上下文,叠加起来很恐怖。你试试把num_workers降到0或者2,要是还涨再考虑别的。另外del loss和output确实没用,因为PyTorch的autograd图在backward之后通常会自动释放,除非你把中间tensor存到了list里或者返回了多个输出没解包。你说的自定义Dataset里存中间变量,这个很可疑,如果__getitem__里做了transform还保留了原始tensor的引用,那每个epoch都会累积。建议你在训练循环里用torch.cuda.reset_peak_memory_stats()和torch.cuda.max_memory_allocated()打印一下峰值,对比看是单batch泄漏还是跨batch累积。还有个冷门工具叫nvidia-smi dmon,能看每块GPU的显存变化曲线,但定位不到具体变量。真想查哪个对象占着,可以试试用gc模块配合objgraph库,在OOM前抓一下所有tensor的引用链,不过那个操作本身挺耗时的。对了,你用的是交叉熵loss吧?如果label是one-hot编码的,那个中间张量也可能没释放,但ResNet18+CIFAR-10按理说不该这么吃显存。
说到这个我太有同感了,之前调一个分割模型也碰到过一模一样的鬼问题,batch size都压到8了还是眼睁睁看着显存曲线往上爬。你那个怀疑方向我觉得挺靠谱的,自定义Dataset里如果__getitem__里存了啥临时tensor而且没及时释放,确实会悄悄累积,但更常见的坑其实是训练循环里那行loss.backward()之后的optimizer.step(),有些人会忘了把optimizer的梯度清掉,不过看你描述应该不是这个。我建议你先别急着手动del和empty_cache,那玩意儿说实话治标不治本,不如装个pytorch的memory profiler或者直接用nvidia-smi配合watch命令看每个时间点显存被谁占了,重点盯一下是不是DataLoader的num_workers在搞事,有时候多进程加载数据会把缓存也带到显存里。另外你确认下是不是在验证阶段也开着梯度,如果val的时候忘了加torch.no_grad(),那每个batch的loss和输出都会继续挂计算图,十几个epoch下来不炸才怪。还有个偏方,试试在训练循环里把input和target复制到CPU再算loss,虽然慢点但能立刻定位是不是GPU端的缓存累积。反正这种问题多半不是显存真的不够,而是哪里的引用没断干净,你多试几轮应该能揪出来。
这种情况我遇到过,八成不是Dataset的问题,而是优化器或者loss计算里某个操作把计算图给保留了。你试试在backward()之后加一句optimizer.zero_grad(set_to_none=True),比del管用多了。另外监控显存的话,nvidia-smi只能看整体,建议用pytorch的torch.cuda.memory_summary(),能打印每个tensor的占用情况。还有个小细节,CIFAR-10这种小数据集别用shuffle=True,换shuffle=False有时候能减少一些显存碎片,虽然听着玄学但确实有人这么解决过。
八成是loss.backward()之后optimizer.step()没包在with torch.no_grad()里,试试把整个训练循环的梯度清零和更新都放进去。
你这个问题我太熟了,之前调一个分割模型的时候也被折磨过。你怀疑的方向基本对,但最可能漏掉的其实是loss.backward()之后optimizer.step()之前,如果loss是复合的(比如加了正则项或者多个loss加权求和),那中间产生的临时tensor也会挂在计算图上,光del loss和output不够,得把中间的loss项也一起清。另外,DataLoader的num_workers如果设得高,每个worker会缓存一部分数据在显存里,虽然你batch小,但worker多了累积起来也吓人,可以试着把num_workers降到0或者2看看曲线平不平。还有个隐蔽的点,就是你自定义Dataset里如果对图像做了随机增强,而且每次__getitem__都生成新的tensor并保留在实例属性里,那整个epoch跑完这些临时变量可能都没被回收,建议把所有中间量都改成局部变量,别挂在self上。监控工具的话,pytorch的torch.cuda.memory_summary()能打印当前分配情况,但更推荐用nvidia-smi配合py-spy或者直接插桩记录每行代码的显存增量,我之前用pympler查python对象,再配合torch.cuda.memory_allocated()做差分,很快就定位到是某个transform的缓存没释放。最后提醒一句,torch.cuda.empty_cache()只是把缓存池还给驱动,不代表你的tensor被释放,真正要查的是哪些变量还在计算图里,建议每个batch结束用gc.collect()配合检查tensor的grad_fn是否还挂着。你要是实在排查不出来,可以试试把模型换成没有BN的版本跑一下,排除是BN的running_mean/var在累积导致的,虽然这个一般不会涨这么快。
大概率是loss.backward()之后optimizer.step()没包在with torch.no_grad()里,或者梯度累积变量没清零。
我之前也踩过这个坑,大概率是loss.backward()之后没把optimizer.zero_grad()放在合适的位置,或者模型里有用到类似detach不当的写法,导致梯度图累积。你可以试试在训练循环里把每个batch的loss和output都加上.item()再存,然后用nvidia-smi -l 1配合pytorch的torch.cuda.memory_summary()看看具体是哪一层在涨。另外,如果自定义Dataset里做了数据增强但没释放中间张量,也可能有影响,建议把增强操作挪到__getitem__里并且确保返回的是tensor的clone。
我之前也踩过类似的坑,而且最后发现根本不是显存不够,是优化器状态在偷偷膨胀。你试过用nvidia-smi的PID去对应Python进程吗?如果显存是阶梯式上涨而不是每个step都涨,那大概率是DataLoader的num_workers在搞鬼,worker进程会复制一部分CUDA context,多轮迭代后内存碎片化特别严重。
再一个,你那个自定义Dataset里如果用了Python的list存图像路径或者做了数据增强,强烈建议检查下有没有把tensor转成numpy又转回来,这种隐式拷贝每次都会在GPU上留一份临时变量。还有个土办法:在训练循环里每50个batch打印一次torch.cuda.memory_summary(),看哪个环节的allocated峰值在涨。
至于del和empty_cache,说实话对真正的内存泄漏基本没用,因为PyTorch的缓存分配器只要进程还在,就不会把显存还给驱动。你不如试试固定随机种子,然后只跑5个epoch,每个epoch结束看memory allocated是否回到起点——如果回不到,那就是有变量被全局引用了,比如loss的item被存进list或者tensor的grad属性没清。
顺便说一句,ResNet18跑CIFAR-10,batch16理论显存占用应该不到2G,你如果看到4G以上,检查下是不是开了cudnn.benchmark,这玩意在某些卡上会预分配额外工作空间。工具的话,pytorch-memlab是个好东西,能按行号定位到具体哪行代码创建了tensor,比手写hook省事多了。
这问题我踩过坑,大概率不是Dataset的锅,而是训练循环里某个地方把tensor留在图里了。你试试把loss.backward()和optimizer.step()之间加上optimizer.zero_grad(set_to_none=True),同时确认一下是不是在计算accuracy时用了torch.no_grad()包住。另外torch.cuda.empty_cache()只是清缓存池,不解决根本问题,建议用nvidia-smi的PID监控配合pytorch的torch.cuda.memory_summary()看看峰值分配在哪。我之前是忘了把input和target转成非叶子tensor,结果每个batch都累积了梯度图。
试试在loss.backward()后用optimizer.zero_grad(),我怀疑你梯度累积没清干净。写个脚本每步打印tensor的ref_count就能定位。
我之前也踩过这坑,del loss和output其实不够,得把optimizer.zero_grad()放在backward之前,而且最好检查下是不是在累加梯度或者保存了每个batch的prediction。你可以用nvidia-smi -l 1配合pytorch的torch.cuda.memory_summary(),能看具体是哪个tensor在涨,我怀疑是你自定义Dataset里对图像做了多次transform导致缓存了增强后的图,试试在__getitem__里用局部变量别存self上。
del loss和empty_cache其实治标不治本,重点看下优化器那步是不是没做zero_grad,或者loss.backward()之后梯度累积导致计算图没释放。我之前遇到过类似情况,最后发现是自定义Dataset里对图片做了太多transform并且没释放中间tensor,建议把transform挪到__getitem__外面或者用torch.no_grad()包一下预处理。监控的话可以用nvidia-smi加pytorch的memory_summary(),能定位到具体是哪一行分配的张量。
del loss和output确实没用,问题八成出在loss.backward()之后optimizer.step()之前没清梯度,或者自定义Dataset的__getitem__里每次都new了tensor,建议把每个batch的loss.item()存下来,别保留整个loss张量。想查显存占用可以试试pytorch的torch.cuda.memory_summary(),或者用nvidia-smi配合py-spy看进程堆栈,我之前遇到过是DataLoader的num_workers开太多导致缓存堆积,你把workers降到0试试。