CANN ops-math 算子详解:aclnnLogAddExp 两段式接口原理、参数与 NPU 调用实战

  • 算子库
  • 人工智能
  • CANN

【免费下载链接】ops-math

本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。

项目地址: https://gitcode.com/cann/ops-math
点击查看 免费下载

本文是 CANN ops-math 数学算子库中 LogAddExp 算子的 aclnn 接口技术指南,完整讲解 aclnnLogAddExp 的数学定义、产品支持范围、两段式接口的函数原型、全部入参约束与错误码语义,并结合仓库源码(op_api / op_host / op_kernel)剖析其底层调用链与实现原理。读完本文,你将掌握在 Atlas 系列产品上通过 aclnn 接口编写、编译并运行 LogAddExp(幂和取自然对数)计算任务的完整实战方法,也能理解 aclnnLogAddExp2(底为 2)的同构用法。

一、功能说明与数学原理

aclnnLogAddExp 的接口功能为:对输入的 Tensor 逐元素计算幂和的对数(以 e 为底),即数值计算中广泛使用的 LogAddExp 算子,常用于 log-sum-exp 类数值稳定化计算(softmax 前向、log 域概率运算等)。

计算公式如下:

$$ out = \log(e^x+e^y)=\log[e^m(1+e^{-|x-y|})]=m+\log(1+e^{-|x-y|}) \quad其中m=\max(x,y) $$

其中,$x$ 代表 self,$y$ 代表 other。推导过程的核心思路是提取最大值 $m=\max(x,y)$ 作为公因子:e^x + e^y = e^m(1 + e^{-|x-y|}),从而把直接计算 e^x + e^y 可能导致的指数上溢(大数)与下溢(小数)问题,转化为对 e^{-|x-y|} 的计算——该值恒落在 (0, 1] 区间内,数值稳定性显著优于朴素实现。该公式即文档中所描述的计算式,也是算子 kernel 与 tiling 判断采用简化公式(simplified formula)还是完整公式(full formula)的依据:当 base == -1.0f && scale == 1.0f && shift == 0.0f(即默认自然对数配置)时使用简化公式,否则走通用完整公式(见 log_add_exp_tiling_arch35.cpp 中 GetShapeAttrsInfo 对 base_ / scale_ / shift_ 的校验与 useFullFormula_ 计算)。

仓库 README 说明:该目录仅包含 LogAddExp 算子对应的 aclnn 接口;若需贡献该算子的 AscendC 实现,可参考 贡献流程。

二、产品支持情况

aclnnLogAddExp 在如下产品上支持/不支持:

产品支持情况
Ascend 950PR / Ascend 950DT支持
Atlas A3 训练系列产品 / Atlas A3 推理系列产品支持
Atlas A2 训练系列产品 / Atlas A2 推理系列产品支持
Atlas 200I/500 A2 推理产品不支持
Atlas 推理系列产品支持
Atlas 训练系列产品支持

从源码看,不同 SoC 版本对应不同的数据类型支持列表:aclnn_logaddexp.cpp 中分别定义了 ASCEND910_DTYPE_SUPPORT_LIST(FLOAT16、FLOAT)、ASCEND910B_DTYPE_SUPPORT_LIST(FLOAT16、FLOAT、BF16)与 ASCEND950_DTYPE_SUPPORT_LIST(FLOAT16、FLOAT、BF16)作为输出数据类型支持列表,CheckDtypeValid 会根据当前平台的 SoC 版本(GetCurrentPlatformInfo().GetSocVersion())选用对应列表进行校验。这与 log_add_exp_def.cpp 中 OpDef 声明的 dataType = {ge::DT_FLOAT16, ge::DT_FLOAT, ge::DT_BF16} 保持一致。

三、两段式接口与函数原型

每个算子都遵循 两段式接口 规范:必须先调用 第一段接口 aclnnLogAddExpGetWorkspaceSize 获取计算所需的 workspace 大小以及包含算子计算流程的执行器(executor),再调用 第二段接口 aclnnLogAddExp 执行计算。

第一段接口原型:

aclnnStatus aclnnLogAddExpGetWorkspaceSize(
  const aclTensor*     self,
  const aclTensor*     other,
  aclTensor*           out,
  uint64_t*            workspaceSize,
  aclOpExecutor**      executor)

第二段接口原型:

aclnnStatus aclnnLogAddExp(
  void*          workspace,
  uint64_t       workspaceSize,
  aclOpExecutor* executor,
  aclrtStream    stream)

头文件声明位于 aclnn_logaddexp.h,两个接口均以 extern "C" 导出,编译时包含 aclnnop/aclnn_logaddexp.h 即可使用。

四、aclnnLogAddExpGetWorkspaceSize 参数说明

参数名输入/输出描述使用说明数据类型数据格式维度(shape)非连续Tensor
self(aclTensor*)输入公式中的x数据类型需要可转换成out的数据类型(参见互转换关系);self与other的shape满足broadcast关系FLOAT16、FLOAT、DOUBLE、UINT8、INT8、UINT16、INT16、UINT32、INT32、UINT64、INT64、BOOL、BFLOAT16ND0-8√
other(aclTensor*)输入公式中的y数据类型需要可转换成out的数据类型(参见互转换关系);other与self的shape满足broadcast关系FLOAT16、FLOAT、DOUBLE、UINT8、INT8、UINT16、INT16、UINT32、INT32、UINT64、INT64、BOOL、BFLOAT16ND0-8√
out(aclTensor*)输出公式中的outshape需要是self与other broadcast之后的shapeFLOAT、FLOAT16、BFLOAT16ND0-8√
workspaceSize(uint64_t*)输出返回需要在Device侧申请的workspace大小-----
executor(aclOpExecutor**)输出返回op执行器,包含了算子计算流程-----

补充限制:Atlas 推理系列产品、Atlas 训练系列产品不支持 BFLOAT16 数据类型(即 910/310p 平台的输出仅支持 FLOAT、FLOAT16),这一点与源码中 ASCEND910_DTYPE_SUPPORT_LIST 仅包含 DT_FLOAT16、DT_FLOAT 的事实相互印证。

返回值与错误码

aclnnStatus 为返回状态码,具体参见 aclnn返回码。第一段接口完成入参校验,出现如下场景时报错:

返回值错误码描述
ACLNN_ERR_PARAM_NULLPTR161001传入的self、other或out是空指针
ACLNN_ERR_PARAM_INVALID161002self、other、out的数据类型不在支持的范围之内
ACLNN_ERR_PARAM_INVALID161002self和other的数据类型无法转换为out的数据类型
ACLNN_ERR_PARAM_INVALID161002self、other或out的维度大于8
ACLNN_ERR_PARAM_INVALID161002self和other的shape无法做broadcast
ACLNN_ERR_PARAM_INVALID161002out的shape和self与other broadcast之后的shape不一致

以上校验在 aclnn_logaddexp.cpp 的 CheckParams 中逐项落地:CheckNotNull3Tensor 检查空指针;CheckDtypeValid 检查数据类型支持范围与 self/other 到 out 的可转换性;CheckShape 通过 OP_CHECK_MAX_DIM(self, MAX_SUPPORT_DIMS_NUMS, ...) 限制维度不超过 8;CheckBroadcastLogAddExp 调用 OP_CHECK_BROADCAST 与 BroadcastInferShape 计算 broadcast 后 shape,并通过 OP_CHECK_SHAPE_NOT_EQUAL_WITH_EXPECTED_SIZE 校验 out 的 shape 与 broadcast 结果一致。另外 CheckFormat 要求 self、other 与 out 的存储格式一致,且不支持私有格式(仅支持 ND、NCHW、NHWC、HWCN、NDHWC、NCDHW)。

五、aclnnLogAddExp 参数说明

参数名输入/输出描述
workspace输入在Device侧申请的workspace内存地址
workspaceSize输入在Device侧申请的workspace大小,由第一段接口aclnnLogAddExpGetWorkspaceSize获取
executor输入op执行器,包含了算子计算流程
stream输入指定执行任务的Stream

返回值仍为 aclnnStatus 状态码,参见 aclnn返回码。第二段接口的实现非常简洁:aclnnLogAddExp 在 aclnn_logaddexp.cpp 中直接调用 CommonOpExecutorRun(workspace, workspaceSize, executor, stream) 完成实际计算。

六、约束说明

  • 确定性计算:aclnnLogAddExp 默认确定性实现。关于确定性计算的一般约定,可参考 determinism_compute.md。

七、源码级实现链路解析

为了更深入理解该接口的行为,下面沿仓库源码梳理 aclnnLogAddExpGetWorkspaceSize 的完整执行链:

  1. 创建执行器与入参校验:CREATE_EXECUTOR() 创建唯一执行器,随后执行上述 CheckParams 校验流程。
  2. 空 Tensor 快速路径:当 self->IsEmpty() || other->IsEmpty() 时,直接置 *workspaceSize = 0 并返回成功(空 Tensor 在 kernel 中支持),无需分配 workspace。
  3. 输入预处理:分别对 self 与 other 调用 l0op::Contiguous 转为连续 Tensor、l0op::Cast 转为 out 的数据类型——这也是文档中"数据类型需要可转换成 out 的数据类型"约束在实现层的体现(见 aclnn_logaddexp.cpp)。
  4. 核心计算图:调用 l0op::LogAddExp(selfCasted, otherCasted, LOG_BASE, LOG_SCALE, LOG_SHIFT, executor),其中 LOG_BASE = -1.0f、LOG_SCALE = 1.0f、LOG_SHIFT = 0.0f(aclnn_logaddexp.cpp),-1.0f 的 base 即代表自然对数(e 为底)。
  5. 输出处理:l0op::Cast 将计算结果转为 out 的数据类型,再经 l0op::ViewCopy 拷贝到输出 out 上——该步骤使 out 支持非连续 Tensor。
  6. workspace 计算:*workspaceSize = uniqueExecutor->GetWorkspaceSize() 获取计算全程所需 workspace 大小,最后 uniqueExecutor.ReleaseTo(executor) 将执行器所有权转移给调用方。

其中 l0op::LogAddExp 定义在 logaddexp.cpp:先通过 BroadcastInferShape 求 broadcast 后的 shape 并 AllocTensor 分配中间输出,若输入数据类型属于 AICore 支持列表(DT_FLOAT、DT_FLOAT16、DT_BF16,见 logaddexp.cpp)则走 LogAddExpAiCore 分支,通过 ADD_TO_LAUNCHER_LIST_AICORE(LogAddExp, OP_INPUT(self, other), OP_OUTPUT(out), OP_ATTR(base, scale, shift)) 注册 AICore 算子启动。底层算子原型在 log_add_exp_def.cpp 中声明:输入 x1/x2(REQUIRED)、输出 y(REQUIRED)、可选属性 base(默认 -1.0)、scale(默认 1.0)、shift(默认 0.0),并配置了 ascend950 的 AICore 编译选项(动态编译、支持动态 rank/shape 等);其 shape 推导实现 InferShape4Broadcast 位于 log_add_exp_infershape.cpp。

在 kernel 侧,log_add_exp_apt.cpp 根据模板参数 formulaType(0=simplified,1=full)在 bfloat16 与普通类型间选择对应的计算 DAG(LogAddExpSimplifiedCompute / LogAddExpFullCompute 等),并通过 BroadcastSch 调度框架完成带 broadcast 的逐元素计算;Ascend 950 上的二进制配置(bfloat16/float16/float32 三种 bin)可参见 log_add_exp_binary.json。

八、调用示例(完整可编译)

示例代码参考 test_aclnn_log_add_exp.cpp,具体编译与执行过程请参考 编译与运行样例。下面为完整实现(自文档继承并整理为规范版):

#include <iostream>
#include <vector>
#include "acl/acl.h"
#include "aclnnop/aclnn_logaddexp.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 shapeSize = 1;
  for (auto i : shape) {
    shapeSize *= i;
  }
  return shapeSize;
}

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, aclFormat::ACL_FORMAT_ND,
                            shape.data(), shape.size(), *deviceAddr);
  return 0;
}

int main() {
  // 1.(固定写法)device/stream初始化,参考acl API手册
  // 根据自己的实际device填写deviceId
  int32_t deviceId = 0;
  aclrtStream stream;
  auto ret = Init(deviceId, &stream);
  CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("Init acl failed. ERROR: %d\n", ret); return ret);

  // 2. 构造输入与输出,需要根据API的接口自定义构造
  std::vector<int64_t> selfShape = {4, 2};
  std::vector<int64_t> otherShape = {4, 2};
  std::vector<int64_t> outShape = {4, 2};
  void* selfDeviceAddr = nullptr;
  void* otherDeviceAddr = nullptr;
  void* outDeviceAddr = nullptr;
  aclTensor* self = nullptr;
  aclTensor* other = nullptr;
  aclTensor* out = nullptr;
  std::vector<float> selfHostData = {1, 2, 3, 4, 5, 6, 7, 8};
  std::vector<float> otherHostData = {1, 2, 3, 4, 5, 6, 7, 8};
  std::vector<float> outHostData = {0, 0, 0, 0, 0, 0, 0, 0};
  // 创建self aclTensor
  ret = CreateAclTensor(selfHostData, selfShape, &selfDeviceAddr, aclDataType::ACL_FLOAT, &self);
  CHECK_RET(ret == ACL_SUCCESS, return ret);
  // 创建other aclTensor
  ret = CreateAclTensor(otherHostData, otherShape, &otherDeviceAddr, aclDataType::ACL_FLOAT, &other);
  CHECK_RET(ret == ACL_SUCCESS, return ret);
  // 创建out aclTensor
  ret = CreateAclTensor(outHostData, outShape, &outDeviceAddr, aclDataType::ACL_FLOAT, &out);
  CHECK_RET(ret == ACL_SUCCESS, return ret);

  // 3. 调用CANN算子库API,需要修改为具体的API名称
  uint64_t workspaceSize = 0;
  aclOpExecutor* executor;
  // 调用aclnnLogAddExp第一段接口
  ret = aclnnLogAddExpGetWorkspaceSize(self, other, out, &workspaceSize, &executor);
  CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclnnLogAddExpGetWorkspaceSize 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);
  }
  // 调用aclnnLogAddExp第二段接口
  ret = aclnnLogAddExp(workspaceAddr, workspaceSize, executor, stream);
  CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclnnLogAddExp 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侧,需要根据具体API的接口定义修改
  auto size = GetShapeSize(outShape);
  std::vector<float> resultData(size, 0);
  ret = aclrtMemcpy(resultData.data(), resultData.size() * sizeof(resultData[0]), outDeviceAddr,
                    size * sizeof(resultData[0]), 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: %f\n", i, resultData[i]);
  }

  // 6. 释放aclTensor,需要根据具体API的接口定义修改
  aclDestroyTensor(self);
  aclDestroyTensor(other);
  aclDestroyTensor(out);

  // 7. 释放device资源,需要根据具体API的接口定义修改
  aclrtFree(selfDeviceAddr);
  aclrtFree(otherDeviceAddr);
  aclrtFree(outDeviceAddr);
  if (workspaceSize > 0) {
    aclrtFree(workspaceAddr);
  }
  aclrtDestroyStream(stream);
  aclrtResetDevice(deviceId);
  aclFinalize();
  return 0;
}

对上述示例的要点说明:

  • 步骤 3 是两段式接口的标准用法:先取 workspaceSize 与 executor,仅当 workspaceSize > 0 时才调用 aclrtMalloc 申请 workspace 内存,随后执行第二段接口;示例中 self/other 均为 {4, 2} 的 FLOAT 类型,out 与之同 shape,outHostData 初值为 0 仅占位,真正结果由算子写入。
  • 内存生命周期:selfDeviceAddr、otherDeviceAddr、outDeviceAddr 以及条件申请的 workspaceAddr 均需在结束时通过 aclrtFree 释放;示例第 6、7 步展示了 aclTensor 与设备资源的完整释放顺序。
  • 若输入为 self != other 的广播场景(如 selfShape={4,2}、otherShape={2}),则 outShape 必须是 broadcast 后的 {4,2},否则第一段接口会以 161002 报错。

九、同族算子 aclnnLogAddExp2

LogAddExp 算子族还提供底为 2 的版本 aclnnLogAddExp2,其完整文档见 aclnnLogAddExp2.md。两者在产品支持情况、参数结构、两段式接口模式、错误码语义上完全一致,仅计算公式不同:

$$ out = \log_2(2^x+2^y)=\log_2[2^m(1+2^{-|x-y|})]=m+\log_2(1+2^{-|x-y|}) \quad其中m=\max(x,y) $$

调用时头文件替换为 aclnnop/aclnn_logaddexp2.h,接口名替换为 aclnnLogAddExp2GetWorkspaceSize / aclnnLogAddExp2 即可。对应的示例程序见 test_aclnn_log_add_exp2.cpp,OP API 实现见 aclnn_logaddexp2.cpp。

十、测试与验证

仓库为该算子提供了完整的测试配套,可用于验证本文所述行为:

总结

aclnnLogAddExp 是 CANN ops-math 中实现幂和取自然对数的标准 aclnn 接口,采用两段式(GetWorkspaceSize + 执行)设计:第一段接口完成全部入参校验(空指针、dtype 支持范围、互转换关系、0-8 维约束、broadcast 关系与输出 shape 一致性)并计算 workspace;第二段接口基于 executor 在指定 Stream 上执行。从源码实现看,其核心计算通过 Contiguous → Cast → LogAddExp → Cast → ViewCopy 的计算图完成,底层由 AICore kernel 以"提取最大值 m 的数值稳定公式"实现,并针对默认自然对数配置使用简化公式。配合示例代码与 ATK/UT 测试,开发者可以快速在支持的 Atlas 产品上完成从调用、验证到集成的全流程。

  • 算子库
  • 人工智能
  • CANN

【免费下载链接】ops-math

本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。

项目地址: https://gitcode.com/cann/ops-math
点击查看 免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值