已合并
fix(mfusion): preserve output container kind in contract restore #38662
shaoshengqi创建于 6月16日
fix(mfusion): preserve output container kind in contract restore #38662
已合并
共 2 个文件变更+83-7
| @@ -32,12 +32,12 @@ try: | |||
| 32 | ) | 32 | ) |
| 33 | from torch_npu._inductor.mfusion.subgraph_registry import Payload | 33 | from torch_npu._inductor.mfusion.subgraph_registry import Payload |
| 34 | except ImportError: | 34 | except 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. |
| 43 | HAS_MFUSION_STACK = HAS_AKG_MFUSION and MFusionPatch is not None | 43 | HAS_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 | ) |
| 98 | class TestMfusionOutputContract(TestCase): | 98 | class 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") |
| @@ -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 | + | ||
| 398 | def _copy_output_meta_value(value: Any) -> Any: | 407 | def _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_strides | 487 | 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 | ||
| 483 | def _layout_get_size(layout): | 509 | def _layout_get_size(layout): |