Merged
test(rms_norm_quant): 补充 Ascend 950 golden 测试资产 #11974
raoliang_sacAdd on 24 days ago
test(rms_norm_quant): 补充 Ascend 950 golden 测试资产 #11974
Merged
Total: 5 files changed+983-1
| @@ -0,0 +1,217 @@ | |||
| 1 | +#!/usr/bin/env python3 | ||
| 2 | +# -*- coding: UTF-8 -*- | ||
| 3 | +# 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 | ||
| 5 | +# CANN Open Software License Agreement Version 2.0 (the "License"). | ||
| 6 | +# Please refer to the License for details. You may not use this file except in compliance with the License. | ||
| 7 | +# 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. | ||
| 9 | +# See LICENSE in the root of the software repository for the full text of the License. | ||
| 10 | + | ||
| 11 | +import numpy as np | ||
| 12 | +import torch | ||
| 13 | + | ||
| 14 | + | ||
| 15 | +__golden__ = {"aclnn": {"aclnnRmsNormQuant": "aclnn_rms_norm_quant_golden"}} | ||
| 16 | + | ||
| 17 | +_DST_SAT_MAX = { | ||
| 18 | + "int8": 127.0, | ||
| 19 | + "int4": 7.0, | ||
| 20 | + "float8_e5m2": 57344.0, | ||
| 21 | + "float8_e4m3fn": 448.0, | ||
| 22 | + "hifloat8": 32768.0, | ||
| 23 | +} | ||
| 24 | +_DST_SAT_MIN = { | ||
| 25 | + "int8": -128.0, | ||
| 26 | + "int4": -8.0, | ||
| 27 | + "float8_e5m2": -57344.0, | ||
| 28 | + "float8_e4m3fn": -448.0, | ||
| 29 | + "hifloat8": -32768.0, | ||
| 30 | +} | ||
| 31 | +_INT4_VALUES_PER_BYTE = 2 | ||
| 32 | +_INT4_VALUES_PER_INT32 = np.dtype(np.int32).itemsize * _INT4_VALUES_PER_BYTE | ||
| 33 | + | ||
| 34 | + | ||
| 35 | +def _to_numpy(t): | ||
| 36 | + if t is None: | ||
| 37 | + return None | ||
| 38 | + if isinstance(t, np.ndarray): | ||
| 39 | + return t | ||
| 40 | + if hasattr(t, "numpy"): | ||
| 41 | + try: | ||
| 42 | + return t.numpy() | ||
| 43 | + except (TypeError, RuntimeError): | ||
| 44 | + return t.float().numpy() | ||
| 45 | + if hasattr(t, "detach"): | ||
| 46 | + try: | ||
| 47 | + return t.detach().cpu().numpy() | ||
| 48 | + except (TypeError, RuntimeError): | ||
| 49 | + return t.detach().cpu().float().numpy() | ||
| 50 | + return np.array(t) | ||
| 51 | + | ||
| 52 | + | ||
| 53 | +def _np_to_torch(arr): | ||
| 54 | + if arr is None: | ||
| 55 | + return None | ||
| 56 | + if hasattr(arr, "view") and hasattr(arr, "dim"): | ||
| 57 | + return arr | ||
| 58 | + if isinstance(arr, np.ndarray): | ||
| 59 | + dt = str(arr.dtype) | ||
| 60 | + if np.issubdtype(arr.dtype, np.integer): | ||
| 61 | + return torch.from_numpy(arr) | ||
| 62 | + if "float8_e5m2" in dt: | ||
| 63 | + return torch.from_numpy(arr.view(np.uint8)).view(torch.float8_e5m2) | ||
| 64 | + if "float8_e4m3" in dt: | ||
| 65 | + return torch.from_numpy(arr.view(np.uint8)).view(torch.float8_e4m3fn) | ||
| 66 | + if "hifloat8" in dt: | ||
| 67 | + return torch.from_numpy(arr.view(np.int8)).view(torch.int8) | ||
| 68 | + return torch.from_numpy(arr.astype(np.float32)) | ||
| 69 | + return arr | ||
| 70 | + | ||
| 71 | + | ||
| 72 | +def _quant_to_dtype(y_quant_np, dtype_str): | ||
| 73 | + if dtype_str not in ("int8", "hifloat8", "float8_e5m2", "float8_e4m3fn", "int4"): | ||
| 74 | + return y_quant_np | ||
| 75 | + | ||
| 76 | + y_quant_np = np.nan_to_num( | ||
| 77 | + y_quant_np, | ||
| 78 | + nan=0.0, | ||
| 79 | + posinf=_DST_SAT_MAX.get(dtype_str, 0.0), | ||
| 80 | + neginf=_DST_SAT_MIN.get(dtype_str, 0.0), | ||
| 81 | + ) | ||
| 82 | + | ||
| 83 | + if dtype_str in ("int8", "int4"): | ||
| 84 | + y_quant_np = np.round(y_quant_np, 0) | ||
| 85 | + | ||
| 86 | + if dtype_str in _DST_SAT_MAX: | ||
| 87 | + y_quant_np = np.clip( | ||
| 88 | + y_quant_np, _DST_SAT_MIN[dtype_str], _DST_SAT_MAX[dtype_str] | ||
| 89 | + ) | ||
| 90 | + | ||
| 91 | + if dtype_str == "int8": | ||
| 92 | + return y_quant_np.astype(np.int8) | ||
| 93 | + if dtype_str == "int4": | ||
| 94 | + from ttk.utilities.dtypes import numpy_int4 | ||
| 95 | + | ||
| 96 | + y_quant_np = np.clip(y_quant_np, -8, 7) | ||
| 97 | + return y_quant_np.astype(numpy_int4(), copy=False) | ||
| 98 | + if dtype_str == "hifloat8": | ||
| 99 | + from en_dtypes import hifloat8 | ||
| 100 | + | ||
| 101 | + return y_quant_np.astype(hifloat8, copy=False) | ||
| 102 | + if dtype_str == "float8_e5m2": | ||
| 103 | + from ml_dtypes import float8_e5m2 | ||
| 104 | + | ||
| 105 | + return y_quant_np.astype(float8_e5m2, copy=False) | ||
| 106 | + if dtype_str == "float8_e4m3fn": | ||
| 107 | + from ml_dtypes import float8_e4m3fn | ||
| 108 | + | ||
| 109 | + return y_quant_np.astype(float8_e4m3fn, copy=False) | ||
| 110 | + return y_quant_np | ||
| 111 | + | ||
| 112 | + | ||
| 113 | +def _pack_int4_to_int32(y_int4): | ||
| 114 | + """Pack the public ACLNN INT4 result into its int32 storage ABI.""" | ||
| 115 | + if y_int4.ndim == 0 or y_int4.shape[-1] % _INT4_VALUES_PER_INT32 != 0: | ||
| 116 | + raise ValueError( | ||
| 117 | + f"INT4 last dimension must be divisible by {_INT4_VALUES_PER_INT32}, " | ||
| 118 | + f"but got shape {y_int4.shape}" | ||
| 119 | + ) | ||
| 120 | + from ttk.utilities.dtypes import pack_4bits | ||
| 121 | + | ||
| 122 | + packed_bytes = pack_4bits(np.ascontiguousarray(y_int4)) | ||
| 123 | + packed_shape = y_int4.shape[:-1] + (y_int4.shape[-1] // _INT4_VALUES_PER_INT32,) | ||
| 124 | + return packed_bytes.view(np.int32).reshape(packed_shape) | ||
| 125 | + | ||
| 126 | + | ||
| 127 | +def aclnn_rms_norm_quant_golden( | ||
| 128 | + x, gamma, beta, scale, offset, epsilon=1e-6, y=None, dst_type=2, **kwargs | ||
| 129 | +): | ||
| 130 | + x = _to_numpy(x) | ||
| 131 | + gamma = _to_numpy(gamma) | ||
| 132 | + beta = _to_numpy(beta) | ||
| 133 | + scale = _to_numpy(scale) | ||
| 134 | + offset = _to_numpy(offset) | ||
| 135 | + | ||
| 136 | + if scale is not None and scale.dtype != np.float32: | ||
| 137 | + scale = scale.astype(np.float32) | ||
| 138 | + if offset is not None and offset.dtype != np.float32: | ||
| 139 | + offset = offset.astype(np.float32) | ||
| 140 | + | ||
| 141 | + x_fp32 = x.astype(np.float32) | ||
| 142 | + | ||
| 143 | + # ACLNN squeezes a legal [1, H] gamma to [H] before last-axis RMSNorm. | ||
| 144 | + n = len(x.shape) - 1 | ||
| 145 | + gamma_flat = gamma.reshape(-1) | ||
| 146 | + x_reshaped = x_fp32.reshape(x_fp32.shape[:n] + gamma_flat.shape) | ||
| 147 | + | ||
| 148 | + variance = np.mean(np.power(x_reshaped, 2), axis=n, keepdims=True) | ||
| 149 | + std = np.sqrt(variance + epsilon) | ||
| 150 | + rstd = 1 / std | ||
| 151 | + | ||
| 152 | + result_mid = x_reshaped * rstd | ||
| 153 | + | ||
| 154 | + gamma_fp32 = gamma.astype(np.float32) if gamma.dtype != np.float32 else gamma | ||
| 155 | + y_array = result_mid * gamma_fp32.reshape(gamma_flat.shape) | ||
| 156 | + | ||
| 157 | + if beta is not None: | ||
| 158 | + beta_reshaped = beta.reshape(gamma_flat.shape) | ||
| 159 | + y_array = y_array + beta_reshaped.astype(np.float32) | ||
| 160 | + | ||
| 161 | + y_dtype_str = "int8" | ||
| 162 | + if y is not None: | ||
| 163 | + dt_str = str(y.dtype) | ||
| 164 | + if "int8" in dt_str: | ||
| 165 | + y_dtype_str = "int8" | ||
| 166 | + elif "hifloat8" in dt_str: | ||
| 167 | + y_dtype_str = "hifloat8" | ||
| 168 | + elif "e5m2" in dt_str: | ||
| 169 | + y_dtype_str = "float8_e5m2" | ||
| 170 | + elif "e4m3" in dt_str: | ||
| 171 | + y_dtype_str = "float8_e4m3fn" | ||
| 172 | + elif "int4" in dt_str or "int32" in dt_str: | ||
| 173 | + y_dtype_str = "int4" | ||
| 174 | + else: | ||
| 175 | + DST_TYPE_MAP = { | ||
| 176 | + 2: "int8", | ||
| 177 | + 29: "int4", | ||
| 178 | + 34: "hifloat8", | ||
| 179 | + 35: "float8_e5m2", | ||
| 180 | + 36: "float8_e4m3fn", | ||
| 181 | + } | ||
| 182 | + y_dtype_str = DST_TYPE_MAP.get(dst_type, "int8") | ||
| 183 | + | ||
| 184 | + y_quant = ( | ||
| 185 | + y_array / scale.reshape(-1).reshape(scale.shape) | ||
| 186 | + if scale is not None | ||
| 187 | + else y_array | ||
| 188 | + ) | ||
| 189 | + if offset is not None: | ||
| 190 | + y_quant = y_quant + offset.reshape(-1).reshape(offset.shape) | ||
| 191 | + | ||
| 192 | + y_result = _quant_to_dtype(y_quant, y_dtype_str) | ||
| 193 | + if y_dtype_str == "int4": | ||
| 194 | + y_result = _pack_int4_to_int32(y_result) | ||
| 195 | + return _np_to_torch(y_result) | ||
| 196 | + | ||
| 197 | + | ||
| 198 | +class AclnnRmsNormQuantTestSpec: | ||
| 199 | + """量化输出按 1 LSB 判(standard='quant',判据实现见 comparison/quant.py)。 | ||
| 200 | + | ||
| 201 | + y = round(x / scale) 的 round 是阶跃函数:商落在 .5 附近时,浮点末位 1 ulp 的差异 | ||
| 202 | + 就足以让结果跳到相邻整数格。而末位差异来自归约次序、FMA 融合、硬件舍入等实现自由度, | ||
| 203 | + 不同实现之间必然存在。要求逐位相等等于要求复现某一个实现的浮点运算次序, | ||
| 204 | + 故按绝对误差 <= 1 判定。 | ||
| 205 | + """ | ||
| 206 | + | ||
| 207 | + golden = staticmethod(aclnn_rms_norm_quant_golden) | ||
| 208 | + tolerance = { | ||
| 209 | + "int8": {"standard": "quant"}, | ||
| 210 | + "int4": {"standard": "quant"}, | ||
| 211 | + "int32": {"standard": "quant", "packed_bits": 4}, | ||
| 212 | + "float8_e5m2": {"standard": "requant"}, | ||
| 213 | + "float8_e4m3fn": {"standard": "requant"}, | ||
| 214 | + } | ||
| 215 | + | ||
| 216 | + | ||
| 217 | +__spec__ = {"aclnnRmsNormQuant": "AclnnRmsNormQuantTestSpec"} | ||
| @@ -157,4 +157,4 @@ | |||
| 157 | | 调用方式 | 样例代码 | 说明 | | 157 | | 调用方式 | 样例代码 | 说明 | |
| 158 | | ---------------- | --------------------------- | --------------------------------------------------- | | 158 | | ---------------- | --------------------------- | --------------------------------------------------- | |
| 159 | | aclnn接口 | [test_aclnn_rms_norm_quant](../rms_norm_quant/examples/test_aclnn_rms_norm_quant.cpp) | 通过[aclnnRmsNormQuant](../rms_norm_quant/docs/aclnnRmsNormQuant.md)接口方式调用RmsNormQuantV2算子。 | | 159 | | aclnn接口 | [test_aclnn_rms_norm_quant](../rms_norm_quant/examples/test_aclnn_rms_norm_quant.cpp) | 通过[aclnnRmsNormQuant](../rms_norm_quant/docs/aclnnRmsNormQuant.md)接口方式调用RmsNormQuantV2算子。 | |
| 160 | -| 图模式 | - | 通过[算子IR](op_graph/rms_norm_quant_v2_proto.h)构图方式调用RmsNormQuantV2算子。 | | 160 | +| 图模式 | [test_geir_rms_norm_quant_v2](examples/test_geir_rms_norm_quant_v2.cpp) | 通过[算子IR](op_graph/rms_norm_quant_v2_proto.h)构图方式调用RmsNormQuantV2算子。 | |
| @@ -0,0 +1,131 @@ | |||
| 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 | +/** | ||
| 12 | + * @file test_geir_rms_norm_quant_v2.cpp | ||
| 13 | + * @brief RmsNormQuantV2 GE graph example with two INT8 outputs on Ascend 950. | ||
| 14 | + */ | ||
| 15 | + | ||
| 16 | +#include <cstdint> | ||
| 17 | +#include <cstdio> | ||
| 18 | +#include <map> | ||
| 19 | +#include <utility> | ||
| 20 | +#include <vector> | ||
| 21 | + | ||
| 22 | +#include "ge_api.h" | ||
| 23 | +#include "graph.h" | ||
| 24 | +#include "ops_proto_legacy.h" | ||
| 25 | +#include "tensor.h" | ||
| 26 | + | ||
| 27 | +#include "../op_graph/rms_norm_quant_v2_proto.h" | ||
| 28 | + | ||
| 29 | +namespace { | ||
| 30 | +constexpr int32_t SUCCESS_CODE = 0; | ||
| 31 | +constexpr int32_t FAILED_CODE = 1; | ||
| 32 | + | ||
| 33 | +ge::Tensor MakeTensor(const std::vector<int64_t>& shape, const std::vector<float>& values) | ||
| 34 | +{ | ||
| 35 | + ge::TensorDesc desc(ge::Shape(shape), ge::FORMAT_ND, ge::DT_FLOAT); | ||
| 36 | + desc.SetPlacement(ge::kPlacementHost); | ||
| 37 | + const auto* bytes = static_cast<const uint8_t*>(static_cast<const void*>(values.data())); | ||
| 38 | + return ge::Tensor(desc, bytes, values.size() * sizeof(float)); | ||
| 39 | +} | ||
| 40 | + | ||
| 41 | +bool CheckOutput(const ge::Tensor& tensor, const std::vector<int64_t>& shape, uint8_t expected) | ||
| 42 | +{ | ||
| 43 | + const auto& desc = tensor.GetTensorDesc(); | ||
| 44 | + if (desc.GetShape().GetDims() != shape || desc.GetDataType() != ge::DT_INT8 || tensor.GetData() == nullptr || | ||
| 45 | + tensor.GetSize() != static_cast<size_t>(desc.GetShape().GetShapeSize())) { | ||
| 46 | + std::printf("Unexpected output shape, type or data size\n"); | ||
| 47 | + return false; | ||
| 48 | + } | ||
| 49 | + for (size_t index = 0; index < tensor.GetSize(); ++index) { | ||
| 50 | + if (tensor.GetData()[index] != expected) { | ||
| 51 | + std::printf("Output mismatch at %zu: got %u, expected %u\n", index, | ||
| 52 | + static_cast<unsigned int>(tensor.GetData()[index]), static_cast<unsigned int>(expected)); | ||
| 53 | + return false; | ||
| 54 | + } | ||
| 55 | + } | ||
| 56 | + return true; | ||
| 57 | +} | ||
| 58 | + | ||
| 59 | +int32_t RunExample() | ||
| 60 | +{ | ||
| 61 | + const std::vector<int64_t> xShape = {2, 16}; | ||
| 62 | + // x and gamma are all ones. Division by scales1=0.5 and scales2=0.25 gives y1=2 and y2=4. | ||
| 63 | + const std::vector<ge::Tensor> inputs = {MakeTensor(xShape, std::vector<float>(32, 1.0f)), | ||
| 64 | + MakeTensor({16}, std::vector<float>(16, 1.0f)), MakeTensor({1}, {0.5f}), | ||
| 65 | + MakeTensor({1}, {0.25f})}; | ||
| 66 | + std::vector<ge::Operator> dataNodes; | ||
| 67 | + const char* const inputNames[] = {"x", "gamma", "scales1", "scales2"}; | ||
| 68 | + ge::Graph graph("rms_norm_quant_v2_example"); | ||
| 69 | + for (size_t index = 0; index < inputs.size(); ++index) { | ||
| 70 | + auto data = ge::op::Data(inputNames[index]).set_attr_index(static_cast<int64_t>(index)); | ||
| 71 | + const auto& desc = inputs[index].GetTensorDesc(); | ||
| 72 | + if (data.update_input_desc_x(desc) != ge::GRAPH_SUCCESS || | ||
| 73 | + data.update_output_desc_y(desc) != ge::GRAPH_SUCCESS || graph.AddOp(data) != ge::GRAPH_SUCCESS) { | ||
| 74 | + std::printf("Failed to create input %s\n", inputNames[index]); | ||
| 75 | + return FAILED_CODE; | ||
| 76 | + } | ||
| 77 | + dataNodes.push_back(data); | ||
| 78 | + } | ||
| 79 | + | ||
| 80 | + auto quant = ge::op::RmsNormQuantV2("rms_norm_quant_v2"); | ||
| 81 | + quant.set_input_x(dataNodes[0]) | ||
| 82 | + .set_input_gamma(dataNodes[1]) | ||
| 83 | + .set_input_scales1(dataNodes[2]) | ||
| 84 | + .set_input_scales2(dataNodes[3]) | ||
| 85 | + .set_attr_epsilon(1.0e-6f) | ||
| 86 | + .set_attr_div_mode(true) | ||
| 87 | + .set_attr_dst_type(ge::DT_INT8); | ||
| 88 | + // zero_points1, zero_points2 and beta are omitted. Both outputs are valid because scales2 is present. | ||
| 89 | + const ge::TensorDesc outputDesc(ge::Shape(xShape), ge::FORMAT_ND, ge::DT_INT8); | ||
| 90 | + if (quant.update_output_desc_y1(outputDesc) != ge::GRAPH_SUCCESS || | ||
| 91 | + quant.update_output_desc_y2(outputDesc) != ge::GRAPH_SUCCESS || graph.AddOp(quant) != ge::GRAPH_SUCCESS) { | ||
| 92 | + std::printf("Failed to create RmsNormQuantV2 node\n"); | ||
| 93 | + return FAILED_CODE; | ||
| 94 | + } | ||
| 95 | + graph.SetInputs(dataNodes).SetOutputs(std::vector<std::pair<ge::Operator, std::vector<size_t>>>{{quant, {0, 1}}}); | ||
| 96 | + | ||
| 97 | + const std::map<ge::AscendString, ge::AscendString> sessionOptions; | ||
| 98 | + ge::Session session(sessionOptions); | ||
| 99 | + if (session.AddGraph(0, graph) != ge::SUCCESS) { | ||
| 100 | + std::printf("AddGraph failed\n"); | ||
| 101 | + return FAILED_CODE; | ||
| 102 | + } | ||
| 103 | + std::vector<ge::Tensor> outputs; | ||
| 104 | + if (session.RunGraph(0, inputs, outputs) != ge::SUCCESS) { | ||
| 105 | + std::printf("RunGraph failed\n"); | ||
| 106 | + return FAILED_CODE; | ||
| 107 | + } | ||
| 108 | + if (outputs.size() != 2 || !CheckOutput(outputs[0], xShape, 2) || !CheckOutput(outputs[1], xShape, 4)) { | ||
| 109 | + std::printf("RmsNormQuantV2 output verification failed\n"); | ||
| 110 | + return FAILED_CODE; | ||
| 111 | + } | ||
| 112 | + std::printf("RmsNormQuantV2 PASS: y1=[2,...], y2=[4,...], both shapes=[2,16]\n"); | ||
| 113 | + return SUCCESS_CODE; | ||
| 114 | +} | ||
| 115 | +} // namespace | ||
| 116 | + | ||
| 117 | +int main() | ||
| 118 | +{ | ||
| 119 | + const std::map<ge::AscendString, ge::AscendString> options = {{"ge.exec.deviceId", "0"}, {"ge.graphRunMode", "1"}}; | ||
| 120 | + if (ge::GEInitialize(options) != ge::SUCCESS) { | ||
| 121 | + std::printf("GEInitialize failed\n"); | ||
| 122 | + return FAILED_CODE; | ||
| 123 | + } | ||
| 124 | + const int32_t result = RunExample(); | ||
| 125 | + // RunExample destroys the session before the global GE resources are released. | ||
| 126 | + if (ge::GEFinalize() != ge::SUCCESS) { | ||
| 127 | + std::printf("GEFinalize failed\n"); | ||
| 128 | + return FAILED_CODE; | ||
| 129 | + } | ||
| 130 | + return result; | ||
| 131 | +} | ||
| @@ -0,0 +1,199 @@ | |||
| 1 | +#!/usr/bin/env python3 | ||
| 2 | +# -*- coding: UTF-8 -*- | ||
| 3 | +# 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 | ||
| 5 | +# CANN Open Software License Agreement Version 2.0 (the "License"). | ||
| 6 | +# Please refer to the License for details. You may not use this file except in compliance with the License. | ||
| 7 | +# 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. | ||
| 9 | +# See LICENSE in the root of the software repository for the full text of the License. | ||
| 10 | + | ||
| 11 | +import numpy as np | ||
| 12 | + | ||
| 13 | + | ||
| 14 | +_DST_SAT_MAX = { | ||
| 15 | + "int8": 127.0, | ||
| 16 | + "int4": 7.0, | ||
| 17 | + "float8_e5m2": 57344.0, | ||
| 18 | + "float8_e4m3fn": 448.0, | ||
| 19 | + "hifloat8": 32768.0, | ||
| 20 | +} | ||
| 21 | +_DST_SAT_MIN = { | ||
| 22 | + "int8": -128.0, | ||
| 23 | + "int4": -8.0, | ||
| 24 | + "float8_e5m2": -57344.0, | ||
| 25 | + "float8_e4m3fn": -448.0, | ||
| 26 | + "hifloat8": -32768.0, | ||
| 27 | +} | ||
| 28 | + | ||
| 29 | + | ||
| 30 | +__golden__ = {"kernel": {"rms_norm_quant_v2": "rms_norm_quant_v2"}} | ||
| 31 | +__spec__ = {"rms_norm_quant_v2": "RmsNormQuantV2TestSpec"} | ||
| 32 | + | ||
| 33 | + | ||
| 34 | +def rms_norm_quant_v2( | ||
| 35 | + input_x, | ||
| 36 | + input_gamma, | ||
| 37 | + input_scales1, | ||
| 38 | + input_scales2=None, | ||
| 39 | + input_zero_points1=None, | ||
| 40 | + input_zero_points2=None, | ||
| 41 | + input_beta=None, | ||
| 42 | + epsilon=1e-6, | ||
| 43 | + div_mode=True, | ||
| 44 | + output_dtypes=None, | ||
| 45 | + dst_type=2, | ||
| 46 | + **kwargs, | ||
| 47 | +): | ||
| 48 | + DST_TYPE_MAP = { | ||
| 49 | + 2: "int8", | ||
| 50 | + 29: "int4", | ||
| 51 | + 34: "hifloat8", | ||
| 52 | + 35: "float8_e5m2", | ||
| 53 | + 36: "float8_e4m3fn", | ||
| 54 | + } | ||
| 55 | + if output_dtypes is None: | ||
| 56 | + quant_dtype = DST_TYPE_MAP.get(dst_type, "int8") | ||
| 57 | + output_dtypes = (quant_dtype, quant_dtype) | ||
| 58 | + y1_dtype = str(output_dtypes[0]) if output_dtypes is not None else "int8" | ||
| 59 | + y2_dtype = ( | ||
| 60 | + str(output_dtypes[1]) | ||
| 61 | + if (output_dtypes is not None and len(output_dtypes) > 1) | ||
| 62 | + else None | ||
| 63 | + ) | ||
| 64 | + | ||
| 65 | + # Flatten only the normalization axes. Quantization parameters describe | ||
| 66 | + # their own suffix of x, which need not have the same rank as gamma. | ||
| 67 | + original_x_shape = input_x.shape | ||
| 68 | + norm_dim_num = max(1, input_gamma.ndim) | ||
| 69 | + n = input_x.ndim - norm_dim_num | ||
| 70 | + input_gamma = input_gamma.reshape(-1) | ||
| 71 | + x_shape = input_x.shape[0:n] + input_gamma.shape | ||
| 72 | + input_x = input_x.reshape(x_shape) | ||
| 73 | + | ||
| 74 | + len_shape_x = len(input_x.shape) | ||
| 75 | + len_shape_gamma = len(input_gamma.shape) | ||
| 76 | + axis = len_shape_x - len_shape_gamma | ||
| 77 | + input_x_dtype = input_x.dtype | ||
| 78 | + input_scales_dtype = input_scales1.dtype | ||
| 79 | + input_zp_dtype = None | ||
| 80 | + if input_zero_points1 is not None: | ||
| 81 | + input_zp_dtype = input_zero_points1.dtype | ||
| 82 | + if input_zero_points2 is not None: | ||
| 83 | + input_zp_dtype = input_zero_points2.dtype | ||
| 84 | + | ||
| 85 | + if input_scales_dtype is not np.float32: | ||
| 86 | + input_scales1 = input_scales1.astype(np.float32) | ||
| 87 | + if input_scales2 is not None: | ||
| 88 | + if input_scales_dtype is not np.float32: | ||
| 89 | + input_scales2 = input_scales2.astype(np.float32) | ||
| 90 | + if input_zero_points1 is not None: | ||
| 91 | + if input_zp_dtype == np.int32: | ||
| 92 | + input_zero_points1 = input_zero_points1.astype(np.float32) | ||
| 93 | + elif input_scales_dtype is not np.float32: | ||
| 94 | + input_zero_points1 = input_zero_points1.astype(np.float32) | ||
| 95 | + if input_zero_points2 is not None: | ||
| 96 | + if input_zp_dtype == np.int32: | ||
| 97 | + input_zero_points2 = input_zero_points2.astype(np.float32) | ||
| 98 | + elif input_scales_dtype is not np.float32: | ||
| 99 | + input_zero_points2 = input_zero_points2.astype(np.float32) | ||
| 100 | + | ||
| 101 | + x_fp32 = input_x.astype(np.float32) | ||
| 102 | + variance = np.mean(np.power(x_fp32, 2), axis=axis, keepdims=True) | ||
| 103 | + std = np.sqrt(variance + epsilon) | ||
| 104 | + rstd = 1 / std | ||
| 105 | + result_mid = x_fp32 * rstd | ||
| 106 | + | ||
| 107 | + if input_x_dtype == np.float32: | ||
| 108 | + y_array = result_mid * input_gamma | ||
| 109 | + elif input_x_dtype == np.float16: | ||
| 110 | + input_gamma_fp32 = input_gamma.astype(np.float32) | ||
| 111 | + y_array = result_mid * input_gamma_fp32 | ||
| 112 | + else: | ||
| 113 | + input_gamma_fp32 = input_gamma.astype(np.float32) | ||
| 114 | + y_array = result_mid * input_gamma_fp32 | ||
| 115 | + | ||
| 116 | + if input_beta is not None: | ||
| 117 | + beta_reshaped = input_beta.reshape(input_gamma.shape) | ||
| 118 | + y_array = y_array + beta_reshaped.astype(np.float32) | ||
| 119 | + | ||
| 120 | + # Restore x's axes before broadcasting scalar or multidimensional scales | ||
| 121 | + # and zero points; never index shape[0] on a scalar quantization parameter. | ||
| 122 | + y_array = y_array.reshape(original_x_shape) | ||
| 123 | + | ||
| 124 | + if div_mode: | ||
| 125 | + input_scales1 = 1.0 / input_scales1 | ||
| 126 | + if input_scales2 is not None: | ||
| 127 | + input_scales2 = 1.0 / input_scales2 | ||
| 128 | + | ||
| 129 | + y1_quant = y_array * input_scales1 | ||
| 130 | + y2_quant = None | ||
| 131 | + if input_zero_points1 is not None: | ||
| 132 | + y1_quant = y1_quant + input_zero_points1 | ||
| 133 | + | ||
| 134 | + if input_scales2 is not None: | ||
| 135 | + y2_quant = y_array * input_scales2 | ||
| 136 | + if input_zero_points2 is not None: | ||
| 137 | + y2_quant = y2_quant + input_zero_points2 | ||
| 138 | + | ||
| 139 | + def quant_process(y_quant, q_dtype): | ||
| 140 | + y_quant = np.nan_to_num( | ||
| 141 | + y_quant, | ||
| 142 | + nan=0.0, | ||
| 143 | + posinf=_DST_SAT_MAX.get(q_dtype, 0.0), | ||
| 144 | + neginf=_DST_SAT_MIN.get(q_dtype, 0.0), | ||
| 145 | + ) | ||
| 146 | + if q_dtype in _DST_SAT_MAX: | ||
| 147 | + y_quant = np.clip(y_quant, _DST_SAT_MIN[q_dtype], _DST_SAT_MAX[q_dtype]) | ||
| 148 | + if q_dtype in ("int8", "int4"): | ||
| 149 | + y_quant = y_quant.astype(np.float16).astype(np.float32) | ||
| 150 | + y_quant = np.round(y_quant, 0) | ||
| 151 | + elif q_dtype not in ("hifloat8", "float8_e5m2", "float8_e4m3fn"): | ||
| 152 | + y_quant = np.round(y_quant, 0) | ||
| 153 | + if q_dtype == "int8": | ||
| 154 | + y_quant = y_quant.clip(-128, 127) | ||
| 155 | + y_quant = y_quant.astype("int8", copy=False) | ||
| 156 | + elif q_dtype == "int4": | ||
| 157 | + from ttk.utilities.dtypes import numpy_int4 | ||
| 158 | + | ||
| 159 | + y_quant = np.clip(y_quant, -8, 7) | ||
| 160 | + y_quant = y_quant.astype(numpy_int4(), copy=False) | ||
| 161 | + elif q_dtype == "int32": | ||
| 162 | + y_quant = np.nan_to_num(y_quant, nan=0) | ||
| 163 | + y_quant = np.clip(y_quant, -2147483648, 2147483647) | ||
| 164 | + y_quant = y_quant.astype("int32", copy=False) | ||
| 165 | + elif q_dtype == "float8_e5m2": | ||
| 166 | + from ml_dtypes import float8_e5m2 as numpy_float8_e5m2 | ||
| 167 | + | ||
| 168 | + y_quant = y_quant.astype(numpy_float8_e5m2, copy=False) | ||
| 169 | + elif q_dtype == "float8_e4m3fn": | ||
| 170 | + from ml_dtypes import float8_e4m3fn as numpy_float8_e4m3fn | ||
| 171 | + | ||
| 172 | + y_quant = y_quant.astype(numpy_float8_e4m3fn, copy=False) | ||
| 173 | + elif q_dtype == "hifloat8": | ||
| 174 | + from en_dtypes import hifloat8 as numpy_hifloat8 | ||
| 175 | + | ||
| 176 | + y_quant = y_quant.astype(numpy_hifloat8, copy=False) | ||
| 177 | + return y_quant | ||
| 178 | + | ||
| 179 | + return quant_process(y1_quant, y1_dtype), quant_process( | ||
| 180 | + y2_quant, y2_dtype | ||
| 181 | + ) if y2_quant is not None else None | ||
| 182 | + | ||
| 183 | + | ||
| 184 | +class RmsNormQuantV2TestSpec: | ||
| 185 | + """整数量化输出按 1 LSB 判;FP8 输出按相邻量化码判。 | ||
| 186 | + | ||
| 187 | + y = round(x / scale) 的 round 是阶跃函数:商落在 .5 附近时,浮点末位 1 ulp 的差异 | ||
| 188 | + 就足以让结果跳到相邻整数格。而末位差异来自归约次序、FMA 融合、硬件舍入等实现自由度, | ||
| 189 | + 不同实现之间必然存在。要求逐位相等等于要求复现某一个实现的浮点运算次序, | ||
| 190 | + 故整数按绝对误差 <= 1 判定,FP8 按 requant 的码距判定。 | ||
| 191 | + """ | ||
| 192 | + | ||
| 193 | + golden = staticmethod(rms_norm_quant_v2) | ||
| 194 | + tolerance = { | ||
| 195 | + "int8": {"standard": "quant"}, | ||
| 196 | + "int4": {"standard": "quant"}, | ||
| 197 | + "float8_e5m2": {"standard": "requant"}, | ||
| 198 | + "float8_e4m3fn": {"standard": "requant"}, | ||
| 199 | + } | ||
| @@ -0,0 +1,435 @@ | |||
| 1 | +#!/usr/bin/env python3 | ||
| 2 | +# -*- coding: UTF-8 -*- | ||
| 3 | +# 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 | ||
| 5 | +# CANN Open Software License Agreement Version 2.0 (the "License"). | ||
| 6 | +# Please refer to the License for details. You may not use this file except in compliance with the License. | ||
| 7 | +# 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. | ||
| 9 | +# See LICENSE in the root of the software repository for the full text of the License. | ||
| 10 | + | ||
| 11 | +import numpy as np | ||
| 12 | +import torch | ||
| 13 | + | ||
| 14 | + | ||
| 15 | +_DST_SAT_MAX = { | ||
| 16 | + "int8": 127.0, | ||
| 17 | + "int4": 7.0, | ||
| 18 | + "float8_e5m2": 57344.0, | ||
| 19 | + "float8_e4m3fn": 448.0, | ||
| 20 | + "hifloat8": 32768.0, | ||
| 21 | +} | ||
| 22 | +_DST_SAT_MIN = { | ||
| 23 | + "int8": -128.0, | ||
| 24 | + "int4": -8.0, | ||
| 25 | + "float8_e5m2": -57344.0, | ||
| 26 | + "float8_e4m3fn": -448.0, | ||
| 27 | + "hifloat8": -32768.0, | ||
| 28 | +} | ||
| 29 | + | ||
| 30 | + | ||
| 31 | +__golden__ = { | ||
| 32 | + "kernel": {"rms_norm_quant_v3": "rms_norm_quant_v3"}, | ||
| 33 | + "aclnn": {"aclnnRmsNormQuantV3": "aclnn_rms_norm_quant_v3_golden"}, | ||
| 34 | +} | ||
| 35 | +__spec__ = { | ||
| 36 | + "rms_norm_quant_v3": "RmsNormQuantV3TestSpec", | ||
| 37 | + "aclnnRmsNormQuantV3": "AclnnRmsNormQuantV3TestSpec", | ||
| 38 | +} | ||
| 39 | + | ||
| 40 | + | ||
| 41 | +def rms_norm_quant_v3( | ||
| 42 | + input_x, | ||
| 43 | + input_gamma, | ||
| 44 | + input_scales1, | ||
| 45 | + input_scales2=None, | ||
| 46 | + input_zero_points1=None, | ||
| 47 | + input_zero_points2=None, | ||
| 48 | + input_beta=None, | ||
| 49 | + epsilon=1e-6, | ||
| 50 | + div_mode=True, | ||
| 51 | + output_dtypes=None, | ||
| 52 | + dst_type=2, | ||
| 53 | + output_rstd=False, | ||
| 54 | + **kwargs, | ||
| 55 | +): | ||
| 56 | + DST_TYPE_MAP = { | ||
| 57 | + 2: "int8", | ||
| 58 | + 29: "int4", | ||
| 59 | + 34: "hifloat8", | ||
| 60 | + 35: "float8_e5m2", | ||
| 61 | + 36: "float8_e4m3fn", | ||
| 62 | + } | ||
| 63 | + if output_dtypes is None: | ||
| 64 | + quant_dtype = DST_TYPE_MAP.get(dst_type, "int8") | ||
| 65 | + output_dtypes = (quant_dtype, quant_dtype, "float32") | ||
| 66 | + y1_dtype = str(output_dtypes[0]) if output_dtypes is not None else "int8" | ||
| 67 | + y2_dtype = ( | ||
| 68 | + str(output_dtypes[1]) | ||
| 69 | + if (output_dtypes is not None and len(output_dtypes) > 1) | ||
| 70 | + else None | ||
| 71 | + ) | ||
| 72 | + | ||
| 73 | + # Flatten only the normalization axes. Quantization parameters describe | ||
| 74 | + # their own suffix of x, which need not have the same rank as gamma. | ||
| 75 | + original_x_shape = input_x.shape | ||
| 76 | + norm_dim_num = max(1, input_gamma.ndim) | ||
| 77 | + n = input_x.ndim - norm_dim_num | ||
| 78 | + input_gamma = input_gamma.reshape(-1) | ||
| 79 | + x_shape = input_x.shape[0:n] + input_gamma.shape | ||
| 80 | + input_x = input_x.reshape(x_shape) | ||
| 81 | + | ||
| 82 | + len_shape_x = len(input_x.shape) | ||
| 83 | + len_shape_gamma = len(input_gamma.shape) | ||
| 84 | + axis = len_shape_x - len_shape_gamma | ||
| 85 | + input_x_dtype = input_x.dtype | ||
| 86 | + input_scales_dtype = input_scales1.dtype | ||
| 87 | + input_zp_dtype = None | ||
| 88 | + if input_zero_points1 is not None: | ||
| 89 | + input_zp_dtype = input_zero_points1.dtype | ||
| 90 | + if input_zero_points2 is not None: | ||
| 91 | + input_zp_dtype = input_zero_points2.dtype | ||
| 92 | + | ||
| 93 | + if input_scales_dtype is not np.float32: | ||
| 94 | + input_scales1 = input_scales1.astype(np.float32) | ||
| 95 | + if input_scales2 is not None: | ||
| 96 | + if input_scales_dtype is not np.float32: | ||
| 97 | + input_scales2 = input_scales2.astype(np.float32) | ||
| 98 | + if input_zero_points1 is not None: | ||
| 99 | + if input_zp_dtype == np.int32: | ||
| 100 | + input_zero_points1 = input_zero_points1.astype(np.float32) | ||
| 101 | + elif input_scales_dtype is not np.float32: | ||
| 102 | + input_zero_points1 = input_zero_points1.astype(np.float32) | ||
| 103 | + if input_zero_points2 is not None: | ||
| 104 | + if input_zp_dtype == np.int32: | ||
| 105 | + input_zero_points2 = input_zero_points2.astype(np.float32) | ||
| 106 | + elif input_scales_dtype is not np.float32: | ||
| 107 | + input_zero_points2 = input_zero_points2.astype(np.float32) | ||
| 108 | + | ||
| 109 | + x_fp32 = input_x.astype(np.float32) | ||
| 110 | + variance = np.mean(np.power(x_fp32, 2), axis=axis, keepdims=True) | ||
| 111 | + std = np.sqrt(variance + epsilon) | ||
| 112 | + rstd = 1 / std | ||
| 113 | + result_mid = x_fp32 * rstd | ||
| 114 | + | ||
| 115 | + if input_x_dtype == np.float32: | ||
| 116 | + y_array = result_mid * input_gamma | ||
| 117 | + elif input_x_dtype == np.float16: | ||
| 118 | + input_gamma_fp32 = input_gamma.astype(np.float32) | ||
| 119 | + y_array = result_mid * input_gamma_fp32 | ||
| 120 | + else: | ||
| 121 | + input_gamma_fp32 = input_gamma.astype(np.float32) | ||
| 122 | + y_array = result_mid * input_gamma_fp32 | ||
| 123 | + | ||
| 124 | + if input_beta is not None: | ||
| 125 | + beta_reshaped = input_beta.reshape(input_gamma.shape) | ||
| 126 | + y_array = y_array + beta_reshaped.astype(np.float32) | ||
| 127 | + | ||
| 128 | + # Restore x's axes before broadcasting scalar or multidimensional scales | ||
| 129 | + # and zero points; never index shape[0] on a scalar quantization parameter. | ||
| 130 | + y_array = y_array.reshape(original_x_shape) | ||
| 131 | + | ||
| 132 | + if div_mode: | ||
| 133 | + input_scales1 = 1.0 / input_scales1 | ||
| 134 | + if input_scales2 is not None: | ||
| 135 | + input_scales2 = 1.0 / input_scales2 | ||
| 136 | + | ||
| 137 | + y1_quant = y_array * input_scales1 | ||
| 138 | + y2_quant = None | ||
| 139 | + if input_zero_points1 is not None: | ||
| 140 | + y1_quant = y1_quant + input_zero_points1 | ||
| 141 | + | ||
| 142 | + if input_scales2 is not None: | ||
| 143 | + y2_quant = y_array * input_scales2 | ||
| 144 | + if input_zero_points2 is not None: | ||
| 145 | + y2_quant = y2_quant + input_zero_points2 | ||
| 146 | + | ||
| 147 | + def quant_process(y_quant, q_dtype): | ||
| 148 | + y_quant = np.nan_to_num( | ||
| 149 | + y_quant, | ||
| 150 | + nan=0.0, | ||
| 151 | + posinf=_DST_SAT_MAX.get(q_dtype, 0.0), | ||
| 152 | + neginf=_DST_SAT_MIN.get(q_dtype, 0.0), | ||
| 153 | + ) | ||
| 154 | + if q_dtype in _DST_SAT_MAX: | ||
| 155 | + y_quant = np.clip(y_quant, _DST_SAT_MIN[q_dtype], _DST_SAT_MAX[q_dtype]) | ||
| 156 | + if q_dtype in ("int8", "int4"): | ||
| 157 | + y_quant = y_quant.astype(np.float16).astype(np.float32) | ||
| 158 | + y_quant = np.round(y_quant, 0) | ||
| 159 | + elif q_dtype not in ("hifloat8", "float8_e5m2", "float8_e4m3fn"): | ||
| 160 | + y_quant = np.round(y_quant, 0) | ||
| 161 | + if q_dtype == "int8": | ||
| 162 | + y_quant = y_quant.clip(-128, 127) | ||
| 163 | + y_quant = y_quant.astype("int8", copy=False) | ||
| 164 | + elif q_dtype == "int4": | ||
| 165 | + from ttk.utilities.dtypes import numpy_int4 | ||
| 166 | + | ||
| 167 | + y_quant = np.clip(y_quant, -8, 7) | ||
| 168 | + y_quant = y_quant.astype(numpy_int4(), copy=False) | ||
| 169 | + elif q_dtype == "int32": | ||
| 170 | + y_quant = np.nan_to_num(y_quant, nan=0) | ||
| 171 | + y_quant = np.clip(y_quant, -2147483648, 2147483647) | ||
| 172 | + y_quant = y_quant.astype("int32", copy=False) | ||
| 173 | + elif q_dtype == "float8_e5m2": | ||
| 174 | + from ml_dtypes import float8_e5m2 as numpy_float8_e5m2 | ||
| 175 | + | ||
| 176 | + y_quant = y_quant.astype(numpy_float8_e5m2, copy=False) | ||
| 177 | + elif q_dtype == "float8_e4m3fn": | ||
| 178 | + from ml_dtypes import float8_e4m3fn as numpy_float8_e4m3fn | ||
| 179 | + | ||
| 180 | + y_quant = y_quant.astype(numpy_float8_e4m3fn, copy=False) | ||
| 181 | + elif q_dtype == "hifloat8": | ||
| 182 | + from en_dtypes import hifloat8 as numpy_hifloat8 | ||
| 183 | + | ||
| 184 | + y_quant = y_quant.astype(numpy_hifloat8, copy=False) | ||
| 185 | + return y_quant | ||
| 186 | + | ||
| 187 | + rstd_shape = original_x_shape[:n] + (1,) * norm_dim_num | ||
| 188 | + rstd_out = ( | ||
| 189 | + rstd.reshape(rstd_shape).astype(np.float32, copy=False) if output_rstd else None | ||
| 190 | + ) | ||
| 191 | + return ( | ||
| 192 | + quant_process(y1_quant, y1_dtype), | ||
| 193 | + quant_process(y2_quant, y2_dtype) if y2_quant is not None else None, | ||
| 194 | + rstd_out, | ||
| 195 | + ) | ||
| 196 | + | ||
| 197 | + | ||
| 198 | +class RmsNormQuantV3TestSpec: | ||
| 199 | + """整数量化输出按 1 LSB 判;FP8 输出按相邻量化码判。 | ||
| 200 | + | ||
| 201 | + y = round(x / scale) 的 round 是阶跃函数:商落在 .5 附近时,浮点末位 1 ulp 的差异 | ||
| 202 | + 就足以让结果跳到相邻整数格。而末位差异来自归约次序、FMA 融合、硬件舍入等实现自由度, | ||
| 203 | + 不同实现之间必然存在。要求逐位相等等于要求复现某一个实现的浮点运算次序, | ||
| 204 | + 故整数按绝对误差 <= 1 判定,FP8 按 requant 的码距判定。 | ||
| 205 | + """ | ||
| 206 | + | ||
| 207 | + golden = staticmethod(rms_norm_quant_v3) | ||
| 208 | + tolerance = { | ||
| 209 | + "int8": {"standard": "quant"}, | ||
| 210 | + "int4": {"standard": "quant"}, | ||
| 211 | + "float8_e5m2": {"standard": "requant"}, | ||
| 212 | + "float8_e4m3fn": {"standard": "requant"}, | ||
| 213 | + "float32": {"standard": "stat_rel_err"}, | ||
| 214 | + } | ||
| 215 | + | ||
| 216 | + | ||
| 217 | +_INT4_VALUES_PER_BYTE = 2 | ||
| 218 | +_INT4_VALUES_PER_INT32 = np.dtype(np.int32).itemsize * _INT4_VALUES_PER_BYTE | ||
| 219 | + | ||
| 220 | + | ||
| 221 | +def _to_numpy(t): | ||
| 222 | + if t is None: | ||
| 223 | + return None | ||
| 224 | + if isinstance(t, np.ndarray): | ||
| 225 | + return t | ||
| 226 | + if hasattr(t, "numpy"): | ||
| 227 | + try: | ||
| 228 | + return t.numpy() | ||
| 229 | + except (TypeError, RuntimeError): | ||
| 230 | + return t.float().numpy() | ||
| 231 | + if hasattr(t, "detach"): | ||
| 232 | + try: | ||
| 233 | + return t.detach().cpu().numpy() | ||
| 234 | + except (TypeError, RuntimeError): | ||
| 235 | + return t.detach().cpu().float().numpy() | ||
| 236 | + return np.array(t) | ||
| 237 | + | ||
| 238 | + | ||
| 239 | +def _np_to_torch(arr): | ||
| 240 | + if arr is None: | ||
| 241 | + return None | ||
| 242 | + if hasattr(arr, "view") and hasattr(arr, "dim"): | ||
| 243 | + return arr | ||
| 244 | + if isinstance(arr, np.ndarray): | ||
| 245 | + dt = str(arr.dtype) | ||
| 246 | + if np.issubdtype(arr.dtype, np.integer): | ||
| 247 | + return torch.from_numpy(arr) | ||
| 248 | + if "float8_e5m2" in dt: | ||
| 249 | + return torch.from_numpy(arr.view(np.uint8)).view(torch.float8_e5m2) | ||
| 250 | + if "float8_e4m3" in dt: | ||
| 251 | + return torch.from_numpy(arr.view(np.uint8)).view(torch.float8_e4m3fn) | ||
| 252 | + if "hifloat8" in dt: | ||
| 253 | + return torch.from_numpy(arr.view(np.int8)).view(torch.int8) | ||
| 254 | + return torch.from_numpy(arr.astype(np.float32)) | ||
| 255 | + return arr | ||
| 256 | + | ||
| 257 | + | ||
| 258 | +def _quant_to_dtype(y_quant_np, dtype_str): | ||
| 259 | + if dtype_str not in ("int8", "hifloat8", "float8_e5m2", "float8_e4m3fn", "int4"): | ||
| 260 | + return y_quant_np | ||
| 261 | + | ||
| 262 | + y_quant_np = np.nan_to_num( | ||
| 263 | + y_quant_np, | ||
| 264 | + nan=0.0, | ||
| 265 | + posinf=_DST_SAT_MAX.get(dtype_str, 0.0), | ||
| 266 | + neginf=_DST_SAT_MIN.get(dtype_str, 0.0), | ||
| 267 | + ) | ||
| 268 | + | ||
| 269 | + if dtype_str in ("int8", "int4"): | ||
| 270 | + y_quant_np = np.round(y_quant_np, 0) | ||
| 271 | + | ||
| 272 | + if dtype_str in _DST_SAT_MAX: | ||
| 273 | + y_quant_np = np.clip( | ||
| 274 | + y_quant_np, _DST_SAT_MIN[dtype_str], _DST_SAT_MAX[dtype_str] | ||
| 275 | + ) | ||
| 276 | + | ||
| 277 | + if dtype_str == "int8": | ||
| 278 | + return y_quant_np.astype(np.int8) | ||
| 279 | + if dtype_str == "int4": | ||
| 280 | + from ttk.utilities.dtypes import numpy_int4 | ||
| 281 | + | ||
| 282 | + y_quant_np = np.clip(y_quant_np, -8, 7) | ||
| 283 | + return y_quant_np.astype(numpy_int4(), copy=False) | ||
| 284 | + if dtype_str == "hifloat8": | ||
| 285 | + from en_dtypes import hifloat8 | ||
| 286 | + | ||
| 287 | + return y_quant_np.astype(hifloat8, copy=False) | ||
| 288 | + if dtype_str == "float8_e5m2": | ||
| 289 | + from ml_dtypes import float8_e5m2 | ||
| 290 | + | ||
| 291 | + return y_quant_np.astype(float8_e5m2, copy=False) | ||
| 292 | + if dtype_str == "float8_e4m3fn": | ||
| 293 | + from ml_dtypes import float8_e4m3fn | ||
| 294 | + | ||
| 295 | + return y_quant_np.astype(float8_e4m3fn, copy=False) | ||
| 296 | + return y_quant_np | ||
| 297 | + | ||
| 298 | + | ||
| 299 | +def _pack_int4_to_int32(y_int4): | ||
| 300 | + """Pack the public ACLNN INT4 result into its int32 storage ABI.""" | ||
| 301 | + if y_int4.ndim == 0 or y_int4.shape[-1] % _INT4_VALUES_PER_INT32 != 0: | ||
| 302 | + raise ValueError( | ||
| 303 | + f"INT4 last dimension must be divisible by {_INT4_VALUES_PER_INT32}, " | ||
| 304 | + f"but got shape {y_int4.shape}" | ||
| 305 | + ) | ||
| 306 | + from ttk.utilities.dtypes import pack_4bits | ||
| 307 | + | ||
| 308 | + packed_bytes = pack_4bits(np.ascontiguousarray(y_int4)) | ||
| 309 | + packed_shape = y_int4.shape[:-1] + (y_int4.shape[-1] // _INT4_VALUES_PER_INT32,) | ||
| 310 | + return packed_bytes.view(np.int32).reshape(packed_shape) | ||
| 311 | + | ||
| 312 | + | ||
| 313 | +# 形参表必须逐位对齐 aclnnRmsNormQuantV3 的原型:属性(epsilon/divMode/outputRstd) | ||
| 314 | +# 排在输出 tensor(y/rstd)**前面**。把 y/rstd 提前会让 epsilon 收到一个 tensor, | ||
| 315 | +# 典型报错 'float' object has no attribute 'dtype' / 精度整片错。 | ||
| 316 | +# dst_type 不在原型里,只能挂末尾走 extra_attrs。 | ||
| 317 | +def aclnn_rms_norm_quant_v3_golden( | ||
| 318 | + x, | ||
| 319 | + gamma, | ||
| 320 | + scale, | ||
| 321 | + offset, | ||
| 322 | + beta, | ||
| 323 | + epsilon=1e-6, | ||
| 324 | + div_mode=True, | ||
| 325 | + output_rstd=False, | ||
| 326 | + y=None, | ||
| 327 | + rstd=None, | ||
| 328 | + dst_type=2, | ||
| 329 | + **kwargs, | ||
| 330 | +): | ||
| 331 | + divMode = div_mode | ||
| 332 | + outputRstd = output_rstd | ||
| 333 | + x = _to_numpy(x) | ||
| 334 | + gamma = _to_numpy(gamma) | ||
| 335 | + scale = _to_numpy(scale) | ||
| 336 | + offset = _to_numpy(offset) | ||
| 337 | + beta = _to_numpy(beta) | ||
| 338 | + | ||
| 339 | + if scale is not None and scale.dtype != np.float32: | ||
| 340 | + scale = scale.astype(np.float32) | ||
| 341 | + if offset is not None and offset.dtype != np.float32: | ||
| 342 | + offset = offset.astype(np.float32) | ||
| 343 | + | ||
| 344 | + x_fp32 = x.astype(np.float32) | ||
| 345 | + | ||
| 346 | + # ACLNN squeezes a legal [1, H] gamma to [H] before last-axis RMSNorm. | ||
| 347 | + n = len(x.shape) - 1 | ||
| 348 | + gamma_flat = gamma.reshape(-1) | ||
| 349 | + x_reshaped = x_fp32.reshape(x_fp32.shape[:n] + gamma_flat.shape) | ||
| 350 | + | ||
| 351 | + variance = np.mean(np.power(x_reshaped, 2), axis=n, keepdims=True) | ||
| 352 | + std = np.sqrt(variance + epsilon) | ||
| 353 | + rstd_val = 1 / std | ||
| 354 | + | ||
| 355 | + result_mid = x_reshaped * rstd_val | ||
| 356 | + | ||
| 357 | + gamma_fp32 = gamma.astype(np.float32) if gamma.dtype != np.float32 else gamma | ||
| 358 | + y_array = result_mid * gamma_fp32.reshape(gamma_flat.shape) | ||
| 359 | + | ||
| 360 | + if beta is not None: | ||
| 361 | + beta_reshaped = beta.reshape(gamma_flat.shape) | ||
| 362 | + y_array = y_array + beta_reshaped.astype(np.float32) | ||
| 363 | + | ||
| 364 | + y_dtype_str = "int8" | ||
| 365 | + if y is not None: | ||
| 366 | + dt_str = str(y.dtype) | ||
| 367 | + if "int8" in dt_str: | ||
| 368 | + y_dtype_str = "int8" | ||
| 369 | + elif "hifloat8" in dt_str: | ||
| 370 | + y_dtype_str = "hifloat8" | ||
| 371 | + elif "e5m2" in dt_str: | ||
| 372 | + y_dtype_str = "float8_e5m2" | ||
| 373 | + elif "e4m3" in dt_str: | ||
| 374 | + y_dtype_str = "float8_e4m3fn" | ||
| 375 | + elif "int4" in dt_str or "int32" in dt_str: | ||
| 376 | + y_dtype_str = "int4" | ||
| 377 | + else: | ||
| 378 | + DST_TYPE_MAP = { | ||
| 379 | + 2: "int8", | ||
| 380 | + 29: "int4", | ||
| 381 | + 34: "hifloat8", | ||
| 382 | + 35: "float8_e5m2", | ||
| 383 | + 36: "float8_e4m3fn", | ||
| 384 | + } | ||
| 385 | + y_dtype_str = DST_TYPE_MAP.get(dst_type, "int8") | ||
| 386 | + | ||
| 387 | + if divMode: | ||
| 388 | + y_quant = ( | ||
| 389 | + y_array / scale.reshape(-1).reshape(scale.shape) | ||
| 390 | + if scale is not None | ||
| 391 | + else y_array | ||
| 392 | + ) | ||
| 393 | + else: | ||
| 394 | + y_quant = ( | ||
| 395 | + y_array * scale.reshape(-1).reshape(scale.shape) | ||
| 396 | + if scale is not None | ||
| 397 | + else y_array | ||
| 398 | + ) | ||
| 399 | + if offset is not None: | ||
| 400 | + y_quant = y_quant + offset.reshape(-1).reshape(offset.shape) | ||
| 401 | + | ||
| 402 | + y_result = _quant_to_dtype(y_quant, y_dtype_str) | ||
| 403 | + if y_dtype_str == "int4": | ||
| 404 | + y_result = _pack_int4_to_int32(y_result) | ||
| 405 | + y_t = _np_to_torch(y_result) | ||
| 406 | + | ||
| 407 | + if outputRstd: | ||
| 408 | + rstd_np = rstd_val.astype(np.float32) | ||
| 409 | + rstd_t = _np_to_torch(rstd_np) | ||
| 410 | + return [y_t, rstd_t] | ||
| 411 | + | ||
| 412 | + return [y_t, None] | ||
| 413 | + | ||
| 414 | + | ||
| 415 | +class AclnnRmsNormQuantV3TestSpec: | ||
| 416 | + """量化输出按 1 LSB 判(standard='quant',判据实现见 comparison/quant.py)。 | ||
| 417 | + | ||
| 418 | + y = round(x / scale) 的 round 是阶跃函数:商落在 .5 附近时,浮点末位 1 ulp 的差异 | ||
| 419 | + 就足以让结果跳到相邻整数格。而末位差异来自归约次序、FMA 融合、硬件舍入等实现自由度, | ||
| 420 | + 不同实现之间必然存在。要求逐位相等等于要求复现某一个实现的浮点运算次序, | ||
| 421 | + 故按绝对误差 <= 1 判定。 | ||
| 422 | + """ | ||
| 423 | + | ||
| 424 | + golden = staticmethod(aclnn_rms_norm_quant_v3_golden) | ||
| 425 | + tolerance = { | ||
| 426 | + "int8": {"standard": "quant"}, | ||
| 427 | + "int4": {"standard": "quant"}, | ||
| 428 | + "int32": {"standard": "quant", "packed_bits": 4}, | ||
| 429 | + "float8_e5m2": {"standard": "requant"}, | ||
| 430 | + "float8_e4m3fn": {"standard": "requant"}, | ||
| 431 | + # rstd is already produced as float32 by the golden. Avoid whole-call | ||
| 432 | + # dtype promotion, which cannot cross the Torch boundary for a | ||
| 433 | + # HIFLOAT8 sibling output. | ||
| 434 | + "float32": {"standard": "stat_rel_err"}, | ||
| 435 | + } | ||