已合并
test(golden): 升级 INInferV2 TestSpec #8513
raoliang_sac创建于 8月11日
test(golden): 升级 INInferV2 TestSpec #8513
已合并
共 1 个文件变更+231-84
| @@ -1,6 +1,5 @@ | |||
| 1 | #!/usr/bin/env python3 | 1 | #!/usr/bin/env python3 |
| 2 | -# -*- coding: UTF-8 -*- | 2 | +# -*- coding: utf-8 -*- |
| 3 | -# ---------------------------------------------------------------------------- | ||
| 4 | # Copyright (c) 2026 Huawei Technologies Co., Ltd. | 3 | # Copyright (c) 2026 Huawei Technologies Co., Ltd. |
| 5 | # This program is free software, you can redistribute it and/or modify it under the terms and conditions of | 4 | # This program is free software, you can redistribute it and/or modify it under the terms and conditions of |
| 6 | # CANN Open Software License Agreement Version 2.0 (the "License"). | 5 | # CANN Open Software License Agreement Version 2.0 (the "License"). |
| @@ -9,123 +8,271 @@ | |||
| 9 | # INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE. | 8 | # INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE. |
| 10 | # See LICENSE in the root of the software repository for the full text of the License. | 9 | # See LICENSE in the root of the software repository for the full text of the License. |
| 11 | # ---------------------------------------------------------------------------- | 10 | # ---------------------------------------------------------------------------- |
| 12 | -"""Golden plugin for INInferV2 (instance normalization inference),torch 竞品算子拼接实现。 | ||
| 13 | 11 | ||
| 14 | - y = (x - mean) * (gamma / sqrt(variance + epsilon)) + beta # gamma/beta 可选 | 12 | +"""INInferV2 kernel/GEIR golden in the TestSpec multi-path format. |
| 15 | - 无 gamma/beta 时 y = (x - mean) / sqrt(variance + epsilon) | ||
| 16 | - batch_mean = mean (透传拷贝) | ||
| 17 | - batch_variance = variance (透传拷贝) | ||
| 18 | 13 | ||
| 19 | -与 kernel 一致:全部按 float32 计算(fp16 的 x 先升 fp32),y 单次舍入写回输入 dtype; | 14 | +For an ND input laid out as ``[N, C, R...]`` the operator computes:: |
| 20 | -scale = gamma / sqrt(var + eps) 先算(对齐 910b high_performance 语义)。 | ||
| 21 | 15 | ||
| 22 | -格式说明:算子 ND-only(tiling 仅接受 ND/NCHW 标签),C 恒在 dim1; | 16 | + y = (x - mean) * (gamma / sqrt(variance + epsilon)) + beta |
| 23 | -_is_channels_last 仅为防御性保留。 | 17 | + |
| 18 | +When ``gamma`` and ``beta`` are both absent, the scale/add branch is omitted and | ||
| 19 | +``y = (x - mean) / sqrt(variance + epsilon)``. ``batch_mean`` and | ||
| 20 | +``batch_variance`` are copies of the corresponding inputs. | ||
| 21 | + | ||
| 22 | +The CPU true-value path is a Torch competitor composition. It lifts fp16 to at | ||
| 23 | +least fp32 and preserves fp64 inputs supplied by TTK Promote; promoted values are | ||
| 24 | +never narrowed. The independent third-party composition mirrors the arch35 | ||
| 25 | +kernel's float32 arithmetic and operation order before casting ``y`` back to the | ||
| 26 | +input dtype. | ||
| 24 | """ | 27 | """ |
| 25 | 28 | ||
| 26 | import numpy as np | 29 | import numpy as np |
| 27 | import torch | 30 | import torch |
| 28 | 31 | ||
| 32 | + | ||
| 33 | +# Kernel and GEIR resolve the same snake-case operator key and share one Spec. | ||
| 34 | +__spec__ = {"in_infer_v2": "INInferV2KernelSpec"} | ||
| 35 | + | ||
| 36 | +# Compatibility entry for the historical kernel golden loader. | ||
| 29 | __golden__ = { | 37 | __golden__ = { |
| 30 | "kernel": {"in_infer_v2": "in_infer_v2_golden"}, | 38 | "kernel": {"in_infer_v2": "in_infer_v2_golden"}, |
| 31 | - "aclnn": {"aclnnINInferV2": "aclnn_in_infer_v2_golden"}, | ||
| 32 | } | 39 | } |
| 33 | 40 | ||
| 34 | 41 | ||
| 35 | -def _to_torch_f32(tensor): | 42 | +_TOL = { |
| 36 | - """输入归一为 torch float32(接受 numpy / torch tensor,None 透传;ml_dtypes.bfloat16 等 numpy 扩展 dtype 先升 fp32)。""" | 43 | + "float16": {"standard": "cross_check", "level": "L1"}, |
| 37 | - if tensor is None: | 44 | + "float32": {"standard": "cross_check", "level": "L1"}, |
| 38 | - return None | 45 | +} |
| 39 | - if isinstance(tensor, torch.Tensor): | ||
| 40 | - return tensor.detach().cpu().to(torch.float32) | ||
| 41 | - arr = np.asarray(tensor) | ||
| 42 | - if arr.dtype not in ( | ||
| 43 | - np.float16, | ||
| 44 | - np.float32, | ||
| 45 | - np.float64, | ||
| 46 | - np.int32, | ||
| 47 | - np.int64, | ||
| 48 | - np.int16, | ||
| 49 | - np.int8, | ||
| 50 | - np.uint8, | ||
| 51 | - ): | ||
| 52 | - arr = arr.astype(np.float32) | ||
| 53 | - return torch.from_numpy(arr).to(torch.float32) | ||
| 54 | 46 | ||
| 55 | 47 | ||
| 56 | -def _out_dtype(x): | 48 | +def _attr(kwargs, name, default): |
| 57 | - if isinstance(x, torch.Tensor): | 49 | + """Read a scalar attribute, including the legacy nested-attributes form.""" |
| 58 | - return x.detach().cpu().numpy().dtype | 50 | + value = kwargs.get(name) |
| 59 | - return np.asarray(x).dtype | 51 | + if value is None and isinstance(kwargs.get("attributes"), dict): |
| 52 | + value = kwargs["attributes"].get(name) | ||
| 53 | + if value is None: | ||
| 54 | + return default | ||
| 55 | + if isinstance(value, str): | ||
| 56 | + try: | ||
| 57 | + return type(default)(value) | ||
| 58 | + except (TypeError, ValueError): | ||
| 59 | + return default | ||
| 60 | + return value | ||
| 60 | 61 | ||
| 61 | 62 | ||
| 62 | -def _is_channels_last(x_shape, c): | 63 | +def _resolve_epsilon(epsilon, kwargs): |
| 63 | - """判断 C 是否在末维(NHWC);C 在 dim1(ND/NCHW)时返回 False。 | 64 | + values = dict(kwargs) |
| 65 | + values.setdefault("epsilon", epsilon) | ||
| 66 | + return float(_attr(values, "epsilon", 1e-5)) | ||
| 64 | 67 | ||
| 65 | - 用例 shape 设计保证 d1 与末维只有一个等于 C,无歧义。 | 68 | + |
| 66 | - """ | 69 | +def _as_tensor(value): |
| 67 | - if len(x_shape) < 3: | 70 | + """Convert a Kernel/GEIR NumPy input to a CPU Torch tensor losslessly.""" |
| 68 | - return False | 71 | + if isinstance(value, torch.Tensor): |
| 69 | - return x_shape[1] != c and x_shape[-1] == c | 72 | + return value.detach().cpu() |
| 73 | + return torch.from_numpy(np.ascontiguousarray(np.asarray(value))) | ||
| 74 | + | ||
| 75 | + | ||
| 76 | +def _reference_dtype(*tensors): | ||
| 77 | + """Select at least fp32 while retaining any wider promoted float dtype.""" | ||
| 78 | + dtype = torch.float32 | ||
| 79 | + for tensor in tensors: | ||
| 80 | + if tensor is not None and tensor.dtype.is_floating_point: | ||
| 81 | + dtype = torch.promote_types(dtype, tensor.dtype) | ||
| 82 | + return dtype | ||
| 83 | + | ||
| 84 | + | ||
| 85 | +def _stat_matrix(tensor, n, c, name): | ||
| 86 | + expected = n * c | ||
| 87 | + if tensor.numel() != expected: | ||
| 88 | + raise ValueError( | ||
| 89 | + f"{name} must contain N*C={expected} elements, got {tensor.numel()}" | ||
| 90 | + ) | ||
| 91 | + return torch.reshape(tensor, (n, c)) | ||
| 70 | 92 | ||
| 71 | 93 | ||
| 72 | def _compute(x, gamma, beta, mean, variance, epsilon): | 94 | def _compute(x, gamma, beta, mean, variance, epsilon): |
| 73 | - """核心计算:返回 (y fp32计算后按输入 dtype 舍入, mean, variance)。""" | 95 | + """Sole Torch true-value core; return outputs in def.cpp order.""" |
| 74 | - out_dtype = _out_dtype(x) | 96 | + if mean is None or variance is None: |
| 75 | - x_t = _to_torch_f32(x) | 97 | + raise ValueError("mean and variance are required by INInferV2 tiling") |
| 76 | - mean_t = _to_torch_f32(mean) | 98 | + if (gamma is None) != (beta is None): |
| 77 | - var_t = _to_torch_f32(variance) | 99 | + raise ValueError("gamma and beta must be both present or both absent") |
| 78 | - n, c = mean_t.shape[0], mean_t.shape[1] | ||
| 79 | 100 | ||
| 80 | - if _is_channels_last(tuple(x_t.shape), c): | 101 | + x_tensor = _as_tensor(x) |
| 81 | - bcast_shape = [n] + [1] * (x_t.dim() - 2) + [c] | 102 | + gamma_tensor = _as_tensor(gamma) if gamma is not None else None |
| 103 | + beta_tensor = _as_tensor(beta) if beta is not None else None | ||
| 104 | + mean_tensor = _as_tensor(mean) | ||
| 105 | + variance_tensor = _as_tensor(variance) | ||
| 106 | + | ||
| 107 | + if x_tensor.ndim < 2: | ||
| 108 | + raise ValueError(f"x rank must be at least 2, got {x_tensor.ndim}") | ||
| 109 | + n, c = x_tensor.shape[:2] | ||
| 110 | + compute_dtype = _reference_dtype( | ||
| 111 | + x_tensor, gamma_tensor, beta_tensor, mean_tensor, variance_tensor | ||
| 112 | + ) | ||
| 113 | + x_compute = x_tensor.to(dtype=compute_dtype) | ||
| 114 | + mean_compute = mean_tensor.to(dtype=compute_dtype) | ||
| 115 | + variance_compute = variance_tensor.to(dtype=compute_dtype) | ||
| 116 | + | ||
| 117 | + mean_matrix = _stat_matrix(mean_compute, n, c, "mean") | ||
| 118 | + variance_matrix = _stat_matrix(variance_compute, n, c, "variance") | ||
| 119 | + # The host reads a Float attribute, so preserve that fp32 quantization even | ||
| 120 | + # when Promote has lifted tensor inputs to fp64. | ||
| 121 | + epsilon_f32 = torch.tensor(float(epsilon), dtype=torch.float32) | ||
| 122 | + epsilon_tensor = epsilon_f32.to(dtype=compute_dtype) | ||
| 123 | + inverse_std = torch.rsqrt(torch.add(variance_matrix, epsilon_tensor)) | ||
| 124 | + | ||
| 125 | + broadcast_shape = (n, c) + (1,) * (x_tensor.ndim - 2) | ||
| 126 | + mean_broadcast = torch.reshape(mean_matrix, broadcast_shape) | ||
| 127 | + centered = torch.sub(x_compute, mean_broadcast) | ||
| 128 | + normalized = torch.mul(centered, torch.reshape(inverse_std, broadcast_shape)) | ||
| 129 | + | ||
| 130 | + if gamma_tensor is None: | ||
| 131 | + y = normalized | ||
| 82 | else: | 132 | else: |
| 83 | - bcast_shape = [n, c] + [1] * (x_t.dim() - 2) | 133 | + gamma_compute = gamma_tensor.to(dtype=compute_dtype) |
| 84 | - mean_b = mean_t.reshape(bcast_shape) | 134 | + beta_compute = beta_tensor.to(dtype=compute_dtype) |
| 85 | - var_b = var_t.reshape(bcast_shape) | 135 | + gamma_matrix = _stat_matrix(gamma_compute, n, c, "gamma") |
| 136 | + beta_matrix = _stat_matrix(beta_compute, n, c, "beta") | ||
| 137 | + scaled = torch.mul(normalized, torch.reshape(gamma_matrix, broadcast_shape)) | ||
| 138 | + y = torch.add(scaled, torch.reshape(beta_matrix, broadcast_shape)) | ||
| 86 | 139 | ||
| 87 | - sqrt_var = torch.sqrt(var_b + float(np.float32(epsilon))) | 140 | + # InferShape keeps the original mean/variance shapes for these two outputs. |
| 88 | - gamma_t = _to_torch_f32(gamma) | 141 | + return [y, torch.clone(mean_compute), torch.clone(variance_compute)] |
| 89 | - if gamma_t is not None: | ||
| 90 | - beta_t = _to_torch_f32(beta) | ||
| 91 | - scale = (gamma_t / sqrt_var.reshape(gamma_t.shape)).reshape(bcast_shape) | ||
| 92 | - beta_b = beta_t.reshape(bcast_shape) | ||
| 93 | - y = (x_t - mean_b) * scale + beta_b | ||
| 94 | - else: | ||
| 95 | - y = (x_t - mean_b) / sqrt_var | ||
| 96 | - return y.numpy().astype(out_dtype), mean_t.numpy(), var_t.numpy() | ||
| 97 | 142 | ||
| 98 | 143 | ||
| 99 | -def in_infer_v2_golden(x, gamma, beta, mean, variance, epsilon=1e-5, **kwargs): | 144 | +def _normalize_dtype_name(dtype): |
| 100 | - """Golden for in_infer_v2 kernel 模式。参数顺序同 def(不含输出)。""" | 145 | + if isinstance(dtype, (list, tuple)): |
| 101 | - del kwargs | 146 | + dtype = dtype[0] if dtype else None |
| 102 | - y, mean_np, var_np = _compute(x, gamma, beta, mean, variance, epsilon) | 147 | + if dtype is None: |
| 103 | - return [y, mean_np.copy(), var_np.copy()] | 148 | + return None |
| 149 | + name = str(dtype).lower().replace("torch.", "").replace("numpy.", "") | ||
| 150 | + return { | ||
| 151 | + "fp16": "float16", | ||
| 152 | + "half": "float16", | ||
| 153 | + "fp32": "float32", | ||
| 154 | + "float": "float32", | ||
| 155 | + "fp64": "float64", | ||
| 156 | + "double": "float64", | ||
| 157 | + }.get(name, name) | ||
| 104 | 158 | ||
| 105 | 159 | ||
| 106 | -def aclnn_in_infer_v2_golden( | 160 | +def _input_dtype_name(value): |
| 161 | + if isinstance(value, torch.Tensor): | ||
| 162 | + return _normalize_dtype_name(value.dtype) | ||
| 163 | + return np.asarray(value).dtype.name | ||
| 164 | + | ||
| 165 | + | ||
| 166 | +def _numpy_outputs(outputs, output_dtypes): | ||
| 167 | + dtype_names = [_normalize_dtype_name(dtype) for dtype in (output_dtypes or ())] | ||
| 168 | + result = [] | ||
| 169 | + for index, output in enumerate(outputs): | ||
| 170 | + array = output.detach().cpu().contiguous().numpy() | ||
| 171 | + if index < len(dtype_names) and dtype_names[index] is not None: | ||
| 172 | + array = array.astype(dtype_names[index], copy=False) | ||
| 173 | + result.append(np.ascontiguousarray(array)) | ||
| 174 | + return result | ||
| 175 | + | ||
| 176 | + | ||
| 177 | +def _kernel_golden( | ||
| 178 | + x, | ||
| 179 | + gamma, | ||
| 180 | + beta, | ||
| 181 | + mean, | ||
| 182 | + variance, | ||
| 183 | + epsilon=1e-5, | ||
| 184 | + **kwargs, | ||
| 185 | +): | ||
| 186 | + """Kernel/GEIR adapter: NumPy inputs and a NumPy output list.""" | ||
| 187 | + epsilon_value = _resolve_epsilon(epsilon, kwargs) | ||
| 188 | + outputs = _compute(x, gamma, beta, mean, variance, epsilon_value) | ||
| 189 | + output_dtypes = kwargs.get("output_dtypes") | ||
| 190 | + if not output_dtypes: | ||
| 191 | + output_dtypes = ( | ||
| 192 | + _input_dtype_name(x), | ||
| 193 | + _input_dtype_name(mean), | ||
| 194 | + _input_dtype_name(variance), | ||
| 195 | + ) | ||
| 196 | + return _numpy_outputs(outputs, output_dtypes) | ||
| 197 | + | ||
| 198 | + | ||
| 199 | +class _INInferV2Compose: | ||
| 200 | + """Independent Torch composition matching the arch35 device arithmetic.""" | ||
| 201 | + | ||
| 202 | + def __init__(self, epsilon=1e-5, **kwargs): | ||
| 203 | + epsilon_value = _resolve_epsilon(epsilon, kwargs) | ||
| 204 | + # Tiling stores epsilon as float32 before kernel launch. | ||
| 205 | + self.epsilon = float(torch.tensor(epsilon_value, dtype=torch.float32).item()) | ||
| 206 | + | ||
| 207 | + def __call__(self, x, gamma, beta, mean, variance, **kwargs): | ||
| 208 | + del kwargs | ||
| 209 | + if mean is None or variance is None: | ||
| 210 | + raise ValueError("mean and variance are required by INInferV2 tiling") | ||
| 211 | + if (gamma is None) != (beta is None): | ||
| 212 | + raise ValueError("gamma and beta must be both present or both absent") | ||
| 213 | + if x.dtype not in (torch.float16, torch.float32): | ||
| 214 | + raise TypeError(f"INInferV2 supports only float16/float32 x, got {x.dtype}") | ||
| 215 | + | ||
| 216 | + n, c = x.shape[:2] | ||
| 217 | + broadcast_shape = (n, c) + (1,) * (x.ndim - 2) | ||
| 218 | + | ||
| 219 | + # INInferV2Kernel::Process converts every arithmetic operand to fp32. | ||
| 220 | + x_f32 = x.to(dtype=torch.float32) | ||
| 221 | + mean_f32 = _stat_matrix(mean.to(dtype=torch.float32), n, c, "mean") | ||
| 222 | + variance_f32 = _stat_matrix(variance.to(dtype=torch.float32), n, c, "variance") | ||
| 223 | + epsilon_f32 = variance_f32.new_tensor(self.epsilon) | ||
| 224 | + sqrt_variance = torch.sqrt(torch.add(variance_f32, epsilon_f32)) | ||
| 225 | + centered = torch.sub(x_f32, torch.reshape(mean_f32, broadcast_shape)) | ||
| 226 | + | ||
| 227 | + if gamma is None: | ||
| 228 | + y_f32 = torch.div(centered, torch.reshape(sqrt_variance, broadcast_shape)) | ||
| 229 | + else: | ||
| 230 | + gamma_f32 = _stat_matrix(gamma.to(dtype=torch.float32), n, c, "gamma") | ||
| 231 | + beta_f32 = _stat_matrix(beta.to(dtype=torch.float32), n, c, "beta") | ||
| 232 | + scale = torch.div(gamma_f32, sqrt_variance) | ||
| 233 | + scaled = torch.mul(centered, torch.reshape(scale, broadcast_shape)) | ||
| 234 | + y_f32 = torch.add(scaled, torch.reshape(beta_f32, broadcast_shape)) | ||
| 235 | + | ||
| 236 | + # The first output is stored in x dtype; the two copy outputs are fp32. | ||
| 237 | + return [ | ||
| 238 | + y_f32.to(dtype=x.dtype), | ||
| 239 | + mean.to(dtype=torch.float32).clone(), | ||
| 240 | + variance.to(dtype=torch.float32).clone(), | ||
| 241 | + ] | ||
| 242 | + | ||
| 243 | + | ||
| 244 | +class INInferV2KernelSpec: | ||
| 245 | + """Shared kernel/GEIR TestSpec; parameters follow in_infer_v2_def.cpp.""" | ||
| 246 | + | ||
| 247 | + golden = _kernel_golden | ||
| 248 | + third_party = {"torch": _INInferV2Compose} | ||
| 249 | + tolerance = _TOL | ||
| 250 | + | ||
| 251 | + | ||
| 252 | +def in_infer_v2_golden( | ||
| 107 | x, | 253 | x, |
| 108 | gamma, | 254 | gamma, |
| 109 | beta, | 255 | beta, |
| 110 | mean, | 256 | mean, |
| 111 | variance, | 257 | variance, |
| 112 | epsilon=1e-5, | 258 | epsilon=1e-5, |
| 113 | - y=None, | ||
| 114 | - batch_mean=None, | ||
| 115 | - batch_variance=None, | ||
| 116 | *args, | 259 | *args, |
| 117 | **kwargs, | 260 | **kwargs, |
| 118 | ): | 261 | ): |
| 119 | - """Golden for aclnnINInferV2(ttk aclnn 模式)。 | 262 | + """Compatibility ``__golden__`` entry backed by the same compute core.""" |
| 263 | + del args | ||
| 264 | + return _kernel_golden( | ||
| 265 | + x, | ||
| 266 | + gamma, | ||
| 267 | + beta, | ||
| 268 | + mean, | ||
| 269 | + variance, | ||
| 270 | + epsilon=epsilon, | ||
| 271 | + **kwargs, | ||
| 272 | + ) | ||
| 120 | 273 | ||
| 121 | - 注意:ttk aclnn 模式按 C 头文件参数序位置传参(AclnnParamPlan.build_args)—— | 274 | + |
| 122 | - epsilon 在 outputs 之前,outputs 之后还有 workspaceSize/executor 占位, | 275 | +# 【不存在】ACLNN 通路:op_host/CMakeLists.txt declares ``ACLNNTYPE aclnn_exclude``; |
| 123 | - 故签名必须是 C 序 + *args 吞掉尾部占位,不能按 csv tensor 序。 | 276 | +# the repository provides neither op_api files nor an aclnn interface document. |
| 124 | - batch_mean/batch_variance 为 None(可选输出缺席)时仅返回 [y]。 | 277 | +# 【不存在】e2e 通路:the supported surface is GE graph mode (README.md), and no |
| 125 | - """ | 278 | +# torch binding for this operator is provided in the repository. |
| 126 | - del y, args, kwargs | ||
| 127 | - has_batch_out = batch_mean is not None or batch_variance is not None | ||
| 128 | - y_np, mean_np, var_np = _compute(x, gamma, beta, mean, variance, epsilon) | ||
| 129 | - if not has_batch_out: | ||
| 130 | - return [y_np] | ||
| 131 | - return [y_np, mean_np.copy(), var_np.copy()] | ||