CANN ops-nn 算子 aclnnLogSoftmaxV2 使用指南:接口、参数、错误码与 NPU 实现解析
【免费下载链接】ops-nn本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。项目地址: https://gitcode.com/cann/ops-nn
导读
aclnnLogSoftmaxV2是 CANN ops-nn 仓库中 LogSoftmaxV2 算子的标准调用接口,用于在 Device(NPU)侧对输入 Tensor 沿指定维度执行对数 Softmax 运算,是分类网络、语言模型输出层等场景中常见的数值归一化操作。本文以该算子自带的接口文档(experimental/activation/log_softmax_v2/docs/aclnnLogSoftmaxV2.md)为核心,完整讲解其两段式调用流程、参数约束、返回码校验规则,并结合仓库内 op_host / op_kernel 源码剖析其计算原理、tiling 策略与多核调度实现,最后给出可直接运行的完整 C++ 示例。读完本文,你将能够独立完成aclnnLogSoftmaxV2的工程接入、参数校验与结果验证。
一、产品支持情况与功能定位
1.1 产品支持情况
依据官方接口文档,该算子目前支持的产品如下:
| 产品 | 是否支持 |
|---|---|
| Atlas A2 训练系列产品/Atlas 800I A2 | √ |
对应到仓库实现层面,算子注册时绑定的 AICore 平台配置为ascend910b,见 op_host/log_softmax_v2_def.cpp 中的this->AICore().AddConfig("ascend910b");同时接口层在 op_host/op_api/aclnn_log_softmax.cpp 中通过CheckSocVersionIsSupportBf16判断当前 SoC 版本,非ASCEND910B平台会拒绝 BF16 输入。这意味着在非上述平台(或非 910B 系列)上使用 BF16 数据类型会被显式拦截,接入前请先确认运行环境。
1.2 算子功能
aclnnLogSoftmaxV2实现对输入 Tensor 沿着指定维度dim进行 LogSoftmax 运算。其数学定义等价于:
logsoftmax(x_i) = x_i - log(Σ e^(x_j))其中j遍历dim维度上的所有元素。算子内部实际采用"先减最大值再归一化"的数值稳定策略:先沿指定轴求最大值(Reduce),再通过 Broadcast 做减法、取指数、求和、取对数,最终等效于上述公式。该策略避免了直接计算e^(x_j)时指数溢出导致的精度问题,也是 README.md 中公式out_i = input_i - log(Σ_j exp(input_j))的稳定化实现。
说明:文档中公式为数学上的等价形式,实际 kernel 计算路径是先
ReduceMax再Exp后ReduceSum再Ln,详见本文第四章源码解析。
二、函数原型与两段式调用约定
2.1 函数原型
aclnnStatus aclnnLogSoftmaxV2GetWorkspaceSize( const aclTensor *self, int64_t dim, aclTensor *out, uint64_t *workspaceSize, aclOpExecutor **executor ); aclnnStatus aclnnLogSoftmaxV2( void *workspace, uint64_t workspaceSize, aclOpExecutor *executor, aclrtStream stream );该算子采用 CANN 算子库标准的两段式接口:
- 第一段
aclnnLogSoftmaxV2GetWorkspaceSize:完成入参校验、构图,返回需要的 Device 侧 workspace 大小和已构建好的算子执行器; - 第二段
aclnnLogSoftmaxV2:传入第一段申请好的 workspace 与执行器,在指定 stream 上真正下发执行。
2.2 为什么需要两段式
从接口实现 op_host/op_api/aclnn_log_softmax.cpp 可以看到,第一段接口内部实际构建了一条完整的执行图,依次执行:
l0op::Contiguous:将非连续输入转换为连续 Tensor;l0op::Cast:将输入隐式提升为目标数据类型;l0op::LogSoftmaxV2:调用算子核心 kernel(op_host/op_api/log_softmax_v2.cpp);l0op::Cast/l0op::ViewCopy:把中间结果转换回out的数据类型并拷贝到输出。
因此 workspace 大小由整条图的中间缓冲需求共同决定,必须由第一段接口统一计算后,在第二段执行前完成 Device 内存申请。这是所有 CANN aclnn 算子通用的执行范式。
三、第一段接口 aclnnLogSoftmaxV2GetWorkspaceSize
3.1 参数说明
| 参数 | 说明 |
|---|---|
| self | (aclTensor*,计算输入)Device 侧的 aclTensor,数据格式支持 ND,维度不超过 8 维,支持非连续的 Tensor。Atlas A2 训练系列产品/Atlas 800I A2:数据类型支持 FLOAT32、FLOAT16、BFLOAT16。 |
| dim | (int64_t,计算输入)指定进行 LogSoftmax 运算的维度,取值范围为[-rank, rank-1],其中rank是输入 Tensorself的维度。 |
| out | (aclTensor*,计算输出)Device 侧的 aclTensor,数据格式支持 ND,shape 必须与self一致,维度不超过 8 维,支持非连续 Tensor。Atlas A2 训练系列产品/Atlas 800I A2:数据类型支持 FLOAT32、FLOAT16、BFLOAT16。 |
| workspaceSize | (uint64_t*,出参)返回需要在 Device 侧申请的 workspace 大小,单位为字节。 |
| executor | (aclOpExecutor**,出参)返回算子执行器,封装了完整的算子计算流程。 |
返回值:aclnnStatus,具体取值可参考仓库文档 docs/zh/context/aclnn_return_code.md。
3.2 第一段接口的入参校验规则
结合接口源码 op_host/op_api/aclnn_log_softmax.cpp 中的CheckLogSoftmaxParams校验链,第一段接口在以下场景会报错:
161001(ACLNN_ERR_PARAM_NULLPTR)——空指针:
- 传入的
self或out是空指针(对应源码CheckNotNull)。
161002(ACLNN_ERR_PARAM_INVALID)——参数非法:
self与out的数据类型不在支持范围内(对应CheckDtypeValid,在非 910B 平台还会单独拦截 BF16);self与out的 shape 不一致(对应CheckShape中的OP_CHECK_SHAPE_NOT_EQUAL);self、out的维度超过 8(对应CheckShape中的OP_CHECK_MAX_DIM,源码常量MAX_DIM = 8);dim超出有效范围[-rank, rank-1](对应CheckDim,即dim < (-1) * inputShapeLen || dim > inputShapeLen - 1时报错);- 类型推导失败或推导结果无法转换为输出类型(对应
CheckPromoteType)。
源码细节:
CheckDim中当inputShapeLen == 0(标量)时会先++再比较,说明标量输入按 1 维参与范围判定。另外,当self->IsEmpty()时,第一段接口直接返回workspaceSize = 0且不构建 kernel 图,属于空 Tensor 的合法短路路径。
3.3 dim 参数的使用规则
dim取值范围为[-rank, rank-1],支持负索引语义(例如 rank=2 时dim=-1等价于dim=1)。从 tiling 源码 op_host/log_softmax_v2_tiling.cpp 可以看到,host 侧 tiling 将axes列表解析为归约轴区间[axisL, axisR],其中-1会被转换为dims - 1(最后一维),随后将参与归约的维度合并、把输入重新组织为 2D/3D 视图后交给 kernel。因此负索引在内部会被归一化为正索引处理。
四、第二段接口 aclnnLogSoftmaxV2
4.1 参数说明
| 参数 | 说明 |
|---|---|
| workspace | (void*,入参)在 Device 侧申请的 workspace 内存地址。 |
| workspaceSize | (uint64_t,入参)在 Device 侧申请的 workspace 大小,由第一段接口aclnnLogSoftmaxV2GetWorkspaceSize获取。 |
| executor | (aclOpExecutor*,入参)算子执行器,包含算子计算流程。 |
| stream | (aclrtStream,入参)指定执行任务的 Stream。 |
返回值:aclnnStatus,具体取值参考 docs/zh/context/aclnn_return_code.md。
4.2 调用要点
workspace必须通过aclrtMalloc在 Device 侧分配,大小以第一段接口返回值为准;当workspaceSize == 0时无需申请(可传nullptr);- 两段接口调用之间需要保证 executor 生命周期有效;
- 第二段接口是异步下发,如需读取输出结果,必须先用
aclrtSynchronizeStream(stream)同步等待任务完成; - 第二段接口实现为对
CommonOpExecutorRun的统一封装,见 op_host/op_api/aclnn_log_softmax.cpp。
五、算子内部实现解析(op_host / op_kernel)
5.1 算子定义与注册
op_host/log_softmax_v2_def.cpp 中通过算子定义 DSL 注册 LogSoftmaxV2:
- 输入
input与输出out:数据类型均为ge::DT_FLOAT / ge::DT_FLOAT16 / ge::DT_BF16,格式均为FORMAT_ND; - 属性
axes:ListInt类型,可选属性,默认值为{-1}(即默认沿最后一维归约); - AICore 平台配置:
ascend910b。
shape 推导与数据类型推导在 op_host/log_softmax_v2_infershape.cpp 中注册(当前实现输出 shape 与输入一致,直接返回GRAPH_SUCCESS)。
5.2 Kernel 调度:AICore 与 AICPU 双路径
op_host/op_api/log_softmax_v2.cpp 展示了运行时调度逻辑:
static const std::initializer_list<op::DataType> AICORE_DTYPE_SUPPORT_LIST = { op::DataType::DT_FLOAT, op::DataType::DT_FLOAT16, op::DataType::DT_BF16}; static bool IsAiCoreSupport(const aclTensor* self) { return CheckType(self->GetDataType(), AICORE_DTYPE_SUPPORT_LIST); } const aclTensor* LogSoftmaxV2(const aclTensor* x, int64_t dim, aclOpExecutor* executor) { FVector<int64_t> dimListVector{dim}; auto dimList = executor->AllocIntArray(dimListVector.data(), 1); auto logSoftmaxOut = executor->AllocTensor(x->GetViewShape(), x->GetDataType()); if (IsAiCoreSupport(x)) { return LogSoftmaxAiCore(x, dimList, logSoftmaxOut, executor); } else { return LogSoftmaxAiCpu(x, dimList, logSoftmaxOut, executor); } }- 数据类型为 FLOAT / FLOAT16 / BF16 时走AICore 高性能路径(
ADD_TO_LAUNCHER_LIST_AICORE); - 其他类型回退到AICPU 路径(
ADD_TO_LAUNCHER_LIST_AICPU,属性名axes,static internal::AicpuTaskSpace space("LogSoftmaxV2"))。
因此aclnnLogSoftmaxV2在支持列表之外的数据类型(如 DOUBLE)并不会直接拒绝——接口层 aclnn_log_softmax.cpp 的DTYPE_SUPPORT_LIST实际还包含DT_DOUBLE,DOUBLE 输入会经 Cast 隐式转换或回退 AICPU 处理;而 kernel 直接入参仅支持上述三种 AICore 类型。
5.3 Tiling:形状重组、分片与多核切分
Tiling 逻辑集中在 op_host/log_softmax_v2_tiling.cpp,关键步骤包括:
- 平台信息获取:通过
platform_ascendc::PlatformAscendC获取 UB 大小与可用核数(GetCoreNum),并据此计算 workspace(系统 workspace + 用户 workspace,见GetWorkspaceSize)。 - 维度重组:将任意 shape 与归约轴重排为 2D(行归约场景
shape[0] × shape[1])或 3D(列归约场景shape[0] × shape[1] × shape[2])视图,同时合并非归约维度。 - 核心数裁剪:
coreNum = min(requiredCore, coreNum),其中requiredCore = shape[0] * CORE_NUM_MIN_FACTOR(2D 场景)或由分片总数乘以 2(3D 场景),避免小任务空占过多核。 - 分片策略:针对不同数据类型(FLOAT / FLOAT16 / BF16)与形状特征选择不同的对齐粒度与 chunk 大小:
- 小 Batch(
shape[0]*shape[2] <= 1024)走小粒度对齐(16/64 等); - 大形状(
shape[2] >= 8192,或非 float 时shape[2] >= blockDim * 2048)按 2048 chunk 分片; - 默认场景按 128/256 对齐分片。
- 小 Batch(
- 写回 tiling data:结构体定义在 op_kernel/log_softmax_v2_tiling_data.h:
struct LogSoftmaxV2TilingData { uint64_t axis; uint64_t dims; uint64_t shape[8]; };并通过context->SetBlockDim(coreNum)设定核数。
5.4 Kernel:多核并行下的数值稳定计算
Kernel 入口在 op_kernel/log_softmax_v2.cpp,通过模板匹配 FLOAT / HALF / BF16 三种类型,进入ProcessLogSoftmaxInternal后按dims == 2与dims == 3分派到不同算子类:
- 2D 行归约(如
LogSoftmax/LogSoftmaxHalf):核间按行范围均分start = blockIdx * shape[0] / blockDim,每行内依次执行ReduceMax→ 减最大值 →Exp→ReduceSum→Ln→ 减对数归一化项,最终得到x - max - log(Σe^(x-max))。 - 3D 列归约(如
LogSoftmaxCol/LogSoftmaxHalfCol):核间按 batch × chunk 的二维任务索引均分,chunk 内先对每列求oldmax(Duplicate(oldmax, FLOAT_NEG_INF, ...)初始化),再Sub减最大值、Exp、Add累加指数和、Ln,最后写回。该路径通过DataCopyPad处理非对齐尾列,兼顾了数据搬移的带宽利用率。
每个算子类内部使用TQue/TBuf双缓冲(pipe.InitBuffer(..., 2, ...))实现数据搬移与向量计算的流水线重叠,Process1/Process2/Process3/Process4对应不同分片场景的执行核。数值上,所有中间过程均以 float 精度完成(half/bf16 输入先Cast到 float 计算再转回),最大程度降低累加误差。
六、完整可运行示例(C++)
以下示例与仓库 examples/test_aclnn_log_softmax_v2.cpp 同构,完整演示了从环境初始化到结果回收的七个标准步骤,可直接作为工程模板使用:
#include <iostream> #include <vector> #include "acl/acl.h" // 实际使用时,请包含正确的头文件 // #include "aclnn_log_softmax_v2.h" // 以下为示例代码,函数签名仅为示意 aclnnStatus aclnnLogSoftmaxV2GetWorkspaceSize(const aclTensor *self, int64_t dim, aclTensor *out, uint64_t *workspaceSize, aclOpExecutor **executor); aclnnStatus aclnnLogSoftmaxV2(void *workspace, uint64_t workspaceSize, aclOpExecutor *executor, aclrtStream stream); #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; } void PrintOutResult(std::vector<int64_t> &shape, void** deviceAddr) { auto size = GetShapeSize(shape); std::vector<float> resultData(size, 0); auto ret = aclrtMemcpy(resultData.data(), resultData.size() * sizeof(resultData[0]), *deviceAddr, 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); for (int64_t i = 0; i < size; i++) { LOG_PRINT("logsoftmax result[%ld] is: %f\n", i, resultData[i]); } } 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); aclFinalize(); return ret); ret = aclrtCreateStream(stream); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclrtCreateStream failed. ERROR: %d\n", ret); aclrtResetDevice(deviceId); aclFinalize(); 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); 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); 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); 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]; } *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初始化 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. 构造输入与输出 std::vector<int64_t> selfShape = {2, 3}; std::vector<int64_t> outShape = {2, 3}; int64_t dim = 1; // 沿着第1个维度(每行)做LogSoftmax void* selfDeviceAddr = nullptr; void* outDeviceAddr = nullptr; aclTensor* self = nullptr; aclTensor* out = nullptr; std::vector<float> selfHostData = {0, 1, 2, 5, 2, 1}; // 输入数据 std::vector<float> outHostData(selfHostData.size(), 0); // 输出数据,仅用于占位 ret = CreateAclTensor(selfHostData, selfShape, &selfDeviceAddr, aclDataType::ACL_FLOAT, &self); CHECK_RET(ret == ACL_SUCCESS, return ret); ret = CreateAclTensor(outHostData, outShape, &outDeviceAddr, aclDataType::ACL_FLOAT, &out); CHECK_RET(ret == ACL_SUCCESS, return ret); // 3. 调用CANN算子库API uint64_t workspaceSize = 0; aclOpExecutor* executor; // 调用aclnnLogSoftmaxV2第一段接口 ret = aclnnLogSoftmaxV2GetWorkspaceSize(self, dim, out, &workspaceSize, &executor); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclnnLogSoftmaxV2GetWorkspaceSize 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); } // 调用aclnnLogSoftmaxV2第二段接口 ret = aclnnLogSoftmaxV2(workspaceAddr, workspaceSize, executor, stream); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclnnLogSoftmaxV2 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. 获取输出的值 // 预期输出: {-2.4076, -1.4076, -0.4076, -0.0659, -3.0659, -4.0659} PrintOutResult(outShape, &outDeviceAddr); // 6. 释放aclTensor aclDestroyTensor(self); aclDestroyTensor(out); // 7. 释放device资源 aclrtFree(selfDeviceAddr); aclrtFree(outDeviceAddr); if (workspaceSize > 0) { aclrtFree(workspaceAddr); } aclrtDestroyStream(stream); aclrtResetDevice(deviceId); aclFinalize(); return 0; }6.1 示例输出验证
以输入self = [[0, 1, 2], [5, 2, 1]]、dim = 1为例,逐元素验证:
- 第一行
[0, 1, 2]:logsumexp = log(e^0 + e^1 + e^2) = log(1 + 2.7183 + 7.3891) ≈ 2.4076,因此输出为[0-2.4076, 1-2.4076, 2-2.4076] = [-2.4076, -1.4076, -0.4076]; - 第二行
[5, 2, 1]:logsumexp = log(e^5 + e^2 + e^1) ≈ log(148.413 + 7.389 + 2.718) ≈ 5.0659,因此输出为[5-5.0659, 2-5.0659, 1-5.0659] = [-0.0659, -3.0659, -4.0659]。
与示例中注释的预期输出{-2.4076, -1.4076, -0.4076, -0.0659, -3.0659, -4.0659}完全一致,可据此核验运行结果。
6.2 关键工程细节
CreateAclTensor中通过连续 shape 计算 strides 并以aclFormat::ACL_FORMAT_ND创建 Tensor,确保输入满足 ND 格式要求;dim = 1对 2 维输入等价于dim = -1,均表示沿行方向归约;- 所有 Device 内存(输入、输出、workspace)都必须在使用后
aclrtFree,Tensor 对象用aclDestroyTensor释放,避免内存泄漏; - 示例使用
ACL_FLOAT(FLOAT32),如需测试 FLOAT16 / BFLOAT16,将CreateAclTensor的模板参数与dataType一并替换为对应类型(如aclnnHalf/aclnnBfloat16)即可。
七、测试与验证路径
仓库为 LogSoftmaxV2 提供了完整的 host 与 kernel 两级单元测试,可作为正确性验证与二次开发的参照:
- host 侧 tiling 单测:tests/ut/op_host/test_log_softmax_v2_tiling.cpp 验证 tiling 计算与多核切分逻辑;
- kernel 侧单测:tests/ut/op_kernel/test_log_softmax_v2.cpp 驱动真实 NPU 执行并比对结果;
- 数据生成与比对脚本:tests/ut/op_kernel/logsoftmax_data/gen_data.py 与 tests/ut/op_kernel/logsoftmax_data/compare_data.py 分别负责构造输入数据与计算误差比对,可用于自定义 case 的批量验证。
此外,算子模块根目录 experimental/activation/log_softmax_v2/README.md 汇总了参数表(axes为ListInt归约轴属性,默认{-1};input/out支持 FLOAT、FLOAT16、BFLOAT16 与 ND 格式)、调用方式与贡献说明,接口层约束与内核约束不一致时以接口文档为准。
八、常见问题与排查建议
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 返回 161001 | self/out为空指针 | 检查两个 aclTensor 是否创建成功 |
| 返回 161002 | dtype 不支持、shape 不一致、维度 > 8、dim越界、类型无法转换 | 对照第三章 3.2 节逐项核对;BF16 需确认运行在支持的平台上 |
| 结果全为 0 或乱值 | 第二段执行后未同步 | 补充aclrtSynchronizeStream后再读取输出 |
| 结果与预期偏差大 | dim指定错误 | 确认dim指向的归约轴符合预期(负索引语义) |
| 内存泄漏 | 未释放 workspace / Tensor | 参照示例第 6、7 步完整释放 |
小结
aclnnLogSoftmaxV2是 CANN ops-nn 中 LogSoftmaxV2 算子的标准 aclnn 入口,其两段式接口、严格的入参校验与"ReduceMax + Exp + ReduceSum + Ln"的数值稳定计算路径,共同保证了在 Atlas A2 训练系列产品 / Atlas 800I A2 上沿任意维度高效、准确地完成对数 Softmax 运算。接入时遵循"第一段取 workspace → 申请内存 → 第二段下发 → 同步等待 → 回收资源"的固定流程即可;如需深入性能细节,可从 op_host/log_softmax_v2_tiling.cpp 的分片策略与 op_kernel/log_softmax_v2.cpp 的多核调度入手,结合仓库自带的 host/kernel 单测进行验证与二次开发。
【免费下载链接】ops-nn本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。项目地址: https://gitcode.com/cann/ops-nn
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考