news 2026/9/19 9:16:58

CANN ops-math Less 算子 aclnnLtTensor 与 aclnnInplaceLtTensor 接口调用指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
CANN ops-math Less 算子 aclnnLtTensor 与 aclnnInplaceLtTensor 接口调用指南

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 等底层实现链路。

算子功能与产品支持

aclnnLtTensoraclnnInplaceLtTensor实现的是 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 完成算子适配开源仓,属于社区贡献的数学类基础计算算子。

函数原型:两段式接口

aclnnLtTensoraclnnInplaceLtTensor实现相同的计算功能,二者使用区别如下,请根据自身实际场景选择合适的接口:

  • 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_NULLPTR161001传入的 self、other 或 out 是空指针。
ACLNN_ERR_PARAM_INVALID161002self、other 或 out 的数据类型不在支持的范围之内。
ACLNN_ERR_PARAM_INVALID161002self、other 或 out 的维度大于 8。
ACLNN_ERR_PARAM_INVALID161002self 和 other 的数据类型无法进行推导。
ACLNN_ERR_PARAM_INVALID161002self 和 other 的 shape 无法进行 broadcast。
ACLNN_ERR_PARAM_INVALID161002out 的 shape 与 broadcast 后的 shape 不一致。

源码视角:参数校验的执行顺序

从源码 aclnn_lt_tensor.cpp 可以看到,aclnnLtTensorGetWorkspaceSize内部通过CheckParams按如下顺序完成四步校验:

  1. CheckNotNull:检查 self、other、out 是否为空指针,失败返回ACLNN_ERR_PARAM_NULLPTR
  2. CheckDtypeValid:检查输入数据类型是否在 API 支持列表内(校验逻辑见 aclnn_lt_tensor.cpp)。源码中按 SoC 平台区分了两套支持列表:ASCEND910_DTYPE_SUPPORT_LISTASCEND910B_DTYPE_SUPPORT_LIST(后者额外包含 BF16),GetDtypeSupportList根据当前平台 SoC 版本(ASCEND910B~ASCEND910E)动态选择;输出 out 的类型列表则对应ASCEND910_OUT_DTYPE_SUPPORT_LIST/ASCEND910B_OUT_DTYPE_SUPPORT_LIST,其中包含了 COMPLEX64、COMPLEX128,与文档中 out 的数据类型支持范围一致;
  3. CheckPromoteType:通过op::PromoteType推导 self 与 other 的公共数据类型,若推导结果为DT_UNDEFINED则报ACLNN_ERR_PARAM_INVALID(aclnn_lt_tensor.cpp);同时校验算子内部结果类型 BOOL 能否 Cast 到 out 的类型;
  4. 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_NULLPTR161001传入的 selfRef、other 是空指针时。
ACLNN_ERR_PARAM_INVALID161002selfRef 和 other 的数据类型不在支持的范围之内。
ACLNN_ERR_PARAM_INVALID161002selfRef 和 other 的数据类型无法进行推导。
ACLNN_ERR_PARAM_INVALID161002selfRef 和 other 的 shape 无法做 broadcast。
ACLNN_ERR_PARAM_INVALID161002selfRef 和 other 做 broadcast 后的 shape 不等于 selfRef 的 shape。
ACLNN_ERR_PARAM_INVALID161002selfRef、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同时当作selfout传给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

各步骤含义如下:

  1. 空 tensor 快速路径:若selfother为空 tensor(IsEmpty()),直接返回workspaceSize = 0,无需任何计算(aclnn_lt_tensor.cpp);
  2. 类型推导:调用op::PromoteType得到 self 与 other 的公共类型作为隐式计算类型;特殊地,当推导结果为 BOOL 时会被提升为 UINT8(promoteType == DataType::DT_BOOL时置为DT_UINT8,见 aclnn_lt_tensor.cpp);
  3. Contiguous:对 self 与 other 分别调用l0op::Contiguous得到连续内存布局的中间张量,用于屏蔽非连续 Tensor 带来的访存差异;
  4. Cast:将两个输入分别 Cast 到推导出的公共数据类型,保证 kernel 侧输入类型一致;
  5. Less:调用l0op::Less执行逐元素比较,产生 BOOL 类型的中间结果;
  6. Cast:将 BOOL 结果 Cast 到 out 声明的数据类型(例如示例中的 DOUBLE);
  7. 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:固定写法,完成aclInitaclrtSetDeviceaclrtCreateStream三个初始化步骤;
  • 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_PATHASCEND_OPP_PATH等)已按 CANN 安装要求配置好。

小结

aclnnLtTensoraclnnInplaceLtTensor是 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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/19 9:16:19

屏幕翻译工具全攻略:OCR识别与翻译接口配置优化指南

屏幕翻译工具这类东西&#xff0c;我最早接触是在做外文资料整理的时候。那时候一份几十页的PDF技术手册&#xff0c;全是英文&#xff0c;逐段复制到翻译网站再粘回来&#xff0c;效率低到让人抓狂。后来发现有一类工具可以直接框选屏幕上的任意区域&#xff0c;松开鼠标就把识…

作者头像 李华
网站建设 2026/9/19 9:15:35

C#使用PDFium移除PDF数字签名技术详解

1. 项目背景与核心需求PDF数字签名作为文档认证的核心机制&#xff0c;在合同签署、财务报告等场景中广泛应用。但实际工作中我们常遇到需要移除签名的情况&#xff1a;比如测试环境重复使用已签名模板、修复被错误签名的文档&#xff0c;或是清理归档文件中的过期签名。传统PD…

作者头像 李华
网站建设 2026/9/19 9:15:06

前端入门避坑指南:HTML与CSS核心知识点详解

前端入门的坑&#xff0c;我替你先踩了一遍。小皮这套HTML&CSS学习笔记&#xff0c;不是什么高深理论&#xff0c;全是能直接抄进编辑器里跑起来看的代码和思路。很多人卡在入门阶段&#xff0c;不是笨&#xff0c;是信息太杂不知道先学哪块&#xff0c;或者被教程里跳跃的…

作者头像 李华
网站建设 2026/9/19 9:15:05

EVS7064S-R配置实战:从NVR到IPSAN与AI回放

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/19 9:15:01

Windows音频设备找不到?从硬件ID到驱动签名的全链路排查

1. 这不是“没声音”&#xff0c;而是Windows音频子系统在向你发求救信号 “Windows 找不到音频输入/输出设备”——这句话在设备管理器里一出现&#xff0c;很多人第一反应是&#xff1a;赶紧右键“更新驱动”&#xff0c;点完发现毫无反应&#xff1b;再试试“卸载设备”&am…

作者头像 李华
网站建设 2026/9/19 9:14:40

C++精灵库实现3D小球动画:编程教育与人生隐喻

1. 项目概述&#xff1a;用代码演绎的人生寓言在编程的世界里&#xff0c;我们常常追求功能的实现和性能的优化&#xff0c;却忽略了代码本身也可以成为表达思想的媒介。这个用C精灵库编写的12行核心代码程序&#xff0c;通过一个小球的3D绘制和碰壁反弹动画&#xff0c;巧妙地…

作者头像 李华