最近在部署一个BERT分类模型,单个样本推理时显存占用大概2G,但连续跑几百个样本后,显存直接飙到10G+,最后直接OOM了。我试了torch.no_grad(),也调用了del和torch.cuda.empty_cache(),但好像效果不大。代码里主要用DataLoader分批加载,每批32条,推理完把结果append到列表里。想问下这种情况一般是哪里没清理干净?是不是需要把每个batch的输入和输出都手动清空?还是模型本身有动态图缓存?另外,有没有什么工具能实时监控每个张量的引用计数,方便定位问题?求有经验的大佬指点一下。
PyTorch模型在推理时显存一直涨,是哪里没释放?
全部回复
共 167 条这问题我之前跑GPT类模型也遇到过,大概率不是你没del干净,而是DataLoader的num_workers在搞鬼,worker进程会缓存一部分数据在显存里,尤其是pin_memory=True的时候。你试试把num_workers设成0,或者推理时直接用单条循环不用DataLoader,看显存曲线会不会平缓。另外torch.cuda.empty_cache只是把缓存池清空给其他进程用,不代表显存真的释放了,你监控一下nvidia-smi的进程内存,看看是不是有多个CUDA context。关于定位工具,可以用pytorch的torch.cuda.memory_snapshot(),能dump出每个张量的分配栈,排查起来比看引用计数直观多了。
我之前也踩过这个坑,多半不是模型缓存的问题,而是DataLoader的num_workers在作祟,每个worker都会保留一份显存副本,样本多了自然就爆了,你试试把num_workers设成0或者1看看。另外append结果到列表这个操作本身不会占显存,但如果后续没处理列表里的tensor,它们会一直留在显存里,建议改成存cpu的numpy或者直接写文件。监控的话可以试试pytorch的torch.cuda.memory_summary(),能看出每块显存的分配情况,比手动查引用计数直观多了。
这问题我当年也踩过,多半不是模型缓存,而是DataLoader的num_workers在作妖,worker进程会持有上一批的CUDA tensor不释放,试试把num_workers设成0或者用persistent_workers=False。另外你append结果到列表这个操作,如果列表一直留着所有预测输出,那显存自然只增不减,建议改成直接写文件或只在内存里留最新一批。想监控引用的话,pytorch有个torch.cuda.memory._snapshot()可以导出内存快照,配合memory_profiler能看每个tensor的分配点,比手动查引用计数直观多了。你要是用了transformers库,记得把model.eval()和torch.inference_mode()一起上,比no_grad更彻底。
试试用torch.cuda.synchronize()拦一下,大概率是list里accumulate了tensor,collect后直接存numpy别留引用。
你试试把append的结果换成只在内存里保留必要字段,比如logits和label,别存整个tensor,列表里堆着tensor的话引用一直不释放,显存自然越涨越高。另外DataLoader的num_workers设成0看看,有时候多进程的pin_memory也会占显存。监控的话pytorch有个torch.cuda.memory_snapshot()能看每个张量分配,或者用nvidia-smi配合py-spy看调用栈。我之前遇到过类似问题,最后发现是模型里有个缓存list没清,跟输入输出无关,建议查下模型内部有没有存中间结果。
你试试把结果列表换成只存tensor的cpu版本,或者干脆每批都做一次list.clear(),我怀疑是append的cuda张量把显存占住了,虽然你del了但列表引用还在。另外可以检查下DataLoader的num_workers,多进程有时会复制显存上下文。监控的话用pynvml轮询或者torch.cuda.memory_summary(),比看引用计数直观多了,那个不好使。
大概率是DataLoader的num_workers没设0,子进程缓存没释放,试试workers=0或者用torch.inference_mode()。
试试把结果append换成固定长度数组,列表增长也可能导致缓存碎片化,另外查下DataLoader的num_workers是不是开太多了。
大概率是累积了计算图或者DataLoader里没释放的中间变量,试试把推理包在with torch.inference_mode():里,比no_grad更狠。
这问题太典型了,我之前跑GPT2也踩过坑。你光靠del和empty_cache其实治标不治本,核心是DataLoader的num_workers如果没设成0,子进程会缓存一部分CUDA上下文,加上你的结果列表一直append,梯度图虽然关了但中间变量可能还被引用着。建议把推理逻辑包在函数里,让局部变量及时释放,另外试试用pytorch的memory_stats接口看看到底哪块在涨,或者用nvidia-smi的循环监控对比一下。还有个小技巧,如果只是纯推理,可以试试把模型切到eval模式后调torch.cuda.synchronize(),有时候能强制回收一些临时buffer。
这种情况我之前也踩过坑,问题大概率不在你的del和empty_cache上,而是DataLoader的num_workers在每次迭代时都会预取数据,如果没设置persistent_workers=True,worker进程会反复创建销毁,显存碎片会越积越多。你可以试试把batch_size调小一点,或者干脆不用DataLoader,直接循环列表,看显存曲线是不是平缓了。另外排查工具的话,nvidia-smi dmon能看实时显存,但要看张量引用计数,还得靠gc模块配合objgraph,不过手动查有点费劲,建议先跑个torch.cuda.memory_summary()看看分配器的峰值在哪。我上次是发现模型里有个忘记detach的中间变量,在评估模式下还在累积梯度图,你检查下是不是有类似的操作。
大概率是DataLoader的num_workers在后台攒数据,试下把worker数调成0或pin_memory关掉,显存立马就稳了。
试试关掉梯度再开eval模式,还涨就查下DataLoader的num_workers,多进程也会占显存。
用pytorch的torch.cuda.memory_summary()看下分配详情,比瞎猜引用计数靠谱多了。
大概率是DataLoader的worker进程在缓存,试试num_workers=0或pin_memory=False。
另外用pytorch_memlab查一下张量引用,比空看显存直观多了。
这个问题我之前也踩过坑,最后发现不是没释放,而是DataLoader的num_workers开太多导致每个子进程都缓存了CUDA上下文,你试试把num_workers设成0或者1,然后推理循环里别把整个结果列表都留着,边推理边写文件或者攒够一批就清一下。另外torch.cuda.empty_cache只是清缓存池,不会回收还在引用里的张量,建议用pytorch的memory_profiler或者nvidia-smi配合看下具体是哪一行暴涨。还有个歪招,就是每个batch后强制加一次torch.cuda.synchronize,有时候能缓解,但本质还是得排查有没有变量意外被全局持有。
大概率是DataLoader的num_workers开太多,子进程的缓存没释放,试试把worker数调成0或1看下。
我之前也遇到过一模一样的情况,最后发现是DataLoader的num_workers开太多了,每个worker都会拷贝一份模型和CUDA上下文,显存是叠加的,不是共享的,你跑几百个batch就等于好几个进程同时在吃显存。你试试把num_workers设成0,或者干脆用batch_size=1循环跑,看显存是不是就稳住了。
另外你说调用了empty_cache没用,这很正常,它只是把缓存块释放回CUDA,但PyTorch的缓存分配器本身还会保留一部分显存不还给驱动,所以看着占用没降,实际上可用的显存是够的。你那个2G到10G的涨幅更像是有张量在累积,比如你append到列表里的结果如果没做detach(),梯度图会一直挂在上面,推理模式下虽然不计算梯度,但有些操作还是会累积历史信息,你试试把每个batch的输出直接转成numpy再存,别留着torch tensor。
还有个坑是模型里的dropout或者batch norm在eval()和train()模式下行为不一样,如果你忘了调model.eval(),某些层会保留中间激活值,几百个样本的激活值不释放,显存肯定涨。你确认下推理循环里有没有把模型切到eval模式,并且输入也用with torch.no_grad()包住整个循环,而不是只包前向那一步。
至于监控工具,我推荐用nvidia-smi看进程占用,配合pytorch的torch.cuda.memory_summary(),能列出每个张量的分配情况,但引用计数得用gc模块手动查,或者你装个pympler,能打印出每个对象的引用路径,虽然有点重,但排查这种问题特别好使。
我后来是用torch.jit.trace把模型固化成了TorchScript,推理时就不走动态图了,显存直接降了一半,你可以试试,尤其是BERT这种结构固定的模型,trace之后基本没有动态缓存问题。
对了,你检查下DataLoader的shuffle设置,如果shuffle=True,每个epoch都会重新打乱索引,有时候会触发额外的内存分配,虽然推理时一般不用shuffle,但如果你代码里忘了关,也可能造成隐性增长。
我之前也踩过类似的坑,最后发现是DataLoader的num_workers开太多,子进程的显存没被回收,你把worker数调成0试试。另外append列表里存的如果是GPU tensor,那显存肯定一直涨,得及时转成CPU再存。torch.cuda.empty_cache只是清缓存,不解决引用问题,用pytorch的memory_summary函数能看到每个张量的占用,比手动查引用计数方便,你可以跑一下看看是哪个变量一直没释放。
你这情况大概率不是模型缓存,而是DataLoader的num_workers在搞鬼,每个worker都会复制一份模型和显存上下文,跑完不释放就会持续累加。试试把num_workers设成0或者用persistent_workers=False,应该能明显缓解。另外,append结果到列表不会占显存,但如果是把GPU tensor直接存进去就会,记得用cpu()转换一下。监控工具的话,pytorch自带torch.cuda.memory_snapshot()能看分配详情,或者用nvidia-smi配合python的gc模块查引用,但最直接的办法还是分段跑,每跑100个batch就重启一次推理进程。
碰到过一模一样的坑,最后发现根本不是没释放,而是DataLoader的num_workers在搞鬼。你试试把worker数设成0或者1,大概率显存就稳住了,多进程每个worker都会拷贝一份模型和CUDA上下文,几百个样本下来累积的碎片内存特别吓人。另外你那个append列表如果存的是GPU tensor而不是转成numpy或python标量,那显存当然只增不减,因为结果列表本身还在持有引用,这个比del什么的重要多了。empty_cache只是把缓存池还给驱动,但已经被占用的块不会因为你调用它就立刻归零,它只对碎片整理有点帮助。真要排查的话,用pytorch的torch.cuda.memory_summary()看每个张量的分配情况,比你自己数引用计数直观得多,还不影响性能。还有个冷门技巧,把推理循环包在with torch.inference_mode():里面,比no_grad更彻底,会关掉所有自动求导追踪和版本计数器,能省不少隐藏内存。建议先跑一个只有单batch的循环,把DataLoader去掉,看显存曲线是否还涨,就能区分是数据加载还是模型本身的问题了。