已合并
feat(softmax_grad_ext): 新增 Ascend950 融合规则及 infershape 并补充 UT #8089
feat(softmax_grad_ext): 新增 Ascend950 融合规则及 infershape 并补充 UT #8089
已合并
田野创建于 21 天前
8 个文件变更+1365-4
Aactivation/softmax_grad_ext/op_graph/CMakeLists.txt+9-0
@@ -0,0 +1,9 @@
1+# Copyright (c) 2026 Huawei Technologies Co., Ltd.
2+# This program is free software, you can redistribute it and/or modify it under terms and conditions of
3+# CANN Open Software License Agreement Version 2.0 (the "License").
4+# Please refer to the License for details. You may not use this file except in compliance with the License.
5+# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED,
6+# INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE.
7+ 
8+message(STATUS "=== Debug: start ops.activation.softmax_grad_ext.graph_plugin.CMakeLists.txt ")
S
Ssu-yueming6 天前

[Info] CMakeLists.txt 中遗留 Debug message 输出

文件位置

activation/softmax_grad_ext/op_graph/CMakeLists.txt:8

问题说明

第 8 行遗留调试输出:

message(STATUS "=== Debug: start ops.activation.softmax_grad_ext.graph_plugin.CMakeLists.txt ")

tests/ut/op_graph/CMakeLists.txt:9 同样遗留 message(STATUS "=== Debug: ...")。均为开发阶段调试输出,建议清理。不阻塞合入。

建议修复

删除两行 message(STATUS "=== Debug: ...") 调试输出。

likedislike
9+add_graph_plugin_sources()
Aactivation/softmax_grad_ext/op_graph/fusion_pass/softmax_grad_ext_fusion_pass.cpp+405-0
@@ -0,0 +1,405 @@
1+/**
2+ * Copyright (c) 2026 Huawei Technologies Co., Ltd.
3+ * This program is free software, you can redistribute it and/or modify it under the terms and conditions of
4+ * CANN Open Software License Agreement Version 2.0 (the "License").
5+ * Please refer to the License for details. You may not use this file except in compliance with the License.
6+ * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED,
7+ * INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE.
8+ * See LICENSE in the root of the software repository for the full text of the License.
9+ */
10+#include "common/inc/error_util.h"
11+#include "softmax_grad_ext_fusion_pass.h"
12+#include "es_nn_ops.h"
13+#include "ge/compliant_node_builder.h"
14+#include "platform/platform_info.h"
15+#include "ge/ge_utils.h"
16+#include "version/cann_version.h"
17+ 
18+using namespace ge;
19+using namespace fe;
20+using namespace fusion;
21+ 
22+namespace ops {
23+namespace {
24+const std::string kPassName = "SoftmaxGradExtFusionPass";
25+const std::string kPassNameV2 = "SoftmaxGradExtV2FusionPass";
26+ 
27+const int64_t kCaptureSumIdx = 0;
28+const int64_t kSubgraphInputGrad = 0;
29+const int64_t kSubgraphInputX1 = 1;
30+const int64_t kSubgraphInputX2 = 2;
31+const int32_t kReduceSumAxesInputIdx = 1;
32+ 
33+bool IsUnknownShape(const std::vector<int64_t>& dims)
34+{
35+ for (auto dim : dims) {
36+ if (dim == -1) {
37+ return true;
38+ }
39+ }
40+ return false;
41+}
42+ 
43+bool IsTargetPlatform()
44+{
45+ PlatformInfo platform_info;
46+ OptionalInfo optional_info;
47+ OP_LOGE_IF(
48+ PlatformInfoManager::Instance().GetPlatformInfoWithOutSocVersion(platform_info, optional_info) != SUCCESS,
49+ false, kPassName.c_str(), "Get platform_info failed.");
50+ const std::string soc = platform_info.str_info.short_soc_version;
51+ OPS_LOG_D(kPassName.c_str(), "Platform short soc: %s", soc.c_str());
52+ if (soc != "Ascend950") {
53+ OPS_LOG_D(kPassName.c_str(), "Platform is not support, only support Ascend950.");
54+ return false;
55+ }
56+ return true;
57+}
58+ 
59+bool CheckInputsShapeValid(const std::unique_ptr<MatchResult>& match_result)
60+{
61+ std::vector<SubgraphInput> subgraph_inputs;
62+ match_result->ToSubgraphBoundary()->GetAllInputs(subgraph_inputs);
63+ for (const auto& subgraph_input : subgraph_inputs) {
64+ const auto all_inputs = subgraph_input.GetAllInputs();
65+ if (all_inputs.empty()) {
66+ return false;
67+ }
68+ auto match_node = all_inputs.at(0);
69+ TensorDesc tensor_desc;
70+ if (match_node.node.GetInputDesc(match_node.index, tensor_desc) != GRAPH_SUCCESS) {
71+ return false;
72+ }
73+ if (IsUnknownShape(tensor_desc.GetShape().GetDims())) {
74+ OPS_LOG_D(kPassName.c_str(), "Input has unknown shape, skip fusion.");
75+ return false;
76+ }
77+ }
78+ return true;
79+}
80+ 
81+bool GetCapturedSumNode(const std::unique_ptr<MatchResult>& match_result, GNode& sum_node)
82+{
83+ NodeIo node_io;
84+ OP_LOGE_IF(match_result->GetCapturedTensor(kCaptureSumIdx, node_io) != SUCCESS, false, kPassName.c_str(),
85+ "Failed to get captured sum node.");
86+ sum_node = node_io.node;
87+ return true;
88+}
89+ 
90+// ReduceSum in the new IR carries `axes` as a const input (index 1) and `keep_dims` as an attribute.
91+// SoftmaxGradExt takes a single `axes` (Int) attribute and `keep_dims` (Bool) attribute.
92+bool GetAxisFromReduceSum(const GNode& sum_node, int64_t& axis_value, bool& keep_dims)
93+{
94+ Tensor axes_tensor;
95+ if (sum_node.GetInputConstData(kReduceSumAxesInputIdx, axes_tensor) != GRAPH_SUCCESS) {
96+ OPS_LOG_D(kPassName.c_str(), "Failed to get axes const input from ReduceSum.");
97+ return false;
98+ }
99+ const DataType dtype = axes_tensor.GetDataType();
100+ const int32_t elem_size = GetSizeByDataType(dtype);
101+ if (elem_size <= 0 || axes_tensor.GetSize() != static_cast<size_t>(elem_size)) {
102+ OPS_LOG_D(kPassName.c_str(), "ReduceSum axes must be a single int, but size is %zu.", axes_tensor.GetSize());
103+ return false;
104+ }
105+ const auto* data = axes_tensor.GetData();
106+ if (data == nullptr) {
107+ OPS_LOG_D(kPassName.c_str(), "ReduceSum axes const data is nullptr.");
108+ return false;
109+ }
110+ if (dtype == DT_INT64) {
111+ axis_value = static_cast<int64_t>(*reinterpret_cast<const int64_t*>(data));
112+ } else if (dtype == DT_INT32) {
113+ axis_value = static_cast<int64_t>(*reinterpret_cast<const int32_t*>(data));
114+ } else {
115+ OPS_LOG_D(kPassName.c_str(), "ReduceSum axes dtype %d is not supported.", static_cast<int32_t>(dtype));
116+ return false;
117+ }
118+ 
119+ if (sum_node.GetAttr(AscendString("keep_dims"), keep_dims) != GRAPH_SUCCESS) {
120+ OPS_LOG_D(kPassName.c_str(), "Failed to get keep_dims attr from ReduceSum.");
121+ return false;
122+ }
123+ OPS_LOG_D(kPassName.c_str(), "ReduceSum axis=%ld, keep_dims=%d.", axis_value, static_cast<int32_t>(keep_dims));
S
Ssu-yueming6 天前

[Low] LOG API 格式说明符与 int64_t 类型不匹配

文件位置

activation/softmax_grad_ext/op_graph/fusion_pass/softmax_grad_ext_fusion_pass.cpp:123

问题说明

第 123 行:

OPS_LOG_D(kPassName.c_str(), "ReduceSum axis=%ld, keep_dims=%d.", axis_value, static_cast<int32_t>(keep_dims));

axis_value 类型为 int64_t(8 字节),但 %ld 对应 long。在 32 位系统或 Windows 上 long 为 4 字节,会截断 int64_t 数据。 当前 Ascend950 为 64 位平台,实际运行无影响,但违反 cpp-secure.md §11.3(int64_t 须用 %lld)。

建议修复

OPS_LOG_D(kPassName.c_str(), "ReduceSum axis=%lld, keep_dims=%d.", static_cast<long long>(axis_value), static_cast<int32_t>(keep_dims));
likedislike
124+ return true;
125+}
126+ 
127+std::vector<es::EsTensorHolder> CreateReplacementInputs(es::EsGraphBuilder& graph_builder,
128+ const std::vector<SubgraphInput>& subgraph_inputs)
129+{
130+ std::vector<es::EsTensorHolder> inputs;
131+ for (size_t i = 0; i < subgraph_inputs.size(); ++i) {
132+ const auto all_inputs = subgraph_inputs[i].GetAllInputs();
133+ if (all_inputs.empty()) {
134+ OPS_LOG_E(kPassName.c_str(), "Subgraph input %zu is empty.", i);
135+ return {};
136+ }
137+ TensorDesc tensor_desc;
138+ const auto match_node = all_inputs.at(0);
139+ if (match_node.node.GetInputDesc(match_node.index, tensor_desc) != GRAPH_SUCCESS) {
140+ OPS_LOG_E(kPassName.c_str(), "Get subgraph input %zu desc failed.", i);
141+ return {};
142+ }
143+ auto data = graph_builder.CreateInput(
144+ static_cast<int64_t>(i), ("replacement_input_" + std::to_string(i)).c_str(), tensor_desc.GetDataType(),
145+ tensor_desc.GetFormat(), tensor_desc.GetShape().GetDims());
146+ inputs.emplace_back(data);
147+ }
148+ return inputs;
149+}
150+ 
151+Status InferShape(const GraphUniqPtr& replace_graph, const std::vector<SubgraphInput>& subgraph_inputs)
152+{
153+ std::vector<Shape> input_shapes;
154+ for (const auto& subgraph_input : subgraph_inputs) {
155+ const auto all_inputs = subgraph_input.GetAllInputs();
156+ if (all_inputs.empty()) {
157+ return FAILED;
S
Ssu-yueming6 天前

[Low] InferShape 辅助函数返回 FAILED 未记录具体原因

文件位置

activation/softmax_grad_ext/op_graph/fusion_pass/softmax_grad_ext_fusion_pass.cpp:157

问题说明

InferShape 辅助函数在第 157 行(all_inputs.empty())和第 162 行(GetInputDesc 失败)直接 return FAILED; 未打印日志。 调用方仅记录整体 "InferShape for replacement failed.",无法区分是输入为空还是 GetInputDesc 失败,增加调试难度。 违反 review_checklist.md §10.3(异常返回前必须 OP_LOGE)。

建议修复

在两处 return FAILED; 前补充日志:

if (all_inputs.empty()) {
    OPS_LOG_E(kPassName.c_str(), "InferShape: subgraph input is empty.");
    return FAILED;
}
// ...
if (match_node.node.GetInputDesc(match_node.index, tensor_desc) != GRAPH_SUCCESS) {
    OPS_LOG_E(kPassName.c_str(), "InferShape: GetInputDesc failed, index=%d.", match_node.index);
    return FAILED;
}
likedislike
158+ }
159+ TensorDesc tensor_desc;
160+ const auto match_node = all_inputs.at(0);
161+ if (match_node.node.GetInputDesc(match_node.index, tensor_desc) != GRAPH_SUCCESS) {
162+ return FAILED;
163+ }
164+ input_shapes.emplace_back(tensor_desc.GetShape());
165+ }
166+ return GeUtils::InferShape(*replace_graph, input_shapes);
167+}
168+ 
169+// V2 IR definition APIs (IrDefInputsV2/IrDefOutputsV2/IrDefAttrsV2) use pimpl (IrInputDefV2)
170+// with strings constructed inside the GE library, avoiding ABI mismatch issues that V1 APIs
171+// (IrDefInputs/IrDefOutputs/IrDefAttrs) have due to std::string layout differences across
172+// _GLIBCXX_USE_CXX11_ABI settings. V2 is available since CANN 9.2.0.
S
Ssu-yueming6 天前

[Medium] 注释与代码不一致 — CANN V2 IR API 版本判断

文件位置

activation/softmax_grad_ext/op_graph/fusion_pass/softmax_grad_ext_fusion_pass.cpp:172

问题说明

第 172 行注释声称 V2 is available since CANN 9.2.0.,但第 174 行代码条件为:

#define NN_HAS_V2_IR_API ((CANN_MAJOR > 9) || (CANN_MAJOR == 9 && CANN_MINOR >= 1))

CANN_MINOR >= 1 在 CANN 9.1 时即为 true,与注释声称的 9.2.0 矛盾。

  • 若注释正确(V2 自 9.2.0 可用),则代码在 9.1 上会误启用 V2 路径,可能链接到不存在的符号;
  • 若代码正确(V2 自 9.1 可用),则注释误导维护者。

建议修复

二选一,使注释与代码一致:

// 方案一:若 V2 实际自 9.1 可用,修正注释
// V2 is available since CANN 9.1.0.
#define NN_HAS_V2_IR_API ((CANN_MAJOR > 9) || (CANN_MAJOR == 9 && CANN_MINOR >= 1))

// 方案二:若 V2 实际自 9.2 可用,修正代码
// V2 is available since CANN 9.2.0.
#define NN_HAS_V2_IR_API ((CANN_MAJOR > 9) || (CANN_MAJOR == 9 && CANN_MINOR >= 2))
likedislike
173+#if defined(CANN_MAJOR) && defined(CANN_MINOR)
174+#define NN_HAS_V2_IR_API ((CANN_MAJOR > 9) || (CANN_MAJOR == 9 && CANN_MINOR >= 1))
175+#else
176+#define NN_HAS_V2_IR_API 0
177+#endif
178+ 
179+// Build a two-input one-output element-wise node (Mul/Sub) with CompliantNodeBuilder.
180+es::EsTensorHolder BuildBinaryNode(es::EsGraphBuilder& graph_builder, const es::EsTensorHolder& input0,
181+ const es::EsTensorHolder& input1, const char* op_type)
182+{
183+ auto* c_builder = graph_builder.GetCGraphBuilder();
184+ auto* graph = c_builder->GetGraph();
185+#if NN_HAS_V2_IR_API
186+ GNode node = es::CompliantNodeBuilder(graph)
187+ .OpType(op_type)
188+ .Name(c_builder->GenerateNodeName(op_type).GetString())
189+ .IrDefInputsV2({{"x1", es::CompliantNodeBuilder::kEsIrInputRequired, ""},
190+ {"x2", es::CompliantNodeBuilder::kEsIrInputRequired, ""}})
191+ .IrDefOutputsV2({{"y", es::CompliantNodeBuilder::kEsIrOutputRequired, ""}})
192+ .Build();
193+#else
194+ GNode node = es::CompliantNodeBuilder(graph)
195+ .OpType(op_type)
196+ .Name(c_builder->GenerateNodeName(op_type).GetString())
197+ .IrDefInputs({{"x1", es::CompliantNodeBuilder::kEsIrInputRequired, ""},
198+ {"x2", es::CompliantNodeBuilder::kEsIrInputRequired, ""}})
199+ .IrDefOutputs({{"y", es::CompliantNodeBuilder::kEsIrOutputRequired, ""}})
200+ .Build();
201+#endif
202+ es::AddEdgeAndUpdatePeerDesc(*graph, *input0.GetProducer(), input0.GetProducerOutIndex(), node, 0);
203+ es::AddEdgeAndUpdatePeerDesc(*graph, *input1.GetProducer(), input1.GetProducerOutIndex(), node, 1);
204+ return es::EsTensorHolder(c_builder->GetTensorHolderFromNode(node, 0));
205+}
206+ 
207+// Build a ReduceSum node used inside a pattern. axes is an internal Const node (CreateConst).
208+es::EsTensorHolder BuildPatternReduceSum(es::EsGraphBuilder& graph_builder, const es::EsTensorHolder& input)
209+{
210+ auto axes = graph_builder.CreateConst(std::vector<int64_t>{-1}, std::vector<int64_t>{1});
211+ auto* c_builder = graph_builder.GetCGraphBuilder();
212+ auto* graph = c_builder->GetGraph();
213+#if NN_HAS_V2_IR_API
214+ GNode node = es::CompliantNodeBuilder(graph)
215+ .OpType("ReduceSum")
216+ .Name(c_builder->GenerateNodeName("ReduceSum").GetString())
217+ .IrDefInputsV2({{"x", es::CompliantNodeBuilder::kEsIrInputRequired, ""},
218+ {"axes", es::CompliantNodeBuilder::kEsIrInputRequired, ""}})
219+ .IrDefOutputsV2({{"y", es::CompliantNodeBuilder::kEsIrOutputRequired, ""}})
220+ .IrDefAttrsV2(
221+ {{"keep_dims", es::CompliantNodeBuilder::kEsAttrOptional, "Bool", es::CreateFrom(true)},
222+ {"noop_with_empty_axes", es::CompliantNodeBuilder::kEsAttrOptional, "Bool",
223+ es::CreateFrom(true)}})
224+ .Build();
225+#else
226+ GNode node = es::CompliantNodeBuilder(graph)
227+ .OpType("ReduceSum")
228+ .Name(c_builder->GenerateNodeName("ReduceSum").GetString())
229+ .IrDefInputs({{"x", es::CompliantNodeBuilder::kEsIrInputRequired, ""},
230+ {"axes", es::CompliantNodeBuilder::kEsIrInputRequired, ""}})
231+ .IrDefOutputs({{"y", es::CompliantNodeBuilder::kEsIrOutputRequired, ""}})
232+ .IrDefAttrs(
233+ {{"keep_dims", es::CompliantNodeBuilder::kEsAttrOptional, "Bool", es::CreateFrom(true)},
234+ {"noop_with_empty_axes", es::CompliantNodeBuilder::kEsAttrOptional, "Bool",
235+ es::CreateFrom(true)}})
236+ .Build();
237+#endif
238+ es::AddEdgeAndUpdatePeerDesc(*graph, *input.GetProducer(), input.GetProducerOutIndex(), node, 0);
239+ es::AddEdgeAndUpdatePeerDesc(*graph, *axes.GetProducer(), axes.GetProducerOutIndex(), node, 1);
240+ return es::EsTensorHolder(c_builder->GetTensorHolderFromNode(node, 0));
241+}
242+ 
243+// v1 pattern:
244+// mul = Mul(input0, input1); sum = ReduceSum(mul); sub = Sub(input0, sum);
245+// mul1 = Mul(input2, input1); mulGrad = Mul(mul1, sub)
246+PatternUniqPtr MakePatternSoftmaxGradExt(const std::string& pass_name)
247+{
248+ auto graph_builder = es::EsGraphBuilder(pass_name.c_str());
249+ auto input0 = graph_builder.CreateInput(0, "grad");
250+ auto input1 = graph_builder.CreateInput(1, "x1");
251+ auto input2 = graph_builder.CreateInput(2, "x2");
252+ 
253+ auto mul = BuildBinaryNode(graph_builder, input0, input1, "Mul");
254+ auto sum = BuildPatternReduceSum(graph_builder, mul);
255+ auto sub = BuildBinaryNode(graph_builder, input0, sum, "Sub");
256+ auto mul1 = BuildBinaryNode(graph_builder, input2, input1, "Mul");
257+ auto mul_grad = BuildBinaryNode(graph_builder, mul1, sub, "Mul");
258+ 
259+ auto graph = graph_builder.BuildAndReset({mul_grad});
260+ auto pattern = std::make_unique<Pattern>(std::move(*graph));
261+ pattern->CaptureTensor({*sum.GetProducer(), 0});
262+ return pattern;
263+}
264+ 
265+// v2 patterns (4 variants), differing only in the input order of mul1 and mulGrad:
266+// variant 0: mul1 = Mul(input1, sub); mulGrad = Mul(mul1, input2)
267+// variant 1: mul1 = Mul(sub, input1); mulGrad = Mul(mul1, input2)
268+// variant 2: mul1 = Mul(input1, sub); mulGrad = Mul(input2, mul1)
269+// variant 3: mul1 = Mul(sub, input1); mulGrad = Mul(input2, mul1)
270+PatternUniqPtr MakePatternSoftmaxGradExtV2(const std::string& pass_name, int32_t variant)
271+{
272+ std::string builder_name = pass_name + "_" + std::to_string(variant);
273+ auto graph_builder = es::EsGraphBuilder(builder_name.c_str());
274+ auto input0 = graph_builder.CreateInput(0, "grad");
275+ auto input1 = graph_builder.CreateInput(1, "x1");
276+ auto input2 = graph_builder.CreateInput(2, "x2");
277+ 
278+ auto mul = BuildBinaryNode(graph_builder, input0, input1, "Mul");
279+ auto sum = BuildPatternReduceSum(graph_builder, mul);
280+ auto sub = BuildBinaryNode(graph_builder, input0, sum, "Sub");
281+ 
282+ es::EsTensorHolder mul1;
283+ es::EsTensorHolder mul_grad;
284+ switch (variant) {
285+ case 0:
286+ mul1 = BuildBinaryNode(graph_builder, input1, sub, "Mul");
287+ mul_grad = BuildBinaryNode(graph_builder, mul1, input2, "Mul");
288+ break;
289+ case 1:
290+ mul1 = BuildBinaryNode(graph_builder, sub, input1, "Mul");
291+ mul_grad = BuildBinaryNode(graph_builder, mul1, input2, "Mul");
292+ break;
293+ case 2:
294+ mul1 = BuildBinaryNode(graph_builder, input1, sub, "Mul");
295+ mul_grad = BuildBinaryNode(graph_builder, input2, mul1, "Mul");
296+ break;
297+ default:
298+ mul1 = BuildBinaryNode(graph_builder, sub, input1, "Mul");
299+ mul_grad = BuildBinaryNode(graph_builder, input2, mul1, "Mul");
300+ break;
301+ }
302+ 
303+ auto graph = graph_builder.BuildAndReset({mul_grad});
304+ auto pattern = std::make_unique<Pattern>(std::move(*graph));
305+ pattern->CaptureTensor({*sum.GetProducer(), 0});
306+ return pattern;
307+}
308+ 
309+GraphUniqPtr SoftmaxGradExtReplacementCommon(const std::unique_ptr<MatchResult>& match_result,
310+ const std::string& pass_name)
311+{
312+ OPS_LOG_D(pass_name.c_str(), "Enter Replacement for %s.", pass_name.c_str());
313+ 
314+ GNode sum_node;
315+ OP_LOGE_IF(!GetCapturedSumNode(match_result, sum_node), nullptr, pass_name.c_str(),
316+ "Get captured ReduceSum node failed.");
317+ 
318+ int64_t axis_value = 0;
319+ bool keep_dims = true;
320+ OP_LOGE_IF(!GetAxisFromReduceSum(sum_node, axis_value, keep_dims), nullptr, pass_name.c_str(),
321+ "Failed to get axis/keep_dims from ReduceSum.");
322+ 
323+ std::vector<SubgraphInput> subgraph_inputs;
324+ match_result->ToSubgraphBoundary()->GetAllInputs(subgraph_inputs);
325+ OP_LOGE_IF(subgraph_inputs.size() < 3UL, nullptr, pass_name.c_str(), "Subgraph inputs size %zu is less than 3.",
326+ subgraph_inputs.size());
327+ 
328+ auto graph_builder = es::EsGraphBuilder("replacement");
329+ auto replacement_inputs = CreateReplacementInputs(graph_builder, subgraph_inputs);
330+ OP_LOGE_IF(replacement_inputs.size() < 3UL, nullptr, pass_name.c_str(), "Create replacement inputs failed.");
331+ 
332+ // SoftmaxGradExt(grad, x1, x2): grad=input0, x1=input1, x2=input2.
333+ auto softmax_grad_ext = es::SoftmaxGradExt(replacement_inputs[kSubgraphInputGrad],
334+ replacement_inputs[kSubgraphInputX1],
335+ replacement_inputs[kSubgraphInputX2], axis_value, keep_dims);
336+ 
337+ GraphUniqPtr replace_graph = graph_builder.BuildAndReset({softmax_grad_ext});
338+ if (InferShape(replace_graph, subgraph_inputs) != SUCCESS) {
339+ OPS_LOG_E(pass_name.c_str(), "InferShape for replacement failed.");
340+ return nullptr;
341+ }
342+ return replace_graph;
343+}
344+} // namespace
345+ 
346+// ==================== SoftmaxGradExtFusionPass ====================
347+ 
348+std::vector<PatternUniqPtr> SoftmaxGradExtFusionPass::Patterns()
349+{
350+ OPS_LOG_D(kPassName.c_str(), "Enter Patterns for SoftmaxGradExtFusionPass.");
351+ std::vector<PatternUniqPtr> patterns;
352+ patterns.emplace_back(MakePatternSoftmaxGradExt(kPassName));
353+ return patterns;
354+}
355+ 
356+bool SoftmaxGradExtFusionPass::MeetRequirements(const std::unique_ptr<MatchResult>& match_result)
357+{
358+ OPS_LOG_D(kPassName.c_str(), "Enter MeetRequirements for SoftmaxGradExtFusionPass.");
359+ if (!IsTargetPlatform()) {
360+ return false;
361+ }
362+ if (!CheckInputsShapeValid(match_result)) {
363+ return false;
364+ }
365+ return true;
366+}
367+ 
368+GraphUniqPtr SoftmaxGradExtFusionPass::Replacement(const std::unique_ptr<MatchResult>& match_result)
369+{
370+ return SoftmaxGradExtReplacementCommon(match_result, kPassName);
371+}
372+ 
373+// ==================== SoftmaxGradExtV2FusionPass ====================
374+ 
375+std::vector<PatternUniqPtr> SoftmaxGradExtV2FusionPass::Patterns()
376+{
377+ OPS_LOG_D(kPassNameV2.c_str(), "Enter Patterns for SoftmaxGradExtV2FusionPass.");
378+ std::vector<PatternUniqPtr> patterns;
379+ for (int32_t i = 0; i < 4; ++i) {
380+ patterns.emplace_back(MakePatternSoftmaxGradExtV2(kPassNameV2, i));
381+ }
382+ return patterns;
383+}
384+ 
385+bool SoftmaxGradExtV2FusionPass::MeetRequirements(const std::unique_ptr<MatchResult>& match_result)
386+{
387+ OPS_LOG_D(kPassNameV2.c_str(), "Enter MeetRequirements for SoftmaxGradExtV2FusionPass.");
388+ if (!IsTargetPlatform()) {
389+ return false;
390+ }
391+ if (!CheckInputsShapeValid(match_result)) {
392+ return false;
393+ }
394+ return true;
395+}
396+ 
397+GraphUniqPtr SoftmaxGradExtV2FusionPass::Replacement(const std::unique_ptr<MatchResult>& match_result)
398+{
399+ return SoftmaxGradExtReplacementCommon(match_result, kPassNameV2);
400+}
401+ 
402+REG_FUSION_PASS(SoftmaxGradExtFusionPass).Stage(CustomPassStage::kAfterInferShape);
403+REG_FUSION_PASS(SoftmaxGradExtV2FusionPass).Stage(CustomPassStage::kAfterInferShape);
404+ 
405+} // namespace ops
Aactivation/softmax_grad_ext/op_graph/fusion_pass/softmax_grad_ext_fusion_pass.h+38-0
@@ -0,0 +1,38 @@
1+/**
2+ * Copyright (c) 2026 Huawei Technologies Co., Ltd.
3+ * This program is free software, you can redistribute it and/or modify it under the terms and conditions of
4+ * CANN Open Software License Agreement Version 2.0 (the "License").
5+ * Please refer to the License for details. You may not use this file except in compliance with the License.
6+ * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED,
7+ * INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE.
8+ * See LICENSE in the root of the software repository for the full text of the License.
9+ */
10+#ifndef NN_SOFTMAX_GRAD_EXT_FUSION_PASS_H
11+#define NN_SOFTMAX_GRAD_EXT_FUSION_PASS_H
12+ 
13+#include "ge/fusion/pass/pattern_fusion_pass.h"
14+ 
15+namespace ops {
16+using namespace ge;
17+using namespace fusion;
18+ 
19+class __attribute__((visibility("default"))) SoftmaxGradExtFusionPass : public PatternFusionPass {
20+protected:
21+ std::vector<PatternUniqPtr> Patterns() override;
22+ 
23+ bool MeetRequirements(const std::unique_ptr<MatchResult>& match_result) override;
24+ 
25+ GraphUniqPtr Replacement(const std::unique_ptr<MatchResult>& match_result) override;
26+};
27+ 
28+class __attribute__((visibility("default"))) SoftmaxGradExtV2FusionPass : public PatternFusionPass {
29+protected:
30+ std::vector<PatternUniqPtr> Patterns() override;
31+ 
32+ bool MeetRequirements(const std::unique_ptr<MatchResult>& match_result) override;
33+ 
34+ GraphUniqPtr Replacement(const std::unique_ptr<MatchResult>& match_result) override;
35+};
36+ 
37+} // namespace ops
38+#endif // NN_SOFTMAX_GRAD_EXT_FUSION_PASS_H
Aactivation/softmax_grad_ext/op_host/softmax_grad_ext_infershape.cpp+46-0
@@ -0,0 +1,46 @@
1+/**
2+ * Copyright (c) 2026 Huawei Technologies Co., Ltd.
3+ * This program is free software, you can redistribute it and/or modify it under the terms and conditions of
4+ * CANN Open Software License Agreement Version 2.0 (the "License").
5+ * Please refer to the License for details. You may not use this file except in compliance with the License.
6+ * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED,
7+ * INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE.
8+ * See LICENSE in the root of the software repository for the full text of the License.
9+ */
10+ 
11+#include "platform/platform_info.h"
12+#include "log/log.h"
13+#include "register/op_impl_registry.h"
14+#include <list>
15+ 
16+using namespace ge;
17+namespace ops {
18+ 
19+constexpr size_t X_INDEX = 0;
20+constexpr size_t Y_INDEX = 0;
21+ 
22+static ge::graphStatus InferShape4SoftmaxGradExt(gert::InferShapeContext* context)
23+{
24+ OP_LOGD(context->GetNodeName(), "InferShape4SoftmaxGradExt begin");
25+ 
26+ auto x1_shape = context->GetInputShape(X_INDEX);
27+ OP_CHECK_NULL_WITH_CONTEXT(context, x1_shape);
28+ 
29+ auto y_shape = context->GetOutputShape(Y_INDEX);
30+ OP_CHECK_NULL_WITH_CONTEXT(context, y_shape);
31+ *y_shape = *x1_shape;
32+ 
33+ return ge::GRAPH_SUCCESS;
34+}
35+ 
36+static ge::graphStatus InferDataType4SoftmaxGradExt(gert::InferDataTypeContext* context)
37+{
38+ OP_LOGD(context->GetNodeName(), "InferDataType4SoftmaxGradExt begin");
39+ auto x1_dtype = context->GetInputDataType(X_INDEX);
40+ context->SetOutputDataType(Y_INDEX, x1_dtype);
41+ OP_LOGD(context->GetNodeName(), "InferDataType4SoftmaxGradExt end");
42+ return ge::GRAPH_SUCCESS;
43+}
44+ 
45+IMPL_OP_INFERSHAPE(SoftmaxGradExt).InferShape(InferShape4SoftmaxGradExt).InferDataType(InferDataType4SoftmaxGradExt);
46+} // namespace ops
Aactivation/softmax_grad_ext/tests/ut/op_graph/CMakeLists.txt+11-0
@@ -0,0 +1,11 @@
1+# Copyright (c) 2026 Huawei Technologies Co., Ltd.
2+# This program is free software, you can redistribute it and/or modify it under terms and conditions of
3+# CANN Open Software License Agreement Version 2.0 (the "License").
4+# Please refer to the License for details. You may not use this file except in compliance with the License.
5+# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED,
6+# INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE.
7+file(GLOB CURRENT_DIRS RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/*)
8+if(UT_TEST_ALL OR OP_GRAPH_UT)
9+ message(STATUS "=== Debug: OP_GRAPH_MODULE_NAME=${OP_GRAPH_MODULE_NAME}")
10+ add_modules_ut_sources(HOSTNAME ${OP_GRAPH_MODULE_NAME} MODE PRIVATE DIR ${CMAKE_CURRENT_SOURCE_DIR})
11+endif()
Aactivation/softmax_grad_ext/tests/ut/op_graph/test_softmax_grad_ext_fusion_pass.cpp+732-0
@@ -0,0 +1,732 @@
1+/*
2+ * Copyright (c) 2026 Huawei Technologies Co., Ltd.
3+ * This program is free software, you can redistribute it and/or modify it under the terms and conditions of
4+ * CANN Open Software License Agreement Version 2.0 (the "License").
5+ * Please refer to the License for details. You may not use this file except in compliance with the License.
6+ * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED,
7+ * INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE.
8+ * See LICENSE in the root of the software repository for the full text of the License.
9+ */
10+#include <iostream>
11+#include <vector>
12+#include <gtest/gtest.h>
13+#include "platform/platform_infos_def.h"
14+#include "ut_op_util.h"
15+#include "platform/platform_info.h"
16+#include "ge/es_graph_builder.h"
17+#include "ge/compliant_node_builder.h"
18+#include "../../../op_graph/fusion_pass/softmax_grad_ext_fusion_pass.h"
19+#include "register/register_custom_pass.h"
20+ 
21+using namespace ut_util;
22+using namespace std;
23+using namespace ge;
24+using namespace fe;
25+using namespace es;
26+using namespace ops;
27+ 
28+namespace {
29+const int32_t kVariantV1 = -1;
30+const int32_t kExpectedNodeCount = 5;
31+ 
32+void SetOutDesc(const EsTensorHolder& holder, DataType dtype, const vector<int64_t>& dims)
33+{
34+ TensorDesc desc;
35+ holder.GetProducer()->GetOutputDesc(0, desc);
36+ desc.SetDataType(dtype);
37+ desc.SetShape(ge::Shape(dims));
38+ holder.GetProducer()->UpdateOutputDesc(0, desc);
39+}
40+ 
41+void SetInDesc(const GNode& node, int32_t idx, DataType dtype, const vector<int64_t>& dims)
42+{
43+ TensorDesc desc;
44+ node.GetInputDesc(idx, desc);
45+ desc.SetDataType(dtype);
46+ desc.SetShape(ge::Shape(dims));
47+ const_cast<GNode&>(node).UpdateInputDesc(idx, desc);
48+}
49+ 
50+vector<int64_t> ReduceShape(const vector<int64_t>& dims, int64_t axis, bool keepDims)
51+{
52+ int64_t rank = static_cast<int64_t>(dims.size());
53+ int64_t a = axis < 0 ? axis + rank : axis;
54+ vector<int64_t> out;
55+ for (int64_t i = 0; i < rank; ++i) {
56+ if (i == a) {
57+ if (keepDims) {
58+ out.push_back(1);
59+ }
60+ } else {
61+ out.push_back(dims[i]);
62+ }
63+ }
64+ return out;
65+}
66+EsTensorHolder BuildBinaryNode(EsGraphBuilder& builder, const EsTensorHolder& in0, const EsTensorHolder& in1,
67+ const char* opType)
68+{
69+ auto* c_builder = builder.GetCGraphBuilder();
70+ auto* graph = c_builder->GetGraph();
71+ GNode node = CompliantNodeBuilder(graph)
72+ .OpType(opType)
73+ .Name(c_builder->GenerateNodeName(opType).GetString())
74+ .IrDefInputs({{"x1", CompliantNodeBuilder::kEsIrInputRequired, ""},
75+ {"x2", CompliantNodeBuilder::kEsIrInputRequired, ""}})
76+ .IrDefOutputs({{"y", CompliantNodeBuilder::kEsIrOutputRequired, ""}})
77+ .Build();
78+ AddEdgeAndUpdatePeerDesc(*graph, *in0.GetProducer(), in0.GetProducerOutIndex(), node, 0);
79+ AddEdgeAndUpdatePeerDesc(*graph, *in1.GetProducer(), in1.GetProducerOutIndex(), node, 1);
80+ return EsTensorHolder(c_builder->GetTensorHolderFromNode(node, 0));
81+}
82+ 
83+EsTensorHolder BuildReduceSum(EsGraphBuilder& builder, const EsTensorHolder& in0, const EsTensorHolder& axes,
84+ bool keepDims)
85+{
86+ auto* c_builder = builder.GetCGraphBuilder();
87+ auto* graph = c_builder->GetGraph();
88+ GNode node = CompliantNodeBuilder(graph)
89+ .OpType("ReduceSum")
90+ .Name(c_builder->GenerateNodeName("ReduceSum").GetString())
91+ .IrDefInputs({{"x", CompliantNodeBuilder::kEsIrInputRequired, ""},
92+ {"axes", CompliantNodeBuilder::kEsIrInputRequired, ""}})
93+ .IrDefOutputs({{"y", CompliantNodeBuilder::kEsIrOutputRequired, ""}})
94+ .IrDefAttrs(
95+ {{"keep_dims", CompliantNodeBuilder::kEsAttrOptional, "Bool", CreateFrom(keepDims)},
96+ {"noop_with_empty_axes", CompliantNodeBuilder::kEsAttrOptional, "Bool", CreateFrom(true)}})
97+ .Build();
98+ AddEdgeAndUpdatePeerDesc(*graph, *in0.GetProducer(), in0.GetProducerOutIndex(), node, 0);
99+ AddEdgeAndUpdatePeerDesc(*graph, *axes.GetProducer(), axes.GetProducerOutIndex(), node, 1);
100+ return EsTensorHolder(c_builder->GetTensorHolderFromNode(node, 0));
101+}
102+struct BuiltGraph {
103+ shared_ptr<Graph> graph;
104+};
105+ 
106+// Build the softmax backward graph and manually set all tensor descs (InferShapeForTest equivalent).
107+// The op_graph UT environment does not invoke IMPL_OP_INFERSHAPE, so descs must be set manually before
108+// running the pass. variant == kVariantV1 builds the v1 pattern, otherwise one of the 4 v2 variants.
109+BuiltGraph BuildSoftmaxGradGraph(DataType dtype, const vector<int64_t>& dims, int64_t axis, bool keepDims,
110+ int32_t variant)
111+{
112+ EsGraphBuilder builder("softmax_grad_ext_fusion_test");
113+ auto grad = builder.CreateInput(0, "grad", dtype, FORMAT_ND, dims);
114+ auto x1 = builder.CreateInput(1, "x1", dtype, FORMAT_ND, dims);
115+ auto x2 = builder.CreateInput(2, "x2", dtype, FORMAT_ND, dims);
116+ SetOutDesc(grad, dtype, dims);
117+ SetOutDesc(x1, dtype, dims);
118+ SetOutDesc(x2, dtype, dims);
119+ 
120+ auto mul = BuildBinaryNode(builder, grad, x1, "Mul");
121+ auto axes = builder.CreateConst(std::vector<int64_t>{axis}, {1});
122+ auto sum = BuildReduceSum(builder, mul, axes, keepDims);
123+ auto sub = BuildBinaryNode(builder, grad, sum, "Sub");
124+ 
125+ EsTensorHolder mul1;
126+ EsTensorHolder mulGrad;
127+ if (variant == kVariantV1) {
128+ mul1 = BuildBinaryNode(builder, x2, x1, "Mul");
129+ mulGrad = BuildBinaryNode(builder, mul1, sub, "Mul");
130+ } else {
131+ switch (variant) {
132+ case 0:
133+ mul1 = BuildBinaryNode(builder, x1, sub, "Mul");
134+ mulGrad = BuildBinaryNode(builder, mul1, x2, "Mul");
135+ break;
136+ case 1:
137+ mul1 = BuildBinaryNode(builder, sub, x1, "Mul");
138+ mulGrad = BuildBinaryNode(builder, mul1, x2, "Mul");
139+ break;
140+ case 2:
141+ mul1 = BuildBinaryNode(builder, x1, sub, "Mul");
142+ mulGrad = BuildBinaryNode(builder, x2, mul1, "Mul");
143+ break;
144+ default:
145+ mul1 = BuildBinaryNode(builder, sub, x1, "Mul");
146+ mulGrad = BuildBinaryNode(builder, x2, mul1, "Mul");
147+ break;
148+ }
149+ }
150+ 
151+ // InferShapeForTest: manually set all node descs so the pattern matcher and fusion can proceed.
152+ const vector<int64_t> reduced = ReduceShape(dims, axis, keepDims);
153+ SetInDesc(*mul.GetProducer(), 0, dtype, dims);
154+ SetInDesc(*mul.GetProducer(), 1, dtype, dims);
155+ SetOutDesc(mul, dtype, dims);
156+ SetInDesc(*sum.GetProducer(), 0, dtype, dims);
157+ SetInDesc(*sum.GetProducer(), 1, DT_INT64, {1});
158+ SetOutDesc(sum, dtype, reduced);
159+ SetInDesc(*sub.GetProducer(), 0, dtype, dims);
160+ SetInDesc(*sub.GetProducer(), 1, dtype, reduced);
161+ SetOutDesc(sub, dtype, dims);
162+ SetInDesc(*mul1.GetProducer(), 0, dtype, dims);
163+ SetInDesc(*mul1.GetProducer(), 1, dtype, dims);
164+ SetOutDesc(mul1, dtype, dims);
165+ SetInDesc(*mulGrad.GetProducer(), 0, dtype, dims);
166+ SetInDesc(*mulGrad.GetProducer(), 1, dtype, dims);
167+ SetOutDesc(mulGrad, dtype, dims);
168+ 
169+ BuiltGraph ret;
170+ ret.graph = builder.BuildAndReset({mulGrad});
171+ return ret;
172+}
173+ 
174+void SetPlatform(const string& soc)
175+{
176+ PlatformInfo platformInfo;
177+ OptionalInfo optiCompilationInfo;
178+ platformInfo.soc_info.ai_core_cnt = 64;
179+ platformInfo.str_info.short_soc_version = soc;
180+ optiCompilationInfo.soc_version = soc;
181+ PlatformInfoManager::Instance().platform_info_map_[soc] = platformInfo;
182+ PlatformInfoManager::Instance().SetOptionalCompilationInfo(optiCompilationInfo);
183+}
184+} // namespace
185+ 
186+class SoftmaxGradExtFusionPassTest : public testing::Test {
187+protected:
188+ static void SetUpTestCase() { SetPlatform("Ascend950"); }
189+ void SetUp() override { SetPlatform("Ascend950"); }
190+ 
191+ // Check SoftmaxGradExt input dtype and shape size match expectations (from PR).
192+ bool IsSoftmaxGradExtInputRight(GNode& node, const vector<int64_t>& dims, DataType dtype)
193+ {
194+ TensorDesc input0Desc;
195+ TensorDesc input1Desc;
196+ TensorDesc input2Desc;
197+ node.GetInputDesc(0, input0Desc);
198+ node.GetInputDesc(1, input1Desc);
199+ node.GetInputDesc(2, input2Desc);
200+ if (input0Desc.GetDataType() != dtype || input1Desc.GetDataType() != dtype ||
201+ input2Desc.GetDataType() != dtype) {
202+ return false;
203+ }
204+ int64_t expectedSize = 1;
205+ for (auto d : dims) {
206+ expectedSize *= d;
207+ }
208+ if (input0Desc.GetShape().GetShapeSize() != expectedSize ||
209+ input1Desc.GetShape().GetShapeSize() != expectedSize ||
210+ input2Desc.GetShape().GetShapeSize() != expectedSize) {
211+ return false;
212+ }
213+ return true;
214+ }
215+ 
216+ // Verify the graph contains one SoftmaxGradExt node with correct attrs, input mapping, input
217+ // dtype/shape, node_count == 5 (3 Data + 1 SoftmaxGradExt + 1 NetOutput), and no residual ops.
218+ void ExpectFused(const shared_ptr<Graph>& graph, int64_t axis, bool keepDims, DataType dtype,
219+ const vector<int64_t>& dims)
220+ {
221+ bool found = false;
222+ int32_t nodeCount = 0;
223+ int32_t mulSubReduceCount = 0;
224+ for (auto node : graph->GetAllNodes()) {
225+ nodeCount++;
226+ AscendString type;
227+ node.GetType(type);
228+ if (type == "Mul" || type == "Sub" || type == "ReduceSum") {
229+ mulSubReduceCount++;
230+ }
231+ if (type == "SoftmaxGradExt") {
232+ found = true;
233+ int64_t axesAttr = 1;
234+ bool keepDimsAttr = true;
235+ if (node.GetAttr(AscendString("axes"), axesAttr) != GRAPH_SUCCESS) {
236+ axesAttr = 1; // IR default
237+ }
238+ if (node.GetAttr(AscendString("keep_dims"), keepDimsAttr) != GRAPH_SUCCESS) {
239+ keepDimsAttr = true; // IR default
240+ }
241+ EXPECT_EQ(axesAttr, axis);
242+ EXPECT_EQ(keepDimsAttr, keepDims);
243+ // Verify input mapping: input0=grad, input1=x1, input2=x2.
244+ for (int32_t i = 0; i < 3; ++i) {
245+ auto src = node.GetInDataNodesAndPortIndexs(i);
246+ AscendString srcName;
247+ src.first->GetName(srcName);
248+ std::string expected = (i == 0) ? "grad" : (i == 1) ? "x1" : "x2";
249+ std::string actual = srcName.GetString();
250+ EXPECT_NE(actual.find(expected), std::string::npos)
251+ << "input " << i << " expected " << expected << " got " << actual;
252+ }
253+ EXPECT_TRUE(IsSoftmaxGradExtInputRight(node, dims, dtype));
254+ }
255+ }
256+ EXPECT_TRUE(found);
257+ EXPECT_EQ(mulSubReduceCount, 0);
258+ EXPECT_EQ(nodeCount, kExpectedNodeCount); // 3 Data + 1 SoftmaxGradExt + 1 NetOutput
259+ }
260+};
261+ 
262+// ---------------- SoftmaxGradExtFusionPass (v1) ----------------
263+ 
264+TEST_F(SoftmaxGradExtFusionPassTest, v1_fp16_axis_neg1_keepdims_true_success)
265+{
266+ vector<int64_t> dims{2, 32, 128};
267+ auto built = BuildSoftmaxGradGraph(DT_FLOAT16, dims, -1, true, kVariantV1);
268+ built.graph->DumpToFile(Graph::DumpFormat::kOnnx, "dump_graph_for_softmax_grad_ext_v1_test1");
269+ CustomPassContext ctx;
270+ SoftmaxGradExtFusionPass pass;
271+ EXPECT_EQ(pass.Run(built.graph, ctx), SUCCESS);
272+ built.graph->DumpToFile(Graph::DumpFormat::kOnnx, "dump_afterpass_graph_for_softmax_grad_ext_v1_test1");
273+ ExpectFused(built.graph, -1, true, DT_FLOAT16, dims);
274+}
275+ 
276+TEST_F(SoftmaxGradExtFusionPassTest, v1_fp32_axis1_keepdims_false_success)
277+{
278+ vector<int64_t> dims{1, 64, 256};
279+ auto built = BuildSoftmaxGradGraph(DT_FLOAT, dims, 1, false, kVariantV1);
280+ built.graph->DumpToFile(Graph::DumpFormat::kOnnx, "dump_graph_for_softmax_grad_ext_v1_test2");
281+ CustomPassContext ctx;
282+ SoftmaxGradExtFusionPass pass;
283+ EXPECT_EQ(pass.Run(built.graph, ctx), SUCCESS);
284+ built.graph->DumpToFile(Graph::DumpFormat::kOnnx, "dump_afterpass_graph_for_softmax_grad_ext_v1_test2");
285+ ExpectFused(built.graph, 1, false, DT_FLOAT, dims);
286+}
287+ 
288+TEST_F(SoftmaxGradExtFusionPassTest, v1_unknown_shape_not_changed)
289+{
290+ vector<int64_t> dims{-1, 32, 128};
291+ auto built = BuildSoftmaxGradGraph(DT_FLOAT16, dims, -1, true, kVariantV1);
292+ built.graph->DumpToFile(Graph::DumpFormat::kOnnx, "dump_graph_for_softmax_grad_ext_v1_test3");
293+ CustomPassContext ctx;
294+ SoftmaxGradExtFusionPass pass;
295+ EXPECT_EQ(pass.Run(built.graph, ctx), GRAPH_NOT_CHANGED);
296+}
297+ 
298+TEST_F(SoftmaxGradExtFusionPassTest, v1_unsupported_platform_not_changed)
299+{
300+ SetPlatform("Ascend910_93");
301+ vector<int64_t> dims{2, 32, 128};
302+ auto built = BuildSoftmaxGradGraph(DT_FLOAT16, dims, -1, true, kVariantV1);
303+ built.graph->DumpToFile(Graph::DumpFormat::kOnnx, "dump_graph_for_softmax_grad_ext_v1_test4");
304+ CustomPassContext ctx;
305+ SoftmaxGradExtFusionPass pass;
306+ EXPECT_EQ(pass.Run(built.graph, ctx), GRAPH_NOT_CHANGED);
307+}
308+ 
309+// ---------------- SoftmaxGradExtV2FusionPass (4 variants) ----------------
310+ 
311+TEST_F(SoftmaxGradExtFusionPassTest, v2_variant0_success)
312+{
313+ vector<int64_t> dims{2, 32, 128};
314+ auto built = BuildSoftmaxGradGraph(DT_FLOAT16, dims, -1, true, 0);
315+ built.graph->DumpToFile(Graph::DumpFormat::kOnnx, "dump_graph_for_softmax_grad_ext_v2_test1");
316+ CustomPassContext ctx;
317+ SoftmaxGradExtV2FusionPass pass;
318+ EXPECT_EQ(pass.Run(built.graph, ctx), SUCCESS);
319+ built.graph->DumpToFile(Graph::DumpFormat::kOnnx, "dump_afterpass_graph_for_softmax_grad_ext_v2_test1");
320+ ExpectFused(built.graph, -1, true, DT_FLOAT16, dims);
321+}
322+ 
323+TEST_F(SoftmaxGradExtFusionPassTest, v2_variant1_success)
324+{
325+ vector<int64_t> dims{1, 64, 256};
326+ auto built = BuildSoftmaxGradGraph(DT_FLOAT16, dims, 1, true, 1);
327+ built.graph->DumpToFile(Graph::DumpFormat::kOnnx, "dump_graph_for_softmax_grad_ext_v2_test2");
328+ CustomPassContext ctx;
329+ SoftmaxGradExtV2FusionPass pass;
330+ EXPECT_EQ(pass.Run(built.graph, ctx), SUCCESS);
331+ built.graph->DumpToFile(Graph::DumpFormat::kOnnx, "dump_afterpass_graph_for_softmax_grad_ext_v2_test2");
332+ ExpectFused(built.graph, 1, true, DT_FLOAT16, dims);
333+}
334+ 
335+TEST_F(SoftmaxGradExtFusionPassTest, v2_variant2_success)
336+{
337+ vector<int64_t> dims{2, 32, 128};
338+ auto built = BuildSoftmaxGradGraph(DT_FLOAT, dims, -1, false, 2);
339+ built.graph->DumpToFile(Graph::DumpFormat::kOnnx, "dump_graph_for_softmax_grad_ext_v2_test3");
340+ CustomPassContext ctx;
341+ SoftmaxGradExtV2FusionPass pass;
342+ EXPECT_EQ(pass.Run(built.graph, ctx), SUCCESS);
343+ built.graph->DumpToFile(Graph::DumpFormat::kOnnx, "dump_afterpass_graph_for_softmax_grad_ext_v2_test3");
344+ ExpectFused(built.graph, -1, false, DT_FLOAT, dims);
345+}
346+ 
347+TEST_F(SoftmaxGradExtFusionPassTest, v2_variant3_success)
348+{
349+ vector<int64_t> dims{1, 64, 256};
350+ auto built = BuildSoftmaxGradGraph(DT_FLOAT16, dims, 2, true, 3);
351+ built.graph->DumpToFile(Graph::DumpFormat::kOnnx, "dump_graph_for_softmax_grad_ext_v2_test4");
352+ CustomPassContext ctx;
353+ SoftmaxGradExtV2FusionPass pass;
354+ EXPECT_EQ(pass.Run(built.graph, ctx), SUCCESS);
355+ built.graph->DumpToFile(Graph::DumpFormat::kOnnx, "dump_afterpass_graph_for_softmax_grad_ext_v2_test4");
356+ ExpectFused(built.graph, 2, true, DT_FLOAT16, dims);
357+}
358+ 
359+TEST_F(SoftmaxGradExtFusionPassTest, v2_unknown_shape_not_changed)
360+{
361+ vector<int64_t> dims{-1, 32, 128};
362+ auto built = BuildSoftmaxGradGraph(DT_FLOAT16, dims, -1, true, 0);
363+ built.graph->DumpToFile(Graph::DumpFormat::kOnnx, "dump_graph_for_softmax_grad_ext_v2_test5");
364+ CustomPassContext ctx;
365+ SoftmaxGradExtV2FusionPass pass;
366+ EXPECT_EQ(pass.Run(built.graph, ctx), GRAPH_NOT_CHANGED);
367+}
368+ 
369+TEST_F(SoftmaxGradExtFusionPassTest, v2_unsupported_platform_not_changed)
370+{
371+ SetPlatform("Ascend910_93");
372+ vector<int64_t> dims{2, 32, 128};
373+ auto built = BuildSoftmaxGradGraph(DT_FLOAT16, dims, -1, true, 0);
374+ built.graph->DumpToFile(Graph::DumpFormat::kOnnx, "dump_graph_for_softmax_grad_ext_v2_test6");
375+ CustomPassContext ctx;
376+ SoftmaxGradExtV2FusionPass pass;
377+ EXPECT_EQ(pass.Run(built.graph, ctx), GRAPH_NOT_CHANGED);
378+}
379+ 
380+// v1 pattern must not match a v2 graph (mul1 = Mul(x1, sub)) and vice versa.
381+TEST_F(SoftmaxGradExtFusionPassTest, v1_pass_does_not_match_v2_graph)
382+{
383+ vector<int64_t> dims{2, 32, 128};
384+ auto built = BuildSoftmaxGradGraph(DT_FLOAT16, dims, -1, true, 0);
385+ CustomPassContext ctx;
386+ SoftmaxGradExtFusionPass pass;
387+ EXPECT_EQ(pass.Run(built.graph, ctx), GRAPH_NOT_CHANGED);
388+}
389+ 
390+// v2 patterns must not match a v1 graph (mul1 = Mul(x2, x1), mulGrad = Mul(mul1, sub)).
391+TEST_F(SoftmaxGradExtFusionPassTest, v2_pass_does_not_match_v1_graph)
392+{
393+ vector<int64_t> dims{2, 32, 128};
394+ auto built = BuildSoftmaxGradGraph(DT_FLOAT16, dims, -1, true, kVariantV1);
395+ CustomPassContext ctx;
396+ SoftmaxGradExtV2FusionPass pass;
397+ EXPECT_EQ(pass.Run(built.graph, ctx), GRAPH_NOT_CHANGED);
398+}
399+ 
400+// ---- Suspicious cycle scenarios ----
401+// These cases build target graphs with structures that might cause cycle detection issues
402+// in the pattern matching/replacement phase. They verify the pass handles them gracefully.
403+ 
404+// Scenario 1: mul input order swapped: Mul(x1, grad) instead of Mul(grad, x1).
405+// The v1 pattern expects Mul(grad, x1), so this should NOT match (GRAPH_NOT_CHANGED),
406+// and must not trigger any pattern graph cycle error.
407+TEST_F(SoftmaxGradExtFusionPassTest, v1_mul_input_order_swapped_not_changed)
408+{
409+ vector<int64_t> dims{2, 32, 128};
410+ EsGraphBuilder builder("mul_swapped_test");
411+ auto grad = builder.CreateInput(0, "grad", DT_FLOAT16, FORMAT_ND, dims);
412+ auto x1 = builder.CreateInput(1, "x1", DT_FLOAT16, FORMAT_ND, dims);
413+ auto x2 = builder.CreateInput(2, "x2", DT_FLOAT16, FORMAT_ND, dims);
414+ SetOutDesc(grad, DT_FLOAT16, dims);
415+ SetOutDesc(x1, DT_FLOAT16, dims);
416+ SetOutDesc(x2, DT_FLOAT16, dims);
417+ 
418+ // mul = Mul(x1, grad) — swapped order
419+ auto mul = BuildBinaryNode(builder, x1, grad, "Mul");
420+ auto axes = builder.CreateConst(vector<int64_t>{-1}, {1});
421+ auto sum = BuildReduceSum(builder, mul, axes, true);
422+ auto sub = BuildBinaryNode(builder, x1, sum, "Sub"); // sub uses x1 (matching swapped mul input0)
423+ auto mul1 = BuildBinaryNode(builder, x2, x1, "Mul");
424+ auto mulGrad = BuildBinaryNode(builder, mul1, sub, "Mul");
425+ 
426+ SetInDesc(*mul.GetProducer(), 0, DT_FLOAT16, dims);
427+ SetInDesc(*mul.GetProducer(), 1, DT_FLOAT16, dims);
428+ SetOutDesc(mul, DT_FLOAT16, dims);
429+ SetInDesc(*sum.GetProducer(), 0, DT_FLOAT16, dims);
430+ SetInDesc(*sum.GetProducer(), 1, DT_INT64, {1});
431+ SetOutDesc(sum, DT_FLOAT16, {2, 32, 1});
432+ SetInDesc(*sub.GetProducer(), 0, DT_FLOAT16, dims);
433+ SetInDesc(*sub.GetProducer(), 1, DT_FLOAT16, {2, 32, 1});
434+ SetOutDesc(sub, DT_FLOAT16, dims);
435+ SetInDesc(*mul1.GetProducer(), 0, DT_FLOAT16, dims);
436+ SetInDesc(*mul1.GetProducer(), 1, DT_FLOAT16, dims);
437+ SetOutDesc(mul1, DT_FLOAT16, dims);
438+ SetInDesc(*mulGrad.GetProducer(), 0, DT_FLOAT16, dims);
439+ SetInDesc(*mulGrad.GetProducer(), 1, DT_FLOAT16, dims);
440+ SetOutDesc(mulGrad, DT_FLOAT16, dims);
441+ 
442+ shared_ptr<Graph> graph = builder.BuildAndReset({mulGrad});
443+ CustomPassContext ctx;
444+ SoftmaxGradExtFusionPass pass;
445+ EXPECT_EQ(pass.Run(graph, ctx), GRAPH_NOT_CHANGED);
446+}
447+ 
448+// Scenario 2: mulGrad has a control edge to an external node that depends on grad.
449+// This creates a path: mulGrad -> external -> grad -> mul -> ... -> mulGrad.
450+// WillCauseCycleIfFuse should detect this and skip fusion (GRAPH_NOT_CHANGED).
451+TEST_F(SoftmaxGradExtFusionPassTest, v1_control_edge_cycle_not_changed)
452+{
453+ vector<int64_t> dims{2, 32, 128};
454+ auto built = BuildSoftmaxGradGraph(DT_FLOAT16, dims, -1, true, kVariantV1);
455+ 
456+ // Add an external node that takes mulGrad output and produces grad's input.
457+ // This creates a cycle if fused: SoftmaxGradExt -> external -> grad -> SoftmaxGradExt.
458+ EsGraphBuilder extBuilder("ext_control_cycle");
459+ auto grad = extBuilder.CreateInput(0, "grad", DT_FLOAT16, FORMAT_ND, dims);
460+ auto x1 = extBuilder.CreateInput(1, "x1", DT_FLOAT16, FORMAT_ND, dims);
461+ auto x2 = extBuilder.CreateInput(2, "x2", DT_FLOAT16, FORMAT_ND, dims);
462+ SetOutDesc(grad, DT_FLOAT16, dims);
463+ SetOutDesc(x1, DT_FLOAT16, dims);
464+ SetOutDesc(x2, DT_FLOAT16, dims);
465+ 
466+ auto axes = extBuilder.CreateConst(vector<int64_t>{-1}, {1});
467+ auto mul = BuildBinaryNode(extBuilder, grad, x1, "Mul");
468+ auto sum = BuildReduceSum(extBuilder, mul, axes, true);
469+ auto sub = BuildBinaryNode(extBuilder, grad, sum, "Sub");
470+ auto mul1 = BuildBinaryNode(extBuilder, x2, x1, "Mul");
471+ auto mulGrad = BuildBinaryNode(extBuilder, mul1, sub, "Mul");
472+ 
473+ SetInDesc(*mul.GetProducer(), 0, DT_FLOAT16, dims);
474+ SetInDesc(*mul.GetProducer(), 1, DT_FLOAT16, dims);
475+ SetOutDesc(mul, DT_FLOAT16, dims);
476+ SetInDesc(*sum.GetProducer(), 0, DT_FLOAT16, dims);
477+ SetInDesc(*sum.GetProducer(), 1, DT_INT64, {1});
478+ SetOutDesc(sum, DT_FLOAT16, {2, 32, 1});
479+ SetInDesc(*sub.GetProducer(), 0, DT_FLOAT16, dims);
480+ SetInDesc(*sub.GetProducer(), 1, DT_FLOAT16, {2, 32, 1});
481+ SetOutDesc(sub, DT_FLOAT16, dims);
482+ SetInDesc(*mul1.GetProducer(), 0, DT_FLOAT16, dims);
483+ SetInDesc(*mul1.GetProducer(), 1, DT_FLOAT16, dims);
484+ SetOutDesc(mul1, DT_FLOAT16, dims);
485+ SetInDesc(*mulGrad.GetProducer(), 0, DT_FLOAT16, dims);
486+ SetInDesc(*mulGrad.GetProducer(), 1, DT_FLOAT16, dims);
487+ SetOutDesc(mulGrad, DT_FLOAT16, dims);
488+ 
489+ // external node: takes mulGrad output, output feeds back to grad via control edge.
490+ auto external = BuildBinaryNode(extBuilder, mulGrad, grad, "Add");
491+ SetInDesc(*external.GetProducer(), 0, DT_FLOAT16, dims);
492+ SetInDesc(*external.GetProducer(), 1, DT_FLOAT16, dims);
493+ SetOutDesc(external, DT_FLOAT16, dims);
494+ 
495+ shared_ptr<Graph> graph = extBuilder.BuildAndReset({external});
496+ CustomPassContext ctx;
497+ SoftmaxGradExtFusionPass pass;
498+ // Should detect cycle and skip (GRAPH_NOT_CHANGED), or succeed if no cycle.
499+ // Either way, must not crash or produce a cyclic graph.
500+ auto status = pass.Run(graph, ctx);
501+ EXPECT_TRUE(status == SUCCESS || status == GRAPH_NOT_CHANGED);
502+}
503+ 
504+// Scenario 3: axes const shared by ReduceSum inside the pattern AND another ReduceSum outside.
505+// The axes const node has multiple consumers. If the fusion deletes it, the external ReduceSum breaks.
506+// The pattern should keep axes outside the subgraph (it's a CreateInput boundary).
507+TEST_F(SoftmaxGradExtFusionPassTest, v1_shared_axes_const_success)
508+{
509+ vector<int64_t> dims{2, 32, 128};
510+ EsGraphBuilder builder("shared_axes_test");
511+ auto grad = builder.CreateInput(0, "grad", DT_FLOAT16, FORMAT_ND, dims);
512+ auto x1 = builder.CreateInput(1, "x1", DT_FLOAT16, FORMAT_ND, dims);
513+ auto x2 = builder.CreateInput(2, "x2", DT_FLOAT16, FORMAT_ND, dims);
514+ SetOutDesc(grad, DT_FLOAT16, dims);
515+ SetOutDesc(x1, DT_FLOAT16, dims);
516+ SetOutDesc(x2, DT_FLOAT16, dims);
517+ 
518+ // Shared axes const
519+ auto axes = builder.CreateConst(vector<int64_t>{-1}, {1});
520+ 
521+ // Subgraph matching v1 pattern
522+ auto mul = BuildBinaryNode(builder, grad, x1, "Mul");
523+ auto sum = BuildReduceSum(builder, mul, axes, true);
524+ auto sub = BuildBinaryNode(builder, grad, sum, "Sub");
525+ auto mul1 = BuildBinaryNode(builder, x2, x1, "Mul");
526+ auto mulGrad = BuildBinaryNode(builder, mul1, sub, "Mul");
527+ 
528+ // External ReduceSum also uses the same axes const
529+ auto extReduce = BuildReduceSum(builder, x2, axes, true);
530+ 
531+ SetInDesc(*mul.GetProducer(), 0, DT_FLOAT16, dims);
532+ SetInDesc(*mul.GetProducer(), 1, DT_FLOAT16, dims);
533+ SetOutDesc(mul, DT_FLOAT16, dims);
534+ SetInDesc(*sum.GetProducer(), 0, DT_FLOAT16, dims);
535+ SetInDesc(*sum.GetProducer(), 1, DT_INT64, {1});
536+ SetOutDesc(sum, DT_FLOAT16, {2, 32, 1});
537+ SetInDesc(*sub.GetProducer(), 0, DT_FLOAT16, dims);
538+ SetInDesc(*sub.GetProducer(), 1, DT_FLOAT16, {2, 32, 1});
539+ SetOutDesc(sub, DT_FLOAT16, dims);
540+ SetInDesc(*mul1.GetProducer(), 0, DT_FLOAT16, dims);
541+ SetInDesc(*mul1.GetProducer(), 1, DT_FLOAT16, dims);
542+ SetOutDesc(mul1, DT_FLOAT16, dims);
543+ SetInDesc(*mulGrad.GetProducer(), 0, DT_FLOAT16, dims);
544+ SetInDesc(*mulGrad.GetProducer(), 1, DT_FLOAT16, dims);
545+ SetOutDesc(mulGrad, DT_FLOAT16, dims);
546+ SetInDesc(*extReduce.GetProducer(), 0, DT_FLOAT16, dims);
547+ SetInDesc(*extReduce.GetProducer(), 1, DT_INT64, {1});
548+ SetOutDesc(extReduce, DT_FLOAT16, {2, 32, 1});
549+ 
550+ // Both mulGrad and extReduce are graph outputs
551+ shared_ptr<Graph> graph = builder.BuildAndReset({mulGrad, extReduce});
552+ CustomPassContext ctx;
553+ SoftmaxGradExtFusionPass pass;
554+ auto status = pass.Run(graph, ctx);
555+ EXPECT_TRUE(status == SUCCESS || status == GRAPH_NOT_CHANGED);
556+}
557+ 
558+// Scenario 4: Two overlapping v1 subgraphs in the same graph (chain fusion).
559+// The output of the first subgraph feeds into the second. After first fusion,
560+// the second subgraph's structure changes. This tests iterative matching.
561+TEST_F(SoftmaxGradExtFusionPassTest, v1_two_subgraphs_no_crash)
562+{
563+ vector<int64_t> dims{2, 32, 128};
564+ EsGraphBuilder builder("two_subgraphs_test");
565+ auto grad = builder.CreateInput(0, "grad", DT_FLOAT16, FORMAT_ND, dims);
566+ auto x1 = builder.CreateInput(1, "x1", DT_FLOAT16, FORMAT_ND, dims);
567+ auto x2 = builder.CreateInput(2, "x2", DT_FLOAT16, FORMAT_ND, dims);
568+ SetOutDesc(grad, DT_FLOAT16, dims);
569+ SetOutDesc(x1, DT_FLOAT16, dims);
570+ SetOutDesc(x2, DT_FLOAT16, dims);
571+ 
572+ auto axes = builder.CreateConst(vector<int64_t>{-1}, {1});
573+ 
574+ // First subgraph
575+ auto mul1a = BuildBinaryNode(builder, grad, x1, "Mul");
576+ auto sum1a = BuildReduceSum(builder, mul1a, axes, true);
577+ auto sub1a = BuildBinaryNode(builder, grad, sum1a, "Sub");
578+ auto mul1_1a = BuildBinaryNode(builder, x2, x1, "Mul");
579+ auto mulGrad1 = BuildBinaryNode(builder, mul1_1a, sub1a, "Mul");
580+ 
581+ SetInDesc(*mul1a.GetProducer(), 0, DT_FLOAT16, dims);
582+ SetInDesc(*mul1a.GetProducer(), 1, DT_FLOAT16, dims);
583+ SetOutDesc(mul1a, DT_FLOAT16, dims);
584+ SetInDesc(*sum1a.GetProducer(), 0, DT_FLOAT16, dims);
585+ SetInDesc(*sum1a.GetProducer(), 1, DT_INT64, {1});
586+ SetOutDesc(sum1a, DT_FLOAT16, {2, 32, 1});
587+ SetInDesc(*sub1a.GetProducer(), 0, DT_FLOAT16, dims);
588+ SetInDesc(*sub1a.GetProducer(), 1, DT_FLOAT16, {2, 32, 1});
589+ SetOutDesc(sub1a, DT_FLOAT16, dims);
590+ SetInDesc(*mul1_1a.GetProducer(), 0, DT_FLOAT16, dims);
591+ SetInDesc(*mul1_1a.GetProducer(), 1, DT_FLOAT16, dims);
592+ SetOutDesc(mul1_1a, DT_FLOAT16, dims);
593+ SetInDesc(*mulGrad1.GetProducer(), 0, DT_FLOAT16, dims);
594+ SetInDesc(*mulGrad1.GetProducer(), 1, DT_FLOAT16, dims);
595+ SetOutDesc(mulGrad1, DT_FLOAT16, dims);
596+ 
597+ shared_ptr<Graph> graph = builder.BuildAndReset({mulGrad1});
598+ CustomPassContext ctx;
599+ SoftmaxGradExtFusionPass pass;
600+ auto status = pass.Run(graph, ctx);
601+ EXPECT_TRUE(status == SUCCESS || status == GRAPH_NOT_CHANGED);
602+}
603+ 
604+// Scenario 5: mulGrad output has both data and control consumers.
605+// mulGrad -> data consumer (NetOutput) AND mulGrad -> control edge to external node.
606+TEST_F(SoftmaxGradExtFusionPassTest, v1_mulgrad_control_consumer_not_changed_or_success)
607+{
608+ vector<int64_t> dims{2, 32, 128};
609+ EsGraphBuilder builder("ctrl_consumer_test");
610+ auto grad = builder.CreateInput(0, "grad", DT_FLOAT16, FORMAT_ND, dims);
611+ auto x1 = builder.CreateInput(1, "x1", DT_FLOAT16, FORMAT_ND, dims);
612+ auto x2 = builder.CreateInput(2, "x2", DT_FLOAT16, FORMAT_ND, dims);
613+ SetOutDesc(grad, DT_FLOAT16, dims);
614+ SetOutDesc(x1, DT_FLOAT16, dims);
615+ SetOutDesc(x2, DT_FLOAT16, dims);
616+ 
617+ auto axes = builder.CreateConst(vector<int64_t>{-1}, {1});
618+ auto mul = BuildBinaryNode(builder, grad, x1, "Mul");
619+ auto sum = BuildReduceSum(builder, mul, axes, true);
620+ auto sub = BuildBinaryNode(builder, grad, sum, "Sub");
621+ auto mul1 = BuildBinaryNode(builder, x2, x1, "Mul");
622+ auto mulGrad = BuildBinaryNode(builder, mul1, sub, "Mul");
623+ 
624+ SetInDesc(*mul.GetProducer(), 0, DT_FLOAT16, dims);
625+ SetInDesc(*mul.GetProducer(), 1, DT_FLOAT16, dims);
626+ SetOutDesc(mul, DT_FLOAT16, dims);
627+ SetInDesc(*sum.GetProducer(), 0, DT_FLOAT16, dims);
628+ SetInDesc(*sum.GetProducer(), 1, DT_INT64, {1});
629+ SetOutDesc(sum, DT_FLOAT16, {2, 32, 1});
630+ SetInDesc(*sub.GetProducer(), 0, DT_FLOAT16, dims);
631+ SetInDesc(*sub.GetProducer(), 1, DT_FLOAT16, {2, 32, 1});
632+ SetOutDesc(sub, DT_FLOAT16, dims);
633+ SetInDesc(*mul1.GetProducer(), 0, DT_FLOAT16, dims);
634+ SetInDesc(*mul1.GetProducer(), 1, DT_FLOAT16, dims);
635+ SetOutDesc(mul1, DT_FLOAT16, dims);
636+ SetInDesc(*mulGrad.GetProducer(), 0, DT_FLOAT16, dims);
637+ SetInDesc(*mulGrad.GetProducer(), 1, DT_FLOAT16, dims);
638+ SetOutDesc(mulGrad, DT_FLOAT16, dims);
639+ 
640+ // External node connected via control edge from mulGrad
641+ auto extNode = BuildBinaryNode(builder, x2, x1, "Add");
642+ SetInDesc(*extNode.GetProducer(), 0, DT_FLOAT16, dims);
643+ SetInDesc(*extNode.GetProducer(), 1, DT_FLOAT16, dims);
644+ SetOutDesc(extNode, DT_FLOAT16, dims);
645+ 
646+ // Add control edge: mulGrad -> extNode
647+ auto* graph = builder.GetCGraphBuilder()->GetGraph();
648+ graph->AddControlEdge(*mulGrad.GetProducer(), *extNode.GetProducer());
649+ 
650+ shared_ptr<Graph> graphPtr = builder.BuildAndReset({mulGrad, extNode});
651+ CustomPassContext ctx;
652+ SoftmaxGradExtFusionPass pass;
653+ auto status = pass.Run(graphPtr, ctx);
654+ EXPECT_TRUE(status == SUCCESS || status == GRAPH_NOT_CHANGED);
655+}
656+ 
657+// Scenario 6 (KEY SUSPECT): axes is NOT a Const, but the output of an external op that depends on mulGrad.
658+// Pattern axes is Data → DataMatcher matches ANY node type → matches the external op.
659+// The external op depends on mulGrad (subgraph output). After fusion:
660+// SoftmaxGradExt → (mulGrad's downstream) → ext_op(axes producer) → SoftmaxGradExt control edge → CYCLE
661+// WillCauseCycleIfFuse may NOT catch this because ext_op is not in matched_nodes (DataMatcher matches
662+// axes pattern Data node → filtered from matched_nodes). This is the suspected root cause of CI cycle.
663+TEST_F(SoftmaxGradExtFusionPassTest, v1_axes_from_op_depending_on_mulgrad_cycle)
664+{
665+ vector<int64_t> dims{2, 32, 128};
666+ EsGraphBuilder builder("axes_depends_mulgrad_test");
667+ auto grad = builder.CreateInput(0, "grad", DT_FLOAT16, FORMAT_ND, dims);
668+ auto x1 = builder.CreateInput(1, "x1", DT_FLOAT16, FORMAT_ND, dims);
669+ auto x2 = builder.CreateInput(2, "x2", DT_FLOAT16, FORMAT_ND, dims);
670+ SetOutDesc(grad, DT_FLOAT16, dims);
671+ SetOutDesc(x1, DT_FLOAT16, dims);
672+ SetOutDesc(x2, DT_FLOAT16, dims);
673+ 
674+ // Standard v1 subgraph
675+ auto mul = BuildBinaryNode(builder, grad, x1, "Mul");
676+ auto sum = BuildReduceSum(builder, mul, builder.CreateConst(vector<int64_t>{-1}, {1}), true);
677+ auto sub = BuildBinaryNode(builder, grad, sum, "Sub");
678+ auto mul1 = BuildBinaryNode(builder, x2, x1, "Mul");
679+ auto mulGrad = BuildBinaryNode(builder, mul1, sub, "Mul");
680+ 
681+ SetInDesc(*mul.GetProducer(), 0, DT_FLOAT16, dims);
682+ SetInDesc(*mul.GetProducer(), 1, DT_FLOAT16, dims);
683+ SetOutDesc(mul, DT_FLOAT16, dims);
684+ SetInDesc(*sum.GetProducer(), 0, DT_FLOAT16, dims);
685+ SetInDesc(*sum.GetProducer(), 1, DT_INT64, {1});
686+ SetOutDesc(sum, DT_FLOAT16, {2, 32, 1});
687+ SetInDesc(*sub.GetProducer(), 0, DT_FLOAT16, dims);
688+ SetInDesc(*sub.GetProducer(), 1, DT_FLOAT16, {2, 32, 1});
689+ SetOutDesc(sub, DT_FLOAT16, dims);
690+ SetInDesc(*mul1.GetProducer(), 0, DT_FLOAT16, dims);
691+ SetInDesc(*mul1.GetProducer(), 1, DT_FLOAT16, dims);
692+ SetOutDesc(mul1, DT_FLOAT16, dims);
693+ SetInDesc(*mulGrad.GetProducer(), 0, DT_FLOAT16, dims);
694+ SetInDesc(*mulGrad.GetProducer(), 1, DT_FLOAT16, dims);
695+ SetOutDesc(mulGrad, DT_FLOAT16, dims);
696+ 
697+ // axes comes from an external op (Cast) that takes mulGrad as input.
698+ // This creates: mulGrad → Cast(axes) → ReduceSum(inside subgraph)
699+ // DataMatcher matches Cast as pattern axes Data input (always returns true).
700+ // But Cast depends on mulGrad → after fusion, cycle: SoftmaxGradExt → Cast → SoftmaxGradExt.
701+ auto axesCast = BuildBinaryNode(builder, mulGrad, x1, "Cast");
702+ SetInDesc(*axesCast.GetProducer(), 0, DT_FLOAT16, dims);
703+ SetInDesc(*axesCast.GetProducer(), 1, DT_FLOAT16, dims);
704+ SetOutDesc(axesCast, DT_INT64, {1});
705+ 
706+ // Rebuild sum with axes from Cast instead of Const
707+ auto sum2 = BuildReduceSum(builder, mul, axesCast, true);
708+ SetInDesc(*sum2.GetProducer(), 0, DT_FLOAT16, dims);
709+ SetInDesc(*sum2.GetProducer(), 1, DT_INT64, {1});
710+ SetOutDesc(sum2, DT_FLOAT16, {2, 32, 1});
711+ 
712+ // Rebuild sub with sum2
713+ auto sub2 = BuildBinaryNode(builder, grad, sum2, "Sub");
714+ SetInDesc(*sub2.GetProducer(), 0, DT_FLOAT16, dims);
715+ SetInDesc(*sub2.GetProducer(), 1, DT_FLOAT16, {2, 32, 1});
716+ SetOutDesc(sub2, DT_FLOAT16, dims);
717+ 
718+ // mulGrad2 uses sub2
719+ auto mulGrad2 = BuildBinaryNode(builder, mul1, sub2, "Mul");
720+ SetInDesc(*mulGrad2.GetProducer(), 0, DT_FLOAT16, dims);
721+ SetInDesc(*mulGrad2.GetProducer(), 1, DT_FLOAT16, dims);
722+ SetOutDesc(mulGrad2, DT_FLOAT16, dims);
723+ 
724+ shared_ptr<Graph> graph = builder.BuildAndReset({mulGrad2});
725+ CustomPassContext ctx;
726+ SoftmaxGradExtFusionPass pass;
727+ auto status = pass.Run(graph, ctx);
728+ fprintf(stderr, "[STATUS-axes-cycle] %d\n", static_cast<int32_t>(status));
S
Ssu-yueming6 天前

[Low] 测试代码中遗留 fprintf 调试输出

文件位置

activation/softmax_grad_ext/tests/ut/op_graph/test_softmax_grad_ext_fusion_pass.cpp:728

问题说明

第 728 行:

fprintf(stderr, "[STATUS-axes-cycle] %d\n", static_cast<int32_t>(status));

v1_axes_from_op_depending_on_mulgrad_cycle 测试用例中遗留 fprintf(stderr, ...) 调试输出,其他测试用例均无此模式。违反 review_checklist.md §10.32(禁止调试打印污染代码)。

建议修复

删除该行,或替换为日志宏:

OP_LOGD("test", "axes-cycle status: %d", static_cast<int32_t>(status));
likedislike
729+ // If WillCauseCycleIfFuse catches it -> GRAPH_NOT_CHANGED.
730+ // If not -> SUCCESS but graph has cycle (bad), or FAILED.
731+ EXPECT_TRUE(status == SUCCESS || status == GRAPH_NOT_CHANGED);
732+}
Mactivation/softmax_grad_ext/tests/ut/op_host/CMakeLists.txt+4-4
@@ -1,9 +1,9 @@
1# ----------------------------------------------------------------------------1# ----------------------------------------------------------------------------
2# Copyright (c) 2026 Huawei Technologies Co., Ltd.2# Copyright (c) 2026 Huawei Technologies Co., Ltd.
3-# This program is free software, you can redistribute it and/or modify it under the terms and conditions of 3+# This program is free software, you can redistribute it and/or modify it under the terms and conditions of
4# CANN Open Software License Agreement Version 2.0 (the "License").4# CANN Open Software License Agreement Version 2.0 (the "License").
5# Please refer to the License for details. You may not use this file except in compliance with the License.5# Please refer to the License for details. You may not use this file except in compliance with the License.
6-# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, 6+# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED,
7# INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE.7# INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE.
8# See LICENSE in the root of the software repository for the full text of the License.8# See LICENSE in the root of the software repository for the full text of the License.
9# ----------------------------------------------------------------------------9# ----------------------------------------------------------------------------
@@ -12,5 +12,5 @@
12file(GLOB CURRENT_DIRS RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/*)12file(GLOB CURRENT_DIRS RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/*)
13if(UT_TEST_ALL OR OP_HOST_UT)13if(UT_TEST_ALL OR OP_HOST_UT)
14 add_modules_ut_sources(HOSTNAME ${OP_TILING_MODULE_NAME} MODE PRIVATE DIR ${CMAKE_CURRENT_SOURCE_DIR})14 add_modules_ut_sources(HOSTNAME ${OP_TILING_MODULE_NAME} MODE PRIVATE DIR ${CMAKE_CURRENT_SOURCE_DIR})
15- # add_modules_ut_sources(HOSTNAME ${OP_INFERSHAPE_MODULE_NAME} MODE PRIVATE DIR ${CMAKE_CURRENT_SOURCE_DIR})15+ add_modules_ut_sources(HOSTNAME ${OP_INFERSHAPE_MODULE_NAME} MODE PRIVATE DIR ${CMAKE_CURRENT_SOURCE_DIR})
16-endif()16+endif()
Aactivation/softmax_grad_ext/tests/ut/op_host/test_softmax_grad_ext_infershape.cpp+120-0
@@ -0,0 +1,120 @@
1+/*
2+ * Copyright (c) 2026 Huawei Technologies Co., Ltd.
3+ * This program is free software, you can redistribute it and/or modify it under the terms and conditions of
4+ * CANN Open Software License Agreement Version 2.0 (the "License").
5+ * Please refer to the License for details. You may not use this file except in compliance with the License.
6+ * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED,
7+ * INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE.
8+ * See LICENSE in the root of the software repository for the full text of the License.
9+ */
10+#include <iostream>
11+#include <gtest/gtest.h>
12+#include "register/op_impl_registry.h"
13+#include "kernel_run_context_facker.h"
14+#include "exe_graph/runtime/storage_shape.h"
15+#include "log/log.h"
16+#include "platform/platform_info.h"
17+ 
18+class SoftmaxGradExtInferShapeTest : public testing::Test {
19+protected:
20+ static void SetUpTestCase()
21+ {
22+ fe::PlatformInfo platformInfo;
23+ fe::OptionalInfo optiCompilationInfo;
24+ platformInfo.soc_info.ai_core_cnt = 64;
25+ platformInfo.str_info.short_soc_version = "Ascend950";
26+ optiCompilationInfo.soc_version = "Ascend950";
27+ fe::PlatformInfoManager::Instance().platform_info_map_["Ascend950"] = platformInfo;
28+ fe::PlatformInfoManager::Instance().SetOptionalCompilationInfo(optiCompilationInfo);
29+ }
30+};
31+ 
32+static bool ShapeEquals(const gert::Shape& shape, const std::vector<int64_t>& expected)
33+{
34+ if (shape.GetDimNum() != expected.size()) {
35+ return false;
36+ }
37+ for (size_t i = 0; i < expected.size(); ++i) {
38+ if (shape.GetDim(i) != expected[i]) {
39+ return false;
40+ }
41+ }
42+ return true;
43+}
44+ 
45+// y.shape == grad.shape (input0)
46+TEST_F(SoftmaxGradExtInferShapeTest, infershape_shape_eq_grad_fp16_3d)
47+{
48+ auto opImpl = gert::OpImplRegistry::GetInstance().GetOpImpl("SoftmaxGradExt");
49+ ASSERT_NE(opImpl, nullptr);
50+ 
51+ gert::Shape gradShape = {2, 32, 128};
52+ gert::Shape x1Shape = {2, 32, 128};
53+ gert::Shape x2Shape = {2, 32, 128};
54+ gert::Shape yShape = {};
55+ 
56+ auto holder = gert::InferShapeContextFaker()
57+ .NodeIoNum(3, 1)
58+ .IrInstanceNum({1, 1, 1, 1})
59+ .InputShapes({&gradShape, &x1Shape, &x2Shape})
60+ .OutputShapes({&yShape})
61+ .NodeInputTd(0, ge::DT_FLOAT16, ge::FORMAT_ND, ge::FORMAT_ND)
62+ .NodeInputTd(1, ge::DT_FLOAT16, ge::FORMAT_ND, ge::FORMAT_ND)
63+ .NodeInputTd(2, ge::DT_FLOAT16, ge::FORMAT_ND, ge::FORMAT_ND)
64+ .NodeOutputTd(0, ge::DT_FLOAT16, ge::FORMAT_ND, ge::FORMAT_ND)
65+ .Build();
66+ auto context = holder.GetContext<gert::InferShapeContext>();
67+ EXPECT_EQ(opImpl->infer_shape(context), ge::GRAPH_SUCCESS);
68+ ASSERT_NE(context->GetOutputShape(0), nullptr);
69+ EXPECT_TRUE(ShapeEquals(*context->GetOutputShape(0), {2, 32, 128}));
70+}
71+ 
72+TEST_F(SoftmaxGradExtInferShapeTest, infershape_shape_eq_grad_fp32_4d)
73+{
74+ auto opImpl = gert::OpImplRegistry::GetInstance().GetOpImpl("SoftmaxGradExt");
75+ ASSERT_NE(opImpl, nullptr);
76+ 
77+ gert::Shape gradShape = {1, 64, 256, 512};
78+ gert::Shape x1Shape = {1, 64, 256, 512};
79+ gert::Shape x2Shape = {1, 64, 256, 512};
80+ gert::Shape yShape = {};
81+ 
82+ auto holder = gert::InferShapeContextFaker()
83+ .NodeIoNum(3, 1)
84+ .IrInstanceNum({1, 1, 1, 1})
85+ .InputShapes({&gradShape, &x1Shape, &x2Shape})
86+ .OutputShapes({&yShape})
87+ .NodeInputTd(0, ge::DT_FLOAT, ge::FORMAT_ND, ge::FORMAT_ND)
88+ .NodeInputTd(1, ge::DT_FLOAT, ge::FORMAT_ND, ge::FORMAT_ND)
89+ .NodeInputTd(2, ge::DT_FLOAT, ge::FORMAT_ND, ge::FORMAT_ND)
90+ .NodeOutputTd(0, ge::DT_FLOAT, ge::FORMAT_ND, ge::FORMAT_ND)
91+ .Build();
92+ auto context = holder.GetContext<gert::InferShapeContext>();
93+ EXPECT_EQ(opImpl->infer_shape(context), ge::GRAPH_SUCCESS);
94+ ASSERT_NE(context->GetOutputShape(0), nullptr);
95+ EXPECT_TRUE(ShapeEquals(*context->GetOutputShape(0), {1, 64, 256, 512}));
96+}
97+ 
98+// y.dtype == grad.dtype (input0)
99+TEST_F(SoftmaxGradExtInferShapeTest, infershape_dtype_eq_grad)
100+{
101+ auto opImpl = gert::OpImplRegistry::GetInstance().GetOpImpl("SoftmaxGradExt");
102+ ASSERT_NE(opImpl, nullptr);
103+ ASSERT_NE(opImpl->infer_datatype, nullptr);
104+ 
105+ ge::DataType gradDtype = ge::DT_FLOAT16;
106+ ge::DataType yDtype = ge::DT_UNDEFINED;
107+ auto holder = gert::InferDataTypeContextFaker()
108+ .NodeIoNum(3, 1)
109+ .IrInstanceNum({1, 1, 1, 1})
110+ .NodeInputTd(0, ge::DT_FLOAT16, ge::FORMAT_ND, ge::FORMAT_ND)
111+ .NodeInputTd(1, ge::DT_FLOAT16, ge::FORMAT_ND, ge::FORMAT_ND)
112+ .NodeInputTd(2, ge::DT_FLOAT16, ge::FORMAT_ND, ge::FORMAT_ND)
113+ .NodeOutputTd(0, ge::DT_FLOAT16, ge::FORMAT_ND, ge::FORMAT_ND)
114+ .InputDataTypes({&gradDtype, &gradDtype, &gradDtype})
115+ .OutputDataTypes({&yDtype})
116+ .Build();
117+ auto context = holder.GetContext<gert::InferDataTypeContext>();
118+ EXPECT_EQ(opImpl->infer_datatype(context), ge::GRAPH_SUCCESS);
119+ EXPECT_EQ(context->GetOutputDataType(0), ge::DT_FLOAT16);
120+}