CANN ops-nn AdaLayerNormQuant 算子深度指南:aclnnAdaLayerNormQuant 两段式接口原理、参数与实战调用
2026/9/23 16:36:46 网站建设 项目流程

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 中,第一段接口完成如下关键动作:

  1. 创建 OpExecutor,并调用CheckParams完成入参校验(空指针、数据类型、Shape、Attr);
  2. x/scale/shift及三个可选输入调用l0op::Contiguous做连续性处理(这正是文档中"非连续 Tensor √"的来源);
  3. 调用l0op::AdaLayerNormQuant(见 ada_layer_norm_quant.cpp)构建内核启动列表,内部会自动为outquantScale分配输出张量;
  4. 通过executor->GetWorkspaceSize()返回 workspace 大小,并将 executor 释放给调用方。

第二段接口则统一走CommonOpExecutorRun完成实际计算下发。若输入为空 Tensorx/scale/shift任一为空),第一段接口直接返回ACLNN_SUCCESSworkspaceSize = 0,不会构建计算流程。

五、参数说明(完整约束表)

以下为aclnnAdaLayerNormQuantGetWorkspaceSize的完整参数约束,源自接口文档,并可与 源码校验逻辑 相互印证。

参数名输入/输出描述使用说明数据类型数据格式维度(shape)非连续Tensor
x(aclTensor*)输入输入待处理数据,对应公式中的 x不支持空 Tensor;shape 为 [B…, S, H],B 支持 0~6 个维度FLOAT16、BFLOAT16ND2-8
scale(aclTensor*)输入自适应缩放参数,对应公式中的 scale不支持空 Tensor;数据类型与 x 一致;shape 为 [B…, H] 或 [B…, 1, H],B 的维度数与大小与 x 一致,H 与 x 的 H 维一致FLOAT16、BFLOAT16ND1-8
shift(aclTensor*)输入自适应偏移参数,对应公式中的 shift约束同 scaleFLOAT16、BFLOAT16ND1-8
weightOptional(aclTensor*)输入可选,归一化缩放参数,对应公式中的 weightOptional不支持空 Tensor;数据类型与 x 一致;shape 为 [H]FLOAT16、BFLOAT16ND1
biasOptional(aclTensor*)输入可选,归一化偏移参数,对应公式中的 biasOptional约束同 weightOptionalFLOAT16、BFLOAT16ND1
smoothScalesOptional(aclTensor*)输入可选,量化平滑权重,对应公式中的 smoothScalesOptional约束同 weightOptionalFLOAT16、BFLOAT16ND1
epsilon(double)输入添加到分母中的值,确保数值稳定、防止除 0建议传较小的正数,如 1e-5----
quantMode(char*)输入量化模式当前版本仅支持 "dynamic"----
out(aclTensor*)输出量化输出张量,对应公式中的 out不支持空 Tensor;shape 与 x 保持一致INT8、FLOAT8_E4M3FN、FLOAT8_E5M2、HIFLOAT8ND2-8
quantScale(aclTensor*)输出量化系数,对应公式中的 quantScale不支持空 Tensor;shape 为 [B…, S],B 与 x 一致,S 与 x 的 S 维一致FLOAT32ND1-7
quantOffsetOptional(aclTensor*)输出可选,非对称量化使用的 offset不支持空 Tensor;shape 与 quantScale 一致;当前版本暂不支持,传 nullptrFLOAT16、BFLOAT16ND1-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 = 2MAX_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]
  • CheckAttrquantMode限定为字符串"dynamic",其他取值一律报ACLNN_ERR_PARAM_INVALID
  • quantOffsetOptional非空即报错,印证了"当前版本暂不支持,传 nullptr"。

六、返回值与错误码

接口返回aclnnStatus状态码,具体含义参见 aclnn 返回码说明。

第一段接口完成入参校验,出现以下场景时报错:

返回码错误码描述
ACLNN_ERR_PARAM_NULLPTR161001传入的 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 调用要点总结

  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])。
  2. quantOffsetOptional:当前版本不支持非对称量化,必须传nullptr
  3. workspace 申请:只有当workspaceSize > 0时才需要aclrtMalloc,结束后需aclrtFree
  4. 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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询