最近在试着用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推理时总报错,是我打开方式不对吗?
全部回复
共 182 条这问题太真实了,动态shape在compile下就是容易踩雷,尤其7B这种规模,跨设备报错基本就是graph break后某个tensor没跟上。我建议你先试试torch._dynamo的dynamic=True参数,配合mark_dynamic指定可变维度,能缓解一部分问题。另外inductor后端确实有点抽风,我后来换回默认后端,再配合cudagraphs,稳定性反而好了不少。固定输入长度是最省心的方案,但如果你非要动态,建议把padding逻辑放到model外面处理,别让compile看到可变shape。
这问题太典型了,动态shape在compile下就是容易触发跨设备检查,尤其padding后attention mask的维度变化会让graph模式直接懵。我试过把输入长度按固定bucket切分,比如128/256/512这样,配合torch._dynamo的dynamic=False,能避开大部分报错。另外inductor稳定性确实看版本,建议升级到2.1+或者试试cudagraphs后端,第一次跑慢是正常的,但第二次挂多半是缓存没处理好,可以试试torch._inductor.config.triton.cudagraphs=True。你用的是哪种attention实现?flash attention在compile下有时候会有额外坑。
我最近也踩过这个坑,动态shape在compile下确实容易触发跨设备问题,可以先试试把输入pad到固定长度,或者用torch._dynamo的dynamic参数手动指定可变维度,能缓解不少。inductor后端第一次过第二次挂的问题我也遇到过,感觉像是缓存或图优化在某些边界条件下没处理好,建议升级到最新版pytorch,或者试试把max-autotune关掉换默认模式。另外你如果是做服务端推理,可以考虑把compile结果cache下来,别每次启动都重新编译。
我这边后来干脆用vLLM或者TensorRT-LLM绕开了torch.compile,虽然折腾少但效果直接,如果你只是追求稳定推理的话可以试试。你7B模型具体用的什么量化方案?说不定和这个也有关系。
这问题我太熟了,7B模型用compile基本就是动态shape和跨设备的双重折磨。你那个RuntimeError我怀疑不只是padding的问题,很可能是模型里某些op在compile时被重排了执行顺序,导致中间张量被隐式挪到了默认设备上。我试过把max_length固定到512,然后所有输入都pad到那个长度,确实能绕过大部分报错,但代价是显存占用直接涨了30%,小卡根本扛不住。inductor后端那个“第一次能过第二次挂”的玄学我也遇到过,后来发现是cudagraph捕获时和你的自定义loss或者gradient checkpointing冲突了,你可以试试在compile里加上mode=“reduce-overhead”或者关掉cudagraphs,但推理速度会掉一点。另外如果你用的是HuggingFace的话,试试把model.config里的use_cache和torch.compile配合起来,有时候这个开关会影响动态shape的静态化处理。还有一个歪招,就是把输入长度分桶,比如256、512、1024各写一个compile版本,运行时按实际长度选,虽然代码丑但稳定很多。最后想问问你用的什么GPU和CUDA版本?我怀疑你那个跨设备报错可能和CUDA的peer-to-peer访问权限有关,尤其是多卡环境,单卡反而很少见这个问题。
说实话你这个报错我太熟了,7B模型用compile碰上动态shape基本就是地狱开局。我后来是直接把max_length定死,padding到固定长度,然后配合torch._dynamo.mark_dynamic把某些维度标成动态,才勉强跑通。但你要有心理准备,就算不报跨设备错误,inductor在动态shape下生成的kernel也经常不是最优的,有时候反而比eager慢。另外第二次挂那个问题,我怀疑是CUDA graph缓存或者cudnn benchmark的autotune在搞鬼,你可以试试设置torch.backends.cudnn.benchmark=False,或者干脆每次推理前清一下cache。不过说实话,如果你只是做推理,不如直接用vLLM或者TGI,那才是专门为动态batch和变长输入优化的,torch.compile目前更适合训练或者固定shape的批量推理场景。你要是非要用compile,建议先看下GitHub上那个“torch-compile-for-llm”的issue列表,里面踩坑记录比官方文档实用多了。
试试把max_length固定成2的幂次,或者关掉动态shape那个选项,我之前也被这玩意坑过。
inductor后端确实玄学,回退到默认后端或者用eager模式过渡下吧。
这问题我也踩过,动态shape在compile下确实容易触发设备断言,尤其padding后某些OP的tensor shape推断会乱。我试下来最好先固定到最大长度,或者用torch._dynamo的dynamic=True配置,但代价是编译时间翻倍。inductor不稳定我深有体会,建议先关掉cudagraphs试试,或者干脆回退到eager模式对比一下,确认是不是编译本身的问题。另外7B模型用compile收益其实有限,显存带宽瓶颈可能更明显。
我也踩过这坑,动态shape得用mark_dynamic标记或固定长度,否则inductor后端确实会抽风。
这问题我也踩过,动态shape在compile下确实容易触发跨设备断言,尤其7B这种模型里有些算子会隐式广播。我的做法是给输入长度做个分桶,把相近的长度pad到同一档,配合torch._dynamo.mark_dynamic标注一下,能少很多莫名其妙的错。至于inductor第二次挂,大概率是缓存了第一次的graph,遇到新shape就崩,试试torch._dynamo.config.cache_size_limit调大点,或者干脆每次推理前清一下缓存。
动态shape确实是compile的大坑,建议先固定到最大长度试试,或者用mark_dynamic标记一下。我这边inductor也偶发炸第二次,基本就是workaround凑合。
动态shape确实折磨人,建议先固定长度试跑通再谈优化,不然报错能玩一天。
这问题我熟,之前调stable diffusion的compile也踩过类似的坑。动态shape确实容易触发device断言,建议试试给输入加个最大长度限制,然后padding到固定值,虽然浪费点显存但至少稳定。另外你提到第二次推理挂掉,可能是cudagraphs缓存和动态shape冲突了,试试torch.compile的mode="reduce-overhead"或者关掉cudagraphs看看。inductor后端现在对变长输入支持确实一般,不行就退回eager模式,或者等2.0.1的补丁。
动态shape确实是compile的大坑,建议先固定到最大长度+attention mask试试,能省不少心。inductor那个玄学问题我也遇到过,换cudagraphs后端会稳一些。
同款问题,动态shape在compile下基本就是灾难,我后来直接给dataloader统一pad到固定长度,牺牲点显存换稳定。你试试torch._dynamo.config.supports_dynamic_shape=False,强制静态shape跑一遍,至少能确认是不是这个原因。另外inductor偶尔抽风是常态,可以换个backend比如triton或者cudagraphs对比下,别死磕一个。
动态shape确实是compile的坎,建议先固定长度试下,或者用torch._dynamo的配置把dynamic参数打开试试。
inductor第二次挂多半是缓存问题,清下~/.cache/torchinductor或换backend试试。
动态shape确实是compile的老大难,建议先固定最大长度加mask试试,稳定性会好很多。
我最近也在折腾这个,7B模型加compile确实容易踩坑。你那个动态shape的问题我遇到过类似情况,后来发现torch.compile对完全动态的shape支持还是不太行,尤其是attention mask和position id这种跟着长度变的东西,建议把padding到固定长度(比如256的倍数)再配合torch._dynamo.mark_dynamic标注一下关键维度,能缓解不少。至于inductor后端第一次能跑第二次挂,我猜可能是graph缓存和cudnn benchmark的冲突,试试设置torch.backends.cudnn.benchmark=False,或者换个reduce-overhead模式看看。另外有个偏门但有效的办法是绕开compile,直接用torch.jit.script对attention部分做局部优化,配合flash-attention的kernel,效果可能比全量compile更稳。你用的什么CUDA版本?我之前在11.8上问题频发,升到12.1之后明显好多了。还想问下你是用HF的generate还是自己写的采样循环?有时候是库内部的动态shape触发问题,自己写forward反而能避开。
说实话你这个报错我太熟了,上周刚被同款折磨过。动态shape和torch.compile现在就是天生八字不合,你就算padding到固定长度,只要里面有什么view或者reshape操作在运行时算出来尺寸不一致,它照样给你跨设备。我后来是直接放弃动态输入,把max_seq_len设成512,所有样本都pad到那个长度,再用mark_dynamic标记一下,基本就不报错了,但代价就是短输入时显存浪费挺多。
inductor后端那个“第一次能过第二次挂”的现象我也遇到过,感觉是它在第二次图优化时缓存了某些tensor的device信息,但实际执行时又变了。你可以试试把torch._dynamo.config.cache_size_limit设大点,或者干脆每次推理前清一下torch._dynamo.reset(),虽然丑但确实能缓解。另外我试过用reduce-overhead模式,有时候比inductor稳一点,但编译时间会翻倍。
还有个思路是干脆别对整模型compile,只对里面最重的几个attention层做局部编译,其他部分保持eager模式。我这么搞之后虽然速度提升没那么夸张,但至少不玄学了,跑一晚上测试都没崩过。你那个7B模型如果显存还够,可以试试这个折中方案,说不定能绕过动态shape的坑。
说实话你这情况我太熟了,当时我拿compile跑LLaMA也卡在动态shape上,后来干脆把padding到固定长度才消停。torch.compile对静态shape优化最彻底,动态shape一多,它内部图模式就容易崩,尤其跨设备操作,报错信息还特别误导人。我猜你那个跨设备问题可能不是真跨设备,而是compile在trace时把某些分支当成了独立子图,导致设备信息没传递全。建议你先试试用torch._dynamo.config里的dynamic参数,比如设置dynamic=False强制静态,或者用mark_dynamic标记特定维度,别让所有维度都动态。另外inductor后端确实有点玄,我遇到过第一次跑成功第二次挂的情况,后来发现是cudagraphs缓存和动态shape冲突,关掉mode="reduce-overhead"或者直接换backend="aot_eager"能稳一点。还有个野路子,如果你输入长度真的范围很大,可以考虑分段批处理,比如按长度区间分桶,每个桶固定长度,这样compile能吃到静态shape的红利,速度提升明显。最后想说,大模型推理想用compile省心,现阶段真得牺牲一点灵活性,要么固定长度,要么接受偶尔的抽风,等PyTorch把动态shape支持打磨好了再浪吧。
动态shape确实是compile的大坑,试试把pad统一到最大长度或分桶,能稳不少。
Inductor二次挂多半是缓存问题,加个torch._dynamo.config.suppress_errors=True先跑通再说。