CANN ops-math Less 算子 aclnnLtTensor 与 aclnnInplaceLtTensor 接口调用指南
【免费下载链接】ops-math本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。项目地址: https://gitcode.com/cann/ops-math
本篇技术指南以 CANN ops-math 开源仓库中experimental/math/less目录下的 Less 算子为核心,完整讲解其两个 aclnn 单算子 API ——aclnnLtTensor(非原地版本)与aclnnInplaceLtTensor(原地版本)的接口定义、参数约束、错误码、两段式调用流程与可运行的完整示例代码。读完本文,你将掌握如何在 Atlas A2 系列产品上通过 aclnn 接口完成逐元素"小于"比较计算,理解 workspace 申请机制与原地/非原地两种调用的取舍,并能结合仓库源码看懂算子内部的类型推导、广播、Contiguous/Cast/ViewCopy 等底层实现链路。
算子功能与产品支持
aclnnLtTensor与aclnnInplaceLtTensor实现的是 Less(逐元素小于比较)算子。接口功能为:判断输入self中的每个元素是否小于输入other中的元素,返回一个 Bool 类型的 Tensor。其计算公式为:
$$ out_i = (self_i < other_i) ? [True] : [False] $$
即输出张量中第 i 个元素的取值为:当self[i] < other[i]成立时为True,否则为False。
该算子的产品支持情况如下:
| 产品 | 是否支持 |
|---|---|
| Atlas A2 训练系列产品/Atlas A2 推理系列产品 | √ |
说明:算子对应的 aclnn 接口所属的 Less 算子同时提供 Scalar 版本接口
aclnnLtScalar/aclnnInplaceLtScalar,相关文档见 aclnnLtScalar&aclnnInplaceLtScalar.md,本文聚焦 Tensor 版本。
从 Less 算子 README 可以了解到该算子的贡献信息:Less 算子由浙江工业大学杨旭华团队于 2025/7/30 首次新增,并由个人开发者 fulltower 于 2026/2/27 完成算子适配开源仓,属于社区贡献的数学类基础计算算子。
函数原型:两段式接口
aclnnLtTensor和aclnnInplaceLtTensor实现相同的计算功能,二者使用区别如下,请根据自身实际场景选择合适的接口:
- aclnnLtTensor:需新建一个输出张量对象存储计算结果。
- aclnnInplaceLtTensor:无需新建输出张量对象,直接在输入张量的内存中存储计算结果。
每个算子都采用两段式接口设计:必须先调用aclnnLtTensorGetWorkspaceSize或者aclnnInplaceLtTensorGetWorkspaceSize接口获取计算所需 workspace 大小以及包含了算子计算流程的执行器(executor),再调用aclnnLtTensor或者aclnnInplaceLtTensor接口执行计算。四个接口的原型如下:
aclnnStatus aclnnLtTensorGetWorkspaceSize(const aclTensor *self, const aclTensor *other, aclTensor *out, uint64_t *workspaceSize, aclOpExecutor **executor) aclnnStatus aclnnLtTensor(void *workspace, uint64_t workspaceSize, aclOpExecutor *executor, aclrtStream stream) aclnnStatus aclnnInplaceLtTensorGetWorkspaceSize(const aclTensor *selfRef, const aclTensor *other, uint64_t *workspaceSize, aclOpExecutor **executor) aclnnStatus aclnnInplaceLtTensor(void *workspace, uint64_t workspaceSize, aclOpExecutor *executor, aclrtStream stream)其中第一段接口(xxxGetWorkspaceSize)负责完成入参校验、算子流程编排与 workspace 大小计算;第二段接口(xxx)接收第一段返回的 workspace 与 executor,在指定 stream 上真正执行计算。关于两段式接口的通用机制,可参考 两段式接口说明:workspace 是指除输入/输出外,算子在 NPU 上完成计算所需要的临时内存;第二段接口不能重复调用,同一 executor 只能执行一次。
aclnnLtTensorGetWorkspaceSize 参数说明
第一段接口的入参/出参如下:
| 参数 | 说明 |
|---|---|
| self(aclTensor*,计算输入) | Device 侧的 aclTensor,数据类型需要与 other 满足数据类型推导规则(参见互推导关系),shape 需要与 other 满足 broadcast 关系,shape 维度不高于 8 维。支持非连续的 Tensor,数据格式支持 ND。数据类型支持 FLOAT、FLOAT16、BFLOAT16、INT32、UINT32、INT64、UINT64、INT16、UINT16、INT8、UINT8、DOUBLE、BOOL。 |
| other(aclTensor*,计算输入) | Device 侧的 aclTensor,数据类型需要与 self 满足数据类型推导规则(参见互推导关系),shape 需要与 self 的 shape 满足 broadcast 关系,shape 维度不高于 8 维。支持非连续的 Tensor,数据格式支持 ND。数据类型支持 FLOAT、FLOAT16、BFLOAT16、INT32、UINT32、INT64、UINT64、INT16、UINT16、INT8、UINT8、DOUBLE、BOOL。 |
| out(aclTensor*,计算输出) | Device 侧的 aclTensor,数据类型需要是 BOOL 可转换的数据类型(参见互转换关系),shape 与 self、other 广播之后的 shape(参见 broadcast 关系)一致,shape 维度不高于 8 维,支持非连续的 Tensor,数据格式支持 ND。数据类型支持 FLOAT、FLOAT16、BFLOAT16、INT32、UINT32、INT64、UINT64、INT16、UINT16、INT8、UINT8、DOUBLE、BOOL、COMPLEX64、COMPLEX128。 |
| workspaceSize(uint64_t*,出参) | 返回用户需要在 Device 侧申请的 workspace 大小。 |
| executor(aclOpExecutor**,出参) | 返回 op 执行器,包含了算子计算流程。 |
返回值与错误码
第一段接口返回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 的维度大于 8。 |
| ACLNN_ERR_PARAM_INVALID | 161002 | self 和 other 的数据类型无法进行推导。 |
| ACLNN_ERR_PARAM_INVALID | 161002 | self 和 other 的 shape 无法进行 broadcast。 |
| ACLNN_ERR_PARAM_INVALID | 161002 | out 的 shape 与 broadcast 后的 shape 不一致。 |
源码视角:参数校验的执行顺序
从源码 aclnn_lt_tensor.cpp 可以看到,aclnnLtTensorGetWorkspaceSize内部通过CheckParams按如下顺序完成四步校验:
CheckNotNull:检查 self、other、out 是否为空指针,失败返回ACLNN_ERR_PARAM_NULLPTR;CheckDtypeValid:检查输入数据类型是否在 API 支持列表内(校验逻辑见 aclnn_lt_tensor.cpp)。源码中按 SoC 平台区分了两套支持列表:ASCEND910_DTYPE_SUPPORT_LIST与ASCEND910B_DTYPE_SUPPORT_LIST(后者额外包含 BF16),GetDtypeSupportList根据当前平台 SoC 版本(ASCEND910B~ASCEND910E)动态选择;输出 out 的类型列表则对应ASCEND910_OUT_DTYPE_SUPPORT_LIST/ASCEND910B_OUT_DTYPE_SUPPORT_LIST,其中包含了 COMPLEX64、COMPLEX128,与文档中 out 的数据类型支持范围一致;CheckPromoteType:通过op::PromoteType推导 self 与 other 的公共数据类型,若推导结果为DT_UNDEFINED则报ACLNN_ERR_PARAM_INVALID(aclnn_lt_tensor.cpp);同时校验算子内部结果类型 BOOL 能否 Cast 到 out 的类型;CheckShape:校验 self、other 维度不超过 8 维(MAX_DIM_LEN = 8),并通过OP_CHECK_BROADCAST_AND_INFER_SHAPE计算广播后的 shape,要求与 out 的 view shape 完全一致(aclnn_lt_tensor.cpp)。
aclnnLtTensor 参数说明
第二段接口aclnnLtTensor的参数如下:
| 参数名 | 输入/输出 | 描述 |
|---|---|---|
| workspace | 输入 | 在 Device 侧申请的 workspace 内存地址。 |
| workspaceSize | 输入 | 在 Device 侧申请的 workspace 大小,由第一段接口 aclnnLtTensorGetWorkspaceSize 获取。 |
| executor | 输入 | op 执行器,包含了算子计算流程。 |
| stream | 输入 | 指定执行任务的 Stream。 |
返回值:aclnnStatus返回状态码,具体参见 aclnn 返回码。从实现上看,第二段接口直接调用框架的CommonOpExecutorRun(workspace, workspaceSize, executor, stream)完成异步计算(见 aclnn_lt_tensor.cpp)。
aclnnInplaceLtTensorGetWorkspaceSize 参数说明
原地版本的输入参数 selfRef 同时承担"输入"与"输出"两个角色:
| 参数 | 说明 |
|---|---|
| selfRef(aclTensor*,计算输入|计算输出) | 输入输出 tensor,即公式中的 self 与 out。Device 侧的 aclTensor,输入数据类型需要与 other 满足数据类型推导规则(参见互推导关系),shape 需要与 other 满足 broadcast 关系,且 broadcast 后的 shape 需要与 selfRef 的 shape 一致。支持非连续的 Tensor,数据格式支持 ND。数据类型支持 FLOAT、FLOAT16、INT32、INT64、INT16、INT8、UINT8、DOUBLE、UINT16、UINT32、UINT64、BOOL、BFLOAT16。 |
| other(aclTensor*,计算输入) | Device 侧的 aclTensor,数据类型需要与 selfRef 满足数据类型推导规则(参见互推导关系),shape 需要与 self 满足 broadcast 关系,且 broadcast 后的 shape 需要与 selfRef 的 shape 一致。支持非连续的 Tensor,数据格式支持 ND。数据类型支持 FLOAT、FLOAT16、INT32、INT64、INT16、INT8、UINT8、DOUBLE、UINT16、UINT32、UINT64、BOOL、BFLOAT16。 |
| workspaceSize(uint64_t*,出参) | 返回需要在 Device 侧申请的 workspace 大小。 |
| executor(aclOpExecutor**,出参) | 返回 op 执行器,包含了算子计算流程。 |
注意:原地版本要求广播后的 shape 必须与 selfRef 的 shape 完全一致。这是因为结果要写回 selfRef 所在的内存,如果 other 被广播扩展,而 selfRef 的 shape 小于广播结果,就无法原地写入。
返回值与错误码
| 返回值 | 错误码 | 描述 |
|---|---|---|
| ACLNN_ERR_PARAM_NULLPTR | 161001 | 传入的 selfRef、other 是空指针时。 |
| ACLNN_ERR_PARAM_INVALID | 161002 | selfRef 和 other 的数据类型不在支持的范围之内。 |
| ACLNN_ERR_PARAM_INVALID | 161002 | selfRef 和 other 的数据类型无法进行推导。 |
| ACLNN_ERR_PARAM_INVALID | 161002 | selfRef 和 other 的 shape 无法做 broadcast。 |
| ACLNN_ERR_PARAM_INVALID | 161002 | selfRef 和 other 做 broadcast 后的 shape 不等于 selfRef 的 shape。 |
| ACLNN_ERR_PARAM_INVALID | 161002 | selfRef、other 的维度大于 8。 |
源码视角:原地版本的本质是复用
从源码可以看出,原地版本并未重复实现一套计算逻辑,而是将 selfRef 直接作为 out 复用非原地版本的完整流程(aclnn_lt_tensor.cpp):
aclnnStatus aclnnInplaceLtTensorGetWorkspaceSize( const aclTensor* selfRef, const aclTensor* other, uint64_t* workspaceSize, aclOpExecutor** executor) { auto out = const_cast<aclTensor*>(selfRef); return aclnnLtTensorGetWorkspaceSize(selfRef, other, out, workspaceSize, executor); }也就是说,aclnnInplaceLtTensorGetWorkspaceSize只是把selfRef同时当作self与out传给aclnnLtTensorGetWorkspaceSize,因此它天然继承了非原地版本全部的参数校验逻辑,只是不再需要单独创建输出张量。这也是原地版本要求"broadcast 后的 shape 等于 selfRef 的 shape"的直接原因。
aclnnInplaceLtTensor 参数说明
| 参数名 | 输入/输出 | 描述 |
|---|---|---|
| workspace | 输入 | 在 Device 侧申请的 workspace 内存地址。 |
| workspaceSize | 输入 | 在 Device 侧申请的 workspace 大小,由第一段接口 aclnnInplaceLtTensorGetWorkspaceSize 获取。 |
| executor | 输入 | op 执行器,包含了算子计算流程。 |
| stream | 输入 | 指定执行任务的 Stream。 |
返回值:aclnnStatus返回状态码,具体参见 aclnn 返回码。实现上同样委托给CommonOpExecutorRun完成计算(aclnn_lt_tensor.cpp)。
源码级实现原理:一次 aclnnLtTensor 调用在 NPU 上发生了什么
aclnnLtTensorGetWorkspaceSize的第一段实现(aclnn_lt_tensor.cpp)在完成参数校验后,会通过 l0op 原语把算子的完整计算流程编排成一个执行图。源码注释给出了如下流程:
self other | | \ / Contiguous(workspace_0) Contiguous(workspace_2) \ / Cast(workspace_1) Cast(workspace_3) \ / Less(workspace_4) | Cast(workspace_5) | ViewCopy | result各步骤含义如下:
- 空 tensor 快速路径:若
self或other为空 tensor(IsEmpty()),直接返回workspaceSize = 0,无需任何计算(aclnn_lt_tensor.cpp); - 类型推导:调用
op::PromoteType得到 self 与 other 的公共类型作为隐式计算类型;特殊地,当推导结果为 BOOL 时会被提升为 UINT8(promoteType == DataType::DT_BOOL时置为DT_UINT8,见 aclnn_lt_tensor.cpp); - Contiguous:对 self 与 other 分别调用
l0op::Contiguous得到连续内存布局的中间张量,用于屏蔽非连续 Tensor 带来的访存差异; - Cast:将两个输入分别 Cast 到推导出的公共数据类型,保证 kernel 侧输入类型一致;
- Less:调用
l0op::Less执行逐元素比较,产生 BOOL 类型的中间结果; - Cast:将 BOOL 结果 Cast 到 out 声明的数据类型(例如示例中的 DOUBLE);
- ViewCopy:将结果拷贝到 out 上,兼容 out 为非连续 Tensor 的场景。
最后通过uniqueExecutor->GetWorkspaceSize()汇总整个流程所需的临时内存总量并返回。所有中间张量(workspace_0 ~ workspace_5)都驻留在 Device 侧 workspace 中,这正是第一段接口必须返回 workspaceSize 的原因。
kernel 侧:多核并行与 tiling
第一段接口编排完毕后,真正在 AICore 上执行的 kernel 入口位于 op_kernel/less.cpp。kernel 通过REGISTER_TILING_DEFAULT(LessTilingData)注册并解析 tiling 数据(tiling 在 host 侧由 less_tiling.cpp 完成),并根据isTailBlock标志区分尾块与非尾块的处理:
- 非尾块(
isTailBlock == 0):使用NsLess::Less<DTYPE_X1, DTYPE_X2, DTYPE_Y, false>; - 尾块(
isTailBlock == 1):使用NsLess::Less<DTYPE_X1, DTYPE_X2, DTYPE_Y, true>。
从 tiling 数据结构(bigCoreDataNum、smallCoreDataNum、bigCoreLoopNum、smallCoreLoopNum、ubPartDataNum、tailDataNum 等字段)可以看出,kernel 采用了大核/小核(big/small core)数据切分、UB 分块、循环分批、尾数据处理的多级并行策略,以适配 Atlas A2 系列产品的多核架构。而 host 侧的 shape 推导(less_infershape.cpp)实现较为直接:输出 shape 直接取 x1(self)的 shape,实际广播后的最终 shape 约束由 aclnn 层CheckShape保证。
调用示例
示例代码如下,仅供参考,具体编译和执行过程请参考编译与运行样例。仓库中对应的可运行测试样例位于 examples/test_aclnn_lt_tensor.cpp 与 examples/test_aclnn_inplace_lt_tensor.cpp。
aclnnLtTensor 示例代码
该示例中,self 与 other 均为 shape{4, 2}的 DOUBLE 张量,self 取{0,1,2,3,4,5,6,7},other 全取5,计算结果写入独立的 out 张量:
#include <iostream> #include <vector> #include "acl/acl.h" #include "aclnnop/aclnn_lt_tensor.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; } struct LtTensorData { 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<double> selfHostData = {0, 1, 2, 3, 4, 5, 6, 7}; std::vector<double> otherHostData = {5, 5, 5, 5, 5, 5, 5, 5}; std::vector<double> outHostData = {0, 0, 0, 0, 0, 0, 0, 0}; void* workspaceAddr = nullptr; uint64_t workspaceSize = 0; }; int CreateInputAndOutputTensors(LtTensorData& data) { auto ret = 0; // 创建self aclTensor ret = CreateAclTensor(data.selfHostData, data.selfShape, &data.selfDeviceAddr, aclDataType::ACL_DOUBLE, &data.self); CHECK_RET(ret == ACL_SUCCESS, return ret); // 创建other aclTensor ret = CreateAclTensor(data.otherHostData, data.otherShape, &data.otherDeviceAddr, aclDataType::ACL_DOUBLE, &data.other); CHECK_RET(ret == ACL_SUCCESS, return ret); // 创建out aclTensor ret = CreateAclTensor(data.outHostData, data.outShape, &data.outDeviceAddr, aclDataType::ACL_DOUBLE, &data.out); CHECK_RET(ret == ACL_SUCCESS, return ret); return ret; } int ExecuteLtTensorComputation(aclrtStream stream, LtTensorData& data) { auto ret = 0; aclOpExecutor* executor; // 调用aclnnLtTensor第一段接口 ret = aclnnLtTensorGetWorkspaceSize(data.self, data.other, data.out, &data.workspaceSize, &executor); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclnnLtTensorGetWorkspaceSize failed. ERROR: %d\n", ret); return ret); // 根据第一段接口计算出的workspaceSize申请device内存 data.workspaceAddr = nullptr; if (data.workspaceSize > 0) { ret = aclrtMalloc(&data.workspaceAddr, data.workspaceSize, ACL_MEM_MALLOC_HUGE_FIRST); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("allocate workspace failed. ERROR: %d\n", ret); return ret); } // 调用aclnnLtTensor第二段接口 ret = aclnnLtTensor(data.workspaceAddr, data.workspaceSize, executor, stream); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclnnLtTensor failed. ERROR: %d\n", ret); return ret); // 同步等待任务执行结束 ret = aclrtSynchronizeStream(stream); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclrtSynchronizeStream failed. ERROR: %d\n", ret); return ret); return ret; } int ProcessAndPrintResults(const LtTensorData& data) { auto ret = 0; auto size = GetShapeSize(data.outShape); std::vector<double> resultData(size, 0); ret = aclrtMemcpy(resultData.data(), resultData.size() * sizeof(resultData[0]), data.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: %lf\n", i, resultData[i]); } return ret; } void ReleaseResources(LtTensorData& data) { // 释放aclTensor和aclScalar aclDestroyTensor(data.self); aclDestroyTensor(data.other); aclDestroyTensor(data.out); // 释放device资源 aclrtFree(data.selfDeviceAddr); aclrtFree(data.otherDeviceAddr); aclrtFree(data.outDeviceAddr); if (data.workspaceSize > 0) { aclrtFree(data.workspaceAddr); } } int ExecuteLtTensorOperator(aclrtStream stream) { LtTensorData data; // 创建输入和输出张量 auto ret = CreateInputAndOutputTensors(data); CHECK_RET(ret == ACL_SUCCESS, return ret); // 执行LtTensor算子操作 ret = ExecuteLtTensorComputation(stream, data); CHECK_RET(ret == ACL_SUCCESS, return ret); // 处理并打印结果 ret = ProcessAndPrintResults(data); CHECK_RET(ret == ACL_SUCCESS, return ret); // 释放资源 ReleaseResources(data); return 0; } int main() { 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); // 执行InplaceLtScalar操作 ret = ExecuteLtTensorOperator(stream); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("ExecuteInplaceLtScalarOperator failed. ERROR: %d\n", ret); return ret); // 重置设备和终结ACL aclrtDestroyStream(stream); aclrtResetDevice(deviceId); aclFinalize(); return 0; }对上述代码的调用链路做简单梳理,便于理解整体骨架:
Init:固定写法,完成aclInit、aclrtSetDevice、aclrtCreateStream三个初始化步骤;CreateAclTensor:模板函数,完成 device 内存申请(aclrtMalloc)、host 数据拷贝(aclrtMemcpy,H2D)、连续 strides 计算,最终通过aclCreateTensor创建 aclTensor;ExecuteLtTensorComputation:核心两段式流程——第一段aclnnLtTensorGetWorkspaceSize取 workspaceSize 与 executor,按需aclrtMalloc申请 workspace,第二段aclnnLtTensor执行,最后aclrtSynchronizeStream同步等待;ProcessAndPrintResults:通过aclrtMemcpy(D2H)把结果拷回 host 并打印;ReleaseResources:依次释放 aclTensor、device 内存与 workspace;main:编排整个流程,结束后销毁 stream、重置设备并aclFinalize。
aclnnInplaceLtTensor 示例代码
原地版本省去了 out 张量的创建,计算结果直接写回 self 所在内存。示例中 self 为 DOUBLE 类型、shape{4, 2},other 为 INT32 类型、shape{4, 2},取值为{1,1,1,1,0,0,0,0},演示了两种不同类型输入经过类型推导后仍可正常计算:
#include <iostream> #include <vector> #include "acl/acl.h" #include "aclnnop/aclnn_lt_tensor.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, aclFormat::ACL_FORMAT_ND, shape.data(), shape.size(), *deviceAddr); return 0; } int ExecuteInplaceLtTensorOperator(aclrtStream stream) { auto ret = 0; std::vector<int64_t> selfShape = {4, 2}; std::vector<int64_t> otherShape = {4, 2}; void* selfDeviceAddr = nullptr; void* otherDeviceAddr = nullptr; aclTensor* self = nullptr; aclTensor* other = nullptr; std::vector<double> selfHostData = {0, 1, 2, 3, 4, 5, 6, 7}; std::vector<int> otherHostData = {1, 1, 1, 1, 0, 0, 0, 0}; ret = CreateAclTensor(selfHostData, selfShape, &selfDeviceAddr, aclDataType::ACL_DOUBLE, &self); CHECK_RET(ret == ACL_SUCCESS, return ret); ret = CreateAclTensor(otherHostData, otherShape, &otherDeviceAddr, aclDataType::ACL_INT32, &other); CHECK_RET(ret == ACL_SUCCESS, return ret); uint64_t workspaceSize = 0; aclOpExecutor* executor; ret = aclnnInplaceLtTensorGetWorkspaceSize(self, other, &workspaceSize, &executor); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclnnInplaceLtTensorGetWorkspaceSize failed. ERROR: %d\n", ret); return ret); 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 = aclnnInplaceLtTensor(workspaceAddr, workspaceSize, executor, stream); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclnnInplaceLtTensor failed. ERROR: %d\n", ret); return ret); ret = aclrtSynchronizeStream(stream); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclrtSynchronizeStream failed. ERROR: %d\n", ret); return ret); auto size = GetShapeSize(selfShape); std::vector<double> resultData(size, 0); ret = aclrtMemcpy(resultData.data(), resultData.size() * sizeof(resultData[0]), selfDeviceAddr, 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: %lf\n", i, resultData[i]); } aclDestroyTensor(self); aclDestroyTensor(other); aclrtFree(selfDeviceAddr); aclrtFree(otherDeviceAddr); if (workspaceSize > 0) { aclrtFree(workspaceAddr); } return 0; } int main() { 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); // 执行InplaceLtScalar操作 ret = ExecuteInplaceLtTensorOperator(stream); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("ExecuteInplaceLtScalarOperator failed. ERROR: %d\n", ret); return ret); // 重置设备和终结ACL aclrtDestroyStream(stream); aclrtResetDevice(deviceId); aclFinalize(); return 0; }与第一个示例对比可以发现,原地版本的核心差异在于:不创建 out 张量,第一段接口只传 selfRef 与 other,第二段接口执行完毕后直接从selfDeviceAddr读取结果。这样可以减少一次张量对象创建与一次结果回写,适合在调用方允许覆盖原输入的场景下使用,能够节省 Device 侧显存。
编译与运行
编译运行 aclnn 接口样例前,请先确保基础环境已搭建完成,包括驱动、固件、CANN 软件包、ops 包等,具体编译与运行步骤参见编译与运行样例。这里以合设场景(开发和运行环境在同一台带 AI 处理器的机器上)为例,给出 CMake 编译脚本的关键要素:
cmake_minimum_required(VERSION 3.14) project(ACLNN_EXAMPLE) add_compile_options(-std=c++11) set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "./bin") set(CMAKE_CXX_FLAGS_DEBUG "-fPIC -O0 -g -Wall") set(CMAKE_CXX_FLAGS_RELEASE "-fPIC -O2 -Wall") # 设置可执行文件名,并指定待运行算子文件*.cpp所在目录 add_executable(opapi_test test_aclnn_lt_tensor.cpp) # 设置ASCEND_PATH(CANN软件包目录,请根据实际路径修改)和INCLUDE_BASE_DIR(头文件目录) if(NOT "$ENV{ASCEND_CUSTOM_PATH}" STREQUAL "") set(ASCEND_PATH $ENV{ASCEND_CUSTOM_PATH}) else() set(ASCEND_PATH "/usr/local/Ascend/cann") endif() set(INCLUDE_BASE_DIR "${ASCEND_PATH}/include") include_directories( ${INCLUDE_BASE_DIR} ${INCLUDE_BASE_DIR}/aclnn ) # 设置链接的库文件路径 target_link_libraries(opapi_test PRIVATE ${ASCEND_PATH}/lib64/libascendcl.so ${ASCEND_PATH}/lib64/libnnopbase.so ${ASCEND_PATH}/lib64/libopapi_math.so) install(TARGETS opapi_test DESTINATION ${CMAKE_RUNTIME_OUTPUT_DIRECTORY})要点说明:
- 头文件路径需要同时包含 CANN 的 include 根目录与
include/aclnn子目录,示例代码中引用的aclnnop/aclnn_lt_tensor.h即来自该目录; - 链接时需要引入
libascendcl.so(ACL 运行时)、libnnopbase.so(NPU 算子基础库)与libopapi_math.so(数学类算子 API 库,Less 算子属于 math 域算子); - 编译产物默认输出到
./bin目录,运行前需保证环境变量(如LD_LIBRARY_PATH、ASCEND_OPP_PATH等)已按 CANN 安装要求配置好。
小结
aclnnLtTensor与aclnnInplaceLtTensor是 CANN ops-math 中 Less(逐元素小于比较)算子的标准 aclnn 调用入口。本文完整覆盖了两个接口的四个函数原型、参数与数据类型约束、错误码表、两段式调用流程与两份可运行示例代码,并结合 aclnn_lt_tensor.cpp 源码剖析了 Contiguous → Cast → Less → Cast → ViewCopy 的底层计算链路、原地版本复用非原地版本的实现技巧,以及 op_kernel/less.cpp 中基于 tiling 的多核并行机制。实际使用时,只需记住两条核心准则:先调 GetWorkspaceSize 获取 workspace 并申请内存,再调执行接口完成计算;需要保留原输入时选非原地版本,允许覆盖输入、想节省显存时选原地版本。
【免费下载链接】ops-math本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。项目地址: https://gitcode.com/cann/ops-math
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考