最近把之前写的一个图像分类项目从PyTorch 1.13迁移到2.0,听说torch.compile能白嫖加速,就试着给模型加了个@torch.compile。结果发现训练速度反而慢了20%,报错还一堆,比如什么“dynamic shape not supported”。我用的就是标准ResNet50,输入尺寸固定,数据加载也没啥花活。是不是我姿势不对?还是说这玩意儿只对某些特定场景有效?求有经验的老哥指点一下,到底该怎么用compile才能不翻车?
PyTorch 2.0的torch.compile到底能不能直接加速老项目?踩坑了
全部回复
共 152 条torch.compile对固定shape的ResNet理论上应该有效,但你这情况我怀疑是CUDA graph或者算子融合没吃到好处,反而被inductor的编译开销拖累了。你可以试试把mode设成max-autotune,或者干脆只在推理阶段开compile,训练保持原样。另外检查下是不是开了动态shape的dataloader,有时候worker里隐式改shape也会触发那个报错。我这边之前跑检测头也遇到过类似问题,后来发现是跟amp混用导致的,关了混合精度反而正常了。
我倒是觉得2.0的compile对老代码真心不友好,尤其你这种从1.13直接跳上来的,很多隐式行为变了。有个土办法:先用torch.profiler跑一下看瓶颈在哪,如果GPU利用率本来就不低,那compile很难再挤出空间。我试过用torch.compile配合channels_last内存格式,在ResNet上能快15%,但前提是数据加载和预处理也得跟着改,不然白搭。你那慢20%说不定是编译生成的kernel在某些GPU架构上没优化好,可以试试升级到2.1+,修了不少这类问题。
我刚开始用torch.compile也遇到过类似情况,尤其是老项目直接套上去,经常是负优化。后来发现关键问题在于,compile的默认模式是reduce-overhead,它会对整个计算图做图优化和算子融合,但如果你代码里有一些动态控制流或者Python侧的逻辑干扰了图捕获,就会触发dynamic shape报错,然后退化成解释模式,反而更慢。你的ResNet50按理说很标准,但可能你数据加载里用了某些tensor操作导致shape在trace时不稳定,或者你用了自定义的loss函数,里面有不规则的操作。建议你先试试mode="max-autotune"看看有没有改善,同时把torch._dynamo.config.suppress_errors设为True,能跳过一些兼容性问题,但代价是可能丢失部分加速。另一个坑是,compile对显存占用比较敏感,如果你的batch size本来就小,图优化的开销占比太高,反而得不偿失,我实测下来,batch size至少得翻倍才能看出正向收益。还有一点,老项目的代码风格如果偏PyTorch 1.x时代的写法,比如用了很多in-place操作或者非标准激活函数,都可能干扰编译器的优化。最稳妥的办法是,先跑一个纯ResNet50的基准脚本,喂同样的数据,不动其他逻辑,单独验证compile的效果,如果这一步都没提速,那可能就是你的CUDA版本和cuDNN跟2.0的编译后端不匹配,建议升级到最新驱动再试。我最后是加了个开关,只在特定batch size下启用compile,其他情况fallback回普通模式,虽然麻烦点,但至少不会翻车。
说实话我一开始也跟你一样,直接套compile结果训练慢到怀疑人生。后来发现这玩意儿对batch size和输入尺寸特别敏感,而且跟CUDA版本、显卡型号都有关系,建议先看下是不是CUDA 12配的2.0,老驱动很容易触发fallback。另外你可以试试只compile模型的forward部分,别整个训练循环都包进去,配合torch._dynamo的mode='reduce-overhead',我这边ResNet50大概能提15%左右。不过说真的,如果你的batch size已经比较大了,加速效果真不明显,别太指望白嫖。
torch.compile对ResNet这种静态图确实不太友好,尤其是你batch size小的话,编译开销直接吃光收益。我之前试过,把input改成固定shape或者用torch.compile的mode="max-autotune"能缓解一点,但速度提升也就5%左右,不值得折腾。你这情况建议直接关掉,或者试试只compile模型的forward里最耗时的几个layer,别整个模型一起上。另外报错的话,检查下有没有用动态shape的op,比如reshape用了-1,改成view固定维度就稳了。
说实话我第一次用torch.compile也翻车了,跟你一模一样的现象。后来我发现问题大概率出在cudagraphs和动态shape的检测上,虽然你输入尺寸固定,但dataloader最后一批如果不够batch size,或者模型里有个别op导致shape推断不完整,它就会回退到解释模式,反而比原版慢。我自己的解决方法是把batch size设成能整除数据集长度的数,并且用torch.compile的mode="reduce-overhead"配合fullgraph=True,强制它整图编译,报错反而少了。另外你ResNet50这种CNN,如果batch size不大(比如32以下),GPU利用率本来就低,compile的优化空间有限,可能不如把torch.backends.cudnn.benchmark打开来得实在。我后来在一个Transformer分类任务上试,batch 64以上,compile直接快30%,但小batch下确实负优化。建议你先用torch.profiler看看瓶颈在数据加载还是前向计算,如果GPU利用率已经高,那compile大概率帮不上忙。还有个坑是如果用了AMP混合精度,得保证scaler和compile的配合方式对,不然会反复重新编译。最后可以试试只compile模型的forward部分,不要整个训练step都包进去,我这么改之后稳定性好多了。
小模型别折腾compile,开销比收益大,ResNet50这种经典结构收益真不明显,老代码还是优先升cudnn和开amp实在。
这情况太真实了,我试过一模一样的配置,ResNet50固定尺寸,结果编译后显存还涨了。后来发现torch.compile对旧代码里一些隐式动态操作特别敏感,比如numpy转tensor或者eval模式切换,建议先用torch.profiler看看是不是编译开销占了主导。
小batch下编译本身就要预热,几百步之后才可能回本,你那20%的变慢估计是没跑够。可以试试mode="max-autotune"加reduce-overhead,或者干脆用torch._dynamo的mark_dynamic手动指定静态维度。反正这玩意对CNN老项目收益真没吹得那么神,除非你上A100或者大batch,不然还是老实写个benchmark再决定要不要上。
说实话你这个问题我太有同感了,2.0刚出的时候我也拿老项目试过,一样的ResNet50,一样的固定输入,结果跟你一模一样,慢20%还带各种warning。后来我翻了下源码和issue,感觉torch.compile真正吃香的是那种有动态控制流、或者算子特别碎的小模型,像transformer那种,它能把图优化得很漂亮,反而像ResNet这种已经是高度优化的静态图,编译开销可能比省下来的还多。另外你那个“dynamic shape not supported”大概率不是因为输入尺寸,而是你代码里某个地方用了Python的if或者list append,导致trace的时候被当成动态shape了,这个特别坑。我建议你先别全模型compile,试试只compile里面几个瓶颈层,或者干脆用mode="reduce-overhead"配合max-autotune,我试过有时候能拉平那20%的损失。还有个小技巧,把torch._dynamo.config.suppress_errors设成True,它遇到不支持的算子会自动回退到eager模式,至少不会让整个训练崩掉。总的来说这玩意儿现在还是适合新项目从零设计,老代码迁移真不如先升级到2.x用自带的其他优化,比如channels_last格式加TF32,收益反而更稳。
不过你这情况我倒是有点不同意见,我自己的老项目加了compile之后,虽然第一次迭代慢得离谱,但跑个十来步之后速度就反超了,可能跟你的batch size或者GPU型号有关?我用的A100,编译预热大概要几十秒,如果是小数据集或者训练步数少,那确实白搭。另外你说的报错,我之前也遇到过,最后发现是我在forward里用了torch.no_grad()包了一段推理逻辑,那个就是不被支持的,得拆出去。建议你把模型forward的代码从头到尾看一遍,凡是跟训练无关的trick全挪到外面,比如mixup的alpha采样、EMA更新这种,别留在里面。再有就是环境变量TORCH_COMPILE_DISABLE=1可以临时关掉对比,先确认是不是编译本身的锅。我现在是只在验证阶段用compile,训练保持eager,省心多了。
其实我觉得你踩的坑可能不只是compile本身,PyTorch 2.0对老项目的兼容性还有好多暗雷,比如数据加载的num_workers默认行为变了,或者cudnn benchmark的开关状态不一样,这些都能让性能波动。我上次把项目升上来,compile开不开都是慢的,后来发现是2.0默认把pin_memory和non_blocking的配合逻辑改了,光改个load函数就快回来了。所以建议你先别急着怪compile,把2.0的release note里关于性能的改动全过一遍,尤其是DataLoader和AMP相关的。至于torch.compile,我现在的
小模型和短训练步数上compile开销确实划不来,我试过得batch够大或者模型够深才回本,你这情况建议先把cudnn benchmark开了看看。
torch.compile对老项目真不是无脑套就行的,你ResNet50这种静态图按理说应该能加速,但慢20%大概率是踩了CUDA graph和显存分配的坑,试试把mode改成max-autotune或者关掉dynamic=True。另外报错dynamic shape很可能是数据加载里有个别batch的tensor维度变了,比如最后一批不够64,检查下drop_last。我自己的经验是compile在A100上收益明显,但V100或小显存卡上经常得不偿失。你训练循环里如果有自定义loss或者频繁的tensor操作,建议先profile一下看看是不是编译开销比省下的还多。
说实话你这个问题我太有共鸣了,之前我也在ResNet50上遇到过一模一样的情况,速度不升反降,当时差点把电脑砸了。后来我翻了下源码和issue,发现torch.compile对CNN的优化收益本来就不如Transformer那种动态结构来得明显,尤其是小batch或者GPU比较强的时候,编译开销和CUDA graph的启动成本反而会吃掉那点优化。你那个“dynamic shape not supported”大概率是因为模型里某个op的输出形状被推断成可变的了,哪怕你觉得自己输入固定,但像avg_pool或者某些view操作在trace时会被认为有动态性。我后来试了个笨办法,就是给compile加mode="max-autotune"并且显式指定fullgraph=True,然后把数据loader里的drop_last设为True,确保每个batch形状完全一致,这样至少能稳定跑通。不过说实话,如果你追求的是老项目无脑提速,目前还是直接换torch版本或者用TensorRT更实在,compile更适合新写的、刻意避免动态性的代码。建议你先用torch.profiler看看具体瓶颈在哪,别急着上compile,有时候数据加载或者loss计算才是真慢。
这情况我遇到过,torch.compile对训练场景真不是无脑加速的,尤其是batch size不大的时候,图编译和算子融合的开销可能直接吃掉收益。你试试把dynamic参数设成False,或者先跑个warmup步骤让CUDA图缓存起来,速度会正常不少。另外ResNet50这种老经典,CUDNN本身优化得已经很好了,compile的空间反而有限,我拿它跑inference倒是稳定有10-15%提升,训练就别强求了。
小模型和短训练轮次确实容易负优化,我试过得把编译放epoch循环外并且关掉动态shape才有点效果。
torch.compile对老项目真不是无脑加一行就完事的,它默认会做很多图优化,但如果你代码里有任何轻微的python控制流或者动态shape,哪怕是无意的,也会触发recompile开销,反而拖慢。我之前试过在YOLO系模型上开,也是慢,后来把dynamo的动态shape模式显式关掉,加上mode="max-autotune"才有点提升。你ResNet50按理说应该能加速,但建议先确认下是不是CUDA graph或者cudnn benchmark跟它冲突了,有时候老代码里这些隐式优化反而跟compile打架。要不试试把torch._dynamo.config.suppress_errors设为True,先让它跳过报错跑起来看真实速度,再慢慢调。
同款ResNet50,我这边2.0刚上也是直接compile,速度没提升反而显存涨了。后来发现得配合torch._dynamo.mark_dynamic或者干脆把batchsize固定,并且编译前先跑几个step让shape稳定下来,效果才出来。你试试加个mode="reduce-overhead",然后把数据loader里drop_last设成True,看看是不是shape抖动导致的。另外老项目里如果有自定义loss或者前向里带Python控制流,那大概率是没法走图模式的,建议先用后端eager对比下。
说实话我第一次用torch.compile也这样,后来发现默认模式是reduce-overhead,对小模型反而有额外开销。你可以试试mode="max-autotune",或者干脆把torch._dynamo.config.supports_dynamic_shape设为True,虽然慢点但至少不报错。还有,如果你用的是A100这种卡,多卡训练时compile收益才明显,单卡小batch可能真不如直接跑。我这边ResNet50在batch size 64以上才看到提速,小了就是负优化。
同款ResNet50踩过一样的坑,直接@torch.compile确实容易翻车。我后来发现这玩意儿对CNN的默认优化模式其实没那么友好,尤其是带BN的模型,它内部会尝试做图融合和算子改写,反而破坏了原来cudnn的加速路径。你那个慢20%很可能是编译开销没摊薄,如果训练步数不多或者batch size不够大,前期trace和编译的代价根本回不来。
我现在的做法是先用torch.compile(model, mode="reduce-overhead")试试,再不行就开dynamic=False,明确告诉它输入shape是固定的。还有个小技巧,把compile放在模型实例化之后、第一个batch进来之前,用dummy input先跑一次warmup,这样后续迭代就不会被编译卡住。另外如果你用了混合精度或者自定义loss,最好把整个训练step包成一个函数再compile,而不是只compile模型,不然前后端图切来切去反而更慢。
其实这功能更适合那些有大量重复小算子、或者动态控制流的模型,像Transformer那种,CNN收益真没那么明显。你要是追求速度,不如先检查一下DataLoader的num_workers和pin_memory,有时候这几个参数调好了比compile管用得多。实在不行就回1.13,老版本cudnn benchmark开起来也不差。
ResNet50这种静态图确实不太吃compile,建议试试把batch size调大或者换A100,小卡上开销反而更明显。
说实话你这情况我太熟了,刚出2.0那会儿我也兴冲冲给ResNet系模型套compile,结果跟你一模一样,慢20%都算少的。后来翻了下issue才发现,torch.compile对CNN这种静态图结构其实收益不大,它真正发力的是Transformer那类带自注意力、动态shape又多的模型,像GPT、BLIP这种能吃到图优化红利。你这输入尺寸固定、模型又规整,老版本JIT已经优化得差不多了,compile反而引入额外的编译开销和图分析成本,慢是正常的。我后来实践下来,想用compile不翻车,得先关掉动态shape相关的报错,比如给输入张量显式声明max_length或者用torch._dynamo.config的suppress_errors,再配合mode="reduce-overhead"或"max-autotune"试试,别直接用默认模式。另外训练阶段尽量别开compile,它主要给推理加速,你如果非要训练加速,得配合混合精度和grad_scaler一起调,光加一行装饰器肯定不行。建议你先用torch._dynamo.explain看看编译到底拦在哪,再做决定要不要留着,我最后是只对推理模型开了compile,训练还是老老实实用原版,省心不少。
小模型和短训练周期真没必要上compile,开销都花在图优化上了,换大模型或长训练才划算。
compile对静态图友好,但你得把输入尺寸和shape全固定死,dataloader里别搞动态padding。