最近在部署一个BERT分类模型,单个样本推理时显存占用大概2G,但连续跑几百个样本后,显存直接飙到10G+,最后直接OOM了。我试了torch.no_grad(),也调用了del和torch.cuda.empty_cache(),但好像效果不大。代码里主要用DataLoader分批加载,每批32条,推理完把结果append到列表里。想问下这种情况一般是哪里没清理干净?是不是需要把每个batch的输入和输出都手动清空?还是模型本身有动态图缓存?另外,有没有什么工具能实时监控每个张量的引用计数,方便定位问题?求有经验的大佬指点一下。
PyTorch模型在推理时显存一直涨,是哪里没释放?
全部回复
共 167 条我之前也踩过这个坑,torch.no_grad()和empty_cache()其实治标不治本,问题多半出在DataLoader的num_workers和pin_memory上,尤其是pin_memory=True时会额外占用显存做锁页缓存。建议先试试把batch_size调小,看涨速是不是线性变化,另外检查一下是不是把整个batch的预测结果都存成tensor再append了,改成先detach().cpu()再转list。显存监控的话可以用nvidia-smi加watch命令实时看,想查引用计数可以用tracemalloc配合gc模块,但一般不是这个原因,大概率是计算图累积了。
大概率是DataLoader的num_workers没设0,子进程缓存没释放,试试worker_init_fn里清下cuda缓存。
建议用nvidia-smi看下是不是每个batch都在涨,也可能是loss或logits没detach,append到列表里的张量还在计算图里。
这问题太典型了,我之前跑GPT类模型也遇到过一模一样的坑。你试的那些方法其实都没打在点上,torch.no_grad()只是不建计算图,但tensor本身该占的内存还是占。最可疑的地方其实是DataLoader,如果num_workers设得高,每个worker都会复制一份模型和缓存,显存自然就叠上去了,把workers设成0试试看,或者用prefetch_factor调小点。还有就是你的梯度缓存,推理模式下模型参数如果requires_grad=True,有些算子还是会留中间结果的,建议把model.eval()和torch.inference_mode()一起用,后者比no_grad更彻底。至于手动清空batch输入输出,说实话治标不治本,你append到list里的结果才是真正的大头,如果后续要保存,直接转成numpy或者cpu再存,别让GPU tensor一直留在列表里。监控工具的话,pytorch自带torch.cuda.memory_summary()能看内存分配概况,但引用计数这种底层信息得用gpu-top或者pynvml自己写脚本,一般排查用nvidia-smi加memory_summary就够了。哦对了,还有一个隐藏点,就是模型里如果有BatchNorm层,推理时running_mean和running_var也会更新,虽然不占多少,但如果你是用training模式跑的,那每个batch都会累积状态,最好确认下是不是误开了train()。
我之前也踩过这个坑,torch.no_grad()和empty_cache()其实治标不治本,关键问题可能出在DataLoader的num_workers上,子进程会持有上一批的CUDA上下文,试试把num_workers设为0或者用persistent_workers=True看看。另外你append的列表如果后续没用到,其实可以改成直接写文件或者用list的clear,但我觉得更可能是模型里有个别层(比如BatchNorm)在推理时还在更新状态,手动调成eval模式没?监控张量引用的话,pytorch的torch.cuda.memory_summary()挺直观的,能看到每个tensor的分配情况,比数引用计数好用多了。
这问题我遇到过,多半不是模型没释放,而是DataLoader的num_workers在搞鬼,子进程会复制显存上下文,跑完不回收。你试试把num_workers设成0或者pin_memory关掉,大概率能缓解。另外监控张量引用计数的话,pytorch的memory_stats接口比nvidia-smi好用,能按操作符看分配。
大概率是DataLoader的num_workers没设0,子进程缓存了CUDA上下文,加到主进程里了。试试pynvml或gpustat盯着看是不是随batch线性涨。
这个情况我遇到过,多半不是模型缓存的问题,而是DataLoader的worker进程在偷偷占显存,尤其是num_workers设得比较大的时候。你可以试试把batch_size调成1跑几百个样本看看显存曲线,如果还涨,就把推理循环里对input的梯度设置关掉,比如input.requires_grad_(False)。另外,append结果到列表确实会累积显存,因为梯度图可能还挂在tensor上,建议改成存cpu的numpy数组。监控工具的话,pytorch的torch.cuda.memory_summary()挺好用,能看每块显存是谁分配的,或者用nvidia-smi -r循环刷一下也行。
大概率是DataLoader的num_workers没设0,子进程的缓存没回收,试下pin_memory=False看看。
建议用nvidia-smi盯一下显存变化,同时查查是不是loss或梯度没清,推理时记得model.eval()。
我之前也踩过这个坑,最后发现问题基本不在模型本身,而是DataLoader的num_workers和pin_memory在搞鬼。你试试把这两个参数调成0和False,如果显存曲线立刻平了,那基本就是数据加载进程在偷偷缓存。另外你说append到列表,这个列表如果一直保留着所有推理结果,那它本身占的CPU内存不归显存管,但如果你在GPU上做了类似torch.cat或者stack操作来汇总,那这部分中间变量会一直挂在计算图上,即使有no_grad也可能因为引用没断而滞留。还有个容易被忽略的点,就是模型内部的dropout或者LayerNorm如果有缓存状态,比如某些实现会保存输入张量用于反向,但推理模式下按理说不该这样,你可以试试把model.eval()放到循环外面,别每批都切模式。最直接的排查办法是用nvidia-smi加个watch命令,同时配合gc.collect()看看显存是不是锯齿状波动,如果是每次推理后涨一点但降不回去,那大概率是某个模块的forward里生成了新的张量却被保存到了self上。至于引用计数工具,torch张量本身可以查_grad_fn和data_ptr,但更实用的是用tracemalloc配合py-spy dump一下Python堆,或者直接上torch.profiler看内存分配历史,那个能精确到每一行代码。我一般还会检查一下是否有显式的to(device)操作在循环内部反复执行,比如把输入从CPU拷到GPU时,如果没复用同一个tensor,也会造成显存碎片。对了,你试试把batch size临时改成1跑几百个样本,如果显存涨得慢很多,那可能就是batch内并行计算时某些临时缓存没释放,这种情况可以看看是不是用了transformers库,它有个model.forward的return_dict参数,设成False有时能省下不少缓存。
试试把结果列表改存tensor的cpu版本,大概率是GPU上的计算图没释放。
这种问题八成不是缓存没清,而是DataLoader的num_workers或者pin_memory在搞鬼,尤其是pin_memory=True的时候,内存和显存之间的拷贝会累积。你试试把DataLoader的num_workers设为0,pin_memory关掉,跑一遍看还涨不涨。另外检查下是不是在循环里用了类似loss.item()但保留了output的引用,比如append到列表后忘了把tensor detach到CPU。真要定位的话,用torch.cuda.memory_summary()看当前分配块,或者pytorch的memory_snapshot工具能抓每个张量的分配栈,比手动数引用计数靠谱多了。
这问题我踩过类似的坑,多半不是没清干净,而是DataLoader的num_workers在搞鬼,子进程会拷贝模型和CUDA上下文,每个worker都占一份显存,跑几百个样本后累积起来就爆了。你把num_workers设成0试试,或者用torch.inference_mode()替代no_grad,它连gradient的graph都不建,能省不少缓存。监控的话,可以试试nvidia-smi的周期采样,或者用pytorch的memory_allocated对比cached,能看出真实占用和缓存池的区别。另外你append结果到列表,如果后续没用到,建议直接存盘别留内存里,也可能是Python侧的内存没释放导致触发了CUDA的碎片化。
之前跑生成模型也遇到过这坑,光靠no_grad和empty_cache真不够。你试试把batch的输入tensor在循环末尾显式赋None,另外结果列表千万别存tensor,改成先转成numpy或者直接存cpu上的list,这样图不会被列表引用住。另外可以开一下torch.cuda.set_per_process_memory_fraction限制上限,至少能避免直接OOM崩溃。监控引用的话,pytorch有个torch.cuda.memory._dump_snapshot,能生成snapshot文件,用那个可视化页面看每个tensor的分配点,比手动print快多了。不过你这种数据量,大概率还是DataLoader的worker进程在缓存,试试num_workers=0跑一遍对比下。
遇到过类似的,看起来像显存碎片化而不是泄漏。你试试把推理循环包在一个函数里,让局部变量随函数退出自动释放,比手动del干净。另外DataLoader的num_workers>0时,worker进程会缓存一些CUDA context,也会占显存,设成0或2试试。监控的话用pynvml轮询显存,或者torch.cuda.memory_summary()看分配细节,但引用计数这功能PyTorch没直接给,得靠gc模块配合检查。
试试关掉dataloader的pin_memory,另外把每批的loss或梯度清一下,可能是计算图累积了。
试试关掉梯度后把outputs也detach一下,列表里别存带计算图的tensor,应该能解决。
这问题我太熟了,之前跑GPT类模型也踩过同样的坑。你试的那几个方法其实都治标不治本,torch.no_grad()只是不存梯度,但推理时如果模型里有dropout或者LayerNorm的缓存,或者某些算子(比如attention里的mask)会保留中间结果,照样会涨。最关键的是你的DataLoader,如果num_workers大于0,每个worker的显存缓存不会自动释放,而且你append到列表里的结果如果后续没处理,Python的GC有时候不会立刻回收那些张量,得手动把列表里的元素也删掉。我建议你先用nvidia-smi盯一下显存是匀速涨还是阶梯式跳,如果是每批涨一点,大概率是batch里的输入张量没被释放,试试在循环里每轮迭代后显式把inputs和outputs都置为None,再调一次empty_cache,注意这个函数得在CUDA同步之后调用才有效。另外你问的监控工具,pytorch有个torch.cuda.memory._record_memory_history()可以记录分配栈,配合snapshot能看出具体是哪个变量占着,但实测比较卡,小样本调试还行。还有个小技巧,把推理封装成函数,函数内局部变量出作用域也会被回收,比在循环里手动del更干净,你可以试试。最后提醒下,如果用了transformers库,记得设置model.eval(),但有些版本在generate或者forward里会有缓存bug,换个新版或者加个torch.cuda.synchronize()有时候能解决。
这问题我之前也踩过坑,罪魁祸首往往不是模型本身,而是DataLoader的num_workers和pin_memory在后台攒了一堆没释放的CUDA缓存,试试把num_workers设成0或者pin_memory=False,看显存曲线是不是就平了。另外你append结果到列表如果后面还要用,建议直接存numpy或者cpu上的tensor,别让gpu tensor的引用一直留在list里。监控引用计数的话,pytorch有个torch.cuda.memory._snapshot()能导出内存快照,配合tracemalloc看python侧引用挺管用的,但说实话排查这种问题最快还是每跑一个batch就打印一次torch.cuda.memory_reserved(),看是缓存碎片还是真泄漏。
试试关掉cudnn的benchmark模式,或者用torch.cuda.memory_summary看下是不是缓存碎片化了。
建议查一下DataLoader的num_workers,多进程有时会复制显存句柄导致占用虚高。
试试把predict结果转成numpy再存list,光empty_cache没用,大概率是list里挂住了计算图。