最近在部署一个BERT分类模型,单个样本推理时显存占用大概2G,但连续跑几百个样本后,显存直接飙到10G+,最后直接OOM了。我试了torch.no_grad(),也调用了del和torch.cuda.empty_cache(),但好像效果不大。代码里主要用DataLoader分批加载,每批32条,推理完把结果append到列表里。想问下这种情况一般是哪里没清理干净?是不是需要把每个batch的输入和输出都手动清空?还是模型本身有动态图缓存?另外,有没有什么工具能实时监控每个张量的引用计数,方便定位问题?求有经验的大佬指点一下。
PyTorch模型在推理时显存一直涨,是哪里没释放?
全部回复
共 167 条试试把results列表改成只存tensor.cpu(),别让GPU上的计算图累积,省得append那步拖着显存不放。
py-spy dump能看张量引用,或者直接关掉cudnn.benchmark看看是不是动态图缓存搞的鬼。
这问题我太熟了,之前调GPT2的时候也踩过一模一样的坑。你试的那三件套其实方向没错,但torch.no_grad()只管梯度,对中间激活值的释放没多大帮助,而且DataLoader本身如果有num_workers>0,那子进程的显存缓存也不会自动回收,这个特别隐蔽。我建议你先看下是不是在循环里把每个batch的loss或者logits存进了list,列表会持有整个计算图的引用,哪怕你del了变量,只要list里还挂着,显存就永远释放不了。你那个“把结果append到列表”的操作,我赌八成就是元凶,试试改成只存numpy的cpu拷贝,比如output.cpu().numpy(),再append,这样能斩断对GPU张量的引用。另外,empty_cache()只是把缓存还给pytorch的分配器,实际显存不会立刻还给驱动,所以不能靠它判断有没有泄漏。想定位的话,可以装个pytorch_memlab,它能按行号打印每行张量的引用计数和显存占用,比手动翻代码直观多了。还有个歪招,就是每处理完一个batch,强制跑一次torch.cuda.synchronize(),有时候异步执行会让显存看起来一直涨,但实际上只是没同步而已。
我之前也踩过这个坑,多半不是模型缓存的问题,而是DataLoader的num_workers开太多,子进程里隐式拷贝了CUDA上下文,加上你append的列表里如果存了tensor的logits,那梯度图就算在no_grad下也可能有残留。可以试试把推理结果转成numpy或者python标量再存,另外用pytorch的memory_summary()看下每个阶段峰值分配,比引用计数直观多了。还有一个野路子,就是每跑完一个batch强制调一下gc.collect(),有时候python的循环引用不手动触发就是会卡着显存不还。
试试用torch.cuda.memory_summary()看下分配器快照,大概率是DataLoader的num_workers没关,子进程缓存了CUDA上下文。
换个思路,把推理逻辑挪到子进程里跑,跑完自动销毁,比手动清缓存省心多了。
这情况我太熟了,之前跑GPT类模型也踩过同样的坑。核心问题大概率不在你手动清理那几步,而是DataLoader的num_workers在后台预取数据时,每个worker都会保留一份显存上下文,特别是batch size稍微大点,累积起来非常夸张。你试试把num_workers设成0,或者用persistent_workers=False,看显存曲线是不是立刻平了。另外,torch.no_grad()只关梯度计算,但模型内部的KV cache或者某些层的中间变量如果没显式释放,还是会占着,尤其是BERT这类Transformer结构,长序列下动态图缓存确实会越积越多。我建议你把推理循环里每个batch的输入、输出和模型输出的logits都显式赋None,再配合empty_cache,但别每步都调,那样反而拖慢速度,可以隔几十个batch清一次。至于监控张量引用计数,torch.profiler或者简单点用pympler的asizeof能看对象内存,但最直接的还是nvidia-smi配合python的gc模块,在循环里定期print一下gc.get_objects()里torch.Tensor的数量,基本能定位到是哪个变量在漏。如果实在找不到,还有个野路子,把推理封装成子进程,每个batch跑完就杀进程重启,虽然慢但绝对不OOM,我之前线上服务就这么干过。
我之前也踩过这个坑,排查了半天发现不是模型没释放,而是DataLoader的num_workers开太多了,每个worker都会拷贝一份模型和缓存,显存自然就叠上去了。你试试把num_workers设成0或者1,看看涨速是不是明显下降,我当时从4降到1直接解决了。
另外你提到append结果到列表,如果这个列表一直存着所有样本的预测tensor,那它本身就会累积显存,因为tensor即使很小,几百个加在一起也是可观的。建议把结果转成numpy或者直接存到磁盘,别让tensor留在GPU上。
torch.no_grad()只能管梯度,但模型内部的缓存比如dropout的mask或者某些层的中间激活,如果没显式清空也会残留。你可以试试在推理循环里加上model.eval(),同时把inputs和outputs都挪到CPU再del,这样能强制释放GPU上的临时变量。
至于监控工具,pytorch自带的torch.cuda.memory_summary()能看每个张量的分配情况,但引用计数得用gc模块配合objgraph,不过实战中更推荐用nvidia-smi配合pynvml每秒采样,能看到显存增长的曲线,基本能定位是哪个环节在涨。你最好再检查下是不是用了torch.jit或者torch.compile,这些在某些版本下会有动态图缓存泄漏的bug。
最后说个冷门的,如果批次里有变长序列,padding部分也会参与计算并产生缓存,你可以把attention_mask传进去,或者固定输入长度,有时候能缓解很多。要是还不行,可以试试每个batch结束后手动调用torch.cuda.synchronize(),强制同步一下,有时候异步操作会堆积未释放的内存。
这问题我踩过坑,大概率不是pytorch缓存没释放,而是DataLoader的num_workers在搞鬼,子进程会复制一份模型和CUDA上下文,每个worker都占一份显存,几百个样本跑下来累积起来很吓人。你可以先把num_workers设成0试试,或者用torch.cuda.max_memory_allocated看下峰值到底在哪。至于监控引用计数,pytorch官方没有特别顺手的工具,我一般用nvidia-smi加pdb配合,在循环里打印当前allocated和cached的差值,能看出是不是有张量在悄悄逃逸出作用域。另外,如果你把结果append到list里,最后统一处理,那这些预测结果本身也会占显存,可以考虑改成直接写文件或者用numpy数组预分配。
大概率不是没释放,是DataLoader的num_workers在疯狂预取,试试把worker数调成0或减少batch看看。
用pytorch memlab吧,能直接定位到具体哪行代码的张量没释放。
我之前也踩过这个坑,重点其实不在del和empty_cache,而是DataLoader的num_workers开太高的话,子进程会预取数据导致显存累积,试试把workers设成0或者pin_memory关掉看看。另外你的结果append到列表里,如果列表是全局的,那梯度图可能还挂在中间变量上没释放,推理时记得对整个forward包一下torch.inference_mode()而不是no_grad,这个能省不少。监控的话可以试试pytorch的memory_stats接口,或者直接nvidia-smi配合py-spy看栈,但最快的方法还是把batch size调小跑一下,确认是不是峰值问题。
试试关掉DataLoader的pin_memory,再把logits取出来就detach,列表里别存带梯度的张量。
碰到这种显存一路涨的问题,我第一反应是怀疑DataLoader的num_workers和pin_memory在搞鬼,尤其是pin_memory=True的时候,每个batch的锁页内存如果没有被及时回收,会间接导致显存碎片化。你试试把num_workers设成0,pin_memory关掉,看曲线是不是平了。
另外torch.no_grad()只能管住梯度计算,但如果你在推理循环里无意中把某些tensor留在了计算图外部的变量里,比如那个append列表,它本身不占显存,但列表里存的是GPU上的tensor的话,那就会一直累积。你得确保append的是cpu的numpy或者python对象,而不是cuda tensor。
empty_cache()其实只是把缓存块还给分配器,不是真释放给系统,而且频繁调用反而会拖慢速度。真正该查的是模型内部的self._buffers,比如BERT的position_ids或者attention_mask如果被注册成了buffer,每轮推理都可能重新创建引用。
想定位具体是哪个tensor泄漏,可以用pytorch的torch.cuda.memory._record_memory_history(),或者更直接点,在循环里每跑50个样本就打印torch.cuda.memory_summary(),看哪一行tensor数量在持续增加。我上次遇到类似情况,最后发现是自定义层里有个dropout的mask被存进了self,每轮都保留旧引用,删掉就好了。
还有个冷门坑,如果用了torch.jit.script或者trace,某些动态控制流会被展开成常量图,导致缓存爆炸。建议你试试纯eager模式跑一遍对比下显存曲线,要是eager没问题,那基本就是脚本化带来的副作用。
试试在batch循环里把outputs和inputs都移出作用域,重点查下DataLoader的num_workers是不是泄漏了显存。
这种情况大概率不是模型没清干净,而是DataLoader的num_workers在搞鬼,多进程加载数据时如果pin_memory=True,每个worker都会持有缓存,跑几百个batch后显存就叠上去了。你可以试试把num_workers设为0,或者pin_memory关掉,看看涨幅是不是明显变小。另外,推理结果append到list本身不占显存,但如果后续要转tensor或者做评估,得确保这些操作也在no_grad下。监控张量引用计数的话,用pytorch的memory_snapshot或者nvidia-smi看进程内显存分布比一个个查引用靠谱,也可以试试torch.cuda.memory_summary(),能直接看到缓存块在哪累积。我上次遇到类似问题,最后发现是eval模式下没关dropout,导致每次forward都新建计算图,你检查下model.eval()有没有漏掉。
遇到过类似情况,多半不是模型缓存的问题,而是DataLoader的num_workers在作怪。每个worker进程都会保留一份显存副本,批次多了累积起来很吓人,试试把num_workers设成0或者1,看显存曲线立马就稳了。另外你的结果列表如果存的是Tensor,梯度虽然关了但显存不会自动回收,建议转成numpy再append,或者干脆用list of dict。监控引用计数的话,pytorch自带torch.cuda.memory_snapshot()能看分配器状态,但说实话不如直接在每轮循环后打印torch.cuda.memory_allocated()来的直观。还有个小技巧,如果batch里序列长度不齐,padding太多也会导致显存虚高,试试动态padding。
这问题我踩过坑,大概率不是模型缓存,而是DataLoader的num_workers在偷偷搞事,每个worker都会复制一份显存上下文,batch多了自然就爆了。你试试把num_workers设成0,或者推理时直接用for循环喂数据,别走DataLoader。另外append列表也会累积计算图,哪怕no_grad也得用torch.cuda.current_stream().synchronize()强制清一下。监控工具的话,pytorch的torch.cuda.memory_summary()挺好使,能看每块内存的分配情况,定位到具体是哪个op在涨。
这个问题我之前也踩过坑,八成不是没释放,而是DataLoader的num_workers在背后搞鬼,每个worker都会持有batch的缓存,尤其你推理时还带着梯度计算图的话更夸张。建议试试把with torch.no_grad()包住整个循环,同时把batch数据在循环末尾显式赋None,另外检查下是不是把结果append进了全局列表,那个列表本身也会占显存。监控工具的话,pytorch的torch.cuda.memory_summary()能看分配明细,或者用nvidia-smi配合py-spy看Python调用栈,比手动数引用靠谱多了。
这种问题大概率是DataLoader的worker进程在搞鬼,每个worker都会持有batch数据直到下一轮迭代,加上CUDA的缓存分配器不会主动还显存给驱动,你看到的10G+可能是缓存碎片化而不是泄漏。试试在推理循环里加个torch.cuda.synchronize(),再配合显存快照工具看下峰值,另外检查下是不是把梯度关了但模型还在eval模式之外跑。我之前遇到过类似情况,最后是发现有个变量被list持有没释放,手动清空每轮的中间结果才解决的。
这问题我之前也踩过,多半不是模型缓存,而是DataLoader的worker进程和CUDA context没释放干净。你试试把num_workers设成0,或者推理循环外面统一用torch.inference_mode()替代no_grad,能省不少事。手动清空输入输出其实没必要,真正的坑往往是梯度累积或者loss记录这种隐式引用,你检查下append的列表是不是把整个batch的tensor都存进去了。监控工具的话,pytorch的memory_stats接口能看每类tensor占用,比看引用计数直观多了。
遇到过类似的,重点查下DataLoader的num_workers,多进程下每个worker都会缓存CUDA上下文,推理时建议把num_workers设成0或者用persistent_workers=False试试。另外结果全append到列表里,如果样本多且是tensor的话,这个列表本身也会占显存,最好转成numpy或者直接写盘。torch.cuda.empty_cache只是释放缓存块,真正没释放的显存它管不了,可以试试用pytorch的memory_summary()看下分配细节。动态图缓存基本不用考虑,BERT推理图是固定的,主要还是看数据流里有没有张量被意外持有引用。
这问题我之前也踩过,光靠del和empty_cache其实治标不治本,大概率是DataLoader的num_workers在搞鬼,worker进程会缓存上一批的CUDA上下文,试试把num_workers设成0或者用persistent_workers=False看看。另外你推理完结果append到list,如果后续没及时清空这个list,它本身也会占显存,因为列表里的tensor还持有GPU内存。想定位的话可以用pytorch的torch.cuda.memory_snapshot(),能按分配栈打印出所有张量的归属,比看引用计数直观多了。