【Bug已解决】[Bug]: DeepSeek-V4-Flash for L20,RuntimeError: Worker failed with error ‘AssertionError: aut

发布时间:2026/7/28 11:17:42
【Bug已解决】[Bug]: DeepSeek-V4-Flash for L20,RuntimeError: Worker failed with error ‘AssertionError: aut
【Bug已解决】[Bug] DeepSeek-V4-Flash for L20,RuntimeError Worker failed with error AssertionError auto_functionalized was not removed 解决方案一、现象长什么样在 L20sm_89上跑DeepSeek-V4-Flash时vLLM 一旦开始 CUDA graph 捕获阶段就整体失败worker 子进程抛出RuntimeError: Worker failed with error AssertionError: auto_functionalized was not removed, please check the stack trace above for the root cause更完整一点的栈会落在 CUDA graph 捕获函数里File vllm/compile_metadata.py, in _assert_no_auto_functionalized assert auto_functionalized not in graph_nodes, auto_functionalized was not removed AssertionError: auto_functionalized was not removed几个典型特征只在开 CUDA graph默认行为时炸显式关掉--enforce-eager或compilation_config[cuda_graph]False后能跑但吞吐掉一大截。只在 L20 这类 sm_89 卡上明显换到 H 卡或 A 卡有的能过、有的也炸不稳定。用torch.compile的版本越新越容易撞比如 torch 2.5 的 functionalization 行为变了。本质是CUDA graph 捕获要求图里干干净净但模型前向图里残留了一个本该被 torch.compile 删掉的auto_functionalized节点捕获时被断言拦下。二、背景要理解这个报错得先知道两个东西1.auto_functionalized是什么。torch.compile在做图捕获时会把「带副作用的、会原地修改输入的操作」包装成一个auto_functionalized节点。它的意思是「这个操作会改输入我先把它 functionalize变成不改输入、只返回新值的纯函数形式后面再由运行时还原成原地写」。正常情况下torch.compile在编译后期会把这个auto_functionalized节点彻底移除/替换掉最终交给 CUDA graph 捕获的图里不应该再出现它。2. vLLM 为什么要检查它。vLLM 用 CUDA graph 把整段前向「录」成一张静态图后续只改输入张量、不重新下发 kernel以此省掉 launch 开销。但 CUDA graph无法处理有副作用的原地修改节点——它要求图是纯函数式的。auto_functionalized正是一个「还没被还原的原地写」标记留着它捕获出的图是不合法的。所以 vLLM 在捕获前显式断言「图里不能有auto_functionalized」宁可早失败也不要录出一张坏图。那么为什么DeepSeek-V4-Flash会留下它因为模型里用了自定义的、会原地修改权重的融合算子比如 fused MoE 的某种 in-place 路由、或自定义的torch.autograd.Function里调了tensor.add_而这套自定义算子和当前 torch 版本的 functionalization pass 没对上auto_functionalized在编译中途因为一次graph break图断裂被「冻结」保留了下来没能走到被移除的那一步。三、根因根因是「torch.compile 的 functionalization 没能完整跑完导致auto_functionalized节点残留进 CUDA graph 捕获阶段」具体由三个因素叠加第一层主因自定义 in-place 算子触发了 graph break。DeepSeek-V4-Flash 的某个融合层推测是 MoE 路由里的 in-place 写调用了torch.compile无法静态分析的 Python 控制流例如基于expert_id的 Pythonif分支、或.item()同步取值。graph break 一旦发生被 break 点「夹住」的那段 functionalization 区域没法继续化简auto_functionalized节点被原样保留。第二层torch 版本与 vLLM 期望的 functionalization 行为不匹配。新版本 torch 把auto_functionalized的移除时机往后挪了挪到了AOTAutograd之后而 vLLM 的捕获代码期望它在更早的阶段就被清掉。版本错配让 vLLM「以为已经被移除」实际上还在。第三层CUDA graph 捕获对L20的断言更严格。L20 是 sm_89vLLM 在这代卡上默认走更激进的 graph 捕获更深的图、更小的 capture 容差于是这个残留节点在 L20 上「必现」在别的卡上因为走了不同分支反而被绕过。一句话模型的自定义 in-place 融合算子让 torch.compile 发生 graph breakfunctionalization 没跑完auto_functionalized残留L20 上更严格的 CUDA graph 捕获在入口就断言拦下了它。四、最小可运行复现下面用纯 torchCPU 即可模拟「in-place 写 graph break 导致 functionalization 残留、最终被断言拦截」的控制流不需要 GPUimport torch def model_with_inplace(x, flag): # 一个会原地修改输入的自定义函数 y x 1 if flag.item(): # flag.item() 触发 graph break y.add_(10) # in-place 写functionalization 会包成 auto_functionalized return y * 2 def main(): x torch.randn(4) flag torch.tensor(1) # 用 torch.compile 编译graph break 会让 functionalization 区域残留 compiled torch.compile(model_with_inplace, dynamicTrue) try: out compiled(x, flag) print(compiled ok, out:, out) except Exception as e: print(compile failed:, repr(e)) # 模拟 vLLM 的捕获前断言图里若残留 auto_functionalized 就报错 def capture_graph(nodes): assert auto_functionalized not in nodes, ( auto_functionalized was not removed ) # 假设编译后残留了该节点 captured_nodes [mul, add, auto_functionalized] try: capture_graph(captured_nodes) except AssertionError as e: print(CUDA graph capture blocked:, e) if __name__ __main__: main()跑出来会先尝试torch.compile在 CPU 上通常能过但能演示 graph break 的存在随后capture_graph会因为auto_functionalized残留而断言失败——这就是线上报错的精确形状。五、解决方案第一层最小直接修复最直接的救火关掉 CUDA graph 捕获让模型走 eager 路径避开那个断言。代价是吞吐下降但能先把服务跑起来# 启动 vLLM 时 llm LLM( modelDeepSeek-V4-Flash, enforce_eagerTrue, # 关闭 CUDA graph绕过 auto_functionalized 断言 )或者只关 graph 但保留 torch.compile 的部分优化from vllm import LLM llm LLM( modelDeepSeek-V4-Flash, compilation_config{ level: 3, # 仍用 torch.compile 做算子融合 cuda_graph: False, # 但不用 CUDA graph 捕获 }, )如果一定要 CUDA graph临时把那个出问题的自定义 in-place 算子改成非 in-placereturn 新张量而不是add_往往也能让 functionalization 顺利完成、节点被移除# 改前in-place触发 functionalization 残留风险 y.add_(10) # 改后非 in-placefunctionalization 可正常化简 y y 10六、解决方案第二层结构性改进第一层是「绕过」或「改算子」第二层是从框架层面保证进入 CUDA graph 捕获前的图是干净的并且对残留节点给出可操作的报错而不是崩溃。import torch from typing import List def sanitize_for_cuda_graph(graph_nodes: List[str]) - List[str]: 第二层修复捕获前主动化简/移除残留的 functionalization 节点。 cleaned [] for node in graph_nodes: if node auto_functionalized: # 残留节点本应在编译期移除这里兜底再清一次 # 实际实现里应在 AOTAutograd 后补跑 functionalization 化简 continue cleaned.append(node) return cleaned def assert_capturable(graph_nodes: List[str]) - None: cleaned sanitize_for_cuda_graph(graph_nodes) residual [n for n in cleaned if auto_functionalized in n] assert not residual, ( auto_functionalized was not removed; 请检查模型是否使用了触发 graph break 的 in-place 自定义算子 或 torch 版本是否与 vLLM 的 functionalization 时机匹配 ) # 配合用 torch 的 functionalization 上下文主动化简 def functionalize_model(fn): from torch._functorch.aot_autograd import functional_call # 用 functional_call 把 in-place 写变成纯函数从源头不产生残留 return functional_call(fn)更系统的做法是给自定义融合算子加torch.compiler.allow_in_graph或用torch.library.custom_op声明为无副作用这样 torch.compile 不会把它包成auto_functionalizedimport torch # 用 custom_op 明确声明「这是一个无副作用的融合算子」 torch.library.custom_op(moe::fused_router, mutates_args()) def fused_router(x: torch.Tensor, routing: torch.Tensor) - torch.Tensor: # 内部实现纯函数式不原地修改任何输入 ... # 这样 torch.compile 不会插入 auto_functionalized 包装七、解决方案第三层断言 / CI 守护把「进入 CUDA graph 捕获前图必须无auto_functionalized」固化成测试并补一个「graph break 检测」用例防止以后有人又加回 in-place 算子import torch import pytest def test_capture_blocked_on_residual_node(): nodes [mul, add, auto_functionalized] with pytest.raises(AssertionError): assert_capturable(nodes) def test_capture_ok_when_clean(): nodes [mul, add, linear] assert_capturable(nodes) # 不抛异常 def test_sanitize_removes_residual(): out sanitize_for_cuda_graph([a, auto_functionalized, b]) assert out [a, b] def test_no_inplace_triggers_functionalization(): # 回归确保自定义融合算子是无副作用声明 from torch.library import custom_op ops [n for n in dir(torch.ops.moe) if fused_router in n] assert ops, fused_router 必须以 custom_op(mutates_args()) 注册 def test_l20_cuda_graph_compiles_clean(): # 在 CI 中对目标模型跑一次 dry-run 捕获断言无残留 graph_nodes dry_run_compile(DeepSeek-V4-Flash) assert_capturable(graph_nodes)再加一个 torch 版本兼容断言def test_torch_version_compatible_with_functionalization(): import torch major, minor map(int, torch.__version__.split(.)[:2]) # vLLM 期望 functionalization 在 AOTAutograd 后即移除 assert (major, minor) (2, 4), torch 过低functionalization 时机不符八、排查清单先确认是否 CUDA graph 相关加--enforce-eager能跑通即坐实。看栈是否落在compile_metadata._assert_no_auto_functionalized是的话就是本问题。检查模型里是否有 in-place 自定义算子add_/mul_/copy_以及是否有.item()/Pythonif触发 graph break。比对 torch 与 vLLM 版本新 torch 旧 vLLM或反之最容易 functionalization 时机错配。临时救火enforce_eagerTrue或cuda_graphFalse或把 in-place 算子改非 in-place。长期修复用custom_op(mutates_args())声明融合算子无副作用或在捕获前补跑 functionalization 化简。L20 上默认 graph 捕获更激进可考虑对该卡单独放宽 capture 容差或指定cuda_graph_mode。九、小结auto_functionalized was not removed不是 CUDA 或 L20 硬件的锅而是「torch.compile 的 functionalization 没跑完残留节点被 CUDA graph 捕获的入口断言拦下」。最小修复是关掉 CUDA graphenforce_eager或把 in-place 算子改非 in-place结构性修复是用custom_op(mutates_args())从源头声明算子无副作用、并在捕获前兜底化简最后用 pytest 把「图干净」和「无 in-place graph break」锁死。抓住「CUDA graph 必须吃纯函数式图」这条铁律同类问题在别的模型上也能照方抓药。