已合并
feat:支持融合结果上报的 Python 化 #4070
lfz2812创建于 7月20日
feat:支持融合结果上报的 Python 化 #4070
已合并
共 21 个文件变更+305-56
| @@ -38,6 +38,7 @@ __all__ = [ | |||
| 38 | "pattern", | 38 | "pattern", |
| 39 | "register_decompose_pass", | 39 | "register_decompose_pass", |
| 40 | "register_fusion_pass", | 40 | "register_fusion_pass", |
| 41 | + "report_fuse", | ||
| 41 | ] | 42 | ] |
| 42 | 43 | ||
| 43 | from .pattern import pattern | 44 | from .pattern import pattern |
| @@ -67,6 +68,7 @@ _LAZY_EXPORTS = { | |||
| 67 | "get_registered_passes": ".registry", | 68 | "get_registered_passes": ".registry", |
| 68 | "register_decompose_pass": ".registry", | 69 | "register_decompose_pass": ".registry", |
| 69 | "register_fusion_pass": ".registry", | 70 | "register_fusion_pass": ".registry", |
| 71 | + "report_fuse": ".fuse_inspector", | ||
| 70 | } | 72 | } |
| 71 | 73 | ||
| 72 | 74 | ||
| @@ -29,6 +29,7 @@ __all__: list[str] = [ | |||
| 29 | "SubgraphOutput", | 29 | "SubgraphOutput", |
| 30 | "SubgraphRewriter", | 30 | "SubgraphRewriter", |
| 31 | "can_fuse", | 31 | "can_fuse", |
| 32 | + "report_fuse", | ||
| 32 | ] | 33 | ] |
| 33 | 34 | ||
| 34 | 35 | ||
| @@ -37,6 +38,15 @@ def can_fuse(nodes: typing.Iterable[Node]) -> tuple[bool, str]: | |||
| 37 | ... | 38 | ... |
| 38 | 39 | ||
| 39 | 40 | ||
| 41 | +def report_fuse( | ||
| 42 | + nodes_before: typing.Iterable[Node], | ||
| 43 | + nodes_after: typing.Iterable[Node], | ||
| 44 | + context: PassContext, | ||
| 45 | +) -> None: | ||
| 46 | + """Report a completed graph fusion rewrite.""" | ||
| 47 | + ... | ||
| 48 | + | ||
| 49 | + | ||
| 40 | class PassContext: | 50 | class PassContext: |
| 41 | """Compile-time pass context (Python view of C++ ``CustomPassContext``). | 51 | """Compile-time pass context (Python view of C++ ``CustomPassContext``). |
| 42 | 52 | ||
| @@ -295,6 +305,7 @@ class SubgraphRewriter: | |||
| 295 | """Apply whole-subgraph replacement on the main graph.""" | 305 | """Apply whole-subgraph replacement on the main graph.""" |
| 296 | 306 | ||
| 297 | 307 | ||
| 308 | + | ||
| 298 | def replace(boundary: SubgraphBoundary, replacement: Graph) -> int: | 309 | def replace(boundary: SubgraphBoundary, replacement: Graph) -> int: |
| 299 | """Replace the subgraph described by ``boundary`` with ``replacement``. | 310 | """Replace the subgraph described by ``boundary`` with ``replacement``. |
| 300 | 311 | ||
| @@ -316,3 +327,12 @@ class SubgraphRewriter: | |||
| 316 | 327 | ||
| 317 | """ | 328 | """ |
| 318 | ... | 329 | ... |
| 330 | + | ||
| 331 | + | ||
| 332 | + | ||
| 333 | + def replace(boundary: SubgraphBoundary, replacement: Graph, *, context: PassContext) -> None: | ||
| 334 | + """Replace a subgraph with automatic fusion inspection and reporting. | ||
| 335 | + | ||
| 336 | + Raises ``RuntimeError`` when fusion inspection, replacement, or reporting fails. | ||
| 337 | + """ | ||
| 338 | + ... | ||
| @@ -28,6 +28,7 @@ __all__ = [ | |||
| 28 | "borrow_node", | 28 | "borrow_node", |
| 29 | "can_fuse", | 29 | "can_fuse", |
| 30 | "clone_pattern_matcher_config", | 30 | "clone_pattern_matcher_config", |
| 31 | + "report_fuse", | ||
| 31 | "release_graph", | 32 | "release_graph", |
| 32 | ] | 33 | ] |
| 33 | 34 | ||
| @@ -48,4 +49,5 @@ borrow_match_result = _native.borrow_match_result | |||
| 48 | borrow_node = _native.borrow_node | 49 | borrow_node = _native.borrow_node |
| 49 | can_fuse = _native.can_fuse | 50 | can_fuse = _native.can_fuse |
| 50 | clone_pattern_matcher_config = _native.clone_pattern_matcher_config | 51 | clone_pattern_matcher_config = _native.clone_pattern_matcher_config |
| 52 | +report_fuse = _native.report_fuse | ||
| 51 | release_graph = _native.release_graph | 53 | release_graph = _native.release_graph |
| @@ -17,7 +17,7 @@ from __future__ import annotations | |||
| 17 | from dataclasses import dataclass | 17 | from dataclasses import dataclass |
| 18 | from typing import TYPE_CHECKING, Iterable | 18 | from typing import TYPE_CHECKING, Iterable |
| 19 | 19 | ||
| 20 | -from ._native import can_fuse as _native_can_fuse | 20 | +from . import _native |
| 21 | 21 | ||
| 22 | if TYPE_CHECKING: | 22 | if TYPE_CHECKING: |
| 23 | from ge.graph.node import Node | 23 | from ge.graph.node import Node |
| @@ -31,12 +31,10 @@ class FuseCheckResult: | |||
| 31 | reason: str = "" | 31 | reason: str = "" |
| 32 | 32 | ||
| 33 | 33 | ||
| 34 | +report_fuse = _native.report_fuse | ||
| 35 | + | ||
| 36 | + | ||
| 34 | def can_fuse(nodes: Iterable["Node"]) -> FuseCheckResult: | 37 | def can_fuse(nodes: Iterable["Node"]) -> FuseCheckResult: |
| 35 | """Check whether ``nodes`` can be safely fused into one node.""" | 38 | """Check whether ``nodes`` can be safely fused into one node.""" |
| 36 | - from ge.graph.node import Node | 39 | + ok, reason = _native.can_fuse(nodes) |
| 37 | - | ||
| 38 | - node_list = list(nodes) | ||
| 39 | - if not all(isinstance(node, Node) for node in node_list): | ||
| 40 | - raise TypeError("nodes must contain only ge.graph.Node objects") | ||
| 41 | - ok, reason = _native_can_fuse(node_list) | ||
| 42 | return FuseCheckResult(ok=ok, reason=reason) | 40 | return FuseCheckResult(ok=ok, reason=reason) |
| @@ -11,6 +11,7 @@ | |||
| 11 | 11 | ||
| 12 | 12 | ||
| 13 | 13 | ||
| 14 | + | ||
| 14 | 15 | ||
| 15 | 16 | ||
| 16 | 17 | ||
| @@ -18,12 +19,12 @@ | |||
| 18 | namespace ge { | 19 | namespace ge { |
| 19 | namespace python_pass_native { | 20 | namespace python_pass_native { |
| 20 | namespace { | 21 | namespace { |
| 21 | -py::tuple CanFuse(const py::iterable &node_objects) { | 22 | +std::vector<GNode> ParseNodes(const py::iterable &node_objects, const char *const argument_name) { |
| 22 | const py::object node_type = py::module_::import("ge.graph").attr("Node"); | 23 | const py::object node_type = py::module_::import("ge.graph").attr("Node"); |
| 23 | std::vector<GNode> nodes; | 24 | std::vector<GNode> nodes; |
| 24 | for (const py::handle node_object : node_objects) { | 25 | for (const py::handle node_object : node_objects) { |
| 25 | if (!py::isinstance(node_object, node_type)) { | 26 | if (!py::isinstance(node_object, node_type)) { |
| 26 | - throw py::type_error("nodes must contain only ge.graph.Node objects"); | 27 | + throw py::type_error(std::string(argument_name) + " must contain only ge.graph.Node objects"); |
| 27 | } | 28 | } |
| 28 | const auto *node = BorrowNodeFromPython(node_object); | 29 | const auto *node = BorrowNodeFromPython(node_object); |
| 29 | if (node == nullptr) { | 30 | if (node == nullptr) { |
| @@ -31,16 +32,38 @@ py::tuple CanFuse(const py::iterable &node_objects) { | |||
| 31 | } | 32 | } |
| 32 | nodes.emplace_back(*node); | 33 | nodes.emplace_back(*node); |
| 33 | } | 34 | } |
| 35 | + return nodes; | ||
| 36 | +} | ||
| 34 | 37 | ||
| 38 | +py::tuple CanFuse(const py::iterable &node_objects) { | ||
| 39 | + const auto nodes = ParseNodes(node_objects, "nodes"); | ||
| 35 | AscendString failed_reason; | 40 | AscendString failed_reason; |
| 36 | const bool ok = fusion::GraphFuseInspectorUtils::CanFuse(nodes, failed_reason); | 41 | const bool ok = fusion::GraphFuseInspectorUtils::CanFuse(nodes, failed_reason); |
| 37 | const char *const reason = failed_reason.GetString(); | 42 | const char *const reason = failed_reason.GetString(); |
| 38 | return py::make_tuple(ok, reason == nullptr ? "" : reason); | 43 | return py::make_tuple(ok, reason == nullptr ? "" : reason); |
| 39 | } | 44 | } |
| 45 | + | ||
| 46 | +void ReportFuse(const py::iterable &nodes_before_objects, const py::iterable &nodes_after_objects, | ||
| 47 | + CustomPassContext &context) { | ||
| 48 | + const auto nodes_before = ParseNodes(nodes_before_objects, "nodes_before"); | ||
| 49 | + const auto nodes_after = ParseNodes(nodes_after_objects, "nodes_after"); | ||
| 50 | + const auto status = fusion::GraphFuseInspectorUtils::ReportFuse(nodes_before, nodes_after, context); | ||
| 51 | + if (status != SUCCESS) { | ||
| 52 | + const auto pass_name = context.GetPassName(); | ||
| 53 | + const char *const pass_name_str = pass_name.GetString(); | ||
| 54 | + const std::string message = | ||
| 55 | + "Failed to report fusion result, pass_name=" + std::string(pass_name_str == nullptr ? "" : pass_name_str) + | ||
| 56 | + ", status=" + std::to_string(static_cast<uint32_t>(status)); | ||
| 57 | + context.SetErrorMessage(AscendString(message.c_str())); | ||
| 58 | + throw std::runtime_error(message); | ||
| 59 | + } | ||
| 60 | +} | ||
| 40 | } // namespace | 61 | } // namespace |
| 41 | 62 | ||
| 42 | void BindGraphFuseInspector(py::module_ &m) { | 63 | void BindGraphFuseInspector(py::module_ &m) { |
| 43 | m.def("can_fuse", &CanFuse, py::arg("nodes"), "Check whether nodes can be safely fused into one node"); | 64 | m.def("can_fuse", &CanFuse, py::arg("nodes"), "Check whether nodes can be safely fused into one node"); |
| 65 | + m.def("report_fuse", &ReportFuse, py::arg("nodes_before"), py::arg("nodes_after"), py::arg("context"), | ||
| 66 | + "Report the result of a graph fusion rewrite"); | ||
| 44 | } | 67 | } |
| 45 | 68 | ||
| 46 | } // namespace python_pass_native | 69 | } // namespace python_pass_native |
| @@ -11,6 +11,8 @@ | |||
| 11 | 11 | ||
| 12 | 12 | ||
| 13 | 13 | ||
| 14 | + | ||
| 15 | + | ||
| 14 | 16 | ||
| 15 | 17 | ||
| 16 | 18 | ||
| @@ -30,6 +32,41 @@ NodeIo BuildNodeIo(const py::handle &node_obj, int64_t index) { | |||
| 30 | } | 32 | } |
| 31 | return NodeIo{*node_ptr, index}; | 33 | return NodeIo{*node_ptr, index}; |
| 32 | } | 34 | } |
| 35 | + | ||
| 36 | +void ReplaceWithContext(const SubgraphBoundary &boundary, const py::handle &replacement_graph, | ||
| 37 | + CustomPassContext &context) { | ||
| 38 | + auto *graph_ptr = BorrowGraphFromPython(replacement_graph); | ||
| 39 | + if (graph_ptr == nullptr) { | ||
| 40 | + throw std::runtime_error("Graph handle is empty"); | ||
| 41 | + } | ||
| 42 | + const auto status = SubgraphRewriter::Replace(boundary, *graph_ptr, context); | ||
| 43 | + if (status != SUCCESS) { | ||
| 44 | + const auto pass_name = context.GetPassName(); | ||
| 45 | + const char *const pass_name_str = pass_name.GetString(); | ||
| 46 | + const std::string message = "SubgraphRewriter::Replace with context failed, pass_name=" + | ||
| 47 | + std::string(pass_name_str == nullptr ? "" : pass_name_str) + | ||
| 48 | + ", status=" + std::to_string(static_cast<uint32_t>(status)); | ||
| 49 | + context.SetErrorMessage(AscendString(message.c_str())); | ||
| 50 | + throw std::runtime_error(message); | ||
| 51 | + } | ||
| 52 | +} | ||
| 53 | + | ||
| 54 | +void BindSubgraphRewriter(py::module_ &m) { | ||
| 55 | + py::class_<SubgraphRewriter>(m, "SubgraphRewriter", "Subgraph rewriter") | ||
| 56 | + .def_static( | ||
| 57 | + "replace", | ||
| 58 | + [](const SubgraphBoundary &boundary, const py::handle &replacement_graph) -> uint32_t { | ||
| 59 | + auto *graph_ptr = BorrowGraphFromPython(replacement_graph); | ||
| 60 | + if (graph_ptr == nullptr) { | ||
| 61 | + throw std::runtime_error("Graph handle is empty"); | ||
| 62 | + } | ||
| 63 | + // SubgraphRewriter::Replace(const Graph&) will copy the replacement graph internally. | ||
| 64 | + return static_cast<uint32_t>(SubgraphRewriter::Replace(boundary, *graph_ptr)); | ||
| 65 | + }, | ||
| 66 | + py::arg("boundary"), py::arg("replacement"), "Execute subgraph replacement") | ||
| 67 | + .def_static("replace", &ReplaceWithContext, py::arg("boundary"), py::arg("replacement"), py::kw_only(), | ||
| 68 | + py::arg("context"), "Execute subgraph replacement with fusion inspection and reporting"); | ||
| 69 | +} | ||
| 33 | } // namespace | 70 | } // namespace |
| 34 | 71 | ||
| 35 | constexpr size_t kNodeInputTupleSize = 2UL; | 72 | constexpr size_t kNodeInputTupleSize = 2UL; |
| @@ -83,18 +120,7 @@ void BindGraphRewriter(py::module_ &m) { | |||
| 83 | }, | 120 | }, |
| 84 | py::arg("index"), py::arg("output"), "Bind the index-th boundary output to SubgraphOutput"); | 121 | py::arg("index"), py::arg("output"), "Bind the index-th boundary output to SubgraphOutput"); |
| 85 | 122 | ||
| 86 | - py::class_<SubgraphRewriter>(m, "SubgraphRewriter", "Subgraph rewriter") | 123 | + BindSubgraphRewriter(m); |
| 87 | - .def_static( | ||
| 88 | - "replace", | ||
| 89 | - [](const SubgraphBoundary &boundary, const py::handle &replacement_graph) -> uint32_t { | ||
| 90 | - auto *graph_ptr = BorrowGraphFromPython(replacement_graph); | ||
| 91 | - if (graph_ptr == nullptr) { | ||
| 92 | - throw std::runtime_error("Graph handle is empty"); | ||
| 93 | - } | ||
| 94 | - // SubgraphRewriter::Replace(const Graph&) will copy the replacement graph internally. | ||
| 95 | - return static_cast<uint32_t>(SubgraphRewriter::Replace(boundary, *graph_ptr)); | ||
| 96 | - }, | ||
| 97 | - py::arg("boundary"), py::arg("replacement"), "Execute subgraph replacement"); | ||
| 98 | } | 124 | } |
| 99 | 125 | ||
| 100 | } // namespace python_pass_native | 126 | } // namespace python_pass_native |
| @@ -542,6 +542,7 @@ The run package can carry multiple `ge_py_pass_bridge` native sub-wheels, but th | |||
| 542 | - `SubgraphRewriter.replace(boundary, replacement)` - Execute subgraph replacement | 542 | - `SubgraphRewriter.replace(boundary, replacement)` - Execute subgraph replacement |
| 543 | - `boundary`: `SubgraphBoundary` | 543 | - `boundary`: `SubgraphBoundary` |
| 544 | - `replacement`: `ge.graph.Graph` (the replacement graph is copied and reconnected on the C++ side) | 544 | - `replacement`: `ge.graph.Graph` (the replacement graph is copied and reconnected on the C++ side) |
| 545 | +- `SubgraphRewriter.replace(boundary, replacement, context=context)` - Automatically checks fusion feasibility, replaces the subgraph, and reports the result; returns `None` on success and raises `RuntimeError` on failure | ||
| 545 | 546 | ||
| 546 | ##### 5. Pattern / NodeIo / PatternMatcherConfig | 547 | ##### 5. Pattern / NodeIo / PatternMatcherConfig |
| 547 | 548 | ||
| @@ -569,6 +570,7 @@ The run package can carry multiple `ge_py_pass_bridge` native sub-wheels, but th | |||
| 569 | 570 | ||
| 570 | **Main Interfaces**: | 571 | **Main Interfaces**: |
| 571 | - `can_fuse(nodes: Iterable[Node]) -> FuseCheckResult` - Checks stream-label and cycle constraints for fusing a node set into one node | 572 | - `can_fuse(nodes: Iterable[Node]) -> FuseCheckResult` - Checks stream-label and cycle constraints for fusing a node set into one node |
| 573 | +- `report_fuse(nodes_before, nodes_after, context) -> None` - Reports a custom rewrite after graph modification and before old nodes are deleted | ||
| 572 | - `FuseCheckResult.ok` - Whether fusion is supported | 574 | - `FuseCheckResult.ok` - Whether fusion is supported |
| 573 | - `FuseCheckResult.reason` - Why fusion is not supported; empty on success | 575 | - `FuseCheckResult.reason` - Why fusion is not supported; empty on success |
| 574 | 576 | ||
| @@ -577,6 +579,8 @@ The native binding converts the Python `Node` iterable to `std::vector<GNode>`, | |||
| 577 | immutable dataclass. | 579 | immutable dataclass. |
| 578 | Business-level rejection returns `FuseCheckResult(False, reason)`; invalid input types and stale Node handles raise | 580 | Business-level rejection returns `FuseCheckResult(False, reason)`; invalid input types and stale Node handles raise |
| 579 | Python exceptions. | 581 | Python exceptions. |
| 582 | +`report_fuse` sets the context error message and raises `RuntimeError` on failure. An empty `nodes_after` represents | ||
| 583 | +a deletion-only rewrite. | ||
| 580 | 584 | ||
| 581 | ##### 6. FusionBasePass Class | 585 | ##### 6. FusionBasePass Class |
| 582 | 586 | ||
| @@ -254,6 +254,9 @@ Add new `ge.passes` package, providing following public interfaces: | |||
| 254 | - `capture_tensor` | 254 | - `capture_tensor` |
| 255 | - `create_pattern` | 255 | - `create_pattern` |
| 256 | - `create_replacement` | 256 | - `create_replacement` |
| 257 | +- `FuseCheckResult` | ||
| 258 | +- `can_fuse` | ||
| 259 | +- `report_fuse` | ||
| 257 | - `load_pass_plugins` | 260 | - `load_pass_plugins` |
| 258 | - `get_registered_passes` | 261 | - `get_registered_passes` |
| 259 | 262 | ||
| @@ -1088,6 +1088,7 @@ | |||
| 1088 | - [pattern](python/ge/passes/pattern.md) | 1088 | - [pattern](python/ge/passes/pattern.md) |
| 1089 | - [register\_decompose\_pass](python/ge/passes/register_decompose_pass.md) | 1089 | - [register\_decompose\_pass](python/ge/passes/register_decompose_pass.md) |
| 1090 | - [register\_fusion\_pass](python/ge/passes/register_fusion_pass.md) | 1090 | - [register\_fusion\_pass](python/ge/passes/register_fusion_pass.md) |
| 1091 | + - [report\_fuse](python/ge/passes/report_fuse.md) | ||
| 1091 | 1092 | ||
| 1092 | - [AttrValueType](python/ge/AttrValueType.md) | 1093 | - [AttrValueType](python/ge/AttrValueType.md) |
| 1093 | - [DataType](python/ge/DataType.md) | 1094 | - [DataType](python/ge/DataType.md) |
| @@ -13,6 +13,9 @@ | |||
| 13 | ```python | 13 | ```python |
| 14 | @staticmethod | 14 | @staticmethod |
| 15 | replace(boundary: SubgraphBoundary, replacement: Graph) -> int | 15 | replace(boundary: SubgraphBoundary, replacement: Graph) -> int |
| 16 | + | ||
| 17 | +@staticmethod | ||
| 18 | +replace(boundary: SubgraphBoundary, replacement: Graph, *, context: PassContext) -> None | ||
| 16 | ``` | 19 | ``` |
| 17 | 20 | ||
| 18 | ## 参数说明 | 21 | ## 参数说明 |
| @@ -21,25 +24,56 @@ replace(boundary: SubgraphBoundary, replacement: Graph) -> int | |||
| 21 | | --- | --- | --- | | 24 | | --- | --- | --- | |
| 22 | | boundary | 输入 | 子图边界,类型为SubgraphBoundary,描述待替换子图的输入/输出。 | | 25 | | boundary | 输入 | 子图边界,类型为SubgraphBoundary,描述待替换子图的输入/输出。 | |
| 23 | | replacement | 输入 | 替换图,类型为ge.graph.Graph。 | | 26 | | replacement | 输入 | 替换图,类型为ge.graph.Graph。 | |
| 27 | +| context | 输入 | 可选。当前Pass的PassContext,由GE注入到`FusionBasePass.run(graph, context)`中。传入后,底层自动执行融合可行性检查、子图替换和融合结果上报。 | | ||
| 24 | 28 | ||
| 25 | ## 返回值说明 | 29 | ## 返回值说明 |
| 26 | 30 | ||
| 27 | | 类型 | 说明 | | 31 | | 类型 | 说明 | |
| 28 | | --- | --- | | 32 | | --- | --- | |
| 29 | -| int | 返回C++ Status的整数形式。失败时可查看日志中关于边界完整性与索引对齐的信息。 | | 33 | +| int | 未传入`context`时,返回C++ Status的整数形式。失败时可查看日志中关于边界完整性与索引对齐的信息。 | |
| 34 | +| None | 传入`context`时,成功返回`None`。replacement graph handle失效,或融合检查、子图替换、融合结果上报失败时抛出`RuntimeError`,失败信息会同步到`context`。 | | ||
| 30 | 35 | ||
| 31 | ## 约束说明 | 36 | ## 约束说明 |
| 32 | 37 | ||
| 33 | -replacement必须是非空的图包装器;该图会在C++侧被**拷贝**,但仍需遵循GE对Python Graph所有权的规则。 | 38 | +- replacement必须是非空的图包装器;该图会在C++侧被**拷贝**,但仍需遵循GE对Python Graph所有权的规则。 |
| 39 | +- 推荐在`FusionBasePass.run(graph, context)`中传入`context`,由底层完成融合检查和结果上报。 | ||
| 40 | +- 不传入`context`时保持原有行为,不自动执行融合结果上报。 | ||
| 34 | 41 | ||
| 35 | ## 调用示例 | 42 | ## 调用示例 |
| 36 | 43 | ||
| 44 | +传入`context`和不传入`context`时的逻辑差异如下: | ||
| 45 | + | ||
| 46 | +| 调用方式 | 融合可行性检查和结果上报 | 成功返回值 | 失败处理 | | ||
| 47 | +| --- | --- | --- | --- | | ||
| 48 | +| 不传入`context` | 保持原有行为,不自动执行本接口新增的融合可行性检查和融合结果上报 | C++的`Status`整数形式 | 用户检查返回值 | | ||
| 49 | +| 传入`context` | 自动执行融合可行性检查和融合结果上报 | `None` | 抛出`RuntimeError`,并尽量将失败信息同步到`context` | | ||
| 50 | + | ||
| 37 | ```python | 51 | ```python |
| 38 | from ge.passes import SubgraphBoundary, SubgraphInput, SubgraphOutput, SubgraphRewriter | 52 | from ge.passes import SubgraphBoundary, SubgraphInput, SubgraphOutput, SubgraphRewriter |
| 39 | 53 | ||
| 40 | -# n0、out_node为主图中已存在的节点,replacement_graph为已构建好的替换图 | 54 | +# 以下代码位于FusionBasePass.run(graph, context)中。 |
| 55 | +# n0、out_node为主图中已存在的节点,replacement_graph为已构建好的替换图。 | ||
| 41 | b = SubgraphBoundary() | 56 | b = SubgraphBoundary() |
| 42 | b.add_input(0, SubgraphInput([(n0, 0), (n0, 1)])) | 57 | b.add_input(0, SubgraphInput([(n0, 0), (n0, 1)])) |
| 43 | b.add_output(0, SubgraphOutput(out_node, 0)) | 58 | b.add_output(0, SubgraphOutput(out_node, 0)) |
| 44 | -ret = SubgraphRewriter.replace(b, replacement_graph) | 59 | +``` |
| 60 | + | ||
| 61 | +完成`boundary`构造后,从以下两种调用方式中选择一种: | ||
| 62 | + | ||
| 63 | +### 不传入context | ||
| 64 | + | ||
| 65 | +保持原有行为,由用户检查返回值。 | ||
| 66 | + | ||
| 67 | +```python | ||
| 68 | +ret = SubgraphRewriter.replace(b, replacement_graph) | ||
| 69 | +if ret != 0: | ||
| 70 | + raise RuntimeError(f"SubgraphRewriter.replace failed, ret={ret}") | ||
| 71 | +``` | ||
| 72 | + | ||
| 73 | +### 传入context | ||
| 74 | + | ||
| 75 | +自动执行融合可行性检查和融合结果上报。成功时返回`None`,失败时抛出`RuntimeError`。 | ||
| 76 | + | ||
| 77 | +```python | ||
| 78 | +SubgraphRewriter.replace(b, replacement_graph, context=context) | ||
| 45 | ``` | 79 | ``` |
| @@ -0,0 +1,77 @@ | |||
| 1 | +# report\_fuse | ||
| 2 | + | ||
| 3 | +## 产品支持情况 | ||
| 4 | + | ||
| 5 | +全量芯片支持。 | ||
| 6 | + | ||
| 7 | +## 功能说明 | ||
| 8 | + | ||
| 9 | +GE融合Pass支持以下两种改图方式: | ||
| 10 | + | ||
| 11 | +- 传统直接改图方式:用户在[FusionBasePass.run(graph, context)](FusionBasePass/run.md)中直接调用Graph接口修改原图,例如增删边、删除节点或重连节点。由于改图过程由用户控制,框架无法自动确定融合前后的节点集合。用户需要在改图前调用`can_fuse`执行融合可行性检查,完成自定义改图后、删除旧节点前调用本函数上报融合结果。 | ||
| 12 | +- 基于子图替换的方式:这是后续提供的高效改图方式。用户通过boundary描述待替换子图的边界,通过replacement描述替换图,再调用[SubgraphRewriter.replace](SubgraphRewriter/replace.md)完成改图。传入`context`时,该接口会自动完成融合可行性检查、子图替换和融合结果上报,无需单独调用`can_fuse`和本函数。 | ||
| 13 | + | ||
| 14 | +本函数用于传统直接改图方式。函数根据用户传入的融合前、融合后节点集合更新融合维测信息,并记录当前Pass名称。 | ||
| 15 | + | ||
| 16 | +## 函数原型 | ||
| 17 | + | ||
| 18 | +```python | ||
| 19 | +report_fuse( | ||
| 20 | + nodes_before: Iterable[Node], | ||
| 21 | + nodes_after: Iterable[Node], | ||
| 22 | + context: PassContext, | ||
| 23 | +) -> None | ||
| 24 | +``` | ||
| 25 | + | ||
| 26 | +## 参数说明 | ||
| 27 | + | ||
| 28 | +| 参数名 | 输入/输出 | 说明 | | ||
| 29 | +| --- | --- | --- | | ||
| 30 | +| nodes_before | 输入 | 融合前的连通节点集合,元素必须为ge.graph.Node对象。 | | ||
| 31 | +| nodes_after | 输入 | 融合后的连通节点集合,元素必须为ge.graph.Node对象;空集合表示只删除旧节点、不新增节点。 | | ||
| 32 | +| context | 输入 | 当前Pass的PassContext,由GE注入到`FusionBasePass.run(graph, context)`中。 | | ||
| 33 | + | ||
| 34 | +## 返回值说明 | ||
| 35 | + | ||
| 36 | +成功时返回`None`。节点集合中包含非ge.graph.Node对象,或`context`不是PassContext时抛出`TypeError`;Node handle失效或底层融合结果上报失败时抛出`RuntimeError`,失败信息会同步到`context`。 | ||
| 37 | + | ||
| 38 | +## 约束说明 | ||
| 39 | + | ||
| 40 | +- 必须先完成自定义改图,再调用本函数。 | ||
| 41 | +- 必须在旧节点被删除或失效前调用本函数,确保`nodes_before`仍然有效。 | ||
| 42 | +- `nodes_before`中的节点必须属于同一张图;非空的`nodes_after`也必须属于该图。 | ||
| 43 | +- `PassContext`仅在当前Pass同步调用期间有效,不应保存到实例属性或其他线程中使用。 | ||
| 44 | + | ||
| 45 | +## 调用示例 | ||
| 46 | + | ||
| 47 | +以下示例采用传统直接改图方式,删除一个仅有一个数据输入和一个数据输出的中间节点,并将其上游节点直接连接到所有下游节点。 | ||
| 48 | + | ||
| 49 | +```python | ||
| 50 | +from ge.passes import can_fuse, report_fuse | ||
| 51 | + | ||
| 52 | +def remove_intermediate_node(graph, node, context): | ||
| 53 | + nodes_before = [node] | ||
| 54 | + result = can_fuse(nodes_before) | ||
| 55 | + if not result.ok: | ||
| 56 | + context.set_error_message(result.reason) | ||
| 57 | + return False | ||
| 58 | + | ||
| 59 | + input_node, input_output_index = node.get_in_data_nodes_and_port_indexes(0) | ||
| 60 | + consumers = list(node.get_out_data_nodes_and_port_indexes(0)) | ||
| 61 | + | ||
| 62 | + graph.remove_edge(input_node, input_output_index, node, 0) | ||
| 63 | + for consumer, consumer_input_index in consumers: | ||
| 64 | + graph.remove_edge(node, 0, consumer, consumer_input_index) | ||
| 65 | + graph.add_data_edge( | ||
| 66 | + input_node, | ||
| 67 | + input_output_index, | ||
| 68 | + consumer, | ||
| 69 | + consumer_input_index, | ||
| 70 | + ) | ||
| 71 | + | ||
| 72 | + # 本次改图未新增节点,因此nodes_after传入空列表。 | ||
| 73 | + # 上报完成前,nodes_before中的节点必须保持有效。 | ||
| 74 | + report_fuse(nodes_before, [], context) | ||
| 75 | + graph.remove_node(node) | ||
| 76 | + return True | ||
| 77 | +``` | ||
| @@ -541,6 +541,7 @@ run 包可携带多个 `ge_py_pass_bridge` native 子 wheel,但安装脚本只 | |||
| 541 | - `SubgraphRewriter.replace(boundary, replacement)` - 执行子图替换 | 541 | - `SubgraphRewriter.replace(boundary, replacement)` - 执行子图替换 |
| 542 | - `boundary`:`SubgraphBoundary` | 542 | - `boundary`:`SubgraphBoundary` |
| 543 | - `replacement`:`ge.graph.Graph`(replacement 图会在 C++ 侧拷贝并完成重连) | 543 | - `replacement`:`ge.graph.Graph`(replacement 图会在 C++ 侧拷贝并完成重连) |
| 544 | +- `SubgraphRewriter.replace(boundary, replacement, context=context)` - 自动执行可融合检查、子图替换和融合结果上报;成功返回 `None`,失败抛出 `RuntimeError` | ||
| 544 | 545 | ||
| 545 | ##### 5. Pattern / NodeIo / PatternMatcherConfig | 546 | ##### 5. Pattern / NodeIo / PatternMatcherConfig |
| 546 | 547 | ||
| @@ -564,10 +565,11 @@ run 包可携带多个 `ge_py_pass_bridge` native 子 wheel,但安装脚本只 | |||
| 564 | 565 | ||
| 565 | **文件位置**: `graph_fuse_inspector_binding.cc`、`fuse_inspector.py` | 566 | **文件位置**: `graph_fuse_inspector_binding.cc`、`fuse_inspector.py` |
| 566 | 567 | ||
| 567 | -**功能**: 为 graph base 类 pass 提供改图前的融合可行性检查。 | 568 | +**功能**: 为 graph base 类 pass 提供改图前的融合可行性检查和改图后的融合结果上报。 |
| 568 | 569 | ||
| 569 | **主要接口**: | 570 | **主要接口**: |
| 570 | - `can_fuse(nodes: Iterable[Node]) -> FuseCheckResult` - 检查节点集合融合成单节点后是否满足 stream label 和无环约束 | 571 | - `can_fuse(nodes: Iterable[Node]) -> FuseCheckResult` - 检查节点集合融合成单节点后是否满足 stream label 和无环约束 |
| 572 | +- `report_fuse(nodes_before, nodes_after, context) -> None` - 在自定义改图完成后、旧节点删除前上报融合结果 | ||
| 571 | - `FuseCheckResult.ok` - 是否可融合 | 573 | - `FuseCheckResult.ok` - 是否可融合 |
| 572 | - `FuseCheckResult.reason` - 不可融合原因;可融合时为空字符串 | 574 | - `FuseCheckResult.reason` - 不可融合原因;可融合时为空字符串 |
| 573 | 575 | ||
| @@ -575,6 +577,8 @@ native binding 将 Python `Node` iterable 转换为 `std::vector<GNode>`,调 | |||
| 575 | `GraphFuseInspectorUtils::CanFuse`,再由 `fuse_inspector.py` 将 native 返回的 `(bool, str)` 包装为不可变 | 577 | `GraphFuseInspectorUtils::CanFuse`,再由 `fuse_inspector.py` 将 native 返回的 `(bool, str)` 包装为不可变 |
| 576 | dataclass。 | 578 | dataclass。 |
| 577 | 业务不可融合返回 `FuseCheckResult(False, reason)`;输入类型错误或 Node handle 失效时抛出 Python 异常。 | 579 | 业务不可融合返回 `FuseCheckResult(False, reason)`;输入类型错误或 Node handle 失效时抛出 Python 异常。 |
| 580 | +`report_fuse` 无需适配返回值,由 `fuse_inspector.py` 直接重导出 native 实现;失败时设置 context 错误信息并 | ||
| 581 | +抛出 `RuntimeError`,其中空 `nodes_after` 表示只删除旧节点。 | ||
| 578 | 582 | ||
| 579 | ##### 6. FusionBasePass 类 | 583 | ##### 6. FusionBasePass 类 |
| 580 | 584 | ||
| @@ -358,6 +358,9 @@ PassPluginLoader / ge_compiler.so | |||
| 358 | - `PatternMatcherConfigBuilder` | 358 | - `PatternMatcherConfigBuilder` |
| 359 | - `create_pattern` | 359 | - `create_pattern` |
| 360 | - `create_replacement` | 360 | - `create_replacement` |
| 361 | +- `FuseCheckResult` | ||
| 362 | +- `can_fuse` | ||
| 363 | +- `report_fuse` | ||
| 361 | - `load_pass_plugins` | 364 | - `load_pass_plugins` |
| 362 | - `get_registered_passes` | 365 | - `get_registered_passes` |
| 363 | 366 | ||
| @@ -4,7 +4,7 @@ | |||
| 4 | 4 | ||
| 5 | - 扫描 `ConcatV2 -> Relu` 结构 | 5 | - 扫描 `ConcatV2 -> Relu` 结构 |
| 6 | - 构造 replacement 子图(将 Relu 前移到 Concat 的每个输入上) | 6 | - 构造 replacement 子图(将 Relu 前移到 Concat 的每个输入上) |
| 7 | -- 使用 `SubgraphBoundary` + `SubgraphRewriter.replace()` 做子图替换 | 7 | +- 使用 `SubgraphBoundary` + `SubgraphRewriter.replace(..., context=context)` 做子图替换,并自动完成可融合检查和融合结果上报 |
| 8 | 8 | ||
| 9 | > **FusionBasePass与PatternFusionPass** | 9 | > **FusionBasePass与PatternFusionPass** |
| 10 | > | 10 | > |
| @@ -4,7 +4,7 @@ This directory provides a **pure Python** version example of `graph_base_pass/2_ | |||
| 4 | 4 | ||
| 5 | - Scan `ConcatV2 -> Relu` structure | 5 | - Scan `ConcatV2 -> Relu` structure |
| 6 | - Build replacement subgraph (move Relu to each input of Concat) | 6 | - Build replacement subgraph (move Relu to each input of Concat) |
| 7 | -- Use `SubgraphBoundary` + `SubgraphRewriter.replace()` for subgraph replacement | 7 | +- Use `SubgraphBoundary` + `SubgraphRewriter.replace(..., context=context)` for replacement with automatic fusion inspection and reporting |
| 8 | 8 | ||
| 9 | > **FusionBasePass vs PatternFusionPass** | 9 | > **FusionBasePass vs PatternFusionPass** |
| 10 | > | 10 | > |
| @@ -18,6 +18,7 @@ from ge.es import GraphBuilder, TensorHolder | |||
| 18 | from ge.graph import Graph, Node | 18 | from ge.graph import Graph, Node |
| 19 | from ge.passes import ( | 19 | from ge.passes import ( |
| 20 | FusionBasePass, | 20 | FusionBasePass, |
| 21 | + PassContext, | ||
| 21 | PassStage, | 22 | PassStage, |
| 22 | SubgraphBoundary, | 23 | SubgraphBoundary, |
| 23 | SubgraphInput, | 24 | SubgraphInput, |
| @@ -107,9 +108,11 @@ def _build_boundary(concat_node: Node) -> SubgraphBoundary: | |||
| 107 | return boundary | 108 | return boundary |
| 108 | 109 | ||
| 109 | 110 | ||
| 110 | -@register_fusion_pass(name="PythonMoveReluBeforeConcatPass", stage=PassStage.BEFORE_INFER_SHAPE) | 111 | +@register_fusion_pass( |
| 112 | + name="PythonMoveReluBeforeConcatPass", stage=PassStage.BEFORE_INFER_SHAPE | ||
| 113 | +) | ||
| 111 | class PythonMoveReluBeforeConcatPass(FusionBasePass): | 114 | class PythonMoveReluBeforeConcatPass(FusionBasePass): |
| 112 | - def run(self, graph: Graph, context) -> bool: | 115 | + def run(self, graph: Graph, context: PassContext) -> bool: |
| 113 | print("PythonMoveReluBeforeConcatPass") | 116 | print("PythonMoveReluBeforeConcatPass") |
| 114 | 117 | ||
| 115 | concat_nodes = _find_concat_nodes(graph) | 118 | concat_nodes = _find_concat_nodes(graph) |
| @@ -119,9 +122,7 @@ class PythonMoveReluBeforeConcatPass(FusionBasePass): | |||
| 119 | for concat_node in concat_nodes: | 122 | for concat_node in concat_nodes: |
| 120 | replacement = _build_replacement_graph(concat_node) | 123 | replacement = _build_replacement_graph(concat_node) |
| 121 | boundary = _build_boundary(concat_node) | 124 | boundary = _build_boundary(concat_node) |
| 122 | - ret = SubgraphRewriter.replace(boundary, replacement) | 125 | + SubgraphRewriter.replace(boundary, replacement, context=context) |
| 123 | - if ret != 0: | ||
| 124 | - raise RuntimeError(f"SubgraphRewriter.replace failed, ret={ret}") | ||
| 125 | print("Replacement of PythonMoveReluBeforeConcatPass succeeded") | 126 | print("Replacement of PythonMoveReluBeforeConcatPass succeeded") |
| 126 | return True | 127 | return True |
| 127 | 128 | ||
| @@ -129,4 +130,6 @@ class PythonMoveReluBeforeConcatPass(FusionBasePass): | |||
| 129 | if __name__ == "__main__": | 130 | if __name__ == "__main__": |
| 130 | print("PythonMoveReluBeforeConcatPass 已注册。") | 131 | print("PythonMoveReluBeforeConcatPass 已注册。") |
| 131 | print("请通过 ASCEND_GE_PY_PASS_PATH 指向本文件,例如:") | 132 | print("请通过 ASCEND_GE_PY_PASS_PATH 指向本文件,例如:") |
| 132 | - print(" export ASCEND_GE_PY_PASS_PATH=$PWD/python/src/python_move_relu_before_concat_pass.py") | 133 | + print( |
| 134 | + " export ASCEND_GE_PY_PASS_PATH=$PWD/python/src/python_move_relu_before_concat_pass.py" | ||
| 135 | + ) | ||
| @@ -4,9 +4,11 @@ | |||
| 4 | 4 | ||
| 5 | - 遍历图中 `Conv2D` / `Conv2DV2`,筛选 `data_format == NCHW` 的节点; | 5 | - 遍历图中 `Conv2D` / `Conv2DV2`,筛选 `data_format == NCHW` 的节点; |
| 6 | - 将 `data_format` 改为 `NHWC`; | 6 | - 将 `data_format` 改为 `NHWC`; |
| 7 | -- 从卷积输出做 BFS,按顺序匹配 `perm == [0,2,3,1]` 与 `[0,3,1,2]` 的 `Transpose`,删除对应 `Transpose` 与 perm 常量产点并重连数据边。 | 7 | +- 从卷积输出做 BFS,按顺序匹配 `perm == [0,2,3,1]` 与 `[0,3,1,2]` 的 `Transpose`; |
| 8 | +- 改图前调用`can_fuse`,完成边重连后、删除旧节点前调用`report_fuse`; | ||
| 9 | +- 删除对应`Transpose`与perm常量节点。 | ||
| 8 | 10 | ||
| 9 | -本样例继承 `FusionBasePass` 并重写 `run()`,通过 `Graph.remove_edge` / `add_data_edge` / `remove_node` 与 `Node.set_attr` 完成改写,**不使用** `SubgraphRewriter`。 | 11 | +本样例继承 `FusionBasePass` 并重写 `run()`,通过 `Graph.remove_edge` / `add_data_edge` / `remove_node` 与 `Node.set_attr` 完成改写,**不使用** `SubgraphRewriter`,因此显式调用`can_fuse`和`report_fuse`。 |
| 10 | 12 | ||
| 11 | ## 与 C++ 版本的差异 | 13 | ## 与 C++ 版本的差异 |
| 12 | 14 | ||
| @@ -4,9 +4,11 @@ This directory provides a **pure Python** version example of `graph_base_pass/3_ | |||
| 4 | 4 | ||
| 5 | - Traverse `Conv2D` / `Conv2DV2` in graph, filter nodes with `data_format == NCHW`; | 5 | - Traverse `Conv2D` / `Conv2DV2` in graph, filter nodes with `data_format == NCHW`; |
| 6 | - Change `data_format` to `NHWC`; | 6 | - Change `data_format` to `NHWC`; |
| 7 | -- BFS from Conv output, match `Transpose` with `perm == [0,2,3,1]` and `[0,3,1,2]` in order, delete corresponding `Transpose` and perm constant output nodes and reconnect data edges. | 7 | +- BFS from the Conv output and match `Transpose` nodes with `perm == [0,2,3,1]` and `[0,3,1,2]` in order; |
| 8 | +- Call `can_fuse` before the rewrite, then call `report_fuse` after reconnecting edges and before deleting old nodes; | ||
| 9 | +- Delete the matching `Transpose` and perm constant nodes. | ||
| 8 | 10 | ||
| 9 | -This example inherits `FusionBasePass` and overrides `run()`, completes modification via `Graph.remove_edge` / `add_data_edge` / `remove_node` and `Node.set_attr`, **does not use** `SubgraphRewriter`. | 11 | +This example inherits `FusionBasePass` and overrides `run()`. It performs the rewrite through `Graph.remove_edge` / `add_data_edge` / `remove_node` and `Node.set_attr`. Because it **does not use** `SubgraphRewriter`, it calls `can_fuse` and `report_fuse` explicitly. |
| 10 | 12 | ||
| 11 | ## Differences from C++ Version | 13 | ## Differences from C++ Version |
| 12 | 14 | ||
| @@ -18,7 +18,14 @@ from collections import deque | |||
| 18 | 18 | ||
| 19 | from ge.graph import Graph, Node | 19 | from ge.graph import Graph, Node |
| 20 | from ge.graph.types import DataType | 20 | from ge.graph.types import DataType |
| 21 | -from ge.passes import FusionBasePass, PassStage, register_fusion_pass | 21 | +from ge.passes import ( |
| 22 | + FusionBasePass, | ||
| 23 | + PassContext, | ||
| 24 | + PassStage, | ||
| 25 | + can_fuse, | ||
| 26 | + register_fusion_pass, | ||
| 27 | + report_fuse, | ||
| 28 | +) | ||
| 22 | 29 | ||
| 23 | TARGET_TYPES = frozenset({"Conv2D", "Conv2DV2"}) | 30 | TARGET_TYPES = frozenset({"Conv2D", "Conv2DV2"}) |
| 24 | PERM0 = [0, 2, 3, 1] | 31 | PERM0 = [0, 2, 3, 1] |
| @@ -78,10 +85,24 @@ def _judge_transpose_perm(perm: list[int], cnt_holder: list[int]) -> bool: | |||
| 78 | return False | 85 | return False |
| 79 | 86 | ||
| 80 | 87 | ||
| 81 | -def _remove_transpose_and_relink(graph: Graph, transpose_node: Node) -> bool: | 88 | +def _remove_transpose_and_relink( |
| 82 | - data_node, data_out_idx = transpose_node.get_in_data_nodes_and_port_indexes(DATA_IDX) | 89 | + graph: Graph, transpose_node: Node, context: PassContext |
| 83 | - perm_node, perm_out_idx = transpose_node.get_in_data_nodes_and_port_indexes(PERM_IDX) | 90 | +) -> bool: |
| 84 | - consumers = list(transpose_node.get_out_data_nodes_and_port_indexes(TRANSPOSE_OUT_IDX)) | 91 | + result = can_fuse([transpose_node]) |
| 92 | + if not result.ok: | ||
| 93 | + context.set_error_message(result.reason) | ||
| 94 | + print(f"RemoveTransposeAndRelink can_fuse check failed: {result.reason}") | ||
| 95 | + return False | ||
| 96 | + | ||
| 97 | + data_node, data_out_idx = transpose_node.get_in_data_nodes_and_port_indexes( | ||
| 98 | + DATA_IDX | ||
| 99 | + ) | ||
| 100 | + perm_node, perm_out_idx = transpose_node.get_in_data_nodes_and_port_indexes( | ||
| 101 | + PERM_IDX | ||
| 102 | + ) | ||
| 103 | + consumers = list( | ||
| 104 | + transpose_node.get_out_data_nodes_and_port_indexes(TRANSPOSE_OUT_IDX) | ||
| 105 | + ) | ||
| 85 | try: | 106 | try: |
| 86 | graph.remove_edge(data_node, data_out_idx, transpose_node, DATA_IDX) | 107 | graph.remove_edge(data_node, data_out_idx, transpose_node, DATA_IDX) |
| 87 | graph.remove_edge(perm_node, perm_out_idx, transpose_node, PERM_IDX) | 108 | graph.remove_edge(perm_node, perm_out_idx, transpose_node, PERM_IDX) |
| @@ -97,6 +118,7 @@ def _remove_transpose_and_relink(graph: Graph, transpose_node: Node) -> bool: | |||
| 97 | except RuntimeError: | 118 | except RuntimeError: |
| 98 | return False | 119 | return False |
| 99 | print("Remove output edges success") | 120 | print("Remove output edges success") |
| 121 | + report_fuse([transpose_node], [], context) | ||
| 100 | try: | 122 | try: |
| 101 | graph.remove_node(perm_node) | 123 | graph.remove_node(perm_node) |
| 102 | graph.remove_node(transpose_node) | 124 | graph.remove_node(transpose_node) |
| @@ -105,7 +127,9 @@ def _remove_transpose_and_relink(graph: Graph, transpose_node: Node) -> bool: | |||
| 105 | return True | 127 | return True |
| 106 | 128 | ||
| 107 | 129 | ||
| 108 | -def _delete_transpose_pair_behind_if_exist(graph: Graph, conv_node: Node) -> bool: | 130 | +def _delete_transpose_pair_behind_if_exist( |
| 131 | + graph: Graph, conv_node: Node, context: PassContext | ||
| 132 | +) -> bool: | ||
| 109 | queue: deque[Node] = deque() | 133 | queue: deque[Node] = deque() |
| 110 | transpose_cnt_holder = [0] | 134 | transpose_cnt_holder = [0] |
| 111 | out_sz = conv_node.get_outputs_size() | 135 | out_sz = conv_node.get_outputs_size() |
| @@ -125,7 +149,7 @@ def _delete_transpose_pair_behind_if_exist(graph: Graph, conv_node: Node) -> boo | |||
| 125 | continue | 149 | continue |
| 126 | if not _judge_transpose_perm(perm_list, transpose_cnt_holder): | 150 | if not _judge_transpose_perm(perm_list, transpose_cnt_holder): |
| 127 | continue | 151 | continue |
| 128 | - if not _remove_transpose_and_relink(graph, node_ptr): | 152 | + if not _remove_transpose_and_relink(graph, node_ptr, context): |
| 129 | print("RemoveTransposeAndRelink failed") | 153 | print("RemoveTransposeAndRelink failed") |
| 130 | return False | 154 | return False |
| 131 | if transpose_cnt_holder[0] == 2: | 155 | if transpose_cnt_holder[0] == 2: |
| @@ -147,9 +171,11 @@ def _find_nchw_conv_nodes(graph: Graph) -> list[Node]: | |||
| 147 | return conv_nodes | 171 | return conv_nodes |
| 148 | 172 | ||
| 149 | 173 | ||
| 150 | -@register_fusion_pass(name="PythonConvTransFormatPass", stage=PassStage.BEFORE_INFER_SHAPE) | 174 | +@register_fusion_pass( |
| 175 | + name="PythonConvTransFormatPass", stage=PassStage.BEFORE_INFER_SHAPE | ||
| 176 | +) | ||
| 151 | class PythonConvTransFormatPass(FusionBasePass): | 177 | class PythonConvTransFormatPass(FusionBasePass): |
| 152 | - def run(self, graph: Graph, context) -> bool: | 178 | + def run(self, graph: Graph, context: PassContext) -> bool: |
| 153 | print("PythonConvTransFormatPass is starting") | 179 | print("PythonConvTransFormatPass is starting") |
| 154 | conv_nodes = _find_nchw_conv_nodes(graph) | 180 | conv_nodes = _find_nchw_conv_nodes(graph) |
| 155 | if not conv_nodes: | 181 | if not conv_nodes: |
| @@ -161,9 +187,13 @@ class PythonConvTransFormatPass(FusionBasePass): | |||
| 161 | except RuntimeError: | 187 | except RuntimeError: |
| 162 | print("Modify format of node failed") | 188 | print("Modify format of node failed") |
| 163 | raise | 189 | raise |
| 164 | - if not _delete_transpose_pair_behind_if_exist(graph, node): | 190 | + if not _delete_transpose_pair_behind_if_exist(graph, node, context): |
| 165 | print("DeleteTransposePairBehindIfExist failed") | 191 | print("DeleteTransposePairBehindIfExist failed") |
| 166 | - raise RuntimeError("DeleteTransposePairBehindIfExist failed") | 192 | + error_message = context.get_error_message() |
| 193 | + failure_message = "DeleteTransposePairBehindIfExist failed" | ||
| 194 | + if error_message: | ||
| 195 | + failure_message += f": {error_message}" | ||
| 196 | + raise RuntimeError(failure_message) | ||
| 167 | print("PythonConvTransFormatPass completed") | 197 | print("PythonConvTransFormatPass completed") |
| 168 | return True | 198 | return True |
| 169 | 199 | ||
| @@ -171,4 +201,6 @@ class PythonConvTransFormatPass(FusionBasePass): | |||
| 171 | if __name__ == "__main__": | 201 | if __name__ == "__main__": |
| 172 | print("PythonConvTransFormatPass 已注册。") | 202 | print("PythonConvTransFormatPass 已注册。") |
| 173 | print("请通过 ASCEND_GE_PY_PASS_PATH 指向本文件,例如:") | 203 | print("请通过 ASCEND_GE_PY_PASS_PATH 指向本文件,例如:") |
| 174 | - print(" export ASCEND_GE_PY_PASS_PATH=$PWD/python/src/python_modify_conv_data_format_pass.py") | 204 | + print( |
| 205 | + " export ASCEND_GE_PY_PASS_PATH=$PWD/python/src/python_modify_conv_data_format_pass.py" | ||
| 206 | + ) | ||
| @@ -186,6 +186,11 @@ const std::string &GetSharedPybindMarkerFilePath() { | |||
| 186 | return path; | 186 | return path; |
| 187 | } | 187 | } |
| 188 | 188 | ||
| 189 | +const std::string &GetSharedPybindReportFuseMarkerFilePath() { | ||
| 190 | + static const std::string path = GetSharedPybindPassDir().CreateFilePath("report_fuse_success_marker.txt"); | ||
| 191 | + return path; | ||
| 192 | +} | ||
| 193 | + | ||
| 189 | const std::string &GetSharedPybindPatternPassFilePath() { | 194 | const std::string &GetSharedPybindPatternPassFilePath() { |
| 190 | static const std::string path = GetSharedPybindPassDir().CreateFilePath("pybind_pattern_passes.py"); | 195 | static const std::string path = GetSharedPybindPassDir().CreateFilePath("pybind_pattern_passes.py"); |
| 191 | return path; | 196 | return path; |
| @@ -222,14 +227,24 @@ void EnsureSharedPybindPassFile() { | |||
| 222 | std::ostringstream pass_code; | 227 | std::ostringstream pass_code; |
| 223 | pass_code << "from pathlib import Path\n" | 228 | pass_code << "from pathlib import Path\n" |
| 224 | << "from ge.graph import Graph\n" | 229 | << "from ge.graph import Graph\n" |
| 225 | - << "from ge.passes import FusionBasePass, PassStage, register_fusion_pass, PassContext\n\n" | 230 | + << "from ge.passes import (\n" |
| 231 | + << " FusionBasePass, PassStage, register_fusion_pass, report_fuse\n" | ||
| 232 | + << ")\n\n" | ||
| 226 | << "MARKER_FILE = r'" << GetSharedPybindMarkerFilePath() << "'\n\n" | 233 | << "MARKER_FILE = r'" << GetSharedPybindMarkerFilePath() << "'\n\n" |
| 234 | + << "REPORT_FUSE_MARKER_FILE = r'" << GetSharedPybindReportFuseMarkerFilePath() << "'\n\n" | ||
| 227 | << "@register_fusion_pass(name='PythonPybindBridgePass', stage=PassStage.AFTER_INFER_SHAPE)\n" | 235 | << "@register_fusion_pass(name='PythonPybindBridgePass', stage=PassStage.AFTER_INFER_SHAPE)\n" |
| 228 | << "class PythonPybindBridgePass(FusionBasePass):\n" | 236 | << "class PythonPybindBridgePass(FusionBasePass):\n" |
| 229 | << " def run(self, graph, context):\n" | 237 | << " def run(self, graph, context):\n" |
| 230 | << " assert isinstance(graph, Graph)\n" | 238 | << " assert isinstance(graph, Graph)\n" |
| 231 | << " Path(MARKER_FILE).write_text(f\"{graph.name}|{context.get_pass_name()}\", encoding='utf-8')\n" | 239 | << " Path(MARKER_FILE).write_text(f\"{graph.name}|{context.get_pass_name()}\", encoding='utf-8')\n" |
| 232 | << " return 0\n\n" | 240 | << " return 0\n\n" |
| 241 | + << "@register_fusion_pass(name='PythonPybindReportFusePass', stage=PassStage.AFTER_INFER_SHAPE)\n" | ||
| 242 | + << "class PythonPybindReportFusePass(FusionBasePass):\n" | ||
| 243 | + << " def run(self, graph, context):\n" | ||
| 244 | + << " nodes = graph.get_direct_nodes()\n" | ||
| 245 | + << " report_fuse(nodes, [], context)\n" | ||
| 246 | + << " Path(REPORT_FUSE_MARKER_FILE).write_text(context.get_pass_name(), encoding='utf-8')\n" | ||
| 247 | + << " return 0\n\n" | ||
| 233 | << "@register_fusion_pass(name='PythonPybindBridgeFailedPass', " | 248 | << "@register_fusion_pass(name='PythonPybindBridgeFailedPass', " |
| 234 | "stage=PassStage.AFTER_BUILTIN_FUSION_PASS)\n" | 249 | "stage=PassStage.AFTER_BUILTIN_FUSION_PASS)\n" |
| 235 | << "class PythonPybindBridgeFailedPass(FusionBasePass):\n" | 250 | << "class PythonPybindBridgeFailedPass(FusionBasePass):\n" |
| @@ -1686,7 +1701,9 @@ TEST_F(UtestFusionPassExecutor, PythonPassBridgeCApi_LoadsConfiguredNativeModule | |||
| 1686 | TEST_F(UtestFusionPassExecutor, PythonFusionBasePass_PybindBridge_RunSuccess) { | 1701 | TEST_F(UtestFusionPassExecutor, PythonFusionBasePass_PybindBridge_RunSuccess) { |
| 1687 | EnsureSharedPybindPassFile(); | 1702 | EnsureSharedPybindPassFile(); |
| 1688 | const auto &marker_file = GetSharedPybindMarkerFilePath(); | 1703 | const auto &marker_file = GetSharedPybindMarkerFilePath(); |
| 1704 | + const auto &report_fuse_marker_file = GetSharedPybindReportFuseMarkerFilePath(); | ||
| 1689 | (void)remove(marker_file.c_str()); | 1705 | (void)remove(marker_file.c_str()); |
| 1706 | + (void)remove(report_fuse_marker_file.c_str()); | ||
| 1690 | 1707 | ||
| 1691 | ScopedEnvVar scoped_py_pass_path(kEnvPythonPassPath, GetSharedPybindPassFilePath()); | 1708 | ScopedEnvVar scoped_py_pass_path(kEnvPythonPassPath, GetSharedPybindPassFilePath()); |
| 1692 | ASSERT_EQ(RegisterPythonPassesFromPlugin(), SUCCESS); | 1709 | ASSERT_EQ(RegisterPythonPassesFromPlugin(), SUCCESS); |
| @@ -1698,6 +1715,7 @@ TEST_F(UtestFusionPassExecutor, PythonFusionBasePass_PybindBridge_RunSuccess) { | |||
| 1698 | EXPECT_EQ(pass_executor.RunPasses(target_compute_graph, CustomPassStage::kAfterInferShape), SUCCESS); | 1715 | EXPECT_EQ(pass_executor.RunPasses(target_compute_graph, CustomPassStage::kAfterInferShape), SUCCESS); |
| 1699 | EXPECT_EQ(pass_executor.RunPasses(target_compute_graph, CustomPassStage::kAfterInferShape), SUCCESS); | 1716 | EXPECT_EQ(pass_executor.RunPasses(target_compute_graph, CustomPassStage::kAfterInferShape), SUCCESS); |
| 1700 | EXPECT_EQ(ReadFile(marker_file), expected_graph_name + "|PythonPybindBridgePass"); | 1717 | EXPECT_EQ(ReadFile(marker_file), expected_graph_name + "|PythonPybindBridgePass"); |
| 1718 | + EXPECT_EQ(ReadFile(report_fuse_marker_file), "PythonPybindReportFusePass"); | ||
| 1701 | } | 1719 | } |
| 1702 | 1720 | ||
| 1703 | TEST_F(UtestFusionPassExecutor, PythonFusionBasePass_PybindBridge_RunFailedOnPythonException) { | 1721 | TEST_F(UtestFusionPassExecutor, PythonFusionBasePass_PybindBridge_RunFailedOnPythonException) { |
| @@ -81,8 +81,3 @@ def test_can_fuse_returns_reason_for_nodes_from_different_graphs(): | |||
| 81 | assert graph2 is not None | 81 | assert graph2 is not None |
| 82 | assert result.ok is False | 82 | assert result.ok is False |
| 83 | assert "different graphs" in result.reason | 83 | assert "different graphs" in result.reason |
| 84 | - | ||
| 85 | - | ||
| 86 | -def test_can_fuse_rejects_non_node_elements(): | ||
| 87 | - with pytest.raises(TypeError, match="ge.graph.Node"): | ||
| 88 | - can_fuse([object()]) | ||