Merged
fix: 迁移 RaggedBinCount V2 推导并规范两算子 golden #6538
raoliang_sacAdd on 21 days ago
fix: 迁移 RaggedBinCount V2 推导并规范两算子 golden #6538
Merged
Total: 14 files changed+1517-1805
| @@ -129,6 +129,8 @@ $$ | |||
| 129 | 129 | ||
| 130 | ## 约束说明 | 130 | ## 约束说明 |
| 131 | 131 | ||
| 132 | +- 图编译期用`-1`表示未知维度、`[-2]`表示未知rank。输出rank固定为2;无法由`splits`长度或常量`size`确定的输出轴记为`-1`。执行期各输入须解析为具体非负维度,并满足各参数的rank与元素数约束。 | ||
| 133 | + | ||
| 132 | - 各输入的数据类型必须整组匹配下表之一,不允许跨行组合(`size` 的dtype必须与 `values` 相同): | 134 | - 各输入的数据类型必须整组匹配下表之一,不允许跨行组合(`size` 的dtype必须与 `values` 相同): |
R | |||
| 133 | 135 | ||
| 134 | | splits | values | size | weights | output | 数据格式 | | 136 | | splits | values | size | weights | output | 数据格式 | |
| @@ -236,8 +236,22 @@ bool DumpAndRunGraph(ge::Session* session, Graph& graph, std::vector<ge::Tensor> | |||
| 236 | return true; | 236 | return true; |
| 237 | } | 237 | } |
| 238 | 238 | ||
| 239 | -void ProcessOutputData(std::vector<ge::Tensor>& output) | 239 | +bool ProcessOutputData(std::vector<ge::Tensor>& output) |
| 240 | { | 240 | { |
| 241 | + const std::vector<float> expected = {1, 1, 1, 0, 1, 1, 0, 1}; | ||
| 242 | + if (output.size() != 1 || output[0].GetTensorDesc().GetDataType() != DT_FLOAT || | ||
| 243 | + output[0].GetTensorDesc().GetShape().GetDims() != std::vector<int64_t>({2, 4}) || | ||
| 244 | + output[0].GetSize() != expected.size() * sizeof(float) || output[0].GetData() == nullptr) { | ||
| 245 | + LOG_PRINT("Unexpected output count, dtype, shape or byte size.\n"); | ||
| 246 | + return false; | ||
| 247 | + } | ||
| 248 | + const float* result = reinterpret_cast<const float*>(output[0].GetData()); | ||
| 249 | + for (size_t i = 0; i < expected.size(); ++i) { | ||
| 250 | + if (result[i] != expected[i]) { | ||
| 251 | + LOG_PRINT("Output mismatch at %zu: got %f, expected %f.\n", i, result[i], expected[i]); | ||
| 252 | + return false; | ||
| 253 | + } | ||
| 254 | + } | ||
| 241 | int output_num = output.size(); | 255 | int output_num = output.size(); |
| 242 | for (int i = 0; i < output_num; i++) { | 256 | for (int i = 0; i < output_num; i++) { |
| 243 | std::cout << "output " << i << " dtype : " << output[i].GetTensorDesc().GetDataType() << std::endl; | 257 | std::cout << "output " << i << " dtype : " << output[i].GetTensorDesc().GetDataType() << std::endl; |
| @@ -252,6 +266,7 @@ void ProcessOutputData(std::vector<ge::Tensor>& output) | |||
| 252 | LOG_PRINT("result[%ld] is: %f\n", j, values[j]); | 266 | LOG_PRINT("result[%ld] is: %f\n", j, values[j]); |
| 253 | } | 267 | } |
| 254 | } | 268 | } |
| 269 | + return true; | ||
| 255 | } | 270 | } |
| 256 | 271 | ||
| 257 | int FinalizeRes() | 272 | int FinalizeRes() |
| @@ -284,6 +299,7 @@ int main(int argc, char* argv[]) | |||
| 284 | Graph graph(graph_name); | 299 | Graph graph(graph_name); |
| 285 | std::vector<ge::Tensor> input; | 300 | std::vector<ge::Tensor> input; |
| 286 | if (!CreateAndConfigGraph(graph, input)) { | 301 | if (!CreateAndConfigGraph(graph, input)) { |
| 302 | + ge::GEFinalize(); | ||
| 287 | return FAILED; | 303 | return FAILED; |
| 288 | } | 304 | } |
| 289 | 305 | ||
| @@ -305,8 +321,10 @@ int main(int argc, char* argv[]) | |||
| 305 | return FAILED; | 321 | return FAILED; |
| 306 | } | 322 | } |
| 307 | // 处理输出数据 | 323 | // 处理输出数据 |
| 308 | - ProcessOutputData(output); | 324 | + const bool outputValid = ProcessOutputData(output); |
| 309 | 325 | ||
| 310 | // 清理资源 | 326 | // 清理资源 |
| 311 | - return FinalizeRes(); | 327 | + delete session; |
| 328 | + const int finalizeStatus = FinalizeRes(); | ||
| 329 | + return outputValid ? finalizeStatus : FAILED; | ||
| 312 | } | 330 | } |
| @@ -15,78 +15,6 @@ | |||
| 15 | 15 | ||
| 16 | #include "ragged_bin_count_graph_infer_internal.h" | 16 | #include "ragged_bin_count_graph_infer_internal.h" |
| 17 | 17 | ||
| 18 | -#include <cstdint> | ||
| 19 | -#include <cstring> | ||
| 20 | -#include <utility> | ||
| 21 | -#include <vector> | ||
| 22 | - | ||
| 23 | -#include "graph/operator_reg.h" | ||
| 24 | - | ||
| 25 | -namespace ge { | ||
| 26 | -namespace { | ||
| 27 | -graphStatus ReadLegacyBinCount(const Operator& op, int64_t& bins) | ||
| 28 | -{ | ||
| 29 | - bins = UNKNOWN_DIM; | ||
| 30 | - Tensor sizeTensor; | ||
| 31 | - if (op.GetInputConstData("size", sizeTensor) != GRAPH_SUCCESS) { | ||
| 32 | - return GRAPH_SUCCESS; | ||
| 33 | - } | ||
| 34 | - const auto dtype = op.GetInputDescByName("size").GetDataType(); | ||
| 35 | - if (sizeTensor.GetData() == nullptr || sizeTensor.GetTensorDesc().GetDataType() != dtype) { | ||
| 36 | - return GRAPH_FAILED; | ||
| 37 | - } | ||
| 38 | - if (dtype == DT_INT32 && sizeTensor.GetSize() == sizeof(int32_t)) { | ||
| 39 | - int32_t value = 0; | ||
| 40 | - std::memcpy(&value, sizeTensor.GetData(), sizeof(value)); | ||
| 41 | - bins = value; | ||
| 42 | - } else if (dtype == DT_INT64 && sizeTensor.GetSize() == sizeof(int64_t)) { | ||
| 43 | - std::memcpy(&bins, sizeTensor.GetData(), sizeof(bins)); | ||
| 44 | - } else { | ||
| 45 | - return GRAPH_FAILED; | ||
| 46 | - } | ||
| 47 | - return bins >= 0 ? GRAPH_SUCCESS : GRAPH_FAILED; | ||
| 48 | -} | ||
| 49 | -} // namespace | ||
| 50 | - | ||
| 51 | -IMPLEMT_COMMON_INFERFUNC(RaggedBinCountLegacyInferShape) | ||
| 52 | -{ | ||
| 53 | - const auto splits = op.GetInputDescByName("splits").GetShape().GetDims(); | ||
| 54 | - const auto size = op.GetInputDescByName("size").GetShape().GetDims(); | ||
| 55 | - if (splits != UNKNOWN_RANK && (splits.size() != 1U || (splits[0] != UNKNOWN_DIM && splits[0] < 2))) { | ||
| 56 | - return GRAPH_FAILED; | ||
| 57 | - } | ||
| 58 | - if (size != UNKNOWN_RANK && (size.size() != 1U || (size[0] != UNKNOWN_DIM && size[0] != 1))) { | ||
| 59 | - return GRAPH_FAILED; | ||
| 60 | - } | ||
| 61 | - | ||
| 62 | - const int64_t rows = splits.size() == 1U && splits[0] >= 2 ? splits[0] - 1 : UNKNOWN_DIM; | ||
| 63 | - int64_t bins = UNKNOWN_DIM; | ||
| 64 | - if (ReadLegacyBinCount(op, bins) != GRAPH_SUCCESS) { | ||
| 65 | - return GRAPH_FAILED; | ||
| 66 | - } | ||
| 67 | - auto output = op.GetOutputDescByName("output"); | ||
| 68 | - output.SetShape(Shape({rows, bins})); | ||
| 69 | - // Keep the legacy IR dtype surface; native 950 dtype checks remain in the | ||
| 70 | - // existing InferDataType and tiling callbacks. | ||
| 71 | - output.SetDataType(op.GetInputDescByName("weights").GetDataType()); | ||
| 72 | - if (rows == UNKNOWN_DIM || bins == UNKNOWN_DIM) { | ||
| 73 | - const auto rowRange = rows == UNKNOWN_DIM ? std::make_pair(int64_t{0}, UNKNOWN_DIM) : | ||
| 74 | - std::make_pair(rows, rows); | ||
| 75 | - const auto binRange = bins == UNKNOWN_DIM ? std::make_pair(int64_t{0}, UNKNOWN_DIM) : | ||
| 76 | - std::make_pair(bins, bins); | ||
| 77 | - if (output.SetShapeRange({rowRange, binRange}) != GRAPH_SUCCESS) { | ||
| 78 | - return GRAPH_FAILED; | ||
| 79 | - } | ||
| 80 | - } | ||
| 81 | - return op.UpdateOutputDesc("output", output); | ||
| 82 | -} | ||
| 83 | - | ||
| 84 | -// GEIR can use the legacy registry before the native InferShape callback. | ||
| 85 | -// Its built-in implementation resizes weights to the rank of values, losing | ||
| 86 | -// dimensions (and elements). Infer only the output and preserve every input. | ||
| 87 | -COMMON_INFER_FUNC_REG(RaggedBinCount, RaggedBinCountLegacyInferShape); | ||
| 88 | -} // namespace ge | ||
| 89 | - | ||
| 90 | namespace ops { | 18 | namespace ops { |
| 91 | static ge::graphStatus InferDataTypeForRaggedBinCount(gert::InferDataTypeContext* context) | 19 | static ge::graphStatus InferDataTypeForRaggedBinCount(gert::InferDataTypeContext* context) |
| 92 | { | 20 | { |
| @@ -15,11 +15,11 @@ Only one registration key is needed: kernel and GEIR both resolve the snake-case | |||
| 15 | operator name and share :class:`RaggedBinCountKernelSpec`. The legacy | 15 | operator name and share :class:`RaggedBinCountKernelSpec`. The legacy |
| 16 | ``__golden__`` entry stays available and calls the same Torch computation core. | 16 | ``__golden__`` entry stays available and calls the same Torch computation core. |
| 17 | 17 | ||
| 18 | -The general reference math is a Torch competitor composition | 18 | +The reference uses ``torch.bincount`` per ragged row in the dtype supplied |
| 19 | -(``torch.bincount`` per ragged row). Fixed numerical-corner cases use an | 19 | +by TTK. All testcases use the same computation, including numerical corners; |
| 20 | -independent arbitrary-precision CPU oracle because even FP64 cannot retain | 20 | +the reference does not select a different algorithm from the testcase name. |
| 21 | -every FP32 cancellation gap. ``torch.bincount`` is semantically the per-row | 21 | +``torch.bincount`` is semantically the per-row form of the TensorFlow |
| 22 | -form of the TensorFlow ``RaggedBincount`` operator this IR is compatible with. | 22 | +``RaggedBincount`` operator this IR is compatible with. |
| 23 | 23 | ||
| 24 | Frozen contract points mirrored from ``README.md``: | 24 | Frozen contract points mirrored from ``README.md``: |
| 25 | * within the public rank-0-to-2 range, ``weights`` is judged by element count | 25 | * within the public rank-0-to-2 range, ``weights`` is judged by element count |
| @@ -33,8 +33,6 @@ Frozen contract points mirrored from ``README.md``: | |||
| 33 | bitwise ``+0.0`` and no weight is ever read. | 33 | bitwise ``+0.0`` and no weight is ever read. |
| 34 | """ | 34 | """ |
| 35 | 35 | ||
| 36 | -import math | ||
| 37 | - | ||
| 38 | import numpy as np | 36 | import numpy as np |
| 39 | import torch | 37 | import torch |
| 40 | 38 | ||
| @@ -50,23 +48,10 @@ __golden__ = { | |||
| 50 | 48 | ||
| 51 | 49 | ||
| 52 | _TOL = { | 50 | _TOL = { |
| 53 | - # The only native output dtype is FP32 (ragged_bin_count_def.cpp). | ||
| 54 | - # | ||
| 55 | - # ``cross_check`` compares the NPU and competitor against an FP64 | ||
| 56 | - # high-precision approximation (see :func:`_compute`); its ratio answers | ||
| 57 | - # "is the NPU within the same order of accuracy as a mature GPU | ||
| 58 | - # implementation" -- which is the question worth asking for | ||
| 59 | - # the legacy long-row and unbounded VALUE fallback paths, whose FP32 | ||
| 60 | - # summation order is not reproducible (``asc_atomic_add`` across multiple | ||
| 61 | - # AIV cores). The bounded short ROW and bounded VALUE paths instead use | ||
| 62 | - # fixed-order accumulation/reduction. | ||
| 63 | - # Fixed numerical-corner cases use the exact integer oracle below because | ||
| 64 | - # FP64 is not exact across every possible FP32 cancellation gap. | ||
| 65 | - # | ||
| 66 | - # This requires a reachable remote XPU endpoint to execute ``third_party``. | ||
| 67 | - # Runs without one must pass ``--compare close`` (or ``stat_rel_err``) | ||
| 68 | - # explicitly on the command line, which overrides this entry. | ||
| 69 | "float32": {"standard": "cross_check", "level": "L1"}, | 51 | "float32": {"standard": "cross_check", "level": "L1"}, |
| 52 | + # Integer input types are declared as well; the sole output is float32. | ||
| 53 | + "int32": {"standard": "binary_equal"}, | ||
| 54 | + "int64": {"standard": "binary_equal"}, | ||
| 70 | } | 55 | } |
| 71 | 56 | ||
| 72 | 57 | ||
| @@ -201,9 +186,9 @@ _PRECISION_SPECIAL_WEIGHTS = { | |||
| 201 | _FLOAT32_MAX, | 186 | _FLOAT32_MAX, |
| 202 | ], | 187 | ], |
| 203 | "rbc_k_precision_p0_negative_inf_i32": [-float("inf"), 1.0, 2.0, 3.0], | 188 | "rbc_k_precision_p0_negative_inf_i32": [-float("inf"), 1.0, 2.0, 3.0], |
| 204 | - # FP64 cannot retain the unit term across this cancellation. This case is | 189 | + # FP64 cannot retain the unit term across this cancellation. This input |
| 205 | - # therefore also a regression for the integer-superaccumulator oracle | 190 | + # probes the limit of the promoted reference too; a discrepancy requires |
| 206 | - # below, rather than relying on an FP64 approximation of the exact sum. | 191 | + # numerical analysis before attributing a failure to the device. |
| 207 | "rbc_k_precision_p0_twosum_fp64_gap_i64": [ | 192 | "rbc_k_precision_p0_twosum_fp64_gap_i64": [ |
| 208 | 2.0**100, | 193 | 2.0**100, |
| 209 | 1.0, | 194 | 1.0, |
| @@ -377,141 +362,6 @@ def _runtime_invalid(splits_flat, values_flat): | |||
| 377 | return False | 362 | return False |
| 378 | 363 | ||
| 379 | 364 | ||
| 380 | -def _float32_from_exact_units(total): | ||
| 381 | - """Round an integer multiple of 2^-149 to FP32, ties to even. | ||
| 382 | - | ||
| 383 | - Every finite FP32 input is exactly representable in these units. Keeping | ||
| 384 | - the sum as a Python integer avoids the cancellation gap of an FP64 oracle | ||
| 385 | - (for example 2^100 + 1 - 2^100) and makes the special precision cases an | ||
| 386 | - independent correctly-rounded reference for the device superaccumulator. | ||
| 387 | - """ | ||
| 388 | - negative = total < 0 | ||
| 389 | - magnitude = -total if negative else total | ||
| 390 | - sign_bit = 0x80000000 if negative else 0 | ||
| 391 | - if magnitude == 0: | ||
| 392 | - bits = 0 | ||
| 393 | - else: | ||
| 394 | - highest_bit = magnitude.bit_length() - 1 | ||
| 395 | - if highest_bit <= 22: | ||
| 396 | - bits = sign_bit | magnitude | ||
| 397 | - else: | ||
| 398 | - right_shift = highest_bit - 23 | ||
| 399 | - significand = magnitude >> right_shift | ||
| 400 | - if right_shift != 0: | ||
| 401 | - remainder = magnitude & ((1 << right_shift) - 1) | ||
| 402 | - halfway = 1 << (right_shift - 1) | ||
| 403 | - if remainder > halfway or ( | ||
| 404 | - remainder == halfway and (significand & 1) != 0 | ||
| 405 | - ): | ||
| 406 | - significand += 1 | ||
| 407 | - if significand == 1 << 24: | ||
| 408 | - significand >>= 1 | ||
| 409 | - highest_bit += 1 | ||
| 410 | - exponent = highest_bit - 22 | ||
| 411 | - if exponent >= 255: | ||
| 412 | - bits = sign_bit | 0x7F800000 | ||
| 413 | - else: | ||
| 414 | - bits = sign_bit | (exponent << 23) | (significand & 0x007FFFFF) | ||
| 415 | - return np.asarray([bits], dtype=np.uint32).view(np.float32)[0] | ||
| 416 | - | ||
| 417 | - | ||
| 418 | -def _float64_from_exact_units(total): | ||
| 419 | - """Round an integer multiple of 2^-149 to FP64, ties to even.""" | ||
| 420 | - negative = total < 0 | ||
| 421 | - magnitude = -total if negative else total | ||
| 422 | - sign_bit = 0x8000000000000000 if negative else 0 | ||
| 423 | - if magnitude == 0: | ||
| 424 | - bits = 0 | ||
| 425 | - else: | ||
| 426 | - highest_bit = magnitude.bit_length() - 1 | ||
| 427 | - if highest_bit <= 52: | ||
| 428 | - significand = magnitude << (52 - highest_bit) | ||
| 429 | - else: | ||
| 430 | - right_shift = highest_bit - 52 | ||
| 431 | - significand = magnitude >> right_shift | ||
| 432 | - remainder = magnitude & ((1 << right_shift) - 1) | ||
| 433 | - halfway = 1 << (right_shift - 1) | ||
| 434 | - if remainder > halfway or (remainder == halfway and (significand & 1) != 0): | ||
| 435 | - significand += 1 | ||
| 436 | - if significand == 1 << 53: | ||
| 437 | - significand >>= 1 | ||
| 438 | - highest_bit += 1 | ||
| 439 | - unbiased_exponent = highest_bit - 149 | ||
| 440 | - if unbiased_exponent > 1023: | ||
| 441 | - bits = sign_bit | 0x7FF0000000000000 | ||
| 442 | - else: | ||
| 443 | - exponent = unbiased_exponent + 1023 | ||
| 444 | - bits = sign_bit | (exponent << 52) | (significand & 0x000FFFFFFFFFFFFF) | ||
| 445 | - return np.asarray([bits], dtype=np.uint64).view(np.float64)[0] | ||
| 446 | - | ||
| 447 | - | ||
| 448 | -def _canonical_nan(result_dtype): | ||
| 449 | - if np.dtype(result_dtype) == np.dtype(np.float64): | ||
| 450 | - return np.asarray([0x7FF8000000000000], dtype=np.uint64).view(np.float64)[0] | ||
| 451 | - return np.asarray([0x7FC00000], dtype=np.uint32).view(np.float32)[0] | ||
| 452 | - | ||
| 453 | - | ||
| 454 | -def _exact_sum(values, result_dtype): | ||
| 455 | - """Return the exact FP32-input sum rounded once to ``result_dtype``.""" | ||
| 456 | - result_dtype = np.dtype(result_dtype) | ||
| 457 | - if result_dtype not in (np.dtype(np.float32), np.dtype(np.float64)): | ||
| 458 | - raise ValueError(f"Unsupported exact-reference dtype: {result_dtype}") | ||
| 459 | - | ||
| 460 | - total = 0 | ||
| 461 | - has_nan = False | ||
| 462 | - has_positive_inf = False | ||
| 463 | - has_negative_inf = False | ||
| 464 | - for raw_value in np.asarray(values).reshape(-1): | ||
| 465 | - value = float(raw_value) | ||
| 466 | - if math.isnan(value): | ||
| 467 | - has_nan = True | ||
| 468 | - continue | ||
| 469 | - if math.isinf(value): | ||
| 470 | - if value > 0: | ||
| 471 | - has_positive_inf = True | ||
| 472 | - else: | ||
| 473 | - has_negative_inf = True | ||
| 474 | - continue | ||
| 475 | - | ||
| 476 | - numerator, denominator = value.as_integer_ratio() | ||
| 477 | - denominator_shift = denominator.bit_length() - 1 | ||
| 478 | - unit_shift = 149 - denominator_shift | ||
| 479 | - if unit_shift >= 0: | ||
| 480 | - total += numerator << unit_shift | ||
| 481 | - else: | ||
| 482 | - divisor = 1 << (-unit_shift) | ||
| 483 | - if numerator % divisor != 0: | ||
| 484 | - raise ValueError("Exact-reference input is not on the FP32 value grid") | ||
| 485 | - total += numerator // divisor | ||
| 486 | - | ||
| 487 | - if has_nan or (has_positive_inf and has_negative_inf): | ||
| 488 | - return _canonical_nan(result_dtype) | ||
| 489 | - if has_positive_inf: | ||
| 490 | - return result_dtype.type(float("inf")) | ||
| 491 | - if has_negative_inf: | ||
| 492 | - return result_dtype.type(-float("inf")) | ||
| 493 | - if result_dtype == np.dtype(np.float64): | ||
| 494 | - return _float64_from_exact_units(total) | ||
| 495 | - return _float32_from_exact_units(total) | ||
| 496 | - | ||
| 497 | - | ||
| 498 | -def _compute_exact_weighted_reference( | ||
| 499 | - splits_flat, values_flat, bins, weights_flat, result_dtype | ||
| 500 | -): | ||
| 501 | - """Independent exact oracle for the fixed numerical-corner payloads.""" | ||
| 502 | - rows = max(splits_flat.numel() - 1, 0) | ||
| 503 | - result = np.zeros((rows, bins), dtype=result_dtype) | ||
| 504 | - values_np = values_flat.cpu().numpy() | ||
| 505 | - weights_np = weights_flat.cpu().numpy() | ||
| 506 | - for row in range(rows): | ||
| 507 | - begin = int(splits_flat[row].item()) | ||
| 508 | - end = int(splits_flat[row + 1].item()) | ||
| 509 | - for bin_index in range(bins): | ||
| 510 | - selected = weights_np[begin:end][values_np[begin:end] == bin_index] | ||
| 511 | - result[row, bin_index] = _exact_sum(selected, result_dtype) | ||
| 512 | - return torch.from_numpy(result) | ||
| 513 | - | ||
| 514 | - | ||
| 515 | def _compute(splits, values, size, weights, **kwargs): | 365 | def _compute(splits, values, size, weights, **kwargs): |
| 516 | """Return ``[output]`` as a Torch tensor in def.cpp output order.""" | 366 | """Return ``[output]`` as a Torch tensor in def.cpp output order.""" |
| 517 | binary_output = _attr(kwargs, "binary_output", False) | 367 | binary_output = _attr(kwargs, "binary_output", False) |
| @@ -521,49 +371,17 @@ def _compute(splits, values, size, weights, **kwargs): | |||
| 521 | 371 | ||
| 522 | rows = max(splits_flat.numel() - 1, 0) | 372 | rows = max(splits_flat.numel() - 1, 0) |
| 523 | bins = int(size_flat[0].item()) if size_flat.numel() > 0 else 0 | 373 | bins = int(size_flat[0].item()) if size_flat.numel() > 0 else 0 |
| 524 | - output = torch.zeros((rows, bins), dtype=torch.float32) | 374 | + output = torch.zeros((rows, bins), dtype=weights_flat.dtype) |
| 525 | 375 | ||
| 526 | # Invalid input returns bitwise +0.0 before any weight is read. | 376 | # Invalid input returns bitwise +0.0 before any weight is read. |
| 527 | if _runtime_invalid(splits_flat, values_flat) or rows == 0 or bins == 0: | 377 | if _runtime_invalid(splits_flat, values_flat) or rows == 0 or bins == 0: |
| 528 | return [output] | 378 | return [output] |
| 529 | 379 | ||
| 530 | has_weights = weights_flat.numel() > 0 | 380 | has_weights = weights_flat.numel() > 0 |
| 531 | - testcase_name = str(kwargs.get("testcase_name", "")) | 381 | + # Float32 is the NPU accumulation type. Under Promote, TTK supplies |
| 532 | - if ( | 382 | + # float64 weights; preserve that dtype without additional casts. |
| 533 | - has_weights | 383 | + dtype = weights_flat.dtype |
| 534 | - and not binary_output | 384 | + accumulator = torch.zeros((rows, bins), dtype=dtype) |
| 535 | - and testcase_name in _PRECISION_SPECIAL_WEIGHTS | ||
| 536 | - ): | ||
| 537 | - output_dtypes = _output_dtype_names(kwargs) | ||
| 538 | - result_dtype = ( | ||
| 539 | - np.dtype(output_dtypes[0]) if output_dtypes else np.dtype(np.float32) | ||
| 540 | - ) | ||
| 541 | - return [ | ||
| 542 | - _compute_exact_weighted_reference( | ||
| 543 | - splits_flat, values_flat, bins, weights_flat, result_dtype | ||
| 544 | - ) | ||
| 545 | - ] | ||
| 546 | - | ||
| 547 | - # Accumulate in FP64 and leave the final dtype decision to | ||
| 548 | - # ``_kernel_golden``. Under ``cross_check`` TTK promotes | ||
| 549 | - # ``output_dtypes`` from FP32 to FP64, so the three-way gate must retain | ||
| 550 | - # this accumulator without an intermediate FP32 rounding. Under | ||
| 551 | - # ``--compare close`` the adapter casts it exactly once to the declared | ||
| 552 | - # FP32 interface dtype. | ||
| 553 | - # | ||
| 554 | - # The legacy long-row and | ||
| 555 | - # VALUE fallback NPU paths use ``asc_atomic_add`` across multiple cores, | ||
| 556 | - # while the competitor has its own FP32 order; both can therefore be | ||
| 557 | - # reorderings of the same exact sum. The bounded precise ROW/VALUE paths | ||
| 558 | - # are fixed-order, but use this same independent high-precision reference. | ||
| 559 | - # Comparing fallback outputs against another FP32 reordering would only | ||
| 560 | - # measure "how differently did these two reorder", which is not a precision | ||
| 561 | - # statement about either. The unrounded FP64 accumulation is the | ||
| 562 | - # high-precision approximation used by the three-way gate. | ||
| 563 | - # It is not an exact oracle for arbitrarily wide cancellation gaps; fixed | ||
| 564 | - # numerical-corner payloads take the integer-superaccumulator path above. | ||
| 565 | - accumulator = torch.zeros((rows, bins), dtype=torch.float64) | ||
| 566 | - weights_fp64 = weights_flat.to(torch.float64) if has_weights else None | ||
| 567 | 385 | ||
| 568 | for row in range(rows): | 386 | for row in range(rows): |
| 569 | begin = int(splits_flat[row].item()) | 387 | begin = int(splits_flat[row].item()) |
| @@ -578,39 +396,21 @@ def _compute(splits, values, size, weights, **kwargs): | |||
| 578 | # Bitwise exact 0/1; weights are never read on this path. | 396 | # Bitwise exact 0/1; weights are never read on this path. |
| 579 | accumulator[row] = accumulator[row].index_fill(0, kept, 1.0) | 397 | accumulator[row] = accumulator[row].index_fill(0, kept, 1.0) |
| 580 | elif has_weights: | 398 | elif has_weights: |
| 581 | - segment_weights = weights_fp64[begin:end][keep] | 399 | + segment_weights = weights_flat[begin:end][keep] |
| 582 | accumulator[row] = torch.bincount( | 400 | accumulator[row] = torch.bincount( |
| 583 | kept, weights=segment_weights, minlength=bins | 401 | kept, weights=segment_weights, minlength=bins |
| 584 | )[:bins] | 402 | )[:bins] |
| 585 | else: | 403 | else: |
| 586 | - accumulator[row] = torch.bincount(kept, minlength=bins)[:bins].to( | 404 | + # Integer hit counts are assigned into the declared accumulator. |
| 587 | - torch.float64 | 405 | + accumulator[row] = torch.bincount(kept, minlength=bins)[:bins] |
| 588 | - ) | ||
| 589 | 406 | ||
| 590 | return [accumulator] | 407 | return [accumulator] |
| 591 | 408 | ||
| 592 | 409 | ||
| 593 | -def _output_dtype_names(kwargs): | ||
| 594 | - output_dtypes = kwargs.get("output_dtypes") or [] | ||
| 595 | - names = [] | ||
| 596 | - for dtype in output_dtypes: | ||
| 597 | - if isinstance(dtype, (list, tuple)): | ||
| 598 | - dtype = dtype[0] | ||
| 599 | - names.append(str(dtype)) | ||
| 600 | - return names | ||
| 601 | - | ||
| 602 | - | ||
| 603 | def _kernel_golden(splits, values, size, weights, **kwargs): | 410 | def _kernel_golden(splits, values, size, weights, **kwargs): |
| 604 | """Kernel/GEIR container adapter: NumPy in, NumPy list out.""" | 411 | """Kernel/GEIR container adapter: NumPy in, NumPy list out.""" |
| 605 | outputs = _compute(splits, values, size, weights, **kwargs) | 412 | outputs = _compute(splits, values, size, weights, **kwargs) |
| 606 | - output_dtypes = _output_dtype_names(kwargs) | 413 | + return [np.ascontiguousarray(output.detach().cpu().numpy()) for output in outputs] |
| 607 | - result = [] | ||
| 608 | - for index, output in enumerate(outputs): | ||
| 609 | - array = output.detach().cpu().contiguous().numpy() | ||
| 610 | - if index < len(output_dtypes): | ||
| 611 | - array = array.astype(output_dtypes[index], copy=False) | ||
| 612 | - result.append(np.ascontiguousarray(array)) | ||
| 613 | - return result | ||
| 614 | 414 | ||
| 615 | 415 | ||
| 616 | def _competitor_forward(splits_flat, values_flat, bins, weights_flat, binary_output): | 416 | def _competitor_forward(splits_flat, values_flat, bins, weights_flat, binary_output): |
| @@ -652,7 +452,7 @@ class _RaggedBinCountCompose: | |||
| 652 | 452 | ||
| 653 | * It must not share an implementation with :func:`_compute`, or the two | 453 | * It must not share an implementation with :func:`_compute`, or the two |
| 654 | reference paths cannot cross-validate each other. ``_compute`` is a | 454 | reference paths cannot cross-validate each other. ``_compute`` is a |
| 655 | - per-row ``torch.bincount`` accumulating in FP64; this is a single flat | 455 | + per-row ``torch.bincount`` in the TTK-supplied dtype; this is a single flat |
| 656 | ``index_add_`` in FP32 over a ``repeat_interleave``-derived row index. | 456 | ``index_add_`` in FP32 over a ``repeat_interleave``-derived row index. |
| 657 | * It must be the competitor's **optimal** form, or the G/N ratio is | 457 | * It must be the competitor's **optimal** form, or the G/N ratio is |
| 658 | inflated and the comparison is not an honest one. The whole ragged | 458 | inflated and the comparison is not an honest one. The whole ragged |
| @@ -711,14 +511,10 @@ class RaggedBinCountKernelSpec: | |||
| 711 | def ragged_bin_count_golden(splits, values, size, weights, *args, **kwargs): | 511 | def ragged_bin_count_golden(splits, values, size, weights, *args, **kwargs): |
| 712 | """Compatibility entry for the historical ``__golden__`` kernel loader.""" | 512 | """Compatibility entry for the historical ``__golden__`` kernel loader.""" |
| 713 | del args | 513 | del args |
| 714 | - legacy_kwargs = dict(kwargs) | 514 | + return _kernel_golden(splits, values, size, weights, **kwargs)[0] |
| 715 | - if not legacy_kwargs.get("output_dtypes"): | ||
| 716 | - # The old entry predates TestSpec and promises the fixed FP32 output. | ||
| 717 | - legacy_kwargs["output_dtypes"] = [["float32"]] | ||
| 718 | - return _kernel_golden(splits, values, size, weights, **legacy_kwargs)[0] | ||
| 719 | 515 | ||
| 720 | 516 | ||
| 721 | # 【不存在】ACLNN 通路:CMakeLists.txt 显式配置 ``ACLNNTYPE aclnn_exclude``, | 517 | # 【不存在】ACLNN 通路:CMakeLists.txt 显式配置 ``ACLNNTYPE aclnn_exclude``, |
| 722 | -# 本算子既无 op_host/op_api 目录,也不交付 aclnnRaggedBinCount 符号。 | 518 | +# 本算子无 op_api 目录,不交付 aclnnRaggedBinCount 接口。 |
| 723 | -# 【不存在】e2e 通路:获批支持面为 Graph IR only(README.md 调用说明), | 519 | +# E2E 暂未注册:本文件验证 README 声明的 Graph IR 路径,没有确认独立框架 |
| 724 | -# torch_npu 二进制中无本算子的 aclnn 绑定入口。 | 520 | +# API 的派发绑定;缺少同名 ACLNN 符号不能证明所有框架通路均不存在。 |
| @@ -16,6 +16,6 @@ rbc_x_key_val_b1_w0_int64_01,ragged_bin_count,"((2,),(1025,),(1,),(0,))","('int6 | |||
| 16 | rbc_x_key_val_b1_w1_int32_01,ragged_bin_count,"((2,),(1025,),(1,),(1025,))","('int64','int32','int32','float32')","((1,4),)","('float32',)","{'splits': [0,1025], 'size': [4], 'binary_output': True}","((None,None),(0,7),(None,None),(-2.0,-0.25))","((0,0),)",0,key=7 tiling-matrix val | 16 | rbc_x_key_val_b1_w1_int32_01,ragged_bin_count,"((2,),(1025,),(1,),(1025,))","('int64','int32','int32','float32')","((1,4),)","('float32',)","{'splits': [0,1025], 'size': [4], 'binary_output': True}","((None,None),(0,7),(None,None),(-2.0,-0.25))","((0,0),)",0,key=7 tiling-matrix val |
| 17 | rbc_x_key_val_b1_w1_int64_01,ragged_bin_count,"((2,),(1025,),(1,),(1025,))","('int64','int64','int64','float32')","((1,4),)","('float32',)","{'splits': [0,1025], 'size': [4], 'binary_output': True}","((None,None),(0,7),(None,None),(-2.0,-0.25))","((0,0),)",0,key=7 tiling-matrix val | 17 | rbc_x_key_val_b1_w1_int64_01,ragged_bin_count,"((2,),(1025,),(1,),(1025,))","('int64','int64','int64','float32')","((1,4),)","('float32',)","{'splits': [0,1025], 'size': [4], 'binary_output': True}","((None,None),(0,7),(None,None),(-2.0,-0.25))","((0,0),)",0,key=7 tiling-matrix val |
| 18 | rbc_k_edge_bins0_w_32,ragged_bin_count,"((3,),(8,),(1,),(8,))","('int64','int32','int32','float32')","((2,0),)","('float32',)","{'splits': [0,3,8], 'size': [0], 'binary_output': False}","((None,None),(0,5),(None,None),(0.25,2.0))","((0,0),)",0,key=1 size=0 empty output with weights | 18 | rbc_k_edge_bins0_w_32,ragged_bin_count,"((3,),(8,),(1,),(8,))","('int64','int32','int32','float32')","((2,0),)","('float32',)","{'splits': [0,3,8], 'size': [0], 'binary_output': False}","((None,None),(0,5),(None,None),(0.25,2.0))","((0,0),)",0,key=1 size=0 empty output with weights |
| 19 | -rbc_k_edge_weights_flat_vs_2d_32,ragged_bin_count,"((3,),(2,3),(1,),(6,))","('int64','int32','int32','float32')","((2,4),)","('float32',)","{'splits': [0,3,6], 'size': [4], 'binary_output': False}","((None,None),(0,3),(None,None),(0.25,2.0))","((0.0001,0),)",1e-06,key=1 rank-1 weights against 2D values -- same numel, supported ranks need not match | 19 | +rbc_k_edge_weights_flat_vs_2d_32,ragged_bin_count,"((3,),(2,3),(1,),(6,))","('int64','int32','int32','float32')","((2,4),)","('float32',)","{'splits': [0,3,6], 'size': [4], 'binary_output': False}","((None,None),(0,3),(None,None),(0.25,2.0))","((0.0001,0),)",1e-06,"key=1 rank-1 weights against 2D values -- same numel, supported ranks need not match" |
| 20 | rbc_k_edge_weights_empty_2d_32,ragged_bin_count,"((3,),(2,3),(1,),(0,3))","('int64','int32','int32','float32')","((2,4),)","('float32',)","{'splits': [0,3,6], 'size': [4], 'binary_output': False}","((None,None),(0,3),(None,None),(None,None))","((0,0),)",0,key=0 zero-element weights written as [0 3] means no weights | 20 | rbc_k_edge_weights_empty_2d_32,ragged_bin_count,"((3,),(2,3),(1,),(0,3))","('int64','int32','int32','float32')","((2,4),)","('float32',)","{'splits': [0,3,6], 'size': [4], 'binary_output': False}","((None,None),(0,3),(None,None),(None,None))","((0,0),)",0,key=0 zero-element weights written as [0 3] means no weights |
| 21 | rbc_k_contract_binary_output_default_32,ragged_bin_count,"((3,),(2,3),(1,),(6,))","('int64','int32','int32','float32')","((2,4),)","('float32',)","{'splits': [0,3,6], 'size': [4]}","((None,None),(0,3),(None,None),(0.25,2.0))","((0.0001,0),)",1e-06,contract: omitted binary_output uses default false | 21 | rbc_k_contract_binary_output_default_32,ragged_bin_count,"((3,),(2,3),(1,),(6,))","('int64','int32','int32','float32')","((2,4),)","('float32',)","{'splits': [0,3,6], 'size': [4]}","((None,None),(0,3),(None,None),(0.25,2.0))","((0.0001,0),)",1e-06,contract: omitted binary_output uses default false |
| @@ -1,168 +0,0 @@ | |||
| 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 <cstdint> | ||
| 12 | -#include <vector> | ||
| 13 | - | ||
| 14 | -#include "gtest/gtest.h" | ||
| 15 | -#include "../../../op_graph/ragged_bin_count_proto.h" | ||
| 16 | - | ||
| 17 | -namespace { | ||
| 18 | -ge::TensorDesc MakeDesc(const std::vector<int64_t>& dims, ge::DataType dtype) | ||
| 19 | -{ | ||
| 20 | - ge::TensorDesc desc(ge::Shape(dims), ge::FORMAT_ND, dtype); | ||
| 21 | - desc.SetOriginShape(ge::Shape(dims)); | ||
| 22 | - desc.SetOriginFormat(ge::FORMAT_ND); | ||
| 23 | - return desc; | ||
| 24 | -} | ||
| 25 | - | ||
| 26 | -class SizeConstant : public ge::Operator { | ||
| 27 | -public: | ||
| 28 | - explicit SizeConstant(const ge::Tensor& value) : ge::Operator("size_constant", "Const") | ||
| 29 | - { | ||
| 30 | - OutputRegister("y"); | ||
| 31 | - UpdateOutputDesc("y", value.GetTensorDesc()); | ||
| 32 | - SetAttr("value", value); | ||
| 33 | - } | ||
| 34 | -}; | ||
| 35 | - | ||
| 36 | -ge::op::RaggedBinCount MakeOp(const std::vector<int64_t>& values, const std::vector<int64_t>& weights, | ||
| 37 | - ge::DataType indexType = ge::DT_INT32, const std::vector<int64_t>& splits = {2}) | ||
| 38 | -{ | ||
| 39 | - ge::op::RaggedBinCount op("ragged_bin_count_legacy_infer"); | ||
| 40 | - EXPECT_EQ(op.update_input_desc_splits(MakeDesc(splits, ge::DT_INT64)), ge::GRAPH_SUCCESS); | ||
| 41 | - EXPECT_EQ(op.update_input_desc_values(MakeDesc(values, indexType)), ge::GRAPH_SUCCESS); | ||
| 42 | - EXPECT_EQ(op.update_input_desc_size(MakeDesc({1}, indexType)), ge::GRAPH_SUCCESS); | ||
| 43 | - EXPECT_EQ(op.update_input_desc_weights(MakeDesc(weights, ge::DT_FLOAT)), ge::GRAPH_SUCCESS); | ||
| 44 | - op.update_output_desc_output(ge::TensorDesc(ge::Shape(), ge::FORMAT_ND, ge::DT_FLOAT)); | ||
| 45 | - EXPECT_EQ(op.get_input_desc_splits().GetShape().GetDims(), splits); | ||
| 46 | - EXPECT_EQ(op.get_input_desc_values().GetShape().GetDims(), values); | ||
| 47 | - EXPECT_EQ(op.get_input_desc_size().GetShape().GetDims(), (std::vector<int64_t>{1})); | ||
| 48 | - EXPECT_EQ(op.get_input_desc_weights().GetShape().GetDims(), weights); | ||
| 49 | - return op; | ||
| 50 | -} | ||
| 51 | - | ||
| 52 | -void CheckPreservedShapes(const std::vector<int64_t>& values, const std::vector<int64_t>& weights) | ||
| 53 | -{ | ||
| 54 | - auto op = MakeOp(values, weights); | ||
| 55 | - ASSERT_EQ(op.InferShapeAndType(), ge::GRAPH_SUCCESS); | ||
| 56 | - EXPECT_EQ(op.get_input_desc_values().GetShape().GetDims(), values); | ||
| 57 | - EXPECT_EQ(op.get_input_desc_weights().GetShape().GetDims(), weights); | ||
| 58 | - EXPECT_EQ(op.get_output_desc_output().GetShape().GetDims(), (std::vector<int64_t>{1, -1})); | ||
| 59 | - EXPECT_EQ(op.get_output_desc_output().GetDataType(), ge::DT_FLOAT); | ||
| 60 | -} | ||
| 61 | - | ||
| 62 | -template <typename T> | ||
| 63 | -void CheckConstantSize(ge::DataType indexType, T bins, ge::graphStatus expected) | ||
| 64 | -{ | ||
| 65 | - auto op = MakeOp({6}, {2, 3}, indexType); | ||
| 66 | - const ge::TensorDesc desc = MakeDesc({1}, indexType); | ||
| 67 | - const ge::Tensor tensor(desc, reinterpret_cast<const uint8_t*>(&bins), sizeof(bins)); | ||
| 68 | - SizeConstant constant(tensor); | ||
| 69 | - op.set_input_size(constant, "y"); | ||
| 70 | - ASSERT_EQ(op.InferShapeAndType(), expected); | ||
| 71 | - if (expected == ge::GRAPH_SUCCESS) { | ||
| 72 | - EXPECT_EQ(op.get_output_desc_output().GetShape().GetDims(), | ||
| 73 | - (std::vector<int64_t>{1, static_cast<int64_t>(bins)})); | ||
| 74 | - EXPECT_EQ(op.get_input_desc_weights().GetShape().GetDims(), (std::vector<int64_t>{2, 3})); | ||
| 75 | - } | ||
| 76 | -} | ||
| 77 | -} // namespace | ||
| 78 | - | ||
| 79 | -TEST(RaggedBinCountLegacyInferTest, PreservesTwoDimensionalWeightsForVectorValues) | ||
| 80 | -{ | ||
| 81 | - CheckPreservedShapes({6}, {2, 3}); | ||
| 82 | -} | ||
| 83 | - | ||
| 84 | -TEST(RaggedBinCountLegacyInferTest, PreservesVectorWeightsForTwoDimensionalValues) | ||
| 85 | -{ | ||
| 86 | - CheckPreservedShapes({2, 3}, {6}); | ||
| 87 | -} | ||
| 88 | - | ||
| 89 | -TEST(RaggedBinCountLegacyInferTest, PreservesDifferentTwoDimensionalShapes) { CheckPreservedShapes({2, 3}, {3, 2}); } | ||
| 90 | - | ||
| 91 | -TEST(RaggedBinCountLegacyInferTest, PreservesEmptyWeights) | ||
| 92 | -{ | ||
| 93 | - for (const auto& weights : std::vector<std::vector<int64_t>>{{0}, {0, 3}, {2, 0}}) { | ||
| 94 | - CheckPreservedShapes({2, 3}, weights); | ||
| 95 | - } | ||
| 96 | -} | ||
| 97 | - | ||
| 98 | -TEST(RaggedBinCountLegacyInferTest, PreservesScalarWeights) { CheckPreservedShapes({1}, {}); } | ||
| 99 | - | ||
| 100 | -TEST(RaggedBinCountLegacyInferTest, ReadsInt32ConstantSize) | ||
| 101 | -{ | ||
| 102 | - CheckConstantSize<int32_t>(ge::DT_INT32, 7, ge::GRAPH_SUCCESS); | ||
| 103 | -} | ||
| 104 | - | ||
| 105 | -TEST(RaggedBinCountLegacyInferTest, ReadsInt64ConstantSizeWithoutTruncation) | ||
| 106 | -{ | ||
| 107 | - CheckConstantSize<int64_t>(ge::DT_INT64, (int64_t{1} << 32) + 7, ge::GRAPH_SUCCESS); | ||
| 108 | -} | ||
| 109 | - | ||
| 110 | -TEST(RaggedBinCountLegacyInferTest, AcceptsZeroBins) { CheckConstantSize<int64_t>(ge::DT_INT64, 0, ge::GRAPH_SUCCESS); } | ||
| 111 | - | ||
| 112 | -TEST(RaggedBinCountLegacyInferTest, RejectsNegativeBins) | ||
| 113 | -{ | ||
| 114 | - CheckConstantSize<int32_t>(ge::DT_INT32, -1, ge::GRAPH_FAILED); | ||
| 115 | - CheckConstantSize<int64_t>(ge::DT_INT64, -1, ge::GRAPH_FAILED); | ||
| 116 | -} | ||
| 117 | - | ||
| 118 | -TEST(RaggedBinCountLegacyInferTest, RetainsKnownRowsWhenSizeIsUnknown) | ||
| 119 | -{ | ||
| 120 | - auto op = MakeOp({6}, {2, 3}, ge::DT_INT32, {4}); | ||
| 121 | - ASSERT_EQ(op.InferShapeAndType(), ge::GRAPH_SUCCESS); | ||
| 122 | - EXPECT_EQ(op.get_output_desc_output().GetShape().GetDims(), (std::vector<int64_t>{3, -1})); | ||
| 123 | - std::vector<std::pair<int64_t, int64_t>> ranges; | ||
| 124 | - ASSERT_EQ(op.get_output_desc_output().GetShapeRange(ranges), ge::GRAPH_SUCCESS); | ||
| 125 | - EXPECT_EQ(ranges, (std::vector<std::pair<int64_t, int64_t>>{{3, 3}, {0, -1}})); | ||
| 126 | -} | ||
| 127 | - | ||
| 128 | -TEST(RaggedBinCountLegacyInferTest, PreservesUnknownInputDimensions) | ||
| 129 | -{ | ||
| 130 | - auto op = MakeOp({-1}, {-1, 3}, ge::DT_INT32, {-1}); | ||
| 131 | - ASSERT_EQ(op.InferShapeAndType(), ge::GRAPH_SUCCESS); | ||
| 132 | - EXPECT_EQ(op.get_input_desc_weights().GetShape().GetDims(), (std::vector<int64_t>{-1, 3})); | ||
| 133 | - EXPECT_EQ(op.get_output_desc_output().GetShape().GetDims(), (std::vector<int64_t>{-1, -1})); | ||
| 134 | -} | ||
| 135 | - | ||
| 136 | -TEST(RaggedBinCountLegacyInferTest, PreservesUnknownInputRanks) | ||
| 137 | -{ | ||
| 138 | - auto op = MakeOp({-2}, {-2}, ge::DT_INT32, {-2}); | ||
| 139 | - ASSERT_EQ(op.InferShapeAndType(), ge::GRAPH_SUCCESS); | ||
| 140 | - EXPECT_EQ(op.get_input_desc_weights().GetShape().GetDims(), (std::vector<int64_t>{-2})); | ||
| 141 | - EXPECT_EQ(op.get_output_desc_output().GetShape().GetDims(), (std::vector<int64_t>{-1, -1})); | ||
| 142 | -} | ||
| 143 | - | ||
| 144 | -TEST(RaggedBinCountLegacyInferTest, PreservesLegacyOutputDataTypes) | ||
| 145 | -{ | ||
| 146 | - for (const auto dtype : {ge::DT_INT32, ge::DT_INT64, ge::DT_FLOAT, ge::DT_DOUBLE}) { | ||
| 147 | - auto op = MakeOp({6}, {2, 3}); | ||
| 148 | - ASSERT_EQ(op.update_input_desc_weights(MakeDesc({2, 3}, dtype)), ge::GRAPH_SUCCESS); | ||
| 149 | - ASSERT_EQ(op.InferShapeAndType(), ge::GRAPH_SUCCESS); | ||
| 150 | - EXPECT_EQ(op.get_output_desc_output().GetDataType(), dtype); | ||
| 151 | - } | ||
| 152 | -} | ||
| 153 | - | ||
| 154 | -TEST(RaggedBinCountLegacyInferTest, RejectsInvalidSplitsAndSizeShapes) | ||
| 155 | -{ | ||
| 156 | - for (const auto& splits : std::vector<std::vector<int64_t>>{{}, {1}, {2, 1}}) { | ||
| 157 | - auto op = MakeOp({6}, {2, 3}, ge::DT_INT32, splits); | ||
| 158 | - EXPECT_EQ(op.InferShapeAndType(), ge::GRAPH_FAILED); | ||
| 159 | - } | ||
| 160 | - auto op = MakeOp({6}, {2, 3}); | ||
| 161 | - op.update_input_desc_size(ge::TensorDesc(ge::Shape(), ge::FORMAT_ND, ge::DT_INT32)); | ||
| 162 | - EXPECT_EQ(op.InferShapeAndType(), ge::GRAPH_FAILED); | ||
| 163 | - for (const int64_t sizeElements : {int64_t{1} << 31, int64_t{1} << 32}) { | ||
| 164 | - auto oversizedSizeOp = MakeOp({6}, {2, 3}); | ||
| 165 | - ASSERT_EQ(oversizedSizeOp.update_input_desc_size(MakeDesc({sizeElements}, ge::DT_INT32)), ge::GRAPH_SUCCESS); | ||
| 166 | - EXPECT_EQ(oversizedSizeOp.InferShapeAndType(), ge::GRAPH_FAILED); | ||
| 167 | - } | ||
| 168 | -} | ||
| @@ -0,0 +1,140 @@ | |||
| 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 <gtest/gtest.h> | ||
| 12 | +#include "base/context_builder/op_infer_shape_context_builder.h" | ||
| 13 | +#include "base/registry/op_impl_space_registry_v2.h" | ||
| 14 | +#include "exe_graph/runtime/runtime_tensor.h" | ||
| 15 | + | ||
| 16 | +namespace { | ||
| 17 | +gert::StorageShape MakeStorageShape(const gert::Shape& shape) | ||
| 18 | +{ | ||
| 19 | + gert::StorageShape storage; | ||
| 20 | + storage.MutableOriginShape() = shape; | ||
| 21 | + storage.MutableStorageShape() = shape; | ||
| 22 | + return storage; | ||
| 23 | +} | ||
| 24 | + | ||
| 25 | +void CheckRegisteredInfer(const gert::Shape& valuesShape, const gert::Shape& weightsShape, const gert::Shape& expected, | ||
| 26 | + const gert::Shape& splitsShape = {2}, const gert::Shape& sizeShape = {1}, | ||
| 27 | + ge::DataType indexType = ge::DT_INT32, void* sizeData = nullptr, | ||
| 28 | + ge::graphStatus status = ge::GRAPH_SUCCESS) | ||
| 29 | +{ | ||
| 30 | + auto registry = gert::DefaultOpImplSpaceRegistryV2::GetInstance().GetSpaceRegistry(); | ||
| 31 | + ASSERT_NE(registry, nullptr); | ||
| 32 | + const auto* impl = registry->GetOpImpl("RaggedBinCount"); | ||
| 33 | + ASSERT_NE(impl, nullptr); | ||
| 34 | + ASSERT_NE(impl->infer_shape, nullptr); | ||
| 35 | + EXPECT_TRUE(impl->IsInputDataDependency(2U)); | ||
| 36 | + const gert::StorageFormat format(ge::FORMAT_ND, ge::FORMAT_ND, {}); | ||
| 37 | + gert::Tensor splits(MakeStorageShape(splitsShape), format, ge::DT_INT64); | ||
| 38 | + gert::Tensor values(MakeStorageShape(valuesShape), format, indexType); | ||
| 39 | + gert::Tensor size(MakeStorageShape(sizeShape), format, gert::TensorPlacement::kOnHost, indexType, sizeData); | ||
| 40 | + gert::Tensor weights(MakeStorageShape(weightsShape), format, ge::DT_FLOAT); | ||
| 41 | + gert::OpInferShapeContextBuilder builder; | ||
| 42 | + builder.OpType("RaggedBinCount").OpName("ragged_bin_count_registered").IONum(4, 1); | ||
| 43 | + builder.InputTensors({&splits, &values, &size, &weights}); | ||
| 44 | + builder.OutputTensorDesc(0, ge::DT_FLOAT, ge::FORMAT_ND, ge::FORMAT_ND); | ||
| 45 | + auto holder = builder.Build(); | ||
| 46 | + ASSERT_NE(holder.GetContext(), nullptr); | ||
| 47 | + auto* context = holder.GetContext(); | ||
| 48 | + ASSERT_NE(context->GetOutputShape(0), nullptr); | ||
| 49 | + *context->GetOutputShape(0) = gert::Shape({91, 92, 93}); | ||
| 50 | + ASSERT_EQ(impl->infer_shape(context), status); | ||
| 51 | + EXPECT_EQ(*context->GetInputShape(0), splitsShape); | ||
| 52 | + EXPECT_EQ(*context->GetInputShape(1), valuesShape); | ||
| 53 | + EXPECT_EQ(*context->GetInputShape(2), sizeShape); | ||
| 54 | + EXPECT_EQ(*context->GetInputShape(3), weightsShape); | ||
| 55 | + if (status == ge::GRAPH_SUCCESS) { | ||
| 56 | + EXPECT_EQ(*context->GetOutputShape(0), expected); | ||
| 57 | + ASSERT_EQ(impl->infer_shape(context), ge::GRAPH_SUCCESS); | ||
| 58 | + EXPECT_EQ(*context->GetOutputShape(0), expected); | ||
| 59 | + EXPECT_EQ(*context->GetInputShape(1), valuesShape); | ||
| 60 | + EXPECT_EQ(*context->GetInputShape(3), weightsShape); | ||
| 61 | + } | ||
| 62 | +} | ||
| 63 | +} // namespace | ||
| 64 | + | ||
| 65 | +TEST(RaggedBinCountRegisteredInfer, PreservesMatrixWeightsForVectorValues) | ||
| 66 | +{ | ||
| 67 | + CheckRegisteredInfer({6}, {2, 3}, {1, -1}); | ||
| 68 | +} | ||
| 69 | + | ||
| 70 | +TEST(RaggedBinCountRegisteredInfer, PreservesVectorWeightsForMatrixValues) | ||
| 71 | +{ | ||
| 72 | + CheckRegisteredInfer({2, 3}, {6}, {1, -1}); | ||
| 73 | +} | ||
| 74 | + | ||
| 75 | +TEST(RaggedBinCountRegisteredInfer, PreservesDifferentMatrixShapes) { CheckRegisteredInfer({2, 3}, {3, 2}, {1, -1}); } | ||
| 76 | + | ||
| 77 | +TEST(RaggedBinCountRegisteredInfer, PreservesEmptyWeights) | ||
| 78 | +{ | ||
| 79 | + CheckRegisteredInfer({2, 3}, {0}, {1, -1}); | ||
| 80 | + CheckRegisteredInfer({2, 3}, {0, 3}, {1, -1}); | ||
| 81 | + CheckRegisteredInfer({2, 3}, {2, 0}, {1, -1}); | ||
| 82 | +} | ||
| 83 | + | ||
| 84 | +TEST(RaggedBinCountRegisteredInfer, PreservesScalarWeights) { CheckRegisteredInfer({1}, {}, {1, -1}); } | ||
| 85 | + | ||
| 86 | +TEST(RaggedBinCountRegisteredInfer, ReadsInt32ConstantSize) | ||
| 87 | +{ | ||
| 88 | + int32_t bins = 7; | ||
| 89 | + CheckRegisteredInfer({6}, {2, 3}, {1, 7}, {2}, {1}, ge::DT_INT32, &bins); | ||
| 90 | +} | ||
| 91 | + | ||
| 92 | +TEST(RaggedBinCountRegisteredInfer, ReadsInt64ConstantSizeWithoutTruncation) | ||
| 93 | +{ | ||
| 94 | + int64_t bins = (int64_t{1} << 32) + 7; | ||
| 95 | + CheckRegisteredInfer({6}, {2, 3}, {1, bins}, {2}, {1}, ge::DT_INT64, &bins); | ||
| 96 | +} | ||
| 97 | + | ||
| 98 | +TEST(RaggedBinCountRegisteredInfer, AcceptsZeroBins) | ||
| 99 | +{ | ||
| 100 | + int64_t bins = 0; | ||
| 101 | + CheckRegisteredInfer({6}, {2, 3}, {1, 0}, {2}, {1}, ge::DT_INT64, &bins); | ||
| 102 | +} | ||
| 103 | + | ||
| 104 | +TEST(RaggedBinCountRegisteredInfer, RejectsNegativeBins) | ||
| 105 | +{ | ||
| 106 | + int32_t bins32 = -1; | ||
| 107 | + int64_t bins64 = -1; | ||
| 108 | + CheckRegisteredInfer({6}, {2, 3}, {}, {2}, {1}, ge::DT_INT32, &bins32, ge::GRAPH_FAILED); | ||
| 109 | + CheckRegisteredInfer({6}, {2, 3}, {}, {2}, {1}, ge::DT_INT64, &bins64, ge::GRAPH_FAILED); | ||
| 110 | +} | ||
| 111 | + | ||
| 112 | +TEST(RaggedBinCountRegisteredInfer, RetainsKnownRowsWhenSizeIsUnknown) | ||
| 113 | +{ | ||
| 114 | + CheckRegisteredInfer({6}, {2, 3}, {3, -1}, {4}); | ||
| 115 | +} | ||
| 116 | + | ||
| 117 | +TEST(RaggedBinCountRegisteredInfer, PreservesUnknownInputDimensions) | ||
| 118 | +{ | ||
| 119 | + CheckRegisteredInfer({-1}, {-1, 3}, {-1, -1}, {-1}); | ||
| 120 | +} | ||
| 121 | + | ||
| 122 | +TEST(RaggedBinCountRegisteredInfer, PreservesUnknownInputRanks) | ||
| 123 | +{ | ||
| 124 | + CheckRegisteredInfer({-2}, {-2}, {-1, -1}, {-2}, {-2}); | ||
| 125 | +} | ||
| 126 | + | ||
| 127 | +TEST(RaggedBinCountRegisteredInfer, RejectsInvalidSplitsAndSizeShapes) | ||
| 128 | +{ | ||
| 129 | + CheckRegisteredInfer({6}, {2, 3}, {}, {}, {1}, ge::DT_INT32, nullptr, ge::GRAPH_FAILED); | ||
| 130 | + CheckRegisteredInfer({6}, {2, 3}, {}, {1}, {1}, ge::DT_INT32, nullptr, ge::GRAPH_FAILED); | ||
| 131 | + CheckRegisteredInfer({6}, {2, 3}, {}, {2, 1}, {1}, ge::DT_INT32, nullptr, ge::GRAPH_FAILED); | ||
| 132 | + CheckRegisteredInfer({6}, {2, 3}, {}, {2}, {}, ge::DT_INT32, nullptr, ge::GRAPH_FAILED); | ||
| 133 | + CheckRegisteredInfer({6}, {2, 3}, {}, {2}, {int64_t{1} << 31}, ge::DT_INT32, nullptr, ge::GRAPH_FAILED); | ||
| 134 | + CheckRegisteredInfer({6}, {2, 3}, {}, {2}, {int64_t{1} << 32}, ge::DT_INT32, nullptr, ge::GRAPH_FAILED); | ||
| 135 | +} | ||
| 136 | + | ||
| 137 | +TEST(RaggedBinCountRegisteredInfer, RejectsMismatchedNonemptyElementCounts) | ||
| 138 | +{ | ||
| 139 | + CheckRegisteredInfer({6}, {2, 2}, {}, {2}, {1}, ge::DT_INT32, nullptr, ge::GRAPH_FAILED); | ||
| 140 | +} | ||
| @@ -1,5 +1,3 @@ | |||
| 1 | -#!/usr/bin/env python3 | ||
| 2 | -# -*- coding: utf-8 -*- | ||
| 3 | # Copyright (c) 2026 Huawei Technologies Co., Ltd. | 1 | # Copyright (c) 2026 Huawei Technologies Co., Ltd. |
| 4 | # This program is free software, you can redistribute it and/or modify it under the terms and conditions of | 2 | # This program is free software, you can redistribute it and/or modify it under the terms and conditions of |
| 5 | # CANN Open Software License Agreement Version 2.0 (the "License"). | 3 | # CANN Open Software License Agreement Version 2.0 (the "License"). |
| @@ -7,103 +5,80 @@ | |||
| 7 | # THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, | 5 | # THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, |
| 8 | # INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE. | 6 | # INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE. |
| 9 | # See LICENSE in the root of the software repository for the full text of the License. | 7 | # See LICENSE in the root of the software repository for the full text of the License. |
| 10 | -# ---------------------------------------------------------------------------- | ||
| 11 | 8 | ||
| 12 | -"""Regression tests for the RaggedBinCount TestSpec golden.""" | 9 | +"""CPU-only regression tests for the TTK golden contract (no device access).""" |
| 13 | 10 | ||
| 14 | import importlib.util | 11 | import importlib.util |
| 15 | from pathlib import Path | 12 | from pathlib import Path |
| 16 | 13 | ||
| 17 | import numpy as np | 14 | import numpy as np |
| 15 | +import pytest | ||
| 16 | + | ||
| 17 | +_PATH = Path(__file__).parents[1] / "assets" / "golden.py" | ||
| 18 | +_SPEC = importlib.util.spec_from_file_location( | ||
| 19 | + "golden_contract_" + _PATH.parents[2].name, _PATH | ||
| 20 | +) | ||
| 21 | +_G = importlib.util.module_from_spec(_SPEC) | ||
| 22 | +_SPEC.loader.exec_module(_G) | ||
| 18 | 23 | ||
| 19 | 24 | ||
| 20 | -_GOLDEN_PATH = Path(__file__).parents[1] / "assets" / "golden.py" | 25 | +@pytest.mark.parametrize("dtype", [np.float32, np.float64]) |
| 21 | -_SPEC = importlib.util.spec_from_file_location("ragged_bin_count_golden", _GOLDEN_PATH) | 26 | +@pytest.mark.parametrize("name", ["ordinary", "rbc_k_precision_p0_twosum_fp64_gap_i64"]) |
| 22 | -_MODULE = importlib.util.module_from_spec(_SPEC) | 27 | +def test_precision_follows_weights_not_case_name(dtype, name): |
| 23 | -_SPEC.loader.exec_module(_MODULE) | 28 | + weights = np.array([1, 2**-24], dtype=dtype) |
| 24 | - | 29 | + out = _G.RaggedBinCountKernelSpec.golden( |
| 25 | - | 30 | + np.array([0, 2], np.int64), |
| 26 | -def _golden(output_dtype): | 31 | + np.array([0, 0], np.int32), |
| 27 | - return _MODULE.RaggedBinCountKernelSpec.golden( | 32 | + np.array([1], np.int32), |
| 28 | - np.asarray([0, 2], dtype=np.int64), | ||
| 29 | - np.asarray([0, 0], dtype=np.int32), | ||
| 30 | - np.asarray([1], dtype=np.int32), | ||
| 31 | - np.asarray([1.0, 2.0**-24], dtype=np.float64), | ||
| 32 | - binary_output=False, | ||
| 33 | - output_dtypes=(output_dtype,), | ||
| 34 | - testcase_name="golden_dtype_regression", | ||
| 35 | - )[0] | ||
| 36 | - | ||
| 37 | - | ||
| 38 | -def _special_golden(weights, output_dtype, input_dtype=np.float32): | ||
| 39 | - weights = np.asarray(weights, dtype=input_dtype) | ||
| 40 | - return _MODULE.RaggedBinCountKernelSpec.golden( | ||
| 41 | - np.asarray([0, weights.size], dtype=np.int64), | ||
| 42 | - np.zeros(weights.size, dtype=np.int32), | ||
| 43 | - np.asarray([1], dtype=np.int32), | ||
| 44 | weights, | 33 | weights, |
| 45 | - binary_output=False, | 34 | + golden_mode="Promote", |
| 46 | - output_dtypes=(output_dtype,), | 35 | + testcase_name=name, |
| 47 | - testcase_name="rbc_k_precision_p0_twosum_fp64_gap_i64", | ||
| 48 | )[0] | 36 | )[0] |
| 37 | + assert out.dtype == dtype | ||
| 38 | + assert out[0, 0] == dtype(1 + 2**-24) | ||
| 49 | 39 | ||
| 50 | 40 | ||
| 51 | -def test_cross_check_promote_preserves_fp64_reference(): | 41 | +def test_promoted_finite_overflow_is_not_narrowed(): |
| 52 | - result = _golden("float64") | ||
| 53 | - expected = np.float64(1.0) + np.float64(2.0**-24) | ||
| 54 | - | ||
| 55 | - assert result.dtype == np.float64 | ||
| 56 | - assert result[0, 0] == expected | ||
| 57 | - | ||
| 58 | - | ||
| 59 | -def test_close_rounds_once_to_declared_fp32_output(): | ||
| 60 | - result = _golden("float32") | ||
| 61 | - expected = np.float32(np.float64(1.0) + np.float64(2.0**-24)) | ||
| 62 | - | ||
| 63 | - assert result.dtype == np.float32 | ||
| 64 | - assert result[0, 0] == expected | ||
| 65 | - | ||
| 66 | - | ||
| 67 | -def test_special_cross_check_rounds_exact_sum_directly_to_fp64(): | ||
| 68 | - result = _special_golden([1.0, 2.0**-24], "float64", np.float64) | ||
| 69 | - expected = np.float64(1.0) + np.float64(2.0**-24) | ||
| 70 | - | ||
| 71 | - assert result.dtype == np.float64 | ||
| 72 | - assert result[0, 0] == expected | ||
| 73 | - | ||
| 74 | - | ||
| 75 | -def test_special_close_rounds_exact_sum_once_to_fp32(): | ||
| 76 | - result = _special_golden([1.0, 2.0**-24], "float32") | ||
| 77 | - | ||
| 78 | - assert result.dtype == np.float32 | ||
| 79 | - assert result[0, 0] == np.float32(1.0) | ||
| 80 | - | ||
| 81 | - | ||
| 82 | -def test_special_exact_sum_retains_wide_cancellation_gap(): | ||
| 83 | - result = _special_golden([2.0**100, 1.0, -(2.0**100)], "float64", np.float64) | ||
| 84 | - | ||
| 85 | - assert result.dtype == np.float64 | ||
| 86 | - assert result[0, 0] == np.float64(1.0) | ||
| 87 | - | ||
| 88 | - | ||
| 89 | -def test_special_promote_keeps_finite_truth_beyond_fp32_range(): | ||
| 90 | maximum = np.finfo(np.float32).max | 42 | maximum = np.finfo(np.float32).max |
| 91 | - result = _special_golden([maximum, maximum], "float64", np.float64) | 43 | + out = _G.RaggedBinCountKernelSpec.golden( |
| 92 | - | 44 | + np.array([0, 2], np.int64), |
| 93 | - assert result.dtype == np.float64 | 45 | + np.array([0, 0], np.int32), |
| 94 | - assert result[0, 0] == np.float64(maximum) * np.float64(2.0) | 46 | + np.array([1], np.int32), |
| 47 | + np.array([maximum, maximum], np.float64), | ||
| 48 | + golden_mode="Promote", | ||
| 49 | + )[0] | ||
| 50 | + assert out.dtype == np.float64 | ||
| 51 | + assert out[0, 0] == float(maximum) * 2 | ||
| 95 | 52 | ||
| 96 | 53 | ||
| 97 | -def test_special_nonfinite_semantics_are_preserved(): | 54 | +def test_invalid_splits_return_promoted_positive_zero(): |
| 98 | - result = _special_golden([float("inf"), -float("inf")], "float64", np.float64) | 55 | + out = _G.RaggedBinCountKernelSpec.golden( |
| 99 | - | 56 | + np.array([0, 3], np.int64), |
| 100 | - assert result.dtype == np.float64 | 57 | + np.array([0, 0], np.int32), |
| 101 | - assert np.isnan(result[0, 0]) | 58 | + np.array([1], np.int32), |
| 59 | + np.array([np.nan, np.inf], np.float64), | ||
| 60 | + golden_mode="Promote", | ||
| 61 | + )[0] | ||
| 62 | + assert out.dtype == np.float64 and out[0, 0] == 0 and not np.signbit(out[0, 0]) | ||
| 102 | 63 | ||
| 103 | 64 | ||
| 104 | -def test_special_fp64_rounding_uses_ties_to_even(): | 65 | +def test_binary_output_does_not_read_nan_weights(): |
| 105 | - tie = _special_golden([2.0**100, 2.0**47], "float64", np.float64) | 66 | + out = _G.RaggedBinCountKernelSpec.golden( |
| 106 | - above = _special_golden([2.0**100, 2.0**47, 2.0**46], "float64", np.float64) | 67 | + np.array([0, 2], np.int64), |
| 68 | + np.array([0, 0], np.int32), | ||
| 69 | + np.array([1], np.int32), | ||
| 70 | + np.array([np.nan, np.nan], np.float32), | ||
| 71 | + binary_output=True, | ||
| 72 | + )[0] | ||
| 73 | + assert out.dtype == np.float32 and out[0, 0] == 1 | ||
| 107 | 74 | ||
| 108 | - assert tie[0, 0] == np.float64(2.0**100) | 75 | + |
| 109 | - assert above[0, 0] == np.nextafter(np.float64(2.0**100), np.float64(np.inf)) | 76 | +def test_opposite_infinities_propagate_nan(): |
| 77 | + out = _G.RaggedBinCountKernelSpec.golden( | ||
| 78 | + np.array([0, 2], np.int64), | ||
| 79 | + np.array([0, 0], np.int32), | ||
| 80 | + np.array([1], np.int32), | ||
| 81 | + np.array([np.inf, -np.inf], np.float64), | ||
| 82 | + golden_mode="Promote", | ||
| 83 | + )[0] | ||
| 84 | + assert out.dtype == np.float64 and np.isnan(out[0, 0]) | ||
| @@ -83,9 +83,11 @@ $$ | |||
| 83 | 83 | ||
| 84 | ## 约束说明 | 84 | ## 约束说明 |
| 85 | 85 | ||
| 86 | +- 图编译期用`-1`表示未知维度、`[-2]`表示未知rank;两个输出始终为rank-0标量。执行期须先解析输入shape,再满足下述非空、同shape及格式约束。 | ||
| 87 | + | ||
| 86 | - x1与x2不支持空Tensor;rank为0时表示含1个元素的标量输入,rank大于0时每一维长度必须大于0。 | 88 | - x1与x2不支持空Tensor;rank为0时表示含1个元素的标量输入,rank大于0时每一维长度必须大于0。 |
| 87 | - x1与x2的执行期rank及每一维长度必须完全相同,不支持广播。shape`[2, 3]`与`[6]`即使元素数相同也不合法。 | 89 | - x1与x2的执行期rank及每一维长度必须完全相同,不支持广播。shape`[2, 3]`与`[6]`即使元素数相同也不合法。 |
| 88 | -- 本算子不做隐式数据类型转换与输入数据格式转换,四个参数的数据类型和数据格式必须满足产品差异说明中的合法组合。 | 90 | +- 算子节点的四个参数均须使用FLOAT类型,数据格式必须满足产品差异说明中的合法组合。 |
| 89 | - NaN、Inf及有限值平方上溢按FLOAT语义自然传播;两路累加器与两个输出互不污染,`x1`中的非有限值不会影响`y2`,反之亦然。 | 91 | - NaN、Inf及有限值平方上溢按FLOAT语义自然传播;两路累加器与两个输出互不污染,`x1`中的非有限值不会影响`y2`,反之亦然。 |
| 90 | - 本算子仅提供GE图模式注册,构建配置为`aclnn_exclude`,不提供aclnn接口。 | 92 | - 本算子仅提供GE图模式注册,构建配置为`aclnn_exclude`,不提供aclnn接口。 |
| 91 | 93 | ||
| @@ -119,6 +119,13 @@ int32_t RunOneCase(ge::Session& session, uint32_t graphId, const TestCase& testC | |||
| 119 | std::printf("[FAIL] %s: expected two non-null outputs, got %zu\n", testCase.name.c_str(), outputs.size()); | 119 | std::printf("[FAIL] %s: expected two non-null outputs, got %zu\n", testCase.name.c_str(), outputs.size()); |
| 120 | return FAILED_CODE; | 120 | return FAILED_CODE; |
| 121 | } | 121 | } |
| 122 | + for (const auto& output : outputs) { | ||
| 123 | + if (output.GetTensorDesc().GetDataType() != ge::DT_FLOAT || | ||
| 124 | + !output.GetTensorDesc().GetShape().GetDims().empty() || output.GetSize() != sizeof(float)) { | ||
| 125 | + std::printf("[FAIL] %s: expected FLOAT scalar outputs\n", testCase.name.c_str()); | ||
| 126 | + return FAILED_CODE; | ||
| 127 | + } | ||
| 128 | + } | ||
| 122 | 129 | ||
| 123 | const int64_t elementCount = GetElementCount(testCase.shape); | 130 | const int64_t elementCount = GetElementCount(testCase.shape); |
| 124 | const float expectedY1 = static_cast<float>(static_cast<double>(elementCount) * testCase.x1Value * | 131 | const float expectedY1 = static_cast<float>(static_cast<double>(elementCount) * testCase.x1Value * |
| @@ -18,33 +18,14 @@ | |||
| 18 | | kernel | ✅ | op_kernel/arch35/square_sum_all.cpp 有 arch35 实现 | | 18 | | kernel | ✅ | op_kernel/arch35/square_sum_all.cpp 有 arch35 实现 | |
| 19 | | geir | ✅ | op_graph/square_sum_all_proto.h 有 REG_OP(SquareSumAll) | | 19 | | geir | ✅ | op_graph/square_sum_all_proto.h 有 REG_OP(SquareSumAll) | |
| 20 | | aclnn | ❌ | CMakeLists.txt 配置 ACLNNTYPE aclnn_exclude,无 op_api 目录,无 aclnn 文档 | | 20 | | aclnn | ❌ | CMakeLists.txt 配置 ACLNNTYPE aclnn_exclude,无 op_api 目录,无 aclnn 文档 | |
| 21 | -| e2e | ❌ | libtorch_npu.so 中 `aclnnSquareSumAll` 符号数为 0;对照组 `aclnnAdd*` 有命中,证明该判定方法有效 | | 21 | +| e2e | 未注册 | 尚未验证能独立派发到本算子的框架 API;缺少 ACLNN 符号不能证明没有框架通路 | |
| 22 | 22 | ||
| 23 | kernel 与 geir 共用一个注册键(算子蛇形名),geir 不另写 Spec 类。 | 23 | kernel 与 geir 共用一个注册键(算子蛇形名),geir 不另写 Spec 类。 |
| 24 | 24 | ||
| 25 | -判据(tolerance)为什么是 cross_check: | 25 | +精度约定:默认 cross_check 由 TTK Promote 输入并执行 Torch 三方腿。 |
| 26 | - cross_check 是 TTK 里唯一会去取 third_party 输出来判精度的标准。配任何别的标准, | 26 | +CPU golden 保持 TTK 下发的精度,不额外加宽或窄回;两方 FP32 输入按 |
| 27 | - third_party 照样会被执行、xpu_metrics 照样有 device_us,但三方数据不进判定—— | 27 | +NPU 的 FP32 精度计算。两方运行不代表三方验收通过,缺少 XPU 时应记录 |
| 28 | - "三方精度"那一栏实际上只是两方(NPU vs golden)。本算子交付三方精度腿, | 28 | +三方验证未完成。两路输出均为 rank-0 标量,与 InferShape 的接口一致。 |
| 29 | - 因此 Spec 默认判据必须是 cross_check。 | ||
| 30 | - | ||
| 31 | - 代价:cross_check 拿不到三方输出时判 GOLDEN_FAILURE。泛化档(1000 例)与性能档 | ||
| 32 | - (200 例)不接远端 XPU,跑这两档时必须在命令行显式覆盖判据: | ||
| 33 | - python3 -m ttk kernel ... --compare close | ||
| 34 | - 优先级是 CLI > Spec.tolerance > 默认(见 ttk/core_modules/comparison/resolve.py | ||
| 35 | - 的 _float_choice),所以 CLI 覆盖是受支持的正规用法,不是绕过。 | ||
| 36 | - | ||
| 37 | - 为什么这两档选 close(isclose) 而不是 stat_rel_err:判据完全由用例 CSV 驱动—— | ||
| 38 | - `precision_tolerances` 给 (rtol=2**-10, ptol=0.01)、`absolute_precision` 给 | ||
| 39 | - 2**-16,isclose 按 |o-g| <= atol + rtol*|g| 逐元素判、再按 ptol 卡匹配率;且它是 | ||
| 40 | - 少数会把**匹配率百分比**写进结果 CSV 的标准(stat_rel_err / cross_check 只写 | ||
| 41 | - PASS/FAIL),泛化档要逐例呈现精度百分比,靠的就是这条。 | ||
| 42 | - | ||
| 43 | - 与原先自写 compare 的差异(不是等价替换,如实记):混合容差、99% 匹配率、 | ||
| 44 | - NaN/同号 Inf 三条一致;原先另有一条与量级无关的绝对上限 max_abs_error <= 1e-2, | ||
| 45 | - isclose 没有对应项。该上限并不是更好的判据——它不随量级缩放,当 |y| 超过约 | ||
| 46 | - 8.4e4 时 1 个 fp32 ulp 已大于 1e-2,等于要求逐位相等;平方和的量级随 N 线性增长, | ||
| 47 | - 这条迟早会在合法用例上失效。改用纯相对判据是有意为之,但对大量级输出确实比原先松。 | ||
| 48 | 29 | ||
| 49 | 本 Spec 不实现 compare 钩子: | 30 | 本 Spec 不实现 compare 钩子: |
| 50 | TTK 的 try_custom_compare 只要拿到非 None 返回值就直接采信,**跳过标准判据** | 31 | TTK 的 try_custom_compare 只要拿到非 None 返回值就直接采信,**跳过标准判据** |
| @@ -68,27 +49,14 @@ __golden__ = { | |||
| 68 | 49 | ||
| 69 | 50 | ||
| 70 | def _reduce_one(x): | 51 | def _reduce_one(x): |
| 71 | - """Reduce a single input with torch competitor interfaces (R3: no hand-rolled formula). | 52 | + """Use the supplied precision: FP32 normally, FP64 after TTK Promote.""" |
| 72 | - | 53 | + tensor = torch.from_numpy(np.array(x, copy=True, order="C")) |
| 73 | - torch.square + torch.sum are used instead of a numpy expression so the golden and the | ||
| 74 | - kernel cannot share the same misreading of the semantics. Accumulating in float64 is a | ||
| 75 | - deliberate tier choice for this large-reduction operator rather than a precision cast: | ||
| 76 | - the kernel accumulates in float32, so a float32 golden would sit on the same error | ||
| 77 | - floor and could not flag a real regression. | ||
| 78 | - """ | ||
| 79 | - # Widening to float64 in numpy always yields a fresh writable buffer, so from_numpy | ||
| 80 | - # never has to warn about a read-only input array. | ||
| 81 | - tensor = torch.from_numpy(np.ascontiguousarray(x, dtype=np.float64)) | ||
| 82 | reduced = torch.sum(torch.square(tensor)) | 54 | reduced = torch.sum(torch.square(tensor)) |
| 83 | - # Keep the high-precision reference in FP64. In cross_check mode TTK promotes | 55 | + return reduced.numpy().reshape(()) |
| 84 | - # FP32 inputs to FP64 before invoking the golden; casting the result back to | ||
| 85 | - # FP32 would quantize the reference onto the same grid as the NPU/GPU outputs | ||
| 86 | - # and make scalar error ratios collapse to integer ULP ratios. | ||
| 87 | - return reduced.numpy().reshape(1) | ||
| 88 | 56 | ||
| 89 | 57 | ||
| 90 | def square_sum_all_golden(x1, x2, **kwargs): | 58 | def square_sum_all_golden(x1, x2, **kwargs): |
| 91 | - """Compute and return both independent reductions in float64.""" | 59 | + """Compute both reductions without overriding TTK's input/output precision.""" |
| 92 | del kwargs | 60 | del kwargs |
| 93 | return [_reduce_one(x1), _reduce_one(x2)] | 61 | return [_reduce_one(x1), _reduce_one(x2)] |
| 94 | 62 | ||
| @@ -116,7 +84,7 @@ class _Compose: | |||
| 116 | outputs = [] | 84 | outputs = [] |
| 117 | for tensor in (x1, x2): | 85 | for tensor in (x1, x2): |
| 118 | flat = tensor.reshape(-1).to(torch.float32) | 86 | flat = tensor.reshape(-1).to(torch.float32) |
| 119 | - outputs.append(torch.dot(flat, flat).to(torch.float32).reshape(1)) | 87 | + outputs.append(torch.dot(flat, flat).to(torch.float32).reshape(())) |
| 120 | return outputs | 88 | return outputs |
| 121 | 89 | ||
| 122 | 90 | ||
| @@ -136,8 +104,5 @@ class SquareSumAllTestSpec: | |||
| 136 | 104 | ||
| 137 | # 【不存在】aclnn 通路:CMakeLists.txt 配置 ACLNNTYPE aclnn_exclude,算子无 op_api 目录, | 105 | # 【不存在】aclnn 通路:CMakeLists.txt 配置 ACLNNTYPE aclnn_exclude,算子无 op_api 目录, |
| 138 | # 也不交付 docs/aclnnSquareSumAll.md,因此不注册 SquareSumAllAclnnSpec。 | 106 | # 也不交付 docs/aclnnSquareSumAll.md,因此不注册 SquareSumAllAclnnSpec。 |
| 139 | -# 【不存在】e2e 通路:strings libtorch_npu.so | grep -c aclnnSquareSumAll = 0 | 107 | +# E2E 暂未注册:本文件只验证 Kernel/GEIR 路径,没有确认独立框架 API 的派发绑定。 |
| 140 | -# (对照组 aclnnAdd* 有命中,证明该判定方法有效;legacy SquareSumAll 动态实现于 2021-07-05 入仓, | 108 | +# 不以缺少同名 ACLNN 符号推断所有框架路径均不存在。 |
| 141 | -# 当前 torch_npu 安装于 2026-07-30,版本晚于算子既有接口), | ||
| 142 | -# torch 侧没有任何接口会执行本算子,因此不注册 SquareSumAllTorchSpec。 | ||
| 143 | -# 上述两条不是漏写,勿反复重查。 | ||
This file has too many line changes or character changes, you can directly View source code
| @@ -0,0 +1,47 @@ | |||
| 1 | +# Copyright (c) 2026 Huawei Technologies Co., Ltd. | ||
| 2 | +# This program is free software, you can redistribute it and/or modify it under the 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 | +# See LICENSE in the root of the software repository for the full text of the License. | ||
| 8 | + | ||
| 9 | +"""CPU-only regression tests for the TTK golden contract (no device access).""" | ||
| 10 | + | ||
| 11 | +import importlib.util | ||
| 12 | +from pathlib import Path | ||
| 13 | + | ||
| 14 | +import numpy as np | ||
| 15 | +import pytest | ||
| 16 | + | ||
| 17 | +_PATH = Path(__file__).parents[1] / "assets" / "golden.py" | ||
| 18 | +_SPEC = importlib.util.spec_from_file_location( | ||
| 19 | + "golden_contract_" + _PATH.parents[2].name, _PATH | ||
| 20 | +) | ||
| 21 | +_G = importlib.util.module_from_spec(_SPEC) | ||
| 22 | +_SPEC.loader.exec_module(_G) | ||
| 23 | + | ||
| 24 | + | ||
| 25 | +@pytest.mark.parametrize("dtype", [np.float32, np.float64]) | ||
| 26 | +def test_scalar_shape_and_supplied_precision(dtype): | ||
| 27 | + x = np.array([3, 4], dtype=dtype) | ||
| 28 | + out = _G.SquareSumAllTestSpec.golden(x, x) | ||
| 29 | + for actual in out: | ||
| 30 | + assert actual.shape == () and actual.dtype == dtype and actual.item() == 25 | ||
| 31 | + | ||
| 32 | + | ||
| 33 | +def test_two_party_preserves_fp32_square_overflow(): | ||
| 34 | + x = np.array([np.finfo(np.float32).max], np.float32) | ||
| 35 | + assert np.isposinf(_G.SquareSumAllTestSpec.golden(x, x)[0]) | ||
| 36 | + promoted = x.astype(np.float64) | ||
| 37 | + result = _G.SquareSumAllTestSpec.golden(promoted, promoted, golden_mode="Promote")[ | ||
| 38 | + 0 | ||
| 39 | + ] | ||
| 40 | + assert result.dtype == np.float64 and np.isfinite(result) | ||
| 41 | + | ||
| 42 | + | ||
| 43 | +def test_competitor_returns_scalar_float32(): | ||
| 44 | + import torch | ||
| 45 | + | ||
| 46 | + result = _G._Compose()(torch.tensor([3.0, 4.0]), torch.tensor([1.0, 2.0])) | ||
| 47 | + assert all(x.shape == torch.Size([]) and x.dtype == torch.float32 for x in result) | ||
标记文档整改