最近在试着用PyTorch 2.0的torch.compile优化一个7B的对话模型推理速度,但一跑就报“RuntimeError: Expected all tensors to be on the same device”,debug了半天发现是动态shape的问题。我的输入长度变化比较大,用padding后模型内部有些操作还是跨设备了。想问问大家,现在大模型用compile的最佳实践是什么?是不是必须固定输入长度,或者有什么配置能避免这种报错?另外,我试了用inductor后端,但有时候模型第一次推理能过,第二次就挂,感觉稳定性还是有点玄学。真诚求教,别笑我菜。
PyTorch 2.0 compile在LLM推理时总报错,是我打开方式不对吗?
全部回复
共 190 条动态shape确实是torch.compile踩坑的重灾区,你遇到的设备不一致报错往往不是真的跨设备了,而是guard失败后图被重新捕获时,某些中间tensor的device信息丢了,报错信息有点误导人。我自己的经验是7B这个量级别一上来就全图compile,先把model拆开,只对transformer block或者attention层做区域编译,效果反而更稳。固定输入长度不是必须的,但你要么用mark_only_dynamic加dynamic=True显式标记动态维度,要么就接受它每次遇到新shape都重新编译的代价。inductor第二次挂掉大概率是cache命中逻辑和动态shape的符号推导打架,可以试试开mode="reduce-overhead"之外再配fullgraph=False,让它允许图断裂。另外你padding之后跨设备,检查一下position_ids或者attention_mask是不是在某个分支里被重新创建到了cpu上,这种细节特别容易漏。说实话现在compile在大模型推理上还没到开箱即用的程度,社区里很多人都是半用半关,关键路径手写融合更靠谱。
动态shape确实是torch.compile踩坑最多的地方,你遇到的跨设备报错大概率不是真的设备不一致,而是guard失败后recompile时某些graph break把tensor状态搞乱了,报错信息有点误导人。我自己的经验是先用mode="reduce-overhead"配合dynamic=True试一下,但LLM里RoPE和KV cache那几段对shape特别敏感,经常第一次跑通第二次就崩,基本就是cache命中逻辑和编译缓存打架。固定输入长度确实最稳,但对话场景不现实,可以试试把prefill和decode分开编译,decode阶段尽量用static cache。inductor后端不稳定这事不怪你,7B模型上graph break太多的话,编译出来的东西还不如eager快。建议先开TORCH_LOGS=graph_breaks看看哪里断了,很多时候是某个自定义op或者mask操作在作妖。
动态shape确实是compile踩坑重灾区,尤其你这种变长输入。建议先上dynamic=True试试,不过inductor对动态支持还不太行,很多时候得把不同长度bucket化处理。那个第二次挂的问题我也遇到过,大概率是guard重新编译时状态没清干净,可以开TORCH_LOGS=guard看看哪里触发了重编译。
动态shape确实是compile的坑,我也被这个device报错折磨过。建议试试设置dynamic=True,再配合mode="reduce-overhead",不过inductor对变长输入支持还是不太稳。我后来干脆按长度分桶,把相近的batch到一起,虽然麻烦但稳定多了。你说的第二次挂掉,大概率是guard重新编译时出的问题,可以开TORCH_LOGS看看日志。
这问题我太熟了,动态shape配compile确实是坑。你那个跨设备报错八成不是真的设备不一致,而是inductor在recompile时把某些中间张量放到了meta device上,然后guard失效导致的。固定输入长度不是必须的,但你需要用torch._dynamo.mark_dynamic或者dynamic=True来显式告诉编译器哪些维度是动态的,不然它每次遇到新shape就重新编译,缓存一乱就容易出这种幺蛾子。至于第一次能过第二次挂,我猜你开了mode="reduce-overhead"或者cudagraphs,这玩意儿对内存地址复用很敏感,输入指针一变就崩,先换回default模式跑通再说。7B模型的话,其实可以试试把KV cache单独拿出来做静态管理,别让compile去trace整个generate循环,只compile单步forward会稳很多。另外torch 2.1之后对dynamic shape的支持好了不少,如果方便升个版本可能直接少一半问题。
动态shape确实是个大坑,我这边也踩过。可以试试torch.compile里的dynamic=True参数,配合mark_dynamic手动标记输入维度,别让它默认按静态图去优化。另外inductor第二次挂大概率是guard失效或者缓存命中问题,开TORCH_LOGS=graph_breaks看下哪里断图了。固定长度是最省事的方案,但真要支持变长,建议先拿小模型把compile流程跑通再上7B。
动态shape确实是torch.compile配LLM时最容易踩的坑,你遇到的跨设备报错八成不是真的设备不一致,而是compile在guard阶段推断shape失败后,某些分支走到了fallback路径,把中间张量搞乱了。我自己的经验是先用dynamic=True打开动态shape支持,再配合mode="reduce-overhead"试试,但别指望它一上来就完美,inductor对变长序列的处理还是有边界的。固定输入长度确实最稳,但对话场景里太不现实了,一般会用几个bucket把长度归拢到几档,牺牲一点灵活性换稳定性。你说的第二次推理才挂,很可能是第一次触发了重编译,guard条件变了之后缓存失效,建议开TORCH_LOGS="guards,recompiles"看看到底哪一步在反复编译。另外7B模型如果只是推理,其实可以看看vLLM或者TensorRT-LLM,它们在动态shape和KV cache这块做得比原生compile成熟不少,不一定非得死磕torch.compile。如果非要走compile路线,把attention部分单独拆出来手动处理shape,往往比整体compile省心。
有没有更详细的教程推荐?
我之前也遇到过类似问题,后来换了方案。
动态shape确实是compile踩坑重灾区,你那个跨设备报错大概率不是compile本身的问题,而是某些算子在图捕获时把device信息搞丢了,尤其你padding之后mask相关的操作容易出这种事。固定输入长度不是必须的,但建议先拿一组固定shape跑通,确认图能正常编译,再开dynamic=True逐步放开,这样能定位到具体是哪层在捣乱。第一次过第二次挂这个我也遇到过,多半是guard重新触发编译或者缓存没命中,可以开TORCH_LOGS=“graph_breaks,recompiles”看看它到底在重编译什么,很多时候是某个小tensor的shape或者dtype悄悄变了。inductor对LLM的支持这两版进步挺大,但7B模型里如果有自定义attention或者rotary实现,最好确认下有没有fallback到eager的分支,那些地方最容易出玄学问题。另外fullgraph=False先跑着,别一上来就追求整图编译,能省很多debug时间。