最近在调一个图像分割的模型,训练倒是正常,但到了验证阶段每次加载权重后一跑前向就OOM。我用的是PyTorch 2.0,显卡是4090 24G,batch size已经降到1了,输入图片也resize到256x256。诡异的是,训练时同样的batch size和分辨率都没事,就验证的时候爆。我怀疑是不是我用了torch.no_grad()但忘记把模型切到eval()模式,导致BN层还在更新?还是说DataLoader的pin_memory=True在验证时反而占用更多显存?另外,我加载的是保存的完整checkpoint(包含optimizer状态),会不会是optimizer的动量缓冲也占了一部分显存?有遇到过类似情况的朋友吗?求指点一下排查思路。
PyTorch模型加载到一半显存爆了,是不是我数据预处理有毛病?
全部回复
共 14 条训练和验证的显存差异其实挺常见的,但你这个情况我第一反应不是BN层的问题,因为训练时BN的动量更新反而会更占显存(要存running stats的梯度相关状态),验证时就算忘记切eval,也只是行为不对,显存不会凭空多出来。倒是pin_memory=True确实有可能在验证时多占一块锁页内存,但那通常影响的是CPU内存而不是显存,除非你DataLoader的num_workers开得很大,导致数据预取队列堆积。
我更好奇的是你加载完整checkpoint的方式,如果直接torch.load再model.load_state_dict,optimizer的state_dict会被放到GPU上(尤其包含动量buffer),但这里有个坑——如果你是用torch.load的默认方式,它会先反序列化到CPU,然后再由load_state_dict转移,按理说不会常驻显存,除非你后续没删掉那个临时变量。建议你检查一下加载后有没有del optimizer_state或者用map_location='cpu'。
另一个思路是验证阶段你可能有额外的显存峰值,比如模型里用了torch.cuda.synchronize()或者在forward里做了可视化(保存预测图),这些操作在验证时往往会临时分配大tensor。你可以把验证循环里除了前向和loss计算以外的所有操作都注释掉,跑一次看还爆不爆。
我之前遇到过类似情况,最后发现是验证时用了torch.no_grad()但模型里有个torch.cuda.amp.autocast()没关,混合精度在验证时反而触发了某些层的动态显存分配。你可以试试验证时也保持和训练完全一致的上下文管理器。
如果还不行,建议用torch.cuda.max_memory_allocated()在验证前后各打一次,看看峰值到底出现在哪个环节,这样能直接定位是模型前向、loss计算还是数据加载的问题。24G的4090跑256x256的输入,除非你的backbone特别大,否则正常不该爆,所以大概率是某个临时变量没释放。
这问题大概率不是pin_memory的锅,那个只影响数据拷贝方式不会直接爆显存。你怀疑的eval模式倒是值得查,但BN在验证时不更新也不会瞬间吃掉十几个G。真正可疑的是你加载完整checkpoint时,optimizer里的动量缓冲和梯度历史状态全占着显存,训练时这些本来就是活着的所以没感觉。建议试试只load模型权重,把optimizer的state_dict扔掉,另外前向时临时清一下缓存,torch.cuda.empty_cache()有时候能救急。
验证时把model.eval()加上再试,另外checkpoint里optimizer状态不用load,只load模型权重就行。
说实话我觉得大概率不是pin_memory的问题,那玩意儿影响的是加载速度,跟显存峰值关系不大。你训练时没事验证时爆,我更怀疑是checkpoint里optimizer的state_dict占着显存没释放,加载完整checkpoint时这部分是会被保留在显存里的。你可以试试只load模型权重(model.state_dict),别load整个checkpoint,或者加载后先del掉optimizer再跑前向。另外eval()模式确实要切,不然BN的running stats更新会额外占显存,而且结果也不对。我之前遇到类似情况就是直接改load方式解决的,你先排除这个试试。
这问题大概率不是pin_memory的锅,验证阶段它影响很小。你训练正常但验证爆,很可能是checkpoint里optimizer状态占的显存没释放,load完再跑前向就会叠加上去。建议你load完权重后把optimizer的state_dict清掉或者直接用load_state_dict只加载model部分。另外eval模式确实要记得切,但BN不更新不会导致OOM,顶多影响精度。
说实话我觉得你怀疑的方向都不太对——训练没问题但验证OOM,而且batch和分辨率都降了,这更像是显存碎片化或者验证时计算图没释放干净。eval()模式确实会影响BN的running stats更新,但那主要是影响精度,跟显存爆掉关系不大;pin_memory=True只是把数据锁页传输到GPU,占用是固定的,不至于在验证时突然变多。
我个人经验里最可疑的是你加载完整checkpoint时,optimizer的state_dict里会保留每个参数的动量缓冲,比如Adam的exp_avg和exp_avg_sq,这些和模型参数同尺寸,如果你之前训练时用了混合精度或梯度累积,这些缓冲可能比预期大一圈。但即便如此,24G跑256x256分割模型也不该爆,除非你的模型本身特别大,比如用了DeepLabV3+或者Swin Transformer这类高分辨率特征图。
你可以试试在验证代码开头加一句torch.cuda.empty_cache(),再看看是不是每次前向都递增占用——如果是递增,基本就是有某个操作没包在no_grad里,比如你习惯性地在验证循环里调用了loss.backward()或者不小心把torch.no_grad()写错了作用域。另外也检查下DataLoader的num_workers,验证时如果开太多worker,每个worker会复制一份数据集索引,有些情况下会额外吃显存。
还有个野路子:直接把优化器从checkpoint里省掉,单独存model.state_dict()再加载,如果这样不爆,那就是优化器缓冲的锅没跑了。先试试只load模型权重,跑一次验证看还爆不爆,这能快速定位问题。
你这情况我遇到过类似的,问题大概率不在数据预处理上。验证时爆显存最常见的原因就是忘了切eval模式,BN层统计量会一直更新并且缓存中间变量,哪怕开了no_grad也会占额外显存。另外你提到的完整checkpoint确实会加载optimizer状态,但那只占内存不占显存,除非你用了类似zero冗余存储的插件。建议先试试点只load模型权重,然后明确调用model.eval(),再把pin_memory关掉对比一下显存峰值。如果还爆,就查一下是不是验证集里有个别超大尺寸的mask没resize干净。
说实话你训练没事验证爆这个现象本身就挺反直觉的,因为验证时通常显存峰值更低,除非你训练时用了梯度累积或者混合精度掩盖了真实占用。我怀疑重点不在BN或者pin_memory,而是你加载完整checkpoint时optimizer的state_dict里可能绑定了历史梯度相关的buffer,比如Adam的一阶二阶动量,如果你用的多卡训练或者带EMA,那些额外状态会被一起载入显存。另外eval模式下如果模型里有dropout或者自定义forward里创建了大临时张量,也可能比训练时更吃显存,毕竟训练时反向传播会释放部分中间变量,而推理时所有中间结果都得留到最后。建议你用torch.load的weights_only参数只取model权重,或者干脆load_state_dict后再del掉optimizer变量并清缓存,顺便跑一下torch.cuda.empty_cache()试试。还有个坑是DataLoader的worker进程在验证时如果num_workers开太高,每个worker会复制一份CUDA上下文,24G看着大但真经不起多个进程同时占,尤其你pin_memory=True时锁页内存也会挤占显存预留。我之前遇到过类似问题,最后发现是验证集里混了不同通道数的图,预处理没统一导致某张图意外撑爆显存,你可以加个断言检查输入张量的shape。
大概率不是BN和pin_memory的锅,试试加载完checkpoint后单独del掉optimizer再跑验证,显存能降不少。
我猜大概率不是pin_memory的锅,那个只在数据拷贝进GPU时起作用,验证时爆显存更像是模型本身的问题。你试试加载checkpoint后先跑一个假输入看看显存占用,如果还爆就排除数据预处理因素了。另外,如果你保存的是完整训练状态,即使切到eval模式,optimizer的动量缓存还在显存里占着一块,建议单独保存model_state_dict来验证一下,这招我踩过坑。
省流版:跟eval和pin_memory关系都不大,大概率是checkpoint里optimizer的state_dict占的显存没被释放。你加载完整checkpoint后,如果直接model.load_state_dict(checkpoint['model']),但没删掉optimizer部分,那optimizer的动量缓冲会留在显存里跟你抢空间。验证前手动del checkpoint['optimizer']或者torch.cuda.empty_cache()试试。另外确认下验证时是不是还挂着训练时的计算图,有时候忘了with torch.no_grad()包住整个前向,梯度缓存也会爆。
验证阶段OOM大概率不是数据预处理的问题,而是你加载了带optimizer状态的完整checkpoint。optimizer的动量buffer会额外占一大块显存,验证时根本用不上,建议只保存和加载model.state_dict()。另外no_grad没配eval()确实会让BN层继续累积统计量,虽然不一定直接爆显存,但验证结果会不对。可以先用torch.cuda.memory_summary()看看峰值到底花在哪,别急着改DataLoader。
验证阶段爆显存这事我也踩过,而且往往不是单一原因。你提到的eval()确实值得先查——如果忘了切,BN层会继续更新running stats,Dropout也还在随机丢,虽然这俩本身不直接吃多少显存,但会让计算图和你预期不一致。不过更致命的可能是checkpoint里带了optimizer状态,如果你直接把整个dict load进来还留着,那optimizer的动量buffer会一直挂在显存里,24G也扛不住这么造。建议验证时只load model.state_dict(),别把优化器那堆东西带进来。另外pin_memory=True本身是在CPU锁页内存里做缓存,不会直接占显存,但它配合num_workers会把数据预取到内存再搬到GPU,如果验证集没shuffle、batch又小,反而容易让显存里堆着没释放的中间张量。还有个隐藏坑是验证时没清torch.cuda.empty_cache(),训练完的缓存碎片没回收,一跑前向就顶到上限。你可以先试试把optimizer状态剥掉、加eval()和no_grad(),再在验证循环里手动del中间变量,基本能定位到是哪一块在吃显存。
验证阶段OOM这事我也踩过,而且往往比训练时更隐蔽。你怀疑没切eval模式确实值得先查,BN在train模式下不仅更新running stats,还会保留一批中间激活,显存占用跟训练没差,验证时更容易叠加上其他开销。但更常见的坑是checkpoint里带着optimizer状态,你如果直接torch.load整个dict然后模型也留在GPU上,optimizer的动量缓冲会白占一大块显存,尤其Adam系每个参数两份状态,24G也扛不住。建议加载时只取model的state_dict,或者用map_location先扔CPU再按需搬。pin_memory一般只影响主机内存锁页,不会直接吃显存,但num_workers多、验证集又没shuffle时,预取队列可能把整批数据提前搬上GPU,配合前面那些就爆了。另外验证时用torch.inference_mode()比no_grad更省,能少存一些版本计数相关的元数据。你可以先打印一下torch.cuda.memory_allocated和reserved,看是模型本身占满还是前向中间激活撑爆,基本就能定位到底是哪一块在膨胀。