Merged
fix: 迁移 RaggedBinCount V2 推导并规范两算子 golden #6538
fix: 迁移 RaggedBinCount V2 推导并规范两算子 golden #6538
Merged
raoliang_sacAdd on 21 days ago
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
Rraoliang_sac19 days ago

标记文档整改

likedislike
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 
257int FinalizeRes()272int 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- 
90namespace ops {18namespace ops {
91static ge::graphStatus InferDataTypeForRaggedBinCount(gert::InferDataTypeContext* context)19static 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
15operator name and share :class:`RaggedBinCountKernelSpec`. The legacy15operator 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 composition18+The reference uses ``torch.bincount`` per ragged row in the dtype supplied
19-(``torch.bincount`` per ragged row). Fixed numerical-corner cases use an19+by TTK. All testcases use the same computation, including numerical corners;
20-independent arbitrary-precision CPU oracle because even FP64 cannot retain20+the reference does not select a different algorithm from the testcase name.
21-every FP32 cancellation gap. ``torch.bincount`` is semantically the per-row21+``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 
24Frozen contract points mirrored from ``README.md``:24Frozen contract points mirrored from ``README.md``:
25 * within the public rank-0-to-2 range, ``weights`` is judged by element count25 * 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- 
38import numpy as np36import numpy as np
39import torch37import 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 is189+ # FP64 cannot retain the unit term across this cancellation. This input
205- # therefore also a regression for the integer-superaccumulator oracle190+ # 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 False362 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- 
515def _compute(splits, values, size, weights, **kwargs):365def _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 0373 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() > 0380 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_weights383+ dtype = weights_flat.dtype
534- and not binary_output384+ 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=bins401 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.float64405+ 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- 
603def _kernel_golden(splits, values, size, weights, **kwargs):410def _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 
616def _competitor_forward(splits_flat, values_flat, bins, weights_flat, binary_output):416def _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 two453 * It must not share an implementation with :func:`_compute`, or the two
654 reference paths cannot cross-validate each other. ``_compute`` is a454 reference paths cannot cross-validate each other. ``_compute`` is a
655- per-row ``torch.bincount`` accumulating in FP64; this is a single flat455+ 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 is457 * 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 ragged458 inflated and the comparison is not an honest one. The whole ragged
@@ -711,14 +511,10 @@ class RaggedBinCountKernelSpec:
711def ragged_bin_count_golden(splits, values, size, weights, *args, **kwargs):511def 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 args513 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
16rbc_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 val16rbc_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
17rbc_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 val17rbc_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
18rbc_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 weights18rbc_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 match19+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"
20rbc_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 weights20rbc_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
21rbc_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 false21rbc_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 of2# 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 
14import importlib.util11import importlib.util
15from pathlib import Path12from pathlib import Path
16 13 
17import numpy as np14import 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).max42 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.float6445+ 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.float6457+ 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 
23kernel 与 geir 共用一个注册键(算子蛇形名),geir 不另写 Spec 类。23kernel 与 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 
70def _reduce_one(x):51def _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 promotes55+ 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 
90def square_sum_all_golden(x1, x2, **kwargs):58def 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 kwargs60 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 outputs88 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 = 0107+# 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)