最近在部署一个BERT分类模型,单个样本推理时显存占用大概2G,但连续跑几百个样本后,显存直接飙到10G+,最后直接OOM了。我试了torch.no_grad(),也调用了del和torch.cuda.empty_cache(),但好像效果不大。代码里主要用DataLoader分批加载,每批32条,推理完把结果append到列表里。想问下这种情况一般是哪里没清理干净?是不是需要把每个batch的输入和输出都手动清空?还是模型本身有动态图缓存?另外,有没有什么工具能实时监控每个张量的引用计数,方便定位问题?求有经验的大佬指点一下。
PyTorch模型在推理时显存一直涨,是哪里没释放?
全部回复
共 167 条如果推理时本身没有梯度计算,那问题多半不在张量缓存上,而是DataLoader的num_workers在后台持续预加载,或者你在循环里把每个batch的outputs append到list后,整个list一直持有引用导致显存无法释放。试试把结果改成直接写文件或累积到CPU上的numpy数组,别让GPU tensors留在内存里。另外可以看看是不是模型内部有类似cache的机制在累积(比如某些Transformer层),用torch.cuda.memory_summary()打一下分配快照,基本能看出是哪个环节在涨。至于引用计数,pytorch没有直接的工具,但你可以用gc.get_objects()配合tensor的device和shape过滤一下,手动排查挺管用的。
我之前也踩过这个坑,大概率不是模型缓存问题,而是DataLoader的worker进程在持有上一批的数据引用,尤其是num_workers>0时,显存会被这些进程慢慢吃满。你试试把num_workers设成0或者用pin_memory=False,看看涨幅有没有变缓。另外,append到列表里如果后续没处理,列表本身也占用内存,建议改成直接写文件或用队列。至于监控工具,可以试试pytorch的torch.cuda.memory_snapshot,能看每个tensor的分配情况,但引用计数没有现成的,得自己用gc.get_objects()遍历。
这个问题我之前也踩过坑,多半不是模型缓存,而是DataLoader的num_workers在作怪,worker进程会保留上一批的CUDA上下文,试试把num_workers设成0或者用persistent_workers=False看看。另外你那2G是单样本的峰值吧?连续跑的时候如果每个batch的loss或者梯度没清,确实会累积,但推理模式下大概率是你在循环里保存了中间变量,比如把每个batch的output都append到list后没处理,list本身不占显存,但某些操作会生成计算图。可以用pytorch的memory_profiler或者直接看nvidia-smi的进程内存变化,定位到具体行号,比手动del靠谱。我之前是发现torch.cuda.synchronize()没调用,导致异步操作堆积,加一行就稳了。
这种问题八成不是模型没释放,而是DataLoader的num_workers在搞鬼,worker进程会预取数据到显存里,你试试把num_workers设成0或者pin_memory关掉,显存应该立马就稳了。另外你说调了empty_cache没用,那大概率是显存碎片化而不是泄漏,可以试试torch.cuda.memory_summary()看看是不是缓存块一直没合并。我之前遇到过类似情况,最后发现是apex的amp混合精度在推理时没关,导致梯度缓存没清,检查下是不是也有类似的训练残留。
这种问题我之前也踩过坑,多半不是没释放,而是DataLoader的num_workers在跑完每个epoch后没关干净,或者是你把整个推理结果列表都留在内存里了,虽然del了tensor但list里的累计引用还在。可以试试把结果直接写进文件而不是append到列表,另外检查下是不是有梯度缓存,推理模式下记得model.eval()加torch.inference_mode(),比no_grad更彻底。监控工具的话,pytorch的torch.cuda.memory_summary()能看分配器状态,或者用nvidia-smi dmon实时看,但定位引用计数确实难,我一般靠多打印几条日志分段排查。
这问题太典型了,估计是DataLoader的num_workers在搞鬼,子进程会复制模型和CUDA上下文,每批数据加载时都会累积显存碎片。你可以先试试把num_workers设为0,如果显存稳了那就是这个原因。另外检查下是不是把每个batch的loss或者中间变量存进列表了,列表里的tensor会一直持有引用,append之前记得先转成numpy或者detach。监控的话可以用pytorch的memory_summary(),或者nvidia-smi配合py-spy看线程,但张量引用计数确实不好直接看,我一般靠删代码二分法定位。
这种问题我踩过坑,大概率不是模型缓存,而是DataLoader的num_workers在作怪,worker进程会持有上一批的CUDA上下文,显存就这么被一点点吃掉了,试试把num_workers设为0或者pin_memory关掉,看能不能缓解。
另外你append结果到列表时,如果后续还要用,建议直接转成numpy再存,否则张量会一直留在显存里,就算del了也要等GC真的跑起来才释放。监控的话我一般用nvidia-smi看趋势,想查引用就用gc.get_objects()配合torch.Tensor的data_ptr去筛,但效率不高,不如在关键步骤加个torch.cuda.memory_summary()看分配峰值。
还有个歪招,如果batch推理不要求实时性,可以试试每处理几百条就手动调一次torch.cuda.synchronize()再empty_cache,有时候是异步执行导致看似没释放,实际是pending的kernel没跑完。
八成是DataLoader的num_workers在作怪,关掉多进程试试,或者把batch里的tensor挪到CPU再存结果。
遇到这种显存只涨不跌的情况,我第一反应是DataLoader的num_workers开太多了,每个worker会拷贝一份模型和计算图,虽然推理时不算梯度,但中间变量在worker进程里不会立刻释放,尤其你每批32条,如果worker数大于1,内存和显存都会叠buff。你可以试试把num_workers设成0,或者用persistent_workers=False,看显存曲线是不是就平了。
另外,你append结果到列表这个操作本身不占显存,但如果你在推理循环里还保留了每个batch的logits或者hidden_state(比如为了算指标),那这些张量会一直挂在计算图上,哪怕有torch.no_grad(),只要它们还被Python变量引用着,显存就不会还回去。你可以检查一下是不是把整个batch的输出都存了,而不是只存需要的部分。
torch.cuda.empty_cache()其实只是把缓存池清空,但如果你有张量还活着,它不会帮你释放。我建议你用torch.cuda.memory_summary()打印一下,看是哪个环节的tensor在累积,或者用nvidia-smi配合pytorch的memory_allocated和memory_reserved对比,能看出是实际占用还是缓存碎片。
动态图缓存这个说法不太准确,PyTorch推理时如果没有grad,计算图是会被释放的,但如果你在模型forward里用了dropout或者batch_norm的training模式,可能会保留一些中间状态。你确认下模型.eval()了没,eval模式下BN和dropout不会保留额外状态。
我之前遇到过类似问题,最后发现是torch.no_grad()只在forward外面包了一层,但如果你在循环里把输入tensor从GPU转到CPU,或者反过来,每转一次都会产生新的临时张量,这些临时量如果不及时del,也会堆起来。你可以试试在每个batch结束时显式把inputs和outputs都移动到cpu再del,然后加上gc.collect(),有时候比empty_cache管用。
至于监控引用计数,pytorch没有直接的工具,但你可以用sys.getrefcount()自己查,或者更简单点,在循环里每隔100个batch打印一次当前模型的参数和梯度是否还在,如果模型本身没变但显存涨,那大概率就是数据流里有残留引用。实在不行就上torch.profiler,它能看每个操作的内存分配,定位到具体哪一行。
我之前也踩过这个坑,最后发现是DataLoader的num_workers开太多,子进程的显存没被及时回收,你那批32条如果worker数大于1,试试把num_workers设成0或者1,可能直接就好了。另外append到列表里的结果如果是tensor,梯度虽然关了但显存还是占着,建议转成numpy或者直接存CPU,我一般推理完立刻把tensor移到cpu再append。empty_cache只是清缓存池,不是真正释放显存,所以效果有限。监控引用计数的话可以用pytorch的torch.cuda.memory_snapshot,但比较费劲,我建议先跑个20个样本,然后用nvidia-smi分批看显存曲线,能更快定位。
这问题我踩过类似的坑,2G涨到10G基本不是模型没释放,而是DataLoader的worker进程和缓存造成的,试下把num_workers设成0,或者用torch.cuda.synchronize()强制同步一下。另外列表里append的结果如果后续没用到,确实会占显存,建议改成直接存CPU或者写进文件。监控张量引用的话,pytorch的memory_snapshot()比看引用计数直观,能画出分配栈来。
我之前也踩过这坑,结果发现是DataLoader的num_workers在搞鬼,多进程加载数据时每个worker都会保留一些缓存张量,不一定是你推理代码的问题。你可以先试试把num_workers设成0或者1,看显存还涨不涨,这个排查成本最低。另外append结果到列表如果后面还要用,建议转成numpy再存,不然张量留在计算图里确实会累积。监控的话pytorch自带torch.cuda.memory_snapshot()能看每个张量占用,但引用计数得自己写hook,比较麻烦,我现在都是直接用nvidia-smi -r循环刷,配合gc.collect()看峰值变化。
你那个del和empty_cache没用,大概率是因为有隐藏的引用没断开,比如梯度或者中间变量还在计算图里。我一般推理时除了no_grad,还会把模型切成eval模式,然后对每个batch的输入调用.detach().cpu(),这样能强制切断和GPU的关联。另外DataLoader默认会缓存一部分数据,你可以试试把drop_last=True,或者把batch_size调小一点,看涨速是不是变慢。工具的话,可以装个pytorch_memlab,它能按行打印每个变量的内存占用,比手动猜靠谱多了。
这种情况八成是DataLoader在做数据预处理时产生了很多临时张量,而且num_workers如果大于0,
我遇到过类似的,光靠empty_cache其实治标不治本,你这个情况很可能是DataLoader的num_workers在搞鬼,子进程会预取数据导致显存累积,试试把num_workers设成0或者pin_memory关掉。另外append列表里如果存的是tensor而不是cpu的numpy,梯度虽然关了但计算图可能还挂在上面,建议推理完直接转成numpy再存。监控引用计数的话可以用pytorch的memory_stats接口,或者直接看nvidia-smi的进程PID,对比一下是哪个环节涨的。
跑长序列任务时我也踩过这个坑,后来发现是模型内部的cache,比如attention的key/value缓存没清,BERT虽然不像GPT那样显式用past_key_values,但某些封装库会在forward里偷偷保存中间激活。你试着把模型包一层,每次推理前手动重置一下模型状态,或者干脆用torch.jit.trace把模型固化下来,这样动态图开销就没了。工具的话,pympler能看python对象的大小,但tensor的话用gc.get_objects()配合torch.Tensor的内存属性筛一下也行。
我猜你可能是把整个batch的输入都放在GPU上了,虽然torch.no_grad()能砍掉梯度,但中间变量和输出还在显存里,尤其是32条这种batch,累积起来很可观。我之前是把每个样本单独喂
我之前也踩过这个坑,大概率不是没释放,而是DataLoader的num_workers在后台预取数据,加上CUDA缓存策略导致的“假性增长”。你用empty_cache只是清空未用块,但显存池已经扩大了,试下把batch size调小或者用torch.cuda.set_per_process_memory_fraction限制上限看看。另外,结果append到列表不会占显存,除非你存了梯度,推理时记得model.eval()加torch.inference_mode(),比no_grad更彻底。监控的话,pytorch自带torch.cuda.memory_snapshot()能看每个张量的分配,或者用nvidia-smi dmon看实时变化,比引用计数直观多了。
遇到过类似的坑,感觉你这大概率不是模型缓存的问题,而是DataLoader的worker进程在作怪。如果num_workers设得比较大,每个worker都会持有自己的显存上下文,推理完不释放就会越积越多,试试把num_workers调成0或者用pin_memory=False看看。另外你append结果那个列表,如果后续没用到,建议改成直接写文件或者只保留最终输出,别让中间结果一直占着内存。监控工具的话,pytorch自带torch.cuda.memory_snapshot()能看每个张量的分配情况,或者用nvidia-smi配合py-spy看进程状态,比手动猜靠谱多了。
我之前也踩过这个坑,大概率不是模型缓存的问题,而是DataLoader的num_workers在作祟,子进程会复制一份CUDA上下文,跑着跑着显存就叠上去了。你可以试试把num_workers设为0,或者用persistent_workers=False,看下峰值是不是降下来。另外append到列表里如果后续没用到,确实会持有整个计算图的历史引用,建议改成直接写文件或者用numpy数组保存,这样梯度链就断了。监控工具的话,pytorch的torch.cuda.memory_summary()能看每个张量的占用,但引用计数得靠gc模块自己调试,比较麻烦。
这问题我太熟了,之前调ERNIE的时候也差点被逼疯。你试的那几招其实都是治标不治本,核心原因大概率不在显存没释放,而是pytorch的缓存分配器在作怪——它为了省事儿会预先占着一大块显存,哪怕你empty_cache了也只是把缓存清回给分配器,不是真还给系统。你那个append列表的操作其实很可疑,如果后续不再用那些结果,试试改成直接写文件或者只保留必要字段,因为列表本身也会让Python对象一直引用着计算图。另外建议你查一下是不是在循环里不小心创建了新的计算图,比如把requires_grad的tensor混进了推理流程,哪怕有no_grad,某些操作符比如dropout或者batchnorm在eval和train模式下行为不一样,也可能导致缓存累积。监控工具的话,pytorch的torch.cuda.memory_snapshot()能看每个分配块的来源,但引用计数这活儿真没有现成的,我一般是用nvidia-smi配合py-spy看线程栈。最笨但有效的方法:把推理函数单独拎出来,用torch.profiler跑一遍,看哪个操作符分配的显存没被回收,基本一眼就能定位。你试试把DataLoader的num_workers设成0,有时候子进程的显存占用也算在父进程头上,这个坑也很大。
这问题我碰到过类似的,多半不是模型没释放,而是DataLoader的num_workers在搞鬼,worker进程会持有上一批的CUDA缓存不还给主进程,试试把num_workers设成0或者用persistent_workers=False看看。另外你append结果到列表这个操作,如果列表后面还要用,建议改成直接存到numpy或者disk上,不然梯度图虽然断了,但Python对象引用还在,gc可能来不及回收。监控张量引用计数可以用pytorch_memlab这个库,能按行号输出每个张量的占用和引用情况,比手动查方便多了。
用torch.cuda.memory_summary()看下是不是DataLoader的num_workers在缓存,把worker设0试试。
看到你这个情况我第一反应是DataLoader的num_workers没关干净,尤其是Windows上跑的话子进程会持有显存引用,试试在推理循环里加个if name == 'main'保护,或者把num_workers设成0看看有没有变化。另外你光看torch.cuda.empty_cache没多大用,它只是把缓存池还给驱动,并不代表显存真的释放了,重点要查是不是有张量被隐式保留在计算图里,虽然你用了no_grad,但如果你在循环外用到了中间变量比如attention_mask或者token_type_ids的某个切片,也可能导致整张图被留住。我猜你append到列表的结果是不是没有做detach().cpu()?如果直接append了cuda tensor,那列表会一直持有GPU显存,这个是新手最容易踩的坑。监控引用计数可以用pytorch的memory_stats接口,或者干脆用nvidia-smi每隔几秒采样一下,配合tracemalloc不太行因为它管不到CUDA。还有个土办法,就是每个batch结束强制跑一次torch.cuda.synchronize(),然后打印torch.cuda.memory_allocated()和memory_reserved()对比,看是不是reserved一直涨而allocated不变,如果是那就说明缓存碎片化严重,试试torch.cuda.set_per_process_memory_fraction限个上限。最后建议你把结果list改成先collect到cpu再统一处理,或者用deque限制长度,我上次也遇到类似问题,最后发现是某个embedding层的gradient没清干净,虽然推理模式不该有grad,但如果你没设model.eval()且没冻结bn层,某些op还是会记录状态。