最近在调一个LLM的推理代码,模型是7B的,用PyTorch 2.0的torch.compile优化后,反而比eager模式慢了20%左右。我用的还是默认模式,没加任何特殊配置。看官方博客说compile能提速,但实测下来完全相反。网上查了下,有人说需要配合CUDA graphs,有人说是动态shape导致的recompile开销。想问问各位,实际生产环境里大家真的在开compile吗?还是说只对特定模型尺寸/批大小有效?另外我的输入长度是变化的,是不是这个原因?有没有什么经验性的选择标准,比如什么条件下才值得花时间调compile?谢谢。
PyTorch 2.0的compile到底该不该用?用了反而变慢了咋回事
全部回复
共 32 条说实话动态shape这块确实是compile的大坑,我之前试过带padding的batch推理,recompile开销直接能把收益吃干净。你要是能把输入长度固定或者用bucket把长度分档,再配合cudagraphs,效果会明显不一样。另外7B这种规模,显存带宽可能才是瓶颈,compile主要优化计算,内存密集场景收益本来就有限。我现在的经验是,只有形状固定且batch够大(至少32+)才值得折腾,否则eager模式省心多了。
输入长度老变的话recompile确实要命,先固定shape或padding到统一长度试试。我们7B以下模型开compile收益也不大,大batch才划算。
动shape这个点基本就说到根子上了,torch.compile对静态shape的优化最狠,一变长就得重新编译,来回折腾的开销比省下来的还多。我之前试过把输入padding到固定长度,配合cudagraphs,小模型上确实有提升,但7B这种规模显存本来就紧,padding浪费的空间反而让batch size降了,最后收益被抵消。
另外你用的是默认模式,默认的max-autotune其实没开,很多算子融合和内核选择都没走到最优路径。可以先试试mode="reduce-overhead",这个对推理场景更友好,但前提是你的输入shape得稳。如果实在变长,建议用dynamic=True或者干脆锁到几个常见长度,把recompile次数控制住。
生产环境我见到的用法大多是只在服务端固定了batch和seq_len的离线推理场景用compile,在线那种延迟敏感的反而少人开,因为编译一次要几秒到十几秒,冷启动直接崩。还有个坑是某些自定义算子或者动态控制流,compile会直接报错或者退化成eager,等于白开。
你要是图省事,就先别纠结compile,用半精度加KV cache优化可能收益更直接。真要用,建议先profile看瓶颈在哪,如果已经是memory bound,compile再牛也救不了。最后问一句,你用的是torch2.0的哪个小版本?2.1和2.2对动态shape的处理改进挺大的,升级说不定就好了。
动态shape基本就是compile的命门,recompile开销比收益还大,建议先固定长度试试。
变长输入确实容易触发重编译,先固定shape测下baseline,再决定值不值得开。
变长输入确实是compile翻车的高发场景,你这不是错觉。torch.compile默认走的是dynamic shape那套,seq_len一变就容易触发guard失败然后重新编译,7B模型本来就大,recompile的代价直接吃掉甚至倒贴收益。我自己的经验是先把mode设成reduce-overhead试试,再配合mark_dynamic把batch和seq维度标出来,很多时候能稳下来。另外你可以看下TORCH_LOGS=graph_breaks之类的东西,看看是不是图被切得稀碎,如果break太多那还不如别开。生产里我们其实用得挺挑的,固定shape、固定batch、反复跑同一种推理的service开compile收益明显,像chat这种输入长度飘忽的,往往是开之前先做padding分桶或者干脆eager。至于CUDA graphs,它和compile是能叠加,但对动态shape更敏感,得先解决recompile问题再说。建议你拿真实流量跑个benchmark,别只看单条,compile的价值基本都在稳态吞吐上。
动态shape确实是compile的坑,每次变长都触发recompile,开销全搭进去了。我这边7B模型固定输入长度后compile能快15%左右,但一上变长就崩。建议先用dynamic=True试试,或者把常见长度分桶。生产环境目前还是慎开,调试成本不低。
变长输入基本就是compile在LLM推理上翻车的头号原因,你这20%的倒退八成是动态shape反复触发recompile,每次新长度都要重新走一遍图捕获和编译,短请求还好,长序列一来那开销直接吃掉收益。我这边试过7B和13B,固定batch固定长度的时候compile确实能提个15%到30%,但只要length分布一散,eager反而更稳。CUDA graphs那个思路是对的,但得配合padding到固定bucket,不然capture了也没法复用,而且显存占用会上去,7B在24G卡上很容易卡边界。生产环境里我们最后是分两条路走的,离线批量推理开compile加固定shape,在线服务老老实实eager加vLLM那套paged attention,没人真去硬调compile。你想试的话先拿mode="reduce-overhead"加dynamic=False锁死shape跑一轮,能提再考虑上,不能提就别耗了,这玩意儿调参成本比收益高多了。
变长输入确实会反复触发重编译,试试dynamic=True或者固定shape跑,生产开不开得看场景。
动态shape基本就是罪魁祸首,每次输入长度一变就触发recompile,开销全耗在这上面了。我之前也是7B模型推理,固定长度能快15%左右,一改成变长直接倒挂。建议先加mode="reduce-overhead"配合CUDA graphs试试,再把常见长度分桶固定下来,别让它反复重编。生产环境我现在只在固定batch和固定seqlen的场景开,变长的直接eager省心。
我之前也踩过这个坑,7B模型加compile之后首token延迟直接起飞,后来发现就是变长输入惹的祸。每次序列长度一变,guard就失效,触发重新编译,那个编译开销在推理场景下根本吃不消,尤其你batch里长度参差不齐的时候更明显。后来我用了dynamic=True稍微好点,但说实话提速也没官方吹得那么神。现在我的做法是短固定长度的场景才开compile,比如分类或者固定prompt模板那种,变长的直接老实eager跑,省心。CUDA graphs那个确实有效果,但得配合static shape和pad到固定长度,等于牺牲了一部分灵活性换吞吐。所以我觉得不是compile不行,是它默认假设跟LLM推理的实际输入分布差太远。你要是追求稳定,先profile一下到底卡在编译还是kernel上,别盲目开。
动态shape基本就是罪魁祸首,每次输入长度一变就触发recompile,编译开销直接吃掉收益。你可以试试mode="reduce-overhead"配合mark_static或者把输入padding到固定长度,7B模型在固定shape下compile提升还是挺明显的。生产环境我们现在只在推理服务shape比较稳定的场景开,变长的那种要么分桶要么干脆别碰compile。