【免费下载链接】ops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
本文是 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、BFLOAT16 | ND | 0-8 | √ |
| other(aclTensor*) | 输入 | 公式中的y | 数据类型需要可转换成out的数据类型(参见互转换关系);other与self的shape满足broadcast关系 | FLOAT16、FLOAT、DOUBLE、UINT8、INT8、UINT16、INT16、UINT32、INT32、UINT64、INT64、BOOL、BFLOAT16 | ND | 0-8 | √ |
| out(aclTensor*) | 输出 | 公式中的out | shape需要是self与other broadcast之后的shape | FLOAT、FLOAT16、BFLOAT16 | ND | 0-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_NULLPTR | 161001 | 传入的self、other或out是空指针 |
| ACLNN_ERR_PARAM_INVALID | 161002 | self、other、out的数据类型不在支持的范围之内 |
| ACLNN_ERR_PARAM_INVALID | 161002 | self和other的数据类型无法转换为out的数据类型 |
| ACLNN_ERR_PARAM_INVALID | 161002 | self、other或out的维度大于8 |
| ACLNN_ERR_PARAM_INVALID | 161002 | self和other的shape无法做broadcast |
| ACLNN_ERR_PARAM_INVALID | 161002 | out的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 的完整执行链:
- 创建执行器与入参校验:
CREATE_EXECUTOR()创建唯一执行器,随后执行上述CheckParams校验流程。 - 空 Tensor 快速路径:当
self->IsEmpty() || other->IsEmpty()时,直接置*workspaceSize = 0并返回成功(空 Tensor 在 kernel 中支持),无需分配 workspace。 - 输入预处理:分别对
self与other调用l0op::Contiguous转为连续 Tensor、l0op::Cast转为 out 的数据类型——这也是文档中"数据类型需要可转换成 out 的数据类型"约束在实现层的体现(见 aclnn_logaddexp.cpp)。 - 核心计算图:调用
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 为底)。 - 输出处理:
l0op::Cast将计算结果转为 out 的数据类型,再经l0op::ViewCopy拷贝到输出 out 上——该步骤使 out 支持非连续 Tensor。 - 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。
十、测试与验证
仓库为该算子提供了完整的测试配套,可用于验证本文所述行为:
- ATK 自动化测试:基于 ATK(Ascend Test Kit)框架的用例定义于 atk_aclnnLogAddExp.json,执行脚本 executor_aclnnLogAddExp.py 中通过
@register("aclnn_log_add_exp")注册自定义 API 调用方式,并将 FLOAT16/BFLOAT16/FLOAT64 及各整型、BOOL 等输入统一转换为 FLOAT32 后执行算子,随后与 PyTorch 参考结果对比。 - UT 单元测试:test_aclnn_log_add_exp.cpp、test_logaddexp.cpp、test_logaddexp2.cpp 覆盖接口与底层 l0op 的单元级验证。
总结
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 产品上完成从调用、验证到集成的全流程。
【免费下载链接】ops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



