已合并
fix(mfusion): preserve output container kind in contract restore #38662
fix(mfusion): preserve output container kind in contract restore #38662
已合并
shaoshengqi创建于 6月16日
2 个文件变更+83-7
Mtest/_inductor/test_mfusion_graph_fusion.py+56-6
@@ -32,12 +32,12 @@ try:
32 )32 )
33 from torch_npu._inductor.mfusion.subgraph_registry import Payload33 from torch_npu._inductor.mfusion.subgraph_registry import Payload
34except ImportError:34except ImportError:
35- _emit_mfusion_dvm_codegen = None # type: ignore[misc, assignment]35+ _emit_mfusion_dvm_codegen = None
36- _find_output_node = None # type: ignore[misc, assignment]36+ _find_output_node = None
37- _restore_output_contract = None # type: ignore[misc, assignment]37+ _restore_output_contract = None
38- _snapshot_output_contract = None # type: ignore[misc, assignment]38+ _snapshot_output_contract = None
39- MFusionPatch = None # type: ignore[misc, assignment]39+ MFusionPatch = None
40- Payload = None # type: ignore[misc, assignment]40+ Payload = None
41 41 
42# Gate omits AKG mfusion; keep module importable even if torch_npu mfusion tree is absent.42# Gate omits AKG mfusion; keep module importable even if torch_npu mfusion tree is absent.
43HAS_MFUSION_STACK = HAS_AKG_MFUSION and MFusionPatch is not None43HAS_MFUSION_STACK = HAS_AKG_MFUSION and MFusionPatch is not None
@@ -96,6 +96,56 @@ def _make_add_mul_subgraph():
96 "torch_npu._inductor.mfusion.graph_fusion not available",96 "torch_npu._inductor.mfusion.graph_fusion not available",
97)97)
98class TestMfusionOutputContract(TestCase):98class TestMfusionOutputContract(TestCase):
99+ def test_restore_output_contract_preserves_single_output(self):
100+ graph = Graph()
101+ a = graph.placeholder("a")
102+ b = graph.placeholder("b")
103+ add = graph.call_function(torch.ops.aten.add.Tensor, (a, b))
104+ graph.output(add)
105+ gm = GraphModule(torch.nn.Module(), graph)
106+ 
107+ snapshot = _snapshot_output_contract(_find_output_node(gm.graph))
108+ 
109+ roundtrip_graph = Graph()
110+ rt_a = roundtrip_graph.placeholder("a")
111+ rt_b = roundtrip_graph.placeholder("b")
112+ rt_add = roundtrip_graph.call_function(torch.ops.aten.add.Tensor, (rt_a, rt_b))
113+ roundtrip_graph.output(rt_add)
114+ rt_gm = GraphModule(torch.nn.Module(), roundtrip_graph)
115+ 
116+ _restore_output_contract(_find_output_node(rt_gm.graph), snapshot)
117+ 
118+ restored_output = _find_output_node(rt_gm.graph)
119+ self.assertIsNotNone(restored_output)
120+ self.assertIs(restored_output.args[0], rt_add)
121+ 
122+ def test_restore_output_contract_preserves_list_output(self):
123+ graph = Graph()
124+ a = graph.placeholder("a")
125+ b = graph.placeholder("b")
126+ add = graph.call_function(torch.ops.aten.add.Tensor, (a, b))
127+ graph.output([add, b])
128+ gm = GraphModule(torch.nn.Module(), graph)
129+ 
130+ snapshot = _snapshot_output_contract(_find_output_node(gm.graph))
131+ 
132+ roundtrip_graph = Graph()
133+ rt_a = roundtrip_graph.placeholder("a")
134+ rt_b = roundtrip_graph.placeholder("b")
135+ rt_add = roundtrip_graph.call_function(torch.ops.aten.add.Tensor, (rt_a, rt_b))
136+ roundtrip_graph.output((rt_add, rt_b))
137+ rt_gm = GraphModule(torch.nn.Module(), roundtrip_graph)
138+ 
139+ _restore_output_contract(_find_output_node(rt_gm.graph), snapshot)
140+ 
141+ restored_output = _find_output_node(rt_gm.graph)
142+ self.assertIsNotNone(restored_output)
143+ restored_args = restored_output.args[0]
144+ self.assertIsInstance(restored_args, list)
145+ self.assertEqual(len(restored_args), 2)
146+ self.assertIs(restored_args[0], rt_add)
147+ self.assertIs(restored_args[1], rt_b)
148+ 
99 def test_restore_output_contract_preserves_none_slots_and_stride_meta(self):149 def test_restore_output_contract_preserves_none_slots_and_stride_meta(self):
100 graph = Graph()150 graph = Graph()
101 a = graph.placeholder("a")151 a = graph.placeholder("a")
Mtorch_npu/_inductor/mfusion/graph_fusion.py+27-1
@@ -395,6 +395,15 @@ def _output_arg_list(output_node: Node) -> list[Any]:
395 return list(args) if isinstance(args, (list, tuple)) else [args]395 return list(args) if isinstance(args, (list, tuple)) else [args]
396 396 
397 397 
398+def _output_container_kind(output_node: Node) -> str:
399+ args = output_node.args[0]
400+ if isinstance(args, tuple):
401+ return "tuple"
402+ if isinstance(args, list):
403+ return "list"
404+ return "single"
405+ 
406+ 
398def _copy_output_meta_value(value: Any) -> Any:407def _copy_output_meta_value(value: Any) -> Any:
399 if isinstance(value, list):408 if isinstance(value, list):
400 return list(value)409 return list(value)
@@ -411,6 +420,7 @@ def _snapshot_output_contract(output_node: Node | None) -> dict[str, Any]:
411 420 
412 output_args = _output_arg_list(output_node)421 output_args = _output_arg_list(output_node)
413 return {422 return {
423+ "output_container": _output_container_kind(output_node),
414 "output_len": len(output_args),424 "output_len": len(output_args),
415 "none_positions": [i for i, arg in enumerate(output_args) if arg is None],425 "none_positions": [i for i, arg in enumerate(output_args) if arg is None],
416 "valid_positions": [i for i, arg in enumerate(output_args) if arg is not None],426 "valid_positions": [i for i, arg in enumerate(output_args) if arg is not None],
@@ -477,7 +487,23 @@ def _restore_output_contract(output_node: Node | None, snapshot: dict[str, Any])
477 original_output_strides487 original_output_strides
478 )488 )
479 489 
480- output_node.args = (tuple(restored),)490+ output_container = snapshot.get("output_container", "tuple")
491+ if output_container == "tuple":
492+ output_node.args = (tuple(restored),)
493+ elif output_container == "list":
494+ output_node.args = (list(restored),)
495+ elif output_container == "single":
496+ if len(restored) != 1:
497+ raise RuntimeError(
498+ "mfusion output ABI mismatch while restoring single output: "
499+ f"restored_len={len(restored)}, original_len={output_len}"
500+ )
501+ output_node.args = (restored[0],)
502+ else:
503+ raise RuntimeError(
504+ "mfusion output ABI has unsupported output container: "
505+ f"{output_container!r}"
506+ )
481 507 
482 508 
483def _layout_get_size(layout):509def _layout_get_size(layout):