CANN ops-nn 算子 aclnnLogSoftmaxV2 使用指南:接口、参数、错误码与 NPU 实现解析
2026/9/19 20:06:14 网站建设 项目流程

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 计算路径是先ReduceMaxExpReduceSumLn,详见本文第四章源码解析。


二、函数原型与两段式调用约定

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 算子库标准的两段式接口

  1. 第一段aclnnLogSoftmaxV2GetWorkspaceSize:完成入参校验、构图,返回需要的 Device 侧 workspace 大小和已构建好的算子执行器;
  2. 第二段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)——空指针:

  1. 传入的selfout是空指针(对应源码CheckNotNull)。

161002(ACLNN_ERR_PARAM_INVALID)——参数非法:

  1. selfout的数据类型不在支持范围内(对应CheckDtypeValid,在非 910B 平台还会单独拦截 BF16);
  2. selfout的 shape 不一致(对应CheckShape中的OP_CHECK_SHAPE_NOT_EQUAL);
  3. selfout的维度超过 8(对应CheckShape中的OP_CHECK_MAX_DIM,源码常量MAX_DIM = 8);
  4. dim超出有效范围[-rank, rank-1](对应CheckDim,即dim < (-1) * inputShapeLen || dim > inputShapeLen - 1时报错);
  5. 类型推导失败或推导结果无法转换为输出类型(对应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
  • 属性axesListInt类型,可选属性,默认值为{-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,属性名axesstatic 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,关键步骤包括:

  1. 平台信息获取:通过platform_ascendc::PlatformAscendC获取 UB 大小与可用核数(GetCoreNum),并据此计算 workspace(系统 workspace + 用户 workspace,见GetWorkspaceSize)。
  2. 维度重组:将任意 shape 与归约轴重排为 2D(行归约场景shape[0] × shape[1])或 3D(列归约场景shape[0] × shape[1] × shape[2])视图,同时合并非归约维度。
  3. 核心数裁剪coreNum = min(requiredCore, coreNum),其中requiredCore = shape[0] * CORE_NUM_MIN_FACTOR(2D 场景)或由分片总数乘以 2(3D 场景),避免小任务空占过多核。
  4. 分片策略:针对不同数据类型(FLOAT / FLOAT16 / BF16)与形状特征选择不同的对齐粒度与 chunk 大小:
    • 小 Batch(shape[0]*shape[2] <= 1024)走小粒度对齐(16/64 等);
    • 大形状(shape[2] >= 8192,或非 float 时shape[2] >= blockDim * 2048)按 2048 chunk 分片;
    • 默认场景按 128/256 对齐分片。
  5. 写回 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 == 2dims == 3分派到不同算子类:

  • 2D 行归约(如LogSoftmax/LogSoftmaxHalf):核间按行范围均分start = blockIdx * shape[0] / blockDim,每行内依次执行ReduceMax→ 减最大值 →ExpReduceSumLn→ 减对数归一化项,最终得到x - max - log(Σe^(x-max))
  • 3D 列归约(如LogSoftmaxCol/LogSoftmaxHalfCol):核间按 batch × chunk 的二维任务索引均分,chunk 内先对每列求oldmaxDuplicate(oldmax, FLOAT_NEG_INF, ...)初始化),再Sub减最大值、ExpAdd累加指数和、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 汇总了参数表(axesListInt归约轴属性,默认{-1}input/out支持 FLOAT、FLOAT16、BFLOAT16 与 ND 格式)、调用方式与贡献说明,接口层约束与内核约束不一致时以接口文档为准。


八、常见问题与排查建议

现象可能原因排查方向
返回 161001self/out为空指针检查两个 aclTensor 是否创建成功
返回 161002dtype 不支持、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),仅供参考

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

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

立即咨询