最近在部署一个BERT分类模型,单个样本推理时显存占用大概2G,但连续跑几百个样本后,显存直接飙到10G+,最后直接OOM了。我试了torch.no_grad(),也调用了del和torch.cuda.empty_cache(),但好像效果不大。代码里主要用DataLoader分批加载,每批32条,推理完把结果append到列表里。想问下这种情况一般是哪里没清理干净?是不是需要把每个batch的输入和输出都手动清空?还是模型本身有动态图缓存?另外,有没有什么工具能实时监控每个张量的引用计数,方便定位问题?求有经验的大佬指点一下。
PyTorch模型在推理时显存一直涨,是哪里没释放?
全部回复
共 178 条大概率是DataLoader的worker进程没关干净,试试把num_workers设成0跑一下看还涨不涨。
监控张量引用可以用pytorch的memory_snapshot,但你这情况八成是缓存碎片,关掉cudnn.benchmark试试。
这个问题我之前也踩过坑,最后发现根本不是缓存没清,而是DataLoader的num_workers在搞鬼。你试试把num_workers设成0,或者用torch.cuda.Stream隔离一下每个batch的推理,看看显存曲线是不是就平了。另外你提到append到列表里,如果那个列表后面没被消费,里面存的是tensor的话,即使del了输入输出,列表本身还持有引用,显存也不会还回去——最好转成numpy或者Python对象再存。至于监控工具,torch.cuda.memory_snapshot()能看每个张量的分配栈,但引用计数得靠tracemalloc配合guppy3才能查,调试成本有点高。我怀疑你那个2G是不是包含了模型参数和CUDA上下文,如果单样本推理时显存没降下来,可能是cuDNN的autotune在自适应调整算法,它会在推理时缓存多个benchmark结果,这种只能靠torch.backends.cudnn.benchmark=False来关掉。还有个冷门原因,如果你用了transformers库,某些版本的tokenizer会在推理时往模型里塞额外的buffer,你可以看看模型对象的_dict_里有没有非参数的持久化张量。总之先别手动清空每个batch,那样反而会打断显存复用,优先排查数据加载和列表引用这两块。
大概率是你把每批loss或输出都存进list了,梯度图没释放,试试推理时用torch.inference_mode()替代no_grad。
用pytorch的memory_profiler钩子能查张量分配,但你这情况更像DataLoader的worker缓存问题,调num_workers=0验证下。
你append的是啥?如果是把每批的输出张量直接存列表里,那显存肯定一直涨啊,把结果转成cpu的numpy或者python标量再存。另外DataLoader的num_workers多的话,worker里也可能留着cuda上下文,试试设成0排查一下。监控的话可以看看torch.cuda.memory_summary(),或者用py3的gc加weakref自己查引用,比啥工具都直接。
是不是结果列表里存了带梯度的张量,append时没detach?显存涨多半是计算图没断干净。
你这情况我遇到过,大概率不是模型本身泄漏,而是你把每批的输出append到列表里了。如果append的是tensor,哪怕在no_grad下,这些tensor仍然持有计算图之外的内存,而且一直挂在Python列表里,GC根本回收不了。几百个batch累计下来,显存不炸才怪。可以试试每批推理完直接取.cpu().numpy()或者.item()再存,把GPU上的引用断掉。另外del和empty_cache只是把空闲块还回去,如果还有活引用,empty_cache也没用。监控的话可以用torch.cuda.memory_summary()看allocated和reserved的差异,或者用tracemalloc配合gc.get_objects()查Python侧对象增长。还有个坑是DataLoader的pin_memory和num_workers,如果worker里持有tensor也会拖住显存。建议先确认列表里存的是什么,十有八九问题就在那儿。
你append结果到列表里,如果存的是带梯度的tensor或者挂在计算图上的输出,那显存肯定一直涨。光no_grad不够,得把结果detach().cpu()再存,不然整个图都被引用着。另外DataLoader的pin_memory和num_workers有时也会留缓存,可以试试关掉看看。监控的话torch.cuda.memory_summary()能看个大概,真要追引用计数得上objgraph或者tracemalloc这类工具了。
结果列表里存的是带梯度的tensor吧,detach()一下再append试试。
你这个问题挺典型的,我前段时间部署ALBERT也踩过类似的坑。除了no_grad和empty_cache,最容易被忽略的是你把每个batch的输出append到列表里,如果这些tensor还挂在计算图或者CUDA上,引用不释放显存就不会回收。尤其是输出里如果有logits这种大张量,累积几百个batch直接爆炸。建议你检查一下append的时候是不是把tensor直接存进去了,可以改成.detach().cpu().numpy()或者只存必要的结果。另外DataLoader的num_workers如果开得大,每个worker也会占一部分显存上下文,不过一般不会到10G这么夸张。还有个小点,有些自定义层或者hook没清掉,也会悄悄持有引用。监控工具的话可以试试torch.cuda.memory_summary(),或者用nvidia-smi配合py-spy看进程内对象增长,真要追引用计数可以用gc和objgraph,但对CUDA tensor不太直观。最笨但有效的办法是每个batch手动把inputs、outputs、loss这些变量del掉再empty_cache,虽然丑但能快速验证是不是引用没释放。
结果append到列表这个操作很可疑,如果你把每个batch的输出tensor都存下来了,那显存肯定一直涨,因为计算图虽然no_grad了但tensor本身还活着。建议检查一下是不是把带cuda的tensor直接塞list里了,转成cpu上的numpy或者python标量再存。另外empty_cache只是回收缓存分配器里的空闲块,不会帮你释放还在被引用的张量,所以关键还是找到谁在持有引用。可以用torch.cuda.memory_summary()看看分配情况,或者用objgraph、tracemalloc这类工具查引用链。
把每批推理结果转成cpu再append,别留着cuda张量,引用没断显存就回收不了。
推理结果别老往列表里塞,改成每批直接转CPU或写文件,八成就是它把图给拖住了。
这种情况大概率不是模型本身泄漏,而是你把每个 batch 的输出都 append 到列表里了。如果 append 的是 tensor,尤其是还挂在 GPU 上的 tensor,那它就一直被列表引用着,empty_cache 根本救不了,因为显存还被合法占用着。你可以先确认下 append 的是不是 CPU 上的结果,或者干脆只存必要字段,比如 argmax 后的类别和概率,别把整块 logits 留着。另外 DataLoader 的 num_workers 也可能有影响,worker 里如果持有 CUDA 上下文或者缓存,偶尔会看起来像“显存只涨不降”,但一般不会涨到 10G 这么夸张。BERT 推理时 attention 的中间激活在 no_grad 下不会保留计算图,这块通常没问题,除非你某处不小心又开了 grad。想定位的话,可以试试 torch.cuda.memory_summary() 看 allocated 和 reserved 的差异,再用 gc 加 objgraph 或者 tracemalloc 看看 Python 侧对象是不是越积越多。还有个土办法,每跑几十个 batch 就打印一次 len(结果列表) 和 torch.cuda.memory_allocated(),基本能看出来是不是列表在背锅。
你append结果的时候是不是把带梯度的tensor也存下来了?就算用了no_grad,如果append的是模型输出而不是转成numpy或detach后的,计算图还是会挂着不放。我之前也踩过这个坑,把结果改成.cpu().numpy()或者.detach()再存就好了。另外empty_cache只是释放缓存分配器,不会回收还被引用的张量,所以关键还是找谁在持有引用。可以用torch.cuda.memory_summary()看看是哪个部分在涨,或者用objgraph查引用链。
你这个情况我碰上过,十有八九不是模型本身的问题,而是你那个append结果列表在搞事情。每批32条推理完把输出tensor直接塞进list里,这些tensor还挂在计算图或者至少占着显存不放,跑几百个batch累积下来可不就爆了。你试试把结果先转成cpu上的numpy或者python标量再存,别让cuda tensor长期驻留。另外torch.no_grad()只保证不建反向图,但前向过程中间激活值在单次推理里还是得占着,正常。你可以用torch.cuda.memory_allocated()在每个batch前后打一下,看看到底是输入涨还是输出涨,通常就是输出没挪走。还有个坑是DataLoader如果num_workers开多了,每个worker会复制一份数据到显存,不过你这个单样本2G、batch32的话,更可能是输出累积。真要查引用计数,可以用gc和sys.getrefcount配合objgraph,但对cuda tensor不太直观,不如直接nvidia-smi或者torch.cuda.memory_summary()看。最后记得empty_cache只是把缓存还给驱动,不会释放还被引用的tensor,所以关键还是切断引用。
这个思路不错,收藏了。
我之前也遇到过类似问题,后来换了方案。
我之前也遇到过类似问题,后来换了方案。