Closed
fix(mul_no_nan): 补齐推导校验并修正测试参考 #6971
raoliang_sacAdd on 9 days agoClosed at 7 days ago
fix(mul_no_nan): 补齐推导校验并修正测试参考 #6971
Closed
raoliang_sacAdd on 9 days agoClosed at 7 days ago
Total: 8 files changed+140-25
@@ -65,16 +65,13 @@
65 </tr>65 </tr>
66 </tbody></table>66 </tbody></table>
67 67 
68-### 产品差异说明
69- 
70-- <term>Ascend 950PR&950DT系列产品</term>:x1、x2的维度数均不超过8。
71-- <term>Ascend 950PR&950DT系列产品</term>:GE图模式支持满足广播关系的空Tensor,输出保持广播后的shape,元素数为0。
72- 
73## 约束说明68## 约束说明
74 69 
75- x1、x2、y必须为同一种数据类型,不支持混合数据类型。70- x1、x2、y必须为同一种数据类型,不支持混合数据类型。
76- 仅判x2一侧,`x2 = -0`同样进入零臂(IEEE-754 `-0 == 0`);`x2 != 0`时所有IEEE行为按普通乘法保留(NaN / Inf正常传播)。71- 仅判x2一侧,`x2 = -0`同样进入零臂(IEEE-754 `-0 == 0`);`x2 != 0`时所有IEEE行为按普通乘法保留(NaN / Inf正常传播)。
77-- 支持任意NumPy广播形态(含标量、单维broadcast、跨rank broadcast),支持动态shape与动态rank。72+- x1、x2的维度数均不超过8。在该范围内支持NumPy广播(含标量、单维broadcast、跨rank broadcast),支持动态shape与动态rank;运行时确定的维度数也须满足上述上限。
73+- <term>Ascend 950PR&950DT系列产品</term>:
74+ - GE图模式支持满足广播关系的空Tensor,输出保持广播后的shape,元素数为0。
78 75 
79## 实现方案76## 实现方案
80 77 
@@ -125,4 +122,4 @@ In0/In1 -- CopyInBrc -- Cast(->fp32) --+
125 122 
126| 调用方式 | 样例代码 | 说明 |123| 调用方式 | 样例代码 | 说明 |
127| ---------- | ------------------------------------------------------------ | ------------------------------------------------------------ |124| ---------- | ------------------------------------------------------------ | ------------------------------------------------------------ |
128-| GE图模式 | [test_geir_mul_no_nan](examples/arch35/test_geir_mul_no_nan.cpp) | 通过[算子IR](op_graph/mul_no_nan_proto.h)构图方式调用MulNoNan算子。 |125+| GE图模式 | [test_geir_mul_no_nan](examples/arch35/test_geir_mul_no_nan.cpp) | 通过[算子IR](op_graph/mul_no_nan_proto.h)构图方式调用MulNoNan算子,样例可通过`--device-id=N`选择设备。 |
@@ -13,6 +13,7 @@
13// Each test case builds a one-op graph, runs it on device, and checks the13// Each test case builds a one-op graph, runs it on device, and checks the
14// output against a pre-computed expected value. The example exits with14// output against a pre-computed expected value. The example exits with
15// status 0 only if every case passes.15// status 0 only if every case passes.
16+// Usage: test_geir_mul_no_nan [case_name|--list] [--device-id=N]
16//17//
17// Semantics: y = (x2 == 0) ? 0 : x1 * x2 (element-wise, broadcast).18// Semantics: y = (x2 == 0) ? 0 : x1 * x2 (element-wise, broadcast).
18// Cases below specifically exercise the special-value contract that19// Cases below specifically exercise the special-value contract that
@@ -381,11 +382,6 @@ static int RunOneCase(const CaseSpec& spec)
381 382 
382int main(int argc, char* argv[])383int main(int argc, char* argv[])
383{384{
384- if (argc > 2) {
385- printf("Usage: %s [case_tag|--list]\n", argv[0]);
386- return FAILED;
387- }
388- 
389 const double kInf = std::numeric_limits<double>::infinity();385 const double kInf = std::numeric_limits<double>::infinity();
390 const double kNeg0 = -0.0;386 const double kNeg0 = -0.0;
391 const double kNaN = std::numeric_limits<double>::quiet_NaN();387 const double kNaN = std::numeric_limits<double>::quiet_NaN();
@@ -443,27 +439,48 @@ int main(int argc, char* argv[])
443 {"fp32_empty_bc", ge::DT_FLOAT, 2.0, 3.0, {0, 4}, {1, 4}, {0, 4}, exact(6.0, 1e-5)},439 {"fp32_empty_bc", ge::DT_FLOAT, 2.0, 3.0, {0, 4}, {1, 4}, {0, 4}, exact(6.0, 1e-5)},
444 };440 };
445 441 
446- if (argc == 2 && string(argv[1]) == "--list") {442+ string selectedCase;
443+ string deviceId = "0";
444+ bool listOnly = false;
445+ for (int i = 1; i < argc; ++i) {
446+ const string arg = argv[i];
447+ if (arg == "--list") {
448+ listOnly = true;
449+ } else if (arg.rfind("--device-id=", 0) == 0) {
450+ deviceId = arg.substr(string("--device-id=").size());
451+ if (deviceId.empty() || deviceId.find_first_not_of("0123456789") != string::npos) {
452+ std::cerr << "Invalid device ID: " << deviceId << std::endl;
453+ return FAILED;
454+ }
455+ } else if (selectedCase.empty() && arg.rfind("--", 0) != 0) {
456+ selectedCase = arg;
457+ } else {
458+ std::cerr << "Usage: " << argv[0] << " [case_name|--list] [--device-id=N]" << std::endl;
459+ return FAILED;
460+ }
461+ }
462+ if (listOnly) {
447 for (const auto& c : cases) {463 for (const auto& c : cases) {
448 std::cout << c.tag << std::endl;464 std::cout << c.tag << std::endl;
449 }465 }
450 return SUCCESS;466 return SUCCESS;
451 }467 }
452- if (argc == 2) {468+ if (!selectedCase.empty()) {
453 vector<CaseSpec> selected;469 vector<CaseSpec> selected;
454 for (const auto& c : cases) {470 for (const auto& c : cases) {
455- if (c.tag == argv[1]) {471+ if (c.tag == selectedCase) {
456 selected.push_back(c);472 selected.push_back(c);
457 }473 }
458 }474 }
459 if (selected.empty()) {475 if (selected.empty()) {
460- printf("Unknown case: %s\n", argv[1]);476+ printf("Unknown case: %s\n", selectedCase.c_str());
461 return FAILED;477 return FAILED;
462 }478 }
463 cases = std::move(selected);479 cases = std::move(selected);
464 }480 }
465 481 
466- std::map<AscendString, AscendString> globalOpts = {{"ge.exec.deviceId", "0"}, {"ge.graphRunMode", "1"}};482+ std::map<AscendString, AscendString> globalOpts = {{"ge.exec.deviceId", deviceId.c_str()},
483+ {"ge.graphRunMode", "1"}};
467 if (ge::GEInitialize(globalOpts) != SUCCESS) {484 if (ge::GEInitialize(globalOpts) != SUCCESS) {
468 printf("GEInitialize failed\n");485 printf("GEInitialize failed\n");
469 return FAILED;486 return FAILED;
@@ -18,8 +18,16 @@ ge::graphStatus InferDataType4MulNoNan(gert::InferDataTypeContext* context)
18 return ge::GRAPH_FAILED;18 return ge::GRAPH_FAILED;
19 }19 }
20 const auto dtype = context->GetInputDataType(0);20 const auto dtype = context->GetInputDataType(0);
21+ OP_CHECK_IF(dtype != ge::DT_FLOAT16 && dtype != ge::DT_FLOAT && dtype != ge::DT_INT32 && dtype != ge::DT_BF16,
F
Ffanzhaoyun0018 days ago

[Major] InferDataType 白名单新增 int32,但与“仅支持三种浮点类型”的说明及缺失的 kernel/tiling 适配不一致

新的 dtype 白名单显式放行 DT_INT32,同时新增的 ST CSV 也加入了 int32 用例(mnn_int32_broadcast / mnn_int32_wrap)。但 golden.py 的注释(L129-130)仍称“本算子只暴露该交集中的三种浮点类型”,README 约束亦未体现 int32;并且本 patch 未见 op def/proto、tiling、kernel 的 int32 适配。 若 kernel/tiling 实际未支持 int32,则 InferDataType 放行与 ST 用例会驱动不支持的输入,导致上板运行失败或精度用例误判。建议:确认 op def 与 kernel/tiling 已具备 int32 能力并补齐相应改动;否则移除此白名单中的 DT_INT32 及对应 ST 用例,并统一 README/golden 注释。

likedislike
22+ OP_LOGE_FOR_INVALID_DTYPE_WITH_REASON(context->GetNodeName(), "x1", Ops::Base::ToString(dtype),
23+ "only float16, float32, int32 and bfloat16 are supported"),
24+ return ge::GRAPH_FAILED);
21 OP_CHECK_IF(dtype != context->GetInputDataType(1),25 OP_CHECK_IF(dtype != context->GetInputDataType(1),
22- OP_LOGE(context->GetNodeName(), "x1 and x2 must have the same dtype."), return ge::GRAPH_FAILED);26+ OP_LOGE_FOR_INVALID_DTYPES_WITH_REASON(
27+ context->GetNodeName(), "x1, x2",
28+ Ops::Base::ToString(dtype) + ", " + Ops::Base::ToString(context->GetInputDataType(1)),
29+ "inputs must have the same dtype"),
30+ return ge::GRAPH_FAILED);
23 return context->SetOutputDataType(0, dtype);31 return context->SetOutputDataType(0, dtype);
24}32}
25 33 
@@ -20,7 +20,20 @@ using namespace ge;
20namespace ops {20namespace ops {
21static ge::graphStatus InferShape4MulNoNan(gert::InferShapeContext* context)21static ge::graphStatus InferShape4MulNoNan(gert::InferShapeContext* context)
22{22{
23+ if (context == nullptr) {
24+ return ge::GRAPH_FAILED;
25+ }
23 OP_LOGD(context->GetNodeName(), "Begin to do InferShape4MulNoNan in ops-math");26 OP_LOGD(context->GetNodeName(), "Begin to do InferShape4MulNoNan in ops-math");
27+ constexpr size_t MAX_INPUT_RANK = 8;
28+ for (size_t i = 0; i < 2; ++i) {
29+ const auto* shape = context->GetInputShape(i);
30+ OP_CHECK_NULL_WITH_CONTEXT(context, shape);
31+ OP_CHECK_IF(
32+ shape->GetDimNum() > MAX_INPUT_RANK,
F
Ffanzhaoyun0018 days ago

[Major] rank≤8 校验对所有产品无条件生效,与 README 的 950 专属描述及“动态 rank”约束矛盾

新增的 InferShape 对 x1、x2 统一用 shape->GetDimNum() > 8 直接判失败,未做任何产品/SOC 区分。但 README 改动后仍把“维度数不超过 8”放在 <term>Ascend 950PR&950DT系列产品</term> 的产品差异条目下(L73-75),且原约束 L72 明确写“支持任意 NumPy 广播形态(含…跨 rank broadcast),支持动态 shape 与动态 rank”,暗示更高 rank 是合法的。 这样在非 950 产品上会把原本可接受的 rank>8 输入判为失败,属于行为回归且文档与代码不一致。建议:按 SOC 条件化该校验(仅 950 生效),或将该限制明确为全局限制并同步修正 README 的产品差异与“动态 rank”描述。

likedislike
33+ OP_LOGE_FOR_INVALID_SHAPEDIM_WITH_REASON(context->GetNodeName(), i == 0 ? "x1" : "x2",
34+ std::to_string(shape->GetDimNum()), "rank must not exceed 8"),
35+ return ge::GRAPH_FAILED);
36+ }
24 return Ops::Base::InferShape4Broadcast(context);37 return Ops::Base::InferShape4Broadcast(context);
25}38}
26 39 
@@ -37,7 +37,7 @@ def mul_no_nan_golden(x1, x2, **kwargs):
37 """37 """
38 Kernel golden for mul_no_nan.38 Kernel golden for mul_no_nan.
39 All the parameters follow @mul_no_nan_def.cpp without outputs.39 All the parameters follow @mul_no_nan_def.cpp without outputs.
40- All the input Tensors are numpy.ndarray.40+ Inputs are array-like tensors.
41 kwargs may contain: short_soc_version, input_ori_shapes, output_ori_shapes,41 kwargs may contain: short_soc_version, input_ori_shapes, output_ori_shapes,
42 input_formats, output_formats, input_ori_formats, output_ori_formats,42 input_formats, output_formats, input_ori_formats, output_ori_formats,
43 input_dtypes, output_dtypes.43 input_dtypes, output_dtypes.
@@ -48,6 +48,10 @@ def mul_no_nan_golden(x1, x2, **kwargs):
48 by x1 == inf (inf * 0) or by x1 == NaN. Conversely, where x2 != 0 the48 by x1 == inf (inf * 0) or by x1 == NaN. Conversely, where x2 != 0 the
49 product is returned as is, so x1 == 0 with x2 == inf still yields NaN and49 product is returned as is, so x1 == 0 with x2 == inf still yields NaN and
50 NOT 0. This is the core differentiator vs plain Mul.50 NOT 0. This is the core differentiator vs plain Mul.
51+ 
52+ TensorFlow's CPU MulNoNan lacks INT32 and some valid six-dimensional
53+ broadcasts. Use a Torch composition to cover the full CANN input domain
54+ and retain fp64 intermediate precision for the mathematical reference.
51 """55 """
52 import torch56 import torch
53 57 
@@ -61,11 +65,26 @@ def mul_no_nan_golden(x1, x2, **kwargs):
61 output = torch.where(y == 0, torch.zeros_like(product), product).numpy()65 output = torch.where(y == 0, torch.zeros_like(product), product).numpy()
62 if kwargs.get("golden_mode") == "Promote":66 if kwargs.get("golden_mode") == "Promote":
63 return output67 return output
64- return output.astype(output_dtype)68+ # Use the framework cast for rounding; NumPy only carries the final bytes.
69+ if output_dtype.name == "bfloat16":
70+ return (
71+ torch.from_numpy(output)
72+ .to(torch.bfloat16)
73+ .view(torch.int16)
74+ .numpy()
75+ .view(output_dtype)
76+ )
77+ target_dtype = {
78+ "float16": torch.float16,
79+ "float32": torch.float32,
80+ "float64": torch.float64,
81+ "int32": torch.int32,
82+ }[output_dtype.name]
83+ return torch.from_numpy(output).to(target_dtype).numpy()
65 84 
66 85 
67class _MulNoNanCompose:86class _MulNoNanCompose:
68- """Third-party reference executed on the remote GPU server."""87+ """Torch GPU composition, including INT32 which the TensorFlow API lacks."""
69 88 
70 def __call__(self, x1, x2, **kwargs):89 def __call__(self, x1, x2, **kwargs):
71 del kwargs90 del kwargs
@@ -100,9 +119,13 @@ class _MulNoNanE2ECompose:
100class MulNoNanTensorFlowSpec:119class MulNoNanTensorFlowSpec:
101 """TensorFlow E2E spec registered by the CSV ``api_name``."""120 """TensorFlow E2E spec registered by the CSV ``api_name``."""
102 121 
103- # Omit a custom golden: TTK's native CPU TF fallback applies Promote to122+ @staticmethod
104- # raw inputs. Its custom E2E plugin path bypasses that promotion.123+ def golden(x, y, **kwargs):
124+ # Native CPU TF rejects some valid six-dimensional broadcasts. TTK
125+ # does not promote custom E2E inputs, so keep the reference in fp64.
126+ return [mul_no_nan_golden(x, y, golden_mode="Promote")]
127+ 
105 third_party = {"torch": _MulNoNanE2ECompose}128 third_party = {"torch": _MulNoNanE2ECompose}
106- # tf.raw_ops.MulNoNan itself supports floating/complex tensors, not int32.129+ # This TensorFlow TestSpec covers only float16/float32/bfloat16.
107- # This CANN operator exposes only the three floating types from that overlap.130+ # CANN's kernel/GEIR paths also support int32, which the TF API rejects.
108 tolerance = _TENSORFLOW_TOLERANCE131 tolerance = _TENSORFLOW_TOLERANCE
@@ -0,0 +1,9 @@
1+testcase_name,op_name,input_shapes,input_dtypes,input_formats,output_shapes,output_dtypes,output_formats,input_ori_shapes,input_ori_formats,output_ori_shapes,output_ori_formats,attributes,input_data_ranges,is_enabled,remark
2+mnn_float16_broadcast,mul_no_nan,"((2, 1, 5), (3, 1))","('float16', 'float16')","('ND', 'ND')","((2, 3, 5),)","('float16',)","('ND',)","((2, 1, 5), (3, 1))","('ND', 'ND')","((2, 3, 5),)","('ND',)",{},"((-3, 3), (-3, 3))",TRUE,
3+mnn_float32_broadcast,mul_no_nan,"((2, 1, 5), (3, 1))","('float32', 'float32')","('ND', 'ND')","((2, 3, 5),)","('float32',)","('ND',)","((2, 1, 5), (3, 1))","('ND', 'ND')","((2, 3, 5),)","('ND',)",{},"((-3, 3), (-3, 3))",TRUE,
4+mnn_bfloat16_broadcast,mul_no_nan,"((2, 1, 5), (3, 1))","('bfloat16', 'bfloat16')","('ND', 'ND')","((2, 3, 5),)","('bfloat16',)","('ND',)","((2, 1, 5), (3, 1))","('ND', 'ND')","((2, 3, 5),)","('ND',)",{},"((-3, 3), (-3, 3))",TRUE,
5+mnn_int32_broadcast,mul_no_nan,"((2, 1, 5), (3, 1))","('int32', 'int32')","('ND', 'ND')","((2, 3, 5),)","('int32',)","('ND',)","((2, 1, 5), (3, 1))","('ND', 'ND')","((2, 3, 5),)","('ND',)",{},"((-3, 3), (-3, 3))",TRUE,
6+mnn_float16_nan_zero,mul_no_nan,"((17,), (1,))","('float16', 'float16')","('ND', 'ND')","((17,),)","('float16',)","('ND',)","((17,), (1,))","('ND', 'ND')","((17,),)","('ND',)",{},"((float(""nan""), float(""nan"")), (0, 0))",TRUE,
7+mnn_float32_zero_inf,mul_no_nan,"((17,), (1,))","('float32', 'float32')","('ND', 'ND')","((17,),)","('float32',)","('ND',)","((17,), (1,))","('ND', 'ND')","((17,),)","('ND',)",{},"((0, 0), (float(""inf""), float(""inf"")))",TRUE,
8+mnn_bfloat16_inf_zero,mul_no_nan,"((17,), (1,))","('bfloat16', 'bfloat16')","('ND', 'ND')","((17,),)","('bfloat16',)","('ND',)","((17,), (1,))","('ND', 'ND')","((17,),)","('ND',)",{},"((float(""inf""), float(""inf"")), (0, 0))",TRUE,
9+mnn_int32_wrap,mul_no_nan,"((17,), (1,))","('int32', 'int32')","('ND', 'ND')","((17,),)","('int32',)","('ND',)","((17,), (1,))","('ND', 'ND')","((17,),)","('ND',)",{},"((-2147483648, -2147483648), (-1, -1))",TRUE,
@@ -98,3 +98,18 @@ TEST(MulNoNanGraphInfer, RejectsMismatchedInputsWithoutLegacyVerifier)
98 ASSERT_NE(holder.GetContext(), nullptr);98 ASSERT_NE(holder.GetContext(), nullptr);
99 EXPECT_EQ(ops::InferDataType4MulNoNan(holder.GetContext()), ge::GRAPH_FAILED);99 EXPECT_EQ(ops::InferDataType4MulNoNan(holder.GetContext()), ge::GRAPH_FAILED);
100}100}
101+ 
102+TEST(MulNoNanGraphInfer, RejectsUnsupportedMatchingInputDtypes)
103+{
104+ for (auto dtype : {ge::DT_BOOL, ge::DT_DOUBLE, ge::DT_COMPLEX128}) {
105+ SCOPED_TRACE(static_cast<int>(dtype));
106+ gert::OpInferDataTypeContextBuilder builder;
107+ builder.OpType("MulNoNan").OpName("mul_no_nan_invalid_dtype").IONum(2, 1);
108+ builder.InputTensorDesc(0, dtype, ge::FORMAT_ND, ge::FORMAT_ND);
109+ builder.InputTensorDesc(1, dtype, ge::FORMAT_ND, ge::FORMAT_ND);
110+ builder.OutputTensorDesc(0, ge::FORMAT_ND, ge::FORMAT_ND);
111+ auto holder = builder.Build();
112+ ASSERT_NE(holder.GetContext(), nullptr);
113+ EXPECT_EQ(ops::InferDataType4MulNoNan(holder.GetContext()), ge::GRAPH_FAILED);
114+ }
115+}
@@ -13,6 +13,7 @@
13#include "infershape_context_faker.h"13#include "infershape_context_faker.h"
14#include "infershape_case_executor.h"14#include "infershape_case_executor.h"
15#include "base/registry/op_impl_space_registry_v2.h"15#include "base/registry/op_impl_space_registry_v2.h"
16+#include "base/context_builder/op_infer_shape_context_builder.h"
16 17 
17using namespace ge;18using namespace ge;
18 19 
@@ -23,6 +24,38 @@ protected:
23 static void TearDownTestCase() { std::cout << "MulNoNanInfershape TearDown" << std::endl; }24 static void TearDownTestCase() { std::cout << "MulNoNanInfershape TearDown" << std::endl; }
24};25};
25 26 
27+TEST_F(MulNoNanInfershape, rejects_either_rank9_input_even_with_unknown_other_rank)
28+{
29+ const auto registry = gert::DefaultOpImplSpaceRegistryV2::GetInstance().GetSpaceRegistry();
30+ ASSERT_NE(registry, nullptr);
31+ const auto* impl = registry->GetOpImpl("MulNoNan");
32+ ASSERT_NE(impl, nullptr);
33+ ASSERT_NE(impl->infer_shape, nullptr);
34+ for (size_t rank9Input = 0; rank9Input < 2; ++rank9Input) {
35+ SCOPED_TRACE(rank9Input);
36+ for (bool unknownOther : {false, true}) {
37+ gert::StorageShape shapes[2];
38+ for (size_t i = 0; i < 2; ++i) {
39+ const size_t rank = i == rank9Input ? 9 : 1;
40+ for (size_t dim = 0; dim < rank; ++dim) {
41+ const int64_t size = i != rank9Input && unknownOther ? -2 : 1;
42+ shapes[i].MutableOriginShape().AppendDim(size);
43+ shapes[i].MutableStorageShape().AppendDim(size);
44+ }
45+ }
46+ gert::Tensor x1(shapes[0], {ge::FORMAT_ND, ge::FORMAT_ND, {}}, ge::DT_FLOAT);
47+ gert::Tensor x2(shapes[1], {ge::FORMAT_ND, ge::FORMAT_ND, {}}, ge::DT_FLOAT);
48+ gert::OpInferShapeContextBuilder builder;
49+ builder.OpType("MulNoNan").OpName("mul_no_nan_rank9").IONum(2, 1);
50+ builder.InputTensors({&x1, &x2});
51+ builder.OutputTensorDesc(0, ge::DT_FLOAT, ge::FORMAT_ND, ge::FORMAT_ND);
52+ auto holder = builder.Build();
53+ ASSERT_NE(holder.GetContext(), nullptr);
54+ EXPECT_EQ(impl->infer_shape(holder.GetContext()), ge::GRAPH_FAILED);
55+ }
56+ }
57+}
58+ 
26// Case 1: two inputs share the same shape, fp16.59// Case 1: two inputs share the same shape, fp16.
27TEST_F(MulNoNanInfershape, same_shape_fp16)60TEST_F(MulNoNanInfershape, same_shape_fp16)
28{61{