CANN ops-math 算子开发指南:aclnnLerp 与 aclnnInplaceLerp 线性插值接口详解与实战
【免费下载链接】ops-math本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。项目地址: https://gitcode.com/cann/ops-math
Lerp(Linear Interpolation,线性插值)是数学基础计算算子库 CANN ops-math 中的常用算子,用于在起始 Tensor 与结束 Tensor 之间按权重进行逐元素线性插值。本文以 math/lerp/docs/aclnnLerp&aclnnInplaceLerp.md 为核心,完整梳理 aclnnLerp 与 aclnnInplaceLerp 两套两段式接口的函数原型、参数约束、错误码与返回值,并给出可直接编译运行的 C++ 调用示例;同时结合 op_api/aclnn_lerp_tensor.cpp、op_host/lerp_def.cpp、op_kernel/lerp_apt.cpp 等源码,剖析其参数校验、Broadcast 推导与 NPU 端实现原理。读完本文,你将能够根据自身场景正确选择普通输出或 in-place 接口,并完成从资源初始化、Tensor 构造到两段式调用与结果回拷的完整开发流程。
一、产品支持情况
aclnnLerp 与 aclnnInplaceLerp 在当前仓库中的产品支持情况如下:
| 产品 | 是否支持 |
|---|---|
| Ascend 950PR / Ascend 950DT | 支持 |
| Atlas A3 训练系列产品 / Atlas A3 推理系列产品 | 支持 |
| Atlas A2 训练系列产品 / Atlas A2 推理系列产品 | 支持 |
| Atlas 200I/500 A2 推理产品 | 不支持 |
| Atlas 推理系列产品 | 支持 |
| Atlas 训练系列产品 | 支持 |
与上述支持情况相印证,算子定义文件 op_host/lerp_def.cpp 中仅为ascend950、ascend350两个架构注册了 AICore 配置,并开启了DynamicRankSupportFlag(true)、DynamicShapeSupportFlag(true)、PrecisionReduceFlag(true)等动态能力。此外,在支持的产品中存在一个数据类型差异:Atlas 推理系列产品、Atlas 训练系列产品不支持 BFLOAT16,因此在这两类产品上调用接口时,输入输出仅能使用 FLOAT 或 FLOAT16。
二、功能说明与计算公式
接口功能为:根据给定的权重,在起始和结束 Tensor 之间进行线性插值,返回插值后的 Tensor。计算公式如下:
$$ \text { out }_i=\text { start }_i+\text { weight }_i \times\left(\text { end }_i-\text { start }_i\right) $$
其中start、end、weight三个输入均支持 Broadcast,三个 Tensor 逐元素广播对齐后,按上式逐元素计算得到输出out。
结合源码可以更清晰地看到这一语义的实现路径:
- 算子原语(l0 层)声明位于 op_api/lerp.h,即
l0op::Lerp(self, other, weight, executor); - Host 侧原语描述 op_host/lerp_def.cpp 定义了
start、end、weight三个必选输入与y一个输出; - 形状推导 op_host/lerp_infershape.cpp 直接调用
Ops::Base::InferShape4Broadcast(context, INPUT_NUM_THREE),即对 3 个输入做标准 Broadcast 推导。
三、函数原型与两段式接口机制
aclnnLerp和aclnnInplaceLerp实现完全相同的功能,区别仅在于输出方式,请根据实际场景选择:
- aclnnLerp:需新建一个输出张量对象(
out)来存储计算结果,适合需要保留原始输入的场景; - aclnnInplaceLerp:无需新建输出张量对象,直接在输入张量
selfRef的内存中原地存储计算结果,可节省一份输出内存。
两个算子均遵循 CANN 的两段式接口规范:必须先调用aclnnLerpGetWorkspaceSize(或aclnnInplaceLerpGetWorkspaceSize)获取计算所需 workspace 大小以及包含算子计算流程的执行器,再调用aclnnLerp(或aclnnInplaceLerp)执行计算。
四个接口的函数原型如下:
aclnnStatus aclnnLerpGetWorkspaceSize( const aclTensor* self, const aclTensor* end, const aclTensor* weight, aclTensor* out, uint64_t* workspaceSize, aclOpExecutor** executor)aclnnStatus aclnnLerp( void* workspace, uint64_t workspaceSize, aclOpExecutor* executor, const aclrtStream stream)aclnnStatus aclnnInplaceLerpGetWorkspaceSize( aclTensor* selfRef, const aclTensor* end, const aclTensor* weight, uint64_t* workspaceSize, aclOpExecutor** executor)aclnnStatus aclnnInplaceLerp( void* workspace, uint64_t workspaceSize, aclOpExecutor* executor, const aclrtStream stream)从源码实现 op_api/aclnn_lerp_tensor.cpp 可以看出两段式接口的内在联系:aclnnLerpGetWorkspaceSize内部创建OpExecutor并调用CalculateResult完成参数校验与计算图构建,随后通过uniqueExecutor->GetWorkspaceSize()返回所需 workspace 大小并把 executor 释放给调用方;aclnnLerp则通过CommonOpExecutorRun(workspace, workspaceSize, executor, stream)真正下发执行。值得一提的是,in-place 版本的aclnnInplaceLerpGetWorkspaceSize内部实际是CalculateResult(selfRef, end, weight, selfRef, ...)——即把selfRef同时作为输入与输出传入,从而在原始内存上完成计算。
四、aclnnLerpGetWorkspaceSize 参数详解
4.1 参数说明
| 参数名 | 输入/输出 | 描述 | 使用说明 | 数据类型 | 数据格式 | 维度 | 非连续Tensor |
|---|---|---|---|---|---|---|---|
| self(aclTensor*) | 输入 | 公式中的输入 start | 数据类型与 end、weight、out 一致;shape 需与 end、weight 满足 broadcast 关系 | FLOAT、FLOAT16、BFLOAT16 | ND | 0-8 | √ |
| end(aclTensor*) | 输入 | 公式中的输入 end | 数据类型与 self、weight、out 一致;shape 需与 self、weight 满足 broadcast 关系 | FLOAT、FLOAT16、BFLOAT16 | ND | 0-8 | √ |
| weight(aclTensor*) | 输入 | 公式中的输入 weight | 数据类型与 self、end、out 一致;shape 需与 self、end 满足 broadcast 关系 | FLOAT、FLOAT16、BFLOAT16 | ND | 0-8 | √ |
| out(aclTensor*) | 输出 | 公式中的 out | 数据类型与 self、end、weight 一致;shape 需与 self、end、weight broadcast 后的 shape 一致 | FLOAT、FLOAT16、BFLOAT16 | ND | 0-8 | √ |
| workspaceSize(uint64_t*) | 输出 | 返回需要在 Device 侧申请的 workspace 大小 | - | - | - | - | - |
| executor(aclOpExecutor**) | 输出 | 返回 op 执行器,包含算子计算流程 | - | - | - | - | - |
Atlas 推理系列产品、Atlas 训练系列产品:不支持 BFLOAT16。
参数约束在源码 op_api/aclnn_lerp_tensor.cpp 中有完整对应实现:
- 数据类型支持列表按架构区分:
GetDtypeSupportList()中,DAV_2201架构及寄存器基座(IsRegBase)架构(对应 A2/A3/950 等)支持DT_FLOAT16 / DT_FLOAT / DT_BF16,其余架构仅支持DT_FLOAT16 / DT_FLOAT——与文档中"Atlas 推理/训练系列产品不支持 BFLOAT16"的说明一致; CheckDtypeValid逐一校验 self 的类型是否在支持列表内,并校验 end、weight、out 与 self 类型一致;CheckShape先对 self、end 做BroadcastInferShape,再与 weight 做第二次 broadcast,并要求 broadcast 结果与 out 的 shape 完全一致;- 空 Tensor 处理:当 self/end/weight 任一为空时,广播结果必为空,直接返回
ACLNN_SUCCESS,不执行计算。
4.2 返回值与错误码
返回值为aclnnStatus状态码,具体可参见 aclnn 返回码说明。第一段接口完成入参校验,出现如下场景时报错:
| 返回值 | 错误码 | 描述 |
|---|---|---|
| ACLNN_ERR_PARAM_NULLPTR | 161001 | 传入的 self、end、weight 和 out 是空指针 |
| ACLNN_ERR_PARAM_INVALID | 161002 | self、end、weight 和 out 的数据类型不在支持的范围之内 |
| ACLNN_ERR_PARAM_INVALID | 161002 | self、end、weight 和 out 的数据类型不一致 |
| ACLNN_ERR_PARAM_INVALID | 161002 | self、end 和 weight 无法做 broadcast |
| ACLNN_ERR_PARAM_INVALID | 161002 | self、end 和 weight 做 broadcast 后的 shape 与 out 的 shape 不一致 |
错误码的抛出路径与源码一致:CheckParams中先执行CheckNotNull(失败返回ACLNN_ERR_PARAM_NULLPTR),再执行CheckDtypeValid与CheckShape(失败均返回ACLNN_ERR_PARAM_INVALID)。
五、aclnnLerp 参数详解(第二段接口)
| 参数名 | 输入/输出 | 描述 |
|---|---|---|
| workspace | 输入 | 在 Device 侧申请的 workspace 内存地址 |
| workspaceSize | 输入 | 在 Device 侧申请的 workspace 大小,由第一段接口 aclnnLerpGetWorkspaceSize 获取 |
| executor | 输入 | op 执行器,包含算子计算流程 |
| stream | 输入 | 指定执行任务的 Stream |
返回值同样为aclnnStatus状态码,参见 aclnn 返回码说明。
六、aclnnInplaceLerpGetWorkspaceSize 参数详解
6.1 参数说明
| 参数名 | 输入/输出 | 描述 | 使用说明 | 数据类型 | 数据格式 | 维度 | 非连续Tensor |
|---|---|---|---|---|---|---|---|
| selfRef(aclTensor*) | 输入/输出 | 公式中的输入 start 和输出 out | 数据类型与 end、weight 一致;shape 需与 end、weight 满足 broadcast 关系,且 broadcast 后的 shape 与 selfRef 一致 | FLOAT、FLOAT16、BFLOAT16 | ND | 0-8 | √ |
| end(aclTensor*) | 输入 | 公式中的输入 end | 数据类型与 selfRef、weight 一致;shape 需与 selfRef、weight 满足 broadcast 关系 | FLOAT、FLOAT16、BFLOAT16 | ND | 0-8 | √ |
| weight(aclTensor*) | 输入 | 公式中的输入 weight | 数据类型与 selfRef、end 一致;shape 需与 selfRef、end 满足 broadcast 关系 | FLOAT、FLOAT16、BFLOAT16 | ND | 0-8 | √ |
| workspaceSize(uint64_t*) | 输出 | 返回需要在 Device 侧申请的 workspace 大小 | - | - | - | - | - |
| executor(aclOpExecutor**) | 输出 | 返回 op 执行器,包含算子计算流程 | - | - | - | - | - |
Atlas 推理系列产品、Atlas 训练系列产品:不支持 BFLOAT16。
需要特别注意selfRef的语义:它同时承担 start 输入与 out 输出的角色,且要求 broadcast 后的 shape 与 selfRef 自身 shape 一致——也就是说 in-place 版本要求 selfRef 在广播中必须是"最大"的那个 shape,否则原地计算无法成立。
6.2 返回值与错误码
| 返回值 | 错误码 | 描述 |
|---|---|---|
| ACLNN_ERR_PARAM_NULLPTR | 161001 | 传入的 selfRef、end 和 weight 是空指针 |
| ACLNN_ERR_PARAM_INVALID | 161002 | selfRef、end 和 weight 的数据类型不在支持的范围之内 |
| ACLNN_ERR_PARAM_INVALID | 161002 | selfRef、end 和 weight 的数据类型不一致 |
| ACLNN_ERR_PARAM_INVALID | 161002 | selfRef、end 和 weight 无法做 broadcast |
| ACLNN_ERR_PARAM_INVALID | 161002 | selfRef、end 和 weight 做 broadcast 后的 shape 与 selfRef 的 shape 不一致 |
七、aclnnInplaceLerp 参数详解(第二段接口)
| 参数名 | 输入/输出 | 描述 |
|---|---|---|
| workspace | 输入 | 在 Device 侧申请的 workspace 内存地址 |
| workspaceSize | 输入 | 在 Device 侧申请的 workspace 大小,由第一段接口 aclnnInplaceLerpGetWorkspaceSize 获取 |
| executor | 输入 | op 执行器,包含算子计算流程 |
| stream | 输入 | 指定执行任务的 Stream |
返回值同样为aclnnStatus状态码,参见 aclnn 返回码说明。
八、约束说明
- 确定性计算:aclnnLerp & aclnnInplaceLerp 默认即为确定性实现,即相同输入在多次运行中产生一致的计算结果。关于确定性计算的通用说明可参见 确定性计算。
- 其余隐含约束(从源码与文档整理):三个输入加输出的数据类型必须一致(Atlas 推理/训练系列不支持 BFLOAT16);输入 shape 必须满足 Broadcast 关系,输出 shape 必须等于三输入 broadcast 后的 shape;数据格式仅支持 ND;维度范围为 0-8 维;支持非连续 Tensor(接口内部会自动转为连续再计算)。
九、源码级原理剖析:从 aclnn 接口到 NPU kernel
为了让读者对接口背后的实现有完整认知,这里结合仓库源码梳理一条从 Host 侧 API 到 Device 侧 Kernel 的关键链路:
- Host 侧算子原语(l0 层):
l0op::Lerp声明于 op_api/lerp.h,在 op_api/aclnn_lerp_tensor.cpp 的CalculateResult中被调用。该函数在执行计算前做了三项准备工作:对非连续输入调用l0op::Contiguous转连续、对非 ND 格式打印告警、对非连续输出调用l0op::ViewCopy将计算结果写回非连续视图。这也是文档中"非连续 Tensor:√"能够成立的实现基础。 - 算子原语描述与形状推导:原语
Lerp的定义见 op_host/lerp_def.cpp,配置了DynamicCompileStaticFlag、DynamicShapeSupportFlag等动态编译能力;shape 推导见 op_host/lerp_infershape.cpp,即三输入 Broadcast 推导。 - Device 侧 Kernel:入口函数为 op_kernel/lerp_apt.cpp 中的
lerp(GM_ADDR start, GM_ADDR end, GM_ADDR weight, GM_ADDR y, ...)。Kernel 依据 tiling 阶段生成的TILING_KEY分派到 10 种实现之一,例如:LERP_F32_NDDMA_WITHOUT_LOOPS_TILING_KEY:float32、UB 内最多 5 维、无需循环的 NDDMA 直通实现;LERP_F32_NDDMA_WITH_LOOPS_TILING_KEY:float32、UB 内最多 8 维、需要循环搬移的实现;- 同样的模式覆盖 FLOAT16、BFLOAT16 以及 weight 为 float32 的混合精度组合(
dtype_comb_0/dtype_comb_1)。 每个分支对应 op_kernel/arch35/ 下独立的头文件实现,且 Kernel 限定 AIV 核执行(KERNEL_TASK_TYPE_DEFAULT(KERNEL_TYPE_AIV_ONLY))。Tiling 侧的动态 shape 切分逻辑则位于 op_host/arch35/lerp_tiling_arch35.cpp。
- 测试验证:ST 级用例由 tests/st/aclnnLerp/atk_aclnnLerp.json 描述,覆盖 fp32 / fp16 / bf16 三种精度、从 1 维到 8 维的多种 shape,以及
inf、-inf、nan边界值输入;UT 级用例见 tests/ut/op_api/test_aclnn_lerp_tensor.cpp 与 tests/ut/op_host/test_lerp_infershape.cpp。
十、调用示例
以下示例代码仅供参考,具体编译和执行过程请参考 编译与运行样例。
10.1 aclnnLerp 调用示例
本示例中 self、end 的 shape 均为{4, 2},weight 的 shape 为{1}(广播到{4, 2}),输出 shape 为{4, 2}。验证预期:out_i = self_i + 2 × (end_i - self_i)。
#include <iostream> #include <vector> #include "acl/acl.h" #include "aclnnop/aclnn_lerp_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); 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); // 调用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手册 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> endShape = {4, 2}; std::vector<int64_t> weightShape = {1}; std::vector<int64_t> outShape = {4, 2}; void* selfDeviceAddr = nullptr; void* endDeviceAddr = nullptr; void* weightDeviceAddr = nullptr; void* outDeviceAddr = nullptr; aclTensor* self = nullptr; aclTensor* end = nullptr; aclTensor* weight = nullptr; aclTensor* out = nullptr; std::vector<float> selfHostData = {1, 2, 3, 4, 5, 6, 7, 8}; std::vector<float> endHostData = {4, 5, 6, 7, 8, 9, 10, 11}; std::vector<float> weightHostData = {2}; 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); // 创建end aclTensor ret = CreateAclTensor(endHostData, endShape, &endDeviceAddr, aclDataType::ACL_FLOAT, &end); CHECK_RET(ret == ACL_SUCCESS, return ret); // 创建weight aclTensor ret = CreateAclTensor(weightHostData, weightShape, &weightDeviceAddr, aclDataType::ACL_FLOAT, &weight); 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); uint64_t workspaceSize = 0; aclOpExecutor* executor; // 3. 调用CANN算子库API // 调用aclnnLerp第一段接口 ret = aclnnLerpGetWorkspaceSize(self, end, weight, out, &workspaceSize, &executor); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclnnLerpGetWorkspaceSize 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); } // 调用aclnnLerp第二段接口 ret = aclnnLerp(workspaceAddr, workspaceSize, executor, stream); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclnnLerp 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侧 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和aclScalar aclDestroyTensor(self); aclDestroyTensor(end); aclDestroyTensor(weight); aclDestroyTensor(out); // 7. 释放device资源,需要根据具体API的接口定义修改 aclrtFree(selfDeviceAddr); aclrtFree(endDeviceAddr); aclrtFree(weightDeviceAddr); aclrtFree(outDeviceAddr); if (workspaceSize > 0) { aclrtFree(workspaceAddr); } aclrtDestroyStream(stream); aclrtResetDevice(deviceId); aclFinalize(); return 0; }10.2 aclnnInplaceLerp 调用示例
与普通版本的差异点:不构造 out Tensor,第一段接口传入selfRef(此处为self),计算结果直接写入self的 device 内存,因此第 5 步回拷时读取的是selfDeviceAddr,第 6 步也只需销毁 3 个 Tensor。
#include <iostream> #include <vector> #include "acl/acl.h" #include "aclnnop/aclnn_lerp_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); 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); // 调用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手册 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> endShape = {4, 2}; std::vector<int64_t> weightShape = {1}; void* selfDeviceAddr = nullptr; void* endDeviceAddr = nullptr; void* weightDeviceAddr = nullptr; aclTensor* self = nullptr; aclTensor* end = nullptr; aclTensor* weight = nullptr; std::vector<float> selfHostData = {1, 2, 3, 4, 5, 6, 7, 8}; std::vector<float> endHostData = {4, 5, 6, 7, 8, 9, 10, 11}; std::vector<float> weightHostData = {2}; // 创建self aclTensor ret = CreateAclTensor(selfHostData, selfShape, &selfDeviceAddr, aclDataType::ACL_FLOAT, &self); CHECK_RET(ret == ACL_SUCCESS, return ret); // 创建end aclTensor ret = CreateAclTensor(endHostData, endShape, &endDeviceAddr, aclDataType::ACL_FLOAT, &end); CHECK_RET(ret == ACL_SUCCESS, return ret); // 创建weight aclTensor ret = CreateAclTensor(weightHostData, weightShape, &weightDeviceAddr, aclDataType::ACL_FLOAT, &weight); CHECK_RET(ret == ACL_SUCCESS, return ret); uint64_t workspaceSize = 0; aclOpExecutor* executor; // 3. 调用CANN算子库API // 调用aclnnInplaceLerp第一段接口 ret = aclnnInplaceLerpGetWorkspaceSize(self, end, weight, &workspaceSize, &executor); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclnnInplaceLerpGetWorkspaceSize 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); } // 调用aclnnInplaceLerp第二段接口 ret = aclnnInplaceLerp(workspaceAddr, workspaceSize, executor, stream); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclnnInplaceLerp 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侧 auto size = GetShapeSize(selfShape); std::vector<float> 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: %f\n", i, resultData[i]); } // 6. 释放aclTensor和aclScalar aclDestroyTensor(self); aclDestroyTensor(end); aclDestroyTensor(weight); // 7. 释放device资源,需要根据具体API的接口定义修改 aclrtFree(selfDeviceAddr); aclrtFree(endDeviceAddr); aclrtFree(weightDeviceAddr); if (workspaceSize > 0) { aclrtFree(workspaceAddr); } aclrtDestroyStream(stream); aclrtResetDevice(deviceId); aclFinalize(); return 0; }十一、总结与选型建议
- 需要保留原始 start 数据、或需要独立的输出 Tensor 参与后续运算时,选择aclnnLerp(多申请一份输出内存);
- 对内存敏感、允许覆盖原始输入的场景,选择aclnnInplaceLerp,可省去输出 Tensor 的创建与释放;
- 两者均需严格遵循两段式接口流程:第一段
GetWorkspaceSize完成校验并产出 executor 与 workspaceSize,第二段申请 workspace 后执行;任何入参为空指针、类型越界/不一致、broadcast 失败或输出 shape 不匹配,都会在第一段接口返回 161001 / 161002 错误码; - 调用前请确认目标产品在支持列表内,并注意 Atlas 推理/训练系列产品不支持 BFLOAT16;
- 需要进一步理解 Broadcast、返回码、两段式接口与编译运行机制的读者,可继续阅读仓库内的 broadcast 关系、aclnn 返回码、两段式接口 与 编译与运行样例 等文档,并结合 math/lerp/examples/test_aclnn_lerp.cpp 与 math/lerp/examples/test_aclnn_inplace_lerp.cpp 两个可直接运行的样例加深理解。
【免费下载链接】ops-math本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。项目地址: https://gitcode.com/cann/ops-math
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考