CANN ops-nn AdaLayerNormQuant 算子深度指南:aclnnAdaLayerNormQuant 两段式接口原理、参数与实战调用
【免费下载链接】ops-nn本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。项目地址: https://gitcode.com/cann/ops-nn
本指南以 aclnnAdaLayerNormQuant 接口文档 为主体,结合 CANN ops-nn 仓库中 ada_layer_norm_quant 模块的源码实现、算子定义与单元测试,系统讲解该融合算子的功能原理、两段式 API 的完整调用流程、全部入参出参约束、错误码与典型调用示例。读者学完后可以独立完成 AdaLayerNorm 与 DynamicQuant 融合算子在 NPU 上的调用与结果校验。
一、算子是什么:AdaLayerNormQuant 的功能与融合动机
AdaLayerNormQuant 是 CANN ops-nn 仓库中位于 norm/ada_layer_norm_quant 目录下的一个融合算子,其核心功能是将「自适应层归一化(AdaLayerNorm)」与「下游的动态量化(DynamicQuant)」合并在一次内核计算中完成:先把输入数据做归一化处理,再将其量化为低精度整数(INT8 / FLOAT8 等),从而在保持精度的前提下提高计算效率并减少内存占用。
- 算子功能:AdaLayerNormQuant 将 AdaLayerNorm 和下游量化(目前仅支持 DynamicQuant)融合起来,主要用于执行自适应层归一化的量化操作,即将输入数据进行归一化处理,并量化为低精度整数。
- 典型场景:在 LLM 推理的激活值量化管线中,LayerNorm/AdaLayerNorm 之后通常紧跟一个量化节点,把归一化后的高精度激活张量压成 INT8/FP8 供后续矩阵运算使用。将其融合为单个算子可以省去中间张量的落盘与多次内核启动开销。
从源码结构看,该算子复用了 ada_layer_norm 模块的归一化内核基类(
ada_layer_norm_base.h/ada_layer_norm_base_v1.h),并在此基础上叠加量化输出路径,印证了「AdaLayerNorm + 量化」的融合设计意图。
二、产品支持情况
根据接口文档与 README,各产品线的支持情况如下:
| 产品 | 是否支持 |
|---|---|
| Ascend 950PR/Ascend 950DT | 支持 |
| Atlas A3 训练系列产品/Atlas A3 推理系列产品 | 支持 |
| Atlas A2 训练系列产品/Atlas A2 推理系列产品 | 支持 |
| Atlas 200I/500 A2 推理产品 | 不支持 |
| Atlas 推理系列产品 | 不支持 |
| Atlas 训练系列产品 | 不支持 |
此外,在Atlas A3 / Atlas A2 系列产品上,输出张量out的数据类型仅支持 INT8(详见下文参数说明)。
三、功能说明与计算公式
设输入为x,E(x) 为均值,Var(x) 为方差,row_max表示按行求最大值,算子按以下 5 步完成计算:
第 1 步:LayerNorm 归一化
$$ LayerNorm(x) = {{x-E(x)}\over\sqrt {Var(x)+epsilon}} * weightOptional + biasOptional $$
第 2 步:自适应调整(scale/shift)
$$ y = LayerNorm(x) * (1 + scale) + shift $$
第 3 步:可选平滑缩放(smoothScalesOptional 不为空时)
$$ y = y \cdot smoothScalesOptional $$
第 4 步:计算量化因子
对 y 求每行最大绝对值,并除以目标格式的表示范围上限(FP8_MAX / HIF8_MAX / INT8_MAX):
$$ quantScale = row_max(abs(y)) / (FP8_MAX / HIF8_MAX / INT8_MAX) $$
第 5 步:量化取整得到输出
$$ out = round(y / quantScale) $$
其中epsilon为添加到方差分母上的极小正数,用于防止除零、保证数值稳定(如 1e-5)。上述公式在 README 中给出了等价描述,两者保持一致。
四、两段式接口与函数原型
每个 aclnn 算子都采用两段式接口设计,必须先调用aclnnAdaLayerNormQuantGetWorkspaceSize获取计算所需 workspace 大小以及包含了算子计算流程的执行器,再调用aclnnAdaLayerNormQuant执行计算。
4.1 第一段接口:aclnnAdaLayerNormQuantGetWorkspaceSize
aclnnStatus aclnnAdaLayerNormQuantGetWorkspaceSize( const aclTensor* x, const aclTensor* scale, const aclTensor* shift, const aclTensor* weightOptional, const aclTensor* biasOptional, const aclTensor* smoothScalesOptional, double epsilon, const char* quantMode, aclTensor* out, aclTensor* quantScale, aclTensor* quantOffsetOptional, uint64_t* workspaceSize, aclOpExecutor** executor)4.2 第二段接口:aclnnAdaLayerNormQuant
aclnnStatus aclnnAdaLayerNormQuant( void *workspace, uint64_t workspaceSize, aclOpExecutor *executor, aclrtStream stream)第二段接口的 4 个参数含义如下:
| 参数名 | 输入/输出 | 描述 |
|---|---|---|
| workspace | 输入 | 在 Device 侧申请的 workspace 内存地址 |
| workspaceSize | 输入 | workspace 大小,由第一段接口获取 |
| executor | 输入 | op 执行器,包含算子计算流程 |
| stream | 输入 | 指定执行任务的 Stream |
4.3 从源码看两段式流程
在 aclnn_ada_layer_norm_quant.cpp 中,第一段接口完成如下关键动作:
- 创建 OpExecutor,并调用
CheckParams完成入参校验(空指针、数据类型、Shape、Attr); - 对
x/scale/shift及三个可选输入调用l0op::Contiguous做连续性处理(这正是文档中"非连续 Tensor √"的来源); - 调用
l0op::AdaLayerNormQuant(见 ada_layer_norm_quant.cpp)构建内核启动列表,内部会自动为out和quantScale分配输出张量; - 通过
executor->GetWorkspaceSize()返回 workspace 大小,并将 executor 释放给调用方。
第二段接口则统一走CommonOpExecutorRun完成实际计算下发。若输入为空 Tensor(x/scale/shift任一为空),第一段接口直接返回ACLNN_SUCCESS且workspaceSize = 0,不会构建计算流程。
五、参数说明(完整约束表)
以下为aclnnAdaLayerNormQuantGetWorkspaceSize的完整参数约束,源自接口文档,并可与 源码校验逻辑 相互印证。
| 参数名 | 输入/输出 | 描述 | 使用说明 | 数据类型 | 数据格式 | 维度(shape) | 非连续Tensor |
|---|---|---|---|---|---|---|---|
| x(aclTensor*) | 输入 | 输入待处理数据,对应公式中的 x | 不支持空 Tensor;shape 为 [B…, S, H],B 支持 0~6 个维度 | FLOAT16、BFLOAT16 | ND | 2-8 | √ |
| scale(aclTensor*) | 输入 | 自适应缩放参数,对应公式中的 scale | 不支持空 Tensor;数据类型与 x 一致;shape 为 [B…, H] 或 [B…, 1, H],B 的维度数与大小与 x 一致,H 与 x 的 H 维一致 | FLOAT16、BFLOAT16 | ND | 1-8 | √ |
| shift(aclTensor*) | 输入 | 自适应偏移参数,对应公式中的 shift | 约束同 scale | FLOAT16、BFLOAT16 | ND | 1-8 | √ |
| weightOptional(aclTensor*) | 输入 | 可选,归一化缩放参数,对应公式中的 weightOptional | 不支持空 Tensor;数据类型与 x 一致;shape 为 [H] | FLOAT16、BFLOAT16 | ND | 1 | √ |
| biasOptional(aclTensor*) | 输入 | 可选,归一化偏移参数,对应公式中的 biasOptional | 约束同 weightOptional | FLOAT16、BFLOAT16 | ND | 1 | √ |
| smoothScalesOptional(aclTensor*) | 输入 | 可选,量化平滑权重,对应公式中的 smoothScalesOptional | 约束同 weightOptional | FLOAT16、BFLOAT16 | ND | 1 | √ |
| epsilon(double) | 输入 | 添加到分母中的值,确保数值稳定、防止除 0 | 建议传较小的正数,如 1e-5 | - | - | - | - |
| quantMode(char*) | 输入 | 量化模式 | 当前版本仅支持 "dynamic" | - | - | - | - |
| out(aclTensor*) | 输出 | 量化输出张量,对应公式中的 out | 不支持空 Tensor;shape 与 x 保持一致 | INT8、FLOAT8_E4M3FN、FLOAT8_E5M2、HIFLOAT8 | ND | 2-8 | √ |
| quantScale(aclTensor*) | 输出 | 量化系数,对应公式中的 quantScale | 不支持空 Tensor;shape 为 [B…, S],B 与 x 一致,S 与 x 的 S 维一致 | FLOAT32 | ND | 1-7 | √ |
| quantOffsetOptional(aclTensor*) | 输出 | 可选,非对称量化使用的 offset | 不支持空 Tensor;shape 与 quantScale 一致;当前版本暂不支持,传 nullptr | FLOAT16、BFLOAT16 | ND | 1-7 | √ |
| workspaceSize(uint64_t*) | 输出 | 返回需在 Device 侧申请的 workspace 大小 | - | - | - | - | - |
| executor(aclOpExecutor**) | 输出 | 返回 op 执行器,包含算子计算流程 | - | - | - | - | - |
平台差异:在 Atlas A3 / Atlas A2 系列产品上,输出
out的数据类型仅支持 INT8。
5.1 源码中的校验逻辑解读
从 aclnn_ada_layer_norm_quant.cpp 可以确认以下实现细节:
MIN_X_DIM = 2、MAX_X_DIM = 8:对应文档中 x 的 2~8 维约束;X_DTYPE_SUPPORT_LIST = {DT_FLOAT16, DT_BF16}:输入仅支持 FP16/BF16;OUT_DTYPE_SUPPORT_LIST_REGBASE在 Regbase(950 系列)下允许 INT8/HIFLOAT8/FP8_E5M2/FP8_E4M3FN 四种输出类型,而非 Regbase 平台(910B/A3)仅支持 INT8——这与文档中的平台差异说明一致;CheckShape中要求x各维均大于 0,scale/shift的 shape 必须是[B…, 1, H]或[B…, H]之一,weight/bias/smoothScales必须是[H],out与 x 同形,quantScale形如[B…, S];CheckAttr将quantMode限定为字符串"dynamic",其他取值一律报ACLNN_ERR_PARAM_INVALID;quantOffsetOptional非空即报错,印证了"当前版本暂不支持,传 nullptr"。
六、返回值与错误码
接口返回aclnnStatus状态码,具体含义参见 aclnn 返回码说明。
第一段接口完成入参校验,出现以下场景时报错:
| 返回码 | 错误码 | 描述 |
|---|---|---|
| ACLNN_ERR_PARAM_NULLPTR | 161001 | 传入的 x、scale、shift、out、quantScale 是空指针 |
| ACLNN_ERR_PARAM_INVALID(8 种场景) | 161002 | ① x、scale、shift、out、quantScale 的数据类型或数据格式不在支持范围;② weightOptional 非空时类型/格式不支持;③ biasOptional 非空时类型/格式不支持;④ smoothScalesOptional 非空时类型/格式不支持;⑤ quantMode 不为 "dynamic";⑥ quantOffsetOptional 不为空指针;⑦ scale、shift、weightOptional、biasOptional、smoothScalesOptional 与 x 的数据类型不一致;⑧ 上述张量的 shape 与参数说明不一致 |
上述错误场景在单元测试 test_aclnn_ada_layer_norm_quant.cpp 中有直接覆盖:例如x的 H 维为 0、输入类型使用 FLOAT、scaleshape 与x不匹配、weight长度不等于 H、out/quantScaleshape 错误等场景均断言返回ACLNN_ERR_PARAM_INVALID。
七、约束说明
- 确定性计算:
aclnnAdaLayerNormQuant默认采用确定性实现,即相同输入在多次运行中得到一致的输出结果,便于调试与精度对比(更多背景可参考确定性计算说明)。
八、调用示例(完整可编译流程)
接口文档给出了完整的 C++ 调用示例,仓库中的 examples/test_aclnn_ada_layer_norm_quant.cpp 即为同款可运行样例,编译与运行的具体流程请参考编译与运行样例。核心代码与要点如下:
#include <iostream> #include <vector> #include "acl/acl.h" #include "aclnnop/aclnn_ada_layer_norm_quant.h" #define CHECK_RET(cond, return_expr) \ do { \ if (!(cond)) { \ return_expr; \ } \ } while (0) #define LOG_PRINT(message, ...) \ do { \ printf(message, ##__VA_ARGS__); \ } while (0) int64_t GetShapeSize(const std::vector<int64_t>& shape) { int64_t shape_size = 1; for (auto i : shape) { shape_size *= i; } return shape_size; } int Init(int32_t deviceId, aclrtStream* stream) { // 固定写法,资源初始化 auto ret = aclInit(nullptr); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclInit failed. ERROR: %d\n", ret); return ret); ret = aclrtSetDevice(deviceId); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclrtSetDevice failed. ERROR: %d\n", ret); return ret); ret = aclrtCreateStream(stream); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclrtCreateStream failed. ERROR: %d\n", ret); return ret); return 0; } template <typename T> int CreateAclTensor(const std::vector<T>& hostData, const std::vector<int64_t>& shape, void** deviceAddr, aclDataType dataType, aclTensor** tensor) { auto size = GetShapeSize(shape) * sizeof(T); // 调用aclrtMalloc申请device侧内存 auto ret = aclrtMalloc(deviceAddr, size, ACL_MEM_MALLOC_HUGE_FIRST); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclrtMalloc failed. ERROR: %d\n", ret); return ret); // 调用aclrtMemcpy将host侧数据复制到device侧内存上 ret = aclrtMemcpy(*deviceAddr, size, hostData.data(), size, ACL_MEMCPY_HOST_TO_DEVICE); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclrtMemcpy failed. ERROR: %d\n", ret); return ret); // 计算连续tensor的strides std::vector<int64_t> strides(shape.size(), 1); for (int64_t i = shape.size() - 2; i >= 0; i--) { strides[i] = shape[i + 1] * strides[i + 1]; } // 调用aclCreateTensor接口创建aclTensor *tensor = aclCreateTensor(shape.data(), shape.size(), dataType, strides.data(), 0, ACL_FORMAT_ND, shape.data(), shape.size(), *deviceAddr); return 0; } int main() { // 1. (固定写法)device/stream初始化,参考acl API手册 int32_t deviceId = 0; aclrtStream stream; auto ret = Init(deviceId, &stream); CHECK_RET(ret == 0, LOG_PRINT("Init acl failed. ERROR: %d\n", ret); return ret); // 2. 构造输入与输出(根据API接口自定义构造) std::vector<int64_t> xShape = {2, 4, 8}; std::vector<int64_t> scaleShape = {2, 8}; std::vector<int64_t> shiftShape = {2, 8}; std::vector<int64_t> weightShape = {8}; std::vector<int64_t> biasShape = {8}; std::vector<int64_t> smoothScalesShape = {8}; std::vector<int64_t> outShape = {2, 4, 8}; std::vector<int64_t> quantScaleShape = {2, 4}; double epsilon = 1e-5; const char* quantMode = "dynamic"; // 依次为 x/scale/shift/weight/bias/smoothScales/out/quantScale 申请 device 内存 // 并创建 aclTensor(FP16 输入 + INT8/FP32 输出,ND 格式),过程略 std::vector<short> xHostData(2 * 4 * 8, 1); // FP16 输入数据 std::vector<short> scaleHostData(2 * 8, 1); std::vector<short> shiftHostData(2 * 8, 1); std::vector<short> weightHostData(8, 1); std::vector<short> biasHostData(8, 1); std::vector<short> smoothScalesHostData(8, 1); std::vector<int8_t> outHostData(2 * 4 * 8, 0); // INT8 输出 std::vector<float> quantScaleHostData(2 * 4, 0); // FP32 量化系数 // 3. 调用两段式接口 uint64_t workspaceSize = 0; aclOpExecutor* executor; // 第一段:获取workspace大小与executor;quantOffsetOptional 传 nullptr ret = aclnnAdaLayerNormQuantGetWorkspaceSize(x, scale, shift, weight, bias, smoothScales, epsilon, quantMode, out, quantScale, nullptr, &workspaceSize, &executor); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclnnAdaLayerNormQuantGetWorkspaceSize failed. ERROR: %d\n", ret); return ret); // 根据workspaceSize申请device内存 void* workspaceAddr = nullptr; if (workspaceSize > 0) { ret = aclrtMalloc(&workspaceAddr, workspaceSize, ACL_MEM_MALLOC_HUGE_FIRST); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("allocate workspace failed. ERROR: %d\n", ret); return ret); } // 第二段:执行计算 ret = aclnnAdaLayerNormQuant(workspaceAddr, workspaceSize, executor, stream); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclnnAdaLayerNormQuant failed. ERROR: %d\n", ret); return ret); // 4. (固定写法)同步等待任务执行结束 ret = aclrtSynchronizeStream(stream); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclrtSynchronizeStream failed. ERROR: %d\n", ret); return ret); // 5. 将device侧结果拷回host侧并打印 auto size = GetShapeSize(outShape); std::vector<int8_t> resultData(size, 0); ret = aclrtMemcpy(resultData.data(), resultData.size() * sizeof(resultData[0]), outDeviceAddr, size * sizeof(int8_t), ACL_MEMCPY_DEVICE_TO_HOST); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("copy result from device to host failed. ERROR: %d\n", ret); return ret); for (int64_t i = 0; i < size; i++) { LOG_PRINT("result[%ld] is: %d\n", i, resultData[i]); } // 6. 释放aclTensor(aclDestroyTensor)与device资源(aclrtFree/workspace) // 7. 释放stream并复位设备:aclrtDestroyStream、aclrtResetDevice、aclFinalize return 0; }8.1 调用要点总结
- shape 设计:示例中
x = [2, 4, 8],其中 B=2、S=4、H=8;scale/shift取[2, 8](即 [B…, H] 形式,也可用 [2, 1, 8]);weight/bias/smoothScales为[8];quantScale为[2, 4](即 [B…, S])。 - quantOffsetOptional:当前版本不支持非对称量化,必须传
nullptr。 - workspace 申请:只有当
workspaceSize > 0时才需要aclrtMalloc,结束后需aclrtFree。 - executor 生命周期:executor 由第一段接口创建并交由调用方,第二段接口执行完毕后按 acl 规范释放相关资源。
九、源码实现佐证:算子定义、内核与配置
9.1 算子定义注册
ada_layer_norm_quant_def.cpp 中通过OP_ADD(AdaLayerNormQuant)注册算子:
- 6 个输入(x/scale/shift 为 REQUIRED,weight/bias/smooth_scales 为 OPTIONAL),2 个输出(out、quant_scale 为 REQUIRED);
epsilon作为可选属性(AttrType=OPTIONAL),默认值1e-5;ascend910b(Atlas A2)与ascend910_93(Atlas A3)配置下输出仅支持 INT8;ascend950配置通过Add91095Config()额外开启 DynamicRank/DynamicShape 支持,并允许 INT8/HIFLOAT8/FP8_E4M3FN/FP8_E5M2 四种输出类型。
9.2 内核入口
op_kernel/ada_layer_norm_quant.cpp 展示了 AICore 内核入口ada_layer_norm_quant:它按TILING_KEY分派到复用自 AdaLayerNorm 的AdaLayerNormND<half/bfloat16_t, ..., QUANT_OP_CODE>模板实例上,先执行InitQuant再执行Process,将归一化与量化在同一个内核中完成——这从实现层面印证了"融合算子"的设计。其中 bfloat16 分支在 3003 架构(对应部分 950 芯片)上被排除。
9.3 二进制配置
op_host/config/ascend910b/ada_layer_norm_quant_binary.json 中为 float16 与 bfloat16 两种输入分别登记了二进制产物:输入 x/scale/shift 均为required + ND,weight/bias/smooth_scales 为optional + ND,输出 out 为int8、quant_scale 为float32,属性 epsilon 为 float 类型。shape: [-2]表示动态 shape 场景,说明该算子支持编译期未知 shape 的图模式下发。
十、小结
AdaLayerNormQuant 是 CANN ops-nn 中一个典型的「归一化 + 动态量化」融合算子:它把 AdaLayerNorm 的归一化、自适应 scale/shift 调整、可选的平滑缩放与 DynamicQuant 的量化因子计算和取整输出合并为单次内核执行,在支持的产品上(Ascend 950、Atlas A2/A3)显著减少中间张量搬运。通过两段式 aclnn 接口,开发者可以方便地在推理/训练图中插入该算子,将 FP16/BF16 激活量化为 INT8/FP8 输出,同时拿到每行的量化系数quantScale供反量化使用。建议读者结合本指南给出的参数约束表、错误码清单与完整示例代码,直接基于仓库中的 examples/test_aclnn_ada_layer_norm_quant.cpp 运行验证。
【免费下载链接】ops-nn本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。项目地址: https://gitcode.com/cann/ops-nn
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考