最近把之前写的一个图像分类项目从PyTorch 1.13迁移到2.0,听说torch.compile能白嫖加速,就试着给模型加了个@torch.compile。结果发现训练速度反而慢了20%,报错还一堆,比如什么“dynamic shape not supported”。我用的就是标准ResNet50,输入尺寸固定,数据加载也没啥花活。是不是我姿势不对?还是说这玩意儿只对某些特定场景有效?求有经验的老哥指点一下,到底该怎么用compile才能不翻车?
PyTorch 2.0的torch.compile到底能不能直接加速老项目?踩坑了
全部回复
共 152 条我之前也踩过一模一样的坑,ResNet50这种静态图结构其实不太适合直接无脑上compile。你检查下是不是没设置mode="reduce-overhead",还有试试把torch._dynamo.config.suppress_errors打开看看具体卡在哪。另外老项目里如果有些自定义loss或数据增强带了Python副作用,很容易触发graph break,反而拖慢速度。建议先只在推理阶段开compile,训练还是先用原生模式,等代码完全兼容了再逐步切。
说实话你这个问题我上个月刚踩完,ResNet50固定输入尺寸按理说是最理想的情况了,但torch.compile对老项目真不是无脑加的。我试下来发现,如果你代码里有任何隐式的python控制流,比如在forward里用了if条件判断batch size那种,哪怕实际运行中不走那个分支,编译图也会把动态shape的检查逻辑塞进去,然后就是一堆warning加性能回退。另外torch.compile默认模式是reduce-overhead,它会把CUDA graph的捕获也做了,如果你的数据加载或者loss计算里有任何同步操作,比如.item()或者numpy转换,整个训练循环都会被卡住,反而比eager模式还慢。
我后来是自己手写了个wrapper,只在forward里面包了一下,然后强制设置mode="max-autotune",再把dynamic=True显式关掉,才勉强跟原版速度打平。但说实话,对ResNet这种成熟模型,torch.compile带来的收益可能就5%到10%,你还要冒着各种算子不兼容的风险,真不如把时间花在调batch size和优化数据加载上。要是项目里用了第三方库比如timm或者detectron2,那更得小心,很多自定义算子根本没注册进inductor的fallback逻辑,直接编译就报错。
我现在基本只有跑大模型或者transformer结构才敢开compile,小模型老项目真不建议折腾。你要是实在想试,建议先跑一遍torch.compile的benchmark脚本,看看你那个GPU型号和CUDA版本是不是在官方支持列表里,有时候是编译缓存没命中导致每次重启都重新优化,那种情况慢20%太正常了。
我试过小模型也是负优化,得配合torch.compile的mode和动态shape参数调,不然白折腾。
这玩意儿对静态图和固定shape才有效,你得把数据加载改成固定batch再试下,我这边调完能有个10%提升。
这题我熟,刚踩完同一个坑。torch.compile对静态shape和固定计算图确实有效,但老项目里但凡有个动态list或者条件分支,它就得反复recompile,反而比eager模式还慢。你ResNet50按理说没问题,但检查下有没有混用nn.Module和函数式接口,或者DataLoader里num_workers太大导致CPU瓶颈,我上次就是这俩原因。建议先用torch.compile的mode="reduce-overhead"试试,再把报错里提到的动态shape点用torch._dynamo.mark_static修一下,基本能救回来。
我之前也踩过一样的坑,torch.compile对动态shape特别敏感,哪怕你觉得自己输入固定,有些op内部也会搞出动态维度来。可以试试先关掉cudagraphs,或者加个mode="reduce-overhead"看看,另外确认一下你的数据加载是不是有隐性的tensor shape变化。我这边经验是,batch size调大一点,compile的收益才明显,小batch反而容易负优化。
torch.compile这玩意真不是无脑加的,我之前也试过,跟你一样慢20%多,后来发现是没关gradient checkpointing,俩撞一起反而拖慢。你要是固定shape的话,试试mode="reduce-overhead"或者加上fullgraph=True,有时候能好点。另外老项目里如果有自定义的loss或者数据增强里用了动态shape,编译就会疯狂回退,等于白折腾。我最后是只compile了backbone那部分,其他保持原样,速度才上来一点,你可以拆开试试。
说实话你这个问题我当初也踩过,torch.compile真不是无脑加个装饰器就完事的。我拿自己的检测模型试过,跟你一样固定尺寸,结果也是慢20%多,后来发现是CUDA graph和我的自定义loss里某些Python操作不兼容,导致反复回退到eager模式。建议你先用torch.compile的mode="reduce-overhead"试试,同时把报错里的fallback原因打出来看看,大概率是某个op不支持导致的。另外有个坑是,compile对batch size特别敏感,你如果训练时用了gradient accumulation,等效batch size变了它可能重新编译,反而更慢。我最后是只在推理阶段用compile,训练还是老老实实eager,收益最大。你那个ResNet50的话,可以试试先关掉cudnn benchmark,有时候这个和compile的动态shape检测会打架。总的来说这玩意儿更适合torchserve那种固定batch的线上推理,训练场景除非你代码特别规整,不然收益真不一定正向。
torch.compile对固定shape的ResNet50确实不该这么拉胯,建议先确认下是不是CUDA graph和cudnn benchmark没开,这俩配合起来才能吃满加速。我之前试过,如果batch size偏小(比如8以下),编译启动开销和显存分配反而会拖慢,你可以试着把batch调大点再看。另外报错dynamic shape的话,检查下有没有哪层输出维度跟输入batch绑死了,比如自适应池化或者reshape操作,改成固定张量形状试试。
我最近也试过这玩意儿,跟你情况差不多,后来发现torch.compile对那些动态shape和自定义loss特别敏感,ResNet50这种反而容易触发图优化开销。建议你把compile参数改成mode="max-autotune"试试,或者干脆只在推理阶段用,训练别开。另外报错dynamic shape基本是因为dataloader里有非固定维度操作,哪怕是batch_size固定也可能被某些tensor操作影响。我最后是配合torch._dynamo.config的suppress_errors才跑通的,但收益也就10%不到,老项目真没必要折腾。
torch.compile这玩意真不是无脑加的,我试过几次发现它特别吃batch size和显存,小batch下CUDA graph那套优化反而成了负担。你ResNet50固定尺寸按理说没问题,但得把torch._dynamo.mark_dynamic去掉,还得确保数据加载器里没有隐式的shape变化。另外试试mode="max-autotune"或者关掉cudagraphs,我这边用reduce-overhead反而更慢。还有报错的话先跑一遍torch._dynamo.config.suppress_errors=True看看能不能跑通,能通再慢慢调。实在不行就只在inference阶段用compile,训练还是老实关掉吧。
torch.compile对固定shape的ResNet50理论上该有提升,但慢20%大概率是踩了CUDA graph和autocast的坑,建议先关掉cudnn benchmark试试。另外2.0默认开了动态shape检测,即便你输入固定也可能因为data loader里collate的tensor维度不严格一致被误判。我之前在检测模型上也翻过车,后来发现把max_autotune关掉、用mode="reduce-overhead"反而更稳。老项目迁移别急着上compile,先把torch2.0的编译器和重编译缓存机制摸透了再说,不然就是负优化。
说实话你上来直接@torch.compile然后跑老代码,这个操作本身就容易踩坑,2.0的compile本质是给新写的、或者至少是逻辑结构清晰的模型准备的,你那个ResNet50虽然是标准结构,但老项目里往往藏着一些隐性的动态操作,比如数据增强里偶发的tensor shape变化、或者自定义loss里某个python控制流,这些都会让inductor的graph capture直接崩掉。我自己的经验是,compile对纯卷积、纯Transformer这种“静态图友好”的模型效果最明显,而且你得先把模型和dataloader都彻底定型,最好在训练循环外面用torch.compile(model, mode="reduce-overhead")这种显式mode,别用默认的default,那个会做很多额外优化反而拖慢小batch。另外20%的变慢很可能是因为你GPU利用率本来就不高,compile的开销反而成了瓶颈,可以先试试加大batch size或者用mode="max-autotune"看看有没有转机,但说实话如果你项目里还有自定义的梯度累积或者混合精度逻辑,我建议干脆别用compile,收益真没你想的那么大。还有个小技巧,你可以先用torch._dynamo.config.suppress_errors=True跑一遍,它会跳过不支持的节点只加速能加速的部分,虽然不完美但至少能看出哪些地方是真正拦路虎,总比你直接硬刚报错强。我最近在几个老项目上试下来,真正能白嫖到加速的也就那些纯Conv+BN+ReLU的堆叠结构,但凡有点分支或者attention基本都提升有限,甚至还反向优化。
这问题我也遇到过,一开始图省事直接包一层,结果跟你一样反而变慢了。后来发现torch.compile对显存和batch size挺敏感的,小模型或者batch不够大时,编译开销根本摊不回来。你可以试试把mode设成max-autotune,或者关掉dynamic=True,另外检查下是不是cuDNN的benchmark和compile冲突了。我这边ResNet50在batch 64以上才有明显收益,小batch下确实负优化。
小模型和短训练步数根本不适合compile,ResNet50这种直接上CUDA graphs或者混合精度更实在。
torch.compile这玩意真不是无脑加的,ResNet50这种CNN在CPU上可能有点用,但GPU上小batch反而会亏在编译开销上。我试过把batch size调到64以上才勉强打平,而且必须用固定shape,input张量提前用contiguous()钉死,动态维度一出现直接崩。你试试把torch.compile的mode改成max-autotune,或者干脆用torch._dynamo.mark_dynamic排除掉某些层,可能能救回来。另外老项目里如果有自定义loss或者数据增强里用了Python随机操作,也会被dynamo卡住,建议先看看torch._dynamo.explain的输出再调。
其实你这个问题我之前也踩过,torch.compile对静态shape确实有优化,但老项目里但凡有个动态维度或者自定义算子,它就会疯狂回退到eager模式,反而多了很多开销。建议先试试torch.compile(model, mode="reduce-overhead"),然后关掉cudnn benchmark看看,另外检查下有没有python层面的list操作混进forward里。我那个项目后来是把dataloader的num_workers调大,配合compile才勉强跟原来持平,白嫖是真难。
说实话我第一次用也这样,后来发现torch.compile对显存和batch size特敏感,你ResNet50如果batch开得小,编译开销根本摊不平。可以先试试只compile特征提取部分,不compile全模型,或者用torch.compile(model, dynamic=False)强行关掉动态检查。另外报错一堆的话,多半是某些ops没支持,得看下具体是哪行,有时候换个实现方式就过了。
你这不是姿势问题,是这玩意真得看场景。图像分类这种conv-heavy的,CUDA graph能带来的收益其实有限,反而transformer那种attention的提速明显。我之前试过在检测模型上compile,慢得离谱,后来翻issue发现是某些算子触发graph break,导致每个step都要重新编译。你可以先跑一下torch.compile的profiler,看看有没有break点,要是真没收益就撤了吧,别硬
说实话我第一次用torch.compile也这样,后来发现小模型和CV模型收益确实有限,尤其ResNet这种计算密集型的,编译开销反而盖过了优化收益。建议先试试torch.compile的mode="max-autotune"或者关闭dynamic参数,另外确保batch size别太小,不然图编译时间占比太高。还有报错的话可以把compile放在model.eval()之后试试,有时候训练和推理模式切换会触发重编译。要是还不行,干脆先用torch._dynamo的mark_dynamic把关键张量尺寸标一下,或者直接等2.1稳定版吧,这玩意更新太快了。
说实话你这个情况我太熟了,刚升级2.0那会儿我也被torch.compile坑得够呛。你这个问题大概率不是姿势不对,而是动态shape和CUDA graph的兼容性在作祟,哪怕你觉得自己输入是固定的,但dataloader里如果有个别tensor的维度没写死,或者loss计算里有个view操作,compile就会自动走fallback路径,反而比原来多好几轮graph break的开销。我自己的经验是,先别急着给整个模型套compile,而是把model里的forward拆开,只compile那几个计算密集的卷积block,或者干脆在调用前手动把input的shape固定成(1,3,224,224)试试,有时候能绕开很多隐性的shape推断问题。另外你训练慢20%也可能跟batch size有关,compile在小batch下收益不明显,甚至因为编译开销倒亏,得至少64或者128以上才看得出加速。还有个小技巧,如果环境支持,试试torch.compile(model, mode="reduce-overhead"),这个模式专门针对训练吞吐优化,但要求你的输入shape必须完全静态,否则还是翻车。最后说句实在话,老项目迁移真没必要硬上compile,收益最大的还是推理阶段和那种超大模型,你这种ResNet50的训练场景,先把torch.backends.cudnn.benchmark=True开了,再把数据加载的pin_memory和num_workers调好,可能比折腾compile省心多了。
说实话你这情况我太熟了,刚升2.0那会儿我也这么干过,直接给老模型套compile,结果跟你一模一样,慢20%算少的,我有的层直接崩了。后来翻了不少issue才明白,torch.compile本质上是图优化加算子融合,它最吃香的是那种有大量小算子、或者有动态控制流的模型,像Transformer那种。ResNet50这种纯CNN其实算子都比较规整,GPU本身就已经吃得很满了,编译带来的融合收益没那么大,反而图编译和缓存的开销成了负担。另外你提到dynamic shape那个报错,多半是代码里有些隐式的不定长操作,比如数据增强里的随机裁剪或者某个reshape用了-1,PyTorch编译时无法静态推断就会炸。我现在的做法是,先别急着全模型compile,用torch.compile(model, mode="reduce-overhead")这个模式试试,或者干脆只compile某些瓶颈block,再配合torch._dynamo.config.suppress_errors=True先把错误压下去看能不能跑通。还有个坑是别用老式的DataLoader,得配合2.0的compile用torch.utils.data.DataLoader的persistent_workers和pin_memory,不然数据加载也会拖后腿。反正这功能不是无脑白嫖,得花时间调,你要是项目稳定运行,真没必要为了那点不确定的加速去动刀。
说实话你这个问题我太有共鸣了,当时我拿torch.compile去跑一个nlp的序列模型,也是直接掉坑里,报错比你还多,什么“cannot call size() on tensor with symbolic shape”之类的,看得我头大。后来我仔细翻了下官方文档和issue,发现这玩意儿对于显存里动态shape的容忍度极低,哪怕你觉着输入固定,但像数据加载时如果用了不同长度的padding或者batch里偶尔有个样本尺寸不一致,它都会直接退化成graph break,性能不升反降。你那个ResNet50按理说是最友好的场景了,我猜问题可能出在训练循环里的某些python控制流或者自定义loss操作上,比如用了if条件判断或者对tensor做了python层面的索引,这些都容易打断编译优化。我现在的做法是先用torch.compile的mode="reduce-overhead"试试,同时把模型里能用nn.Sequential的地方尽量模块化,再配合torch._dynamo.config的suppress_errors=True看看到底哪些算子没被支持。另外,你训练变慢也可能是因为编译本身的开销在小模型上比收益还大,ResNet50这种量级其实优化空间有限,我试过在更大的模型比如ViT-L上才明显看到提速。总之别指望无脑白嫖,得先跑通compile的profiler,看看graph break到底出在哪一步。