CANN ATVC ReduceSum 自定义算子单算子 API 调用:AclNNInvocationNaive 工程实战解析
【免费下载链接】atvcATVC(Ascend C Templates for Vector Compute),是为基于Ascend C开发的典型Vector算子封装的一系列模板头文件的集合,可帮助用户快速开发典型Vector算子。项目地址: https://gitcode.com/cann/atvc
本文以 CANN atvc 仓库中 examples/ops_aclnn/reduce_sum/AclNNInvocationNaive 工程样例为主体,系统讲解基于 ATVC(Ascend C Templates for Vector Compute)开发的 ReduceSum 自定义算子,如何通过aclnn 单算子 API(两段式接口)在应用程序中完成端到端调用。读完本文,你将掌握单算子 API 的调用原理、main.cpp的完整代码流程、编译脚本与运行脚本的配置要点,以及从算子编译部署到结果验证的完整实操路径。
一、工程概述:什么是 AclNNInvocationNaive
AclNNInvocationNaive 是 ATVC 仓库中用于aclnn 单算子调用验证的最小化样例工程。相比于标准版 AclNNInvocation 工程,它简化了工程配置——去掉了冗余的构建选项与目录层级,仅保留三个文件,让开发者把注意力集中在单算子 API 的调用逻辑本身:
examples/ops_aclnn/reduce_sum/AclNNInvocationNaive ├── CMakeLists.txt // 编译规则文件 ├── main.cpp // 单算子调用应用的入口 └── run.sh // 编译运行算子的脚本该样例依赖同目录父工程 examples/ops_aclnn/reduce_sum 中基于 ATVC 开发的 ReduceSumCustom 自定义算子。ReduceSum 是对输入 tensor 的指定轴进行规约累加并输出结果的 Reduce 类算子,本样例的规格为:输入x形状8 * 2048(float、ND 格式),输出y形状1 * 2048(float、ND 格式),即对第 0 维(dim=0)做归约求和,核函数名为reduce_sum_custom。
前置说明:运行本样例前,需先完成 ReduceSumCustom 算子的编译与部署,具体流程参见 examples/ops_aclnn/reduce_sum/README.md,本文第四节会同步给出关键步骤。
二、单算子 API 调用原理:两段式接口
完成自定义算子的开发部署后,可以通过单算子调用方式来验证单算子功能。单算子 API 执行是基于 C 语言的 API 执行算子,无需提供单算子描述文件进行离线模型的转换,直接调用单算子 API 接口即可。
自定义算子编译部署后,系统会自动生成单算子 API(即aclnn_xxx接口),可以在应用程序中直接调用。算子 API 的形式一般定义为"两段式接口",本样例对应ReduceSumCustom算子生成的两段接口为:
// 第一段:获取算子执行所需的 workspace 空间大小 aclnnStatus aclnnReduceSumCustomGetWorkspaceSize(const aclTensor *x, const aclIntArrat *dim, const aclTensor *out, uint64_t *workspaceSize, aclOpExecutor **executor); // 第二段:执行算子 aclnnStatus aclnnReduceSumCustom(void *workspace, int64_t workspaceSize, aclOpExecutor *executor, aclrtStream stream);调用流程要点:
aclnnReduceSumCustomGetWorkspaceSize为第一段接口,主要功能是计算本次 API 调用计算过程中需要多少workspace内存,同时返回一个aclOpExecutor执行器对象;- 获取到本次 API 计算需要的
workspaceSize之后,开发者需要按该大小通过aclrtMalloc申请Device 侧内存; - 然后调用第二段接口
aclnnReduceSumCustom执行计算,计算完成后还需显式释放 workspace 与执行器相关资源。
注意,原型描述中的dim参数对应算子 JSON 原型(ReduceSumCustom.json)中声明的attr——一个list_int类型的归约轴列表;x、y则对应其中的input_desc/output_desc(支持float32与int32两种数据类型)。这就是 aclnn 接口自动生成机制中"原型驱动接口"的直接体现。
三、main.cpp 代码实现深度解析
main.cpp 是单算子 API 执行的完整示例。其执行流程可分为六个阶段,下面结合源码逐一展开。
3.1 宏与工具函数
文件开头定义了两个调试宏和两个辅助函数:
#define CHECK_RET(cond, return_expr) \ do { \ if (!(cond)) { \ return_expr; \ } \ } while (0) #define LOG_PRINT(message, ...) \ do { \ printf(message, ##__VA_ARGS__); \ } while (0)CHECK_RET:对每个 API 的返回值做检查,失败时执行给定的返回表达式(通常是打印错误日志并销毁资源),保证了整个调用链的健壮性;GetShapeSize:根据 shape 向量累乘计算元素总数,用于推导内存字节数;VerifyResults:将算子输出与期望结果逐元素比较(示例中使用std::equal),并打印前 10 个元素用于人工核对,最后输出test pass或test failed。
3.2 阶段一:初始化 Device 与 Stream(固定代码)
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 1); ret = aclrtSetDevice(deviceId); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclrtSetDevice failed. ERROR: %d\n", ret); return 1); ret = aclrtCreateStream(stream); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclrtCreateStream failed. ERROR: %d\n", ret); return 1); return 0; }Init完成 acl 运行时的三步初始化:aclInit(初始化 ACL)、aclrtSetDevice(绑定计算设备,示例默认deviceId = 0,实际使用时需改为自己的设备号)、aclrtCreateStream(创建执行流)。这段代码对所有单算子调用样例是通用的。
3.3 阶段二:构造输入输出 aclTensor
std::vector<int64_t> inputXShape = {8, 2048}; std::vector<int64_t> outputYShape = {1, 2048}; ... InitializeData(inputXHostData, outputYHostData, goldenData, inputXShape, outputYShape);InitializeData将输入x全部初始化为1.0,由于是对 8 个元素求和,期望输出golden为8.0,输出y初始化为0.0。随后通过模板函数CreateAclTensor完成 Host 数据到 Device 张量的转换:
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); // 1. 申请Device内存 ret = aclrtMemcpy(*deviceAddr, size, hostData.data(), size, ACL_MEMCPY_HOST_TO_DEVICE); // 2. Host数据拷贝到Device *tensor = aclCreateTensor(shape.data(), shape.size(), dataType, nullptr, 0, aclFormat::ACL_FORMAT_ND, shape.data(), shape.size(), *deviceAddr); // 3. 创建aclTensor return 0; }其内部依次执行:aclrtMalloc申请 Device 侧内存 →aclrtMemcpy完成 Host 到 Device 的拷贝 →aclCreateTensor创建aclTensor对象(格式为ACL_FORMAT_ND,与算子原型声明的 ND 格式一致)。样例中分别创建了inputX(ACL_FLOAT)与outputY(ACL_FLOAT)。
此外,样例还通过aclCreateIntArray构造了归约轴数组:
std::vector<int64_t> dim{0}; aclIntArray* dimOut = aclCreateIntArray(dim.data(), dim.size());这里dim = {0}表示对第 0 维归约,与"8×2048 → 1×2048"的形状变化严格对应。
3.4 阶段三:两段式调用自定义算子库 API(核心)
uint64_t workspaceSize = 0; aclOpExecutor *executor; // 第一段:计算workspace大小 ret = aclnnReduceSumCustomGetWorkspaceSize(inputX, dimOut, outputY, &workspaceSize, &executor); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclnnReduceSumCustomGetWorkspaceSize failed. ERROR: %d\n", ret); ...); void *workspaceAddr = nullptr; if (workspaceSize > 0U) { ret = aclrtMalloc(&workspaceAddr, workspaceSize, ACL_MEM_MALLOC_HUGE_FIRST); // 按workspaceSize申请Device内存 CHECK_RET(ret == ACL_SUCCESS, ...); } // 第二段:执行算子 ret = aclnnReduceSumCustom(workspaceAddr, workspaceSize, executor, stream); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclnnReduceSumCustom failed. ERROR: %d\n", ret); ...);这里完整复现了前面介绍的两段式接口调用规范:先取 workspace 大小,再按需aclrtMalloc(注意当workspaceSize == 0时无需申请),最后传入 workspace、executor 与 stream 执行算子。这段代码即为单算子 API 调用的标准模板,开发者只需按自己算子的接口签名调整张量参数即可复用。
3.5 阶段四、五、六:同步、取回结果与资源释放
// 4. 同步等待任务完成 ret = aclrtSynchronizeStream(stream); CHECK_RET(ret == ACL_SUCCESS, ...); // 5. 结果从Device内存拷回Host auto size = GetShapeSize(outputYShape); std::vector<float> resultData(size, 0); ret = aclrtMemcpy(resultData.data(), resultData.size() * sizeof(resultData[0]), outputYDeviceAddr, size * sizeof(float), ACL_MEMCPY_DEVICE_TO_HOST); CHECK_RET(ret == ACL_SUCCESS, ...); // 6. 释放资源 DestroyResources(tensors, deviceAddrs, stream, deviceId, workspaceAddr);aclrtSynchronizeStream阻塞等待 stream 上的算子任务执行完成,是异步计算模型下获取结果的必要同步点;- 随后用
ACL_MEMCPY_DEVICE_TO_HOST方向把输出拷回 Host 内存; DestroyResources统一释放aclTensor(aclDestroyTensor)、Device 内存(aclrtFree,含 workspace)、销毁 stream、重置设备并aclFinalize,避免资源泄漏。
最后调用VerifyResults(goldenData, resultData)校验结果:全部元素为8.0时输出test pass并返回 0,否则输出test failed并返回 -1。
四、编译与运行
4.1 前置准备:编译部署 ReduceSumCustom 算子
运行本样例前,需要先完成算子的编译与部署,完整步骤参见 examples/ops_aclnn/reduce_sum/README.md,核心过程为:
导入 ATVC 环境变量(不导入则默认使用
./atvc/include路径):export ATVC_PATH=${atvc}/include调用 install.sh 生成并编译算子工程(
SOC_VERSION支持Ascend910B1/Ascend910B2/Ascend910B3/Ascend910B4,可通过在装有昇腾 AI 处理器的服务器上执行npu-smi info查询,在 "Name" 前增加Ascend前缀得到):cd ./examples/ops_aclnn/reduce_sum bash install.sh -v [SOC_VERSION]脚本内部会调用
msopgen gen -i ReduceSumCustom.json -c ai_core-${SOC_VERSION} -lan cpp -out CustomOp生成算子工程框架,再拷贝 op_host/reduce_sum_custom.cpp 与 op_kernel/reduce_sum_custom.cpp 到工程对应目录(同时删除 msopgen 自动生成的 tiling 头文件,改用 ATVC 自身的 tiling 定义),最后调用CustomOp/build.sh编译。成功后在CustomOp/build_out下生成安装包custom_opp_<target os>_<target architecture>.run(如custom_opp_ubuntu_x86_64.run)。部署自定义算子包:先确认
ASCEND_OPP_PATH环境变量存在(若无则source [ASCEND_INSTALL_PATH]/bin/setenv.bash),然后执行:cd CustomOp/build_out ./custom_opp_<target os>_<target architecture>.run算子包将部署到
ASCEND_OPP_PATH指向的vendors/customize目录中,部署完成后系统即自动生成单算子 APIaclnnReduceSumCustom*及其头文件aclnn_reduce_sum_custom.h。
提示:安装脚本会每次删除并重新生成
CustomOp目录,切勿在生成目录内直接修改算子代码,避免丢失。
4.2 修改编译文件路径
进入样例目录,并将 CMakeLists.txt 内的/usr/local/Ascend/ascend-toolkit/latest替换为 CANN 软件包安装后的实际路径:
cd atvc/examples/ops_aclnn/reduce_sum/AclNNInvocationNaive例如改为eg:/home/HwHiAiUser/Ascend/ascend-toolkit/latest。该路径在 CMake 中作为默认INC_PATH(头文件搜索路径),并进一步推导出自定义算子 API 头文件与库的搜索位置:
set(INC_PATH $ENV{DDK_PATH}) if (NOT DEFINED ENV{DDK_PATH}) set(INC_PATH "/usr/local/Ascend/ascend-toolkit/latest") ... endif() set(CUST_PKG_PATH "${INC_PATH}/opp/vendors/customize/op_api") # 单算子API头文件与lib所在目录 ... include_directories(${INC_PATH}/include ${CUST_PKG_PATH}/include) link_directories(${LIB_PATH} ${CUST_PKG_PATH}/lib)其中LIB_PATH默认指向ascend-toolkit/latest/${arch}-${os}/devlib(stub 动态库仅用于编译期链接),也可通过环境变量NPU_HOST_LIB覆盖。最终生成可执行文件execute_reduce_sum_op,并链接以下库:
| 库名 | 作用 |
|---|---|
ascendcl | ACL 运行时基础库(aclInit/aclrtMalloc/aclCreateTensor等接口) |
cust_opapi | 自定义算子单算子 API 库(包含aclnnReduceSumCustom*接口实现) |
acl_op_compiler | 算子编译与执行支撑库 |
nnopbase | 神经网络算子基础库 |
stdc++ | C++ 标准库 |
从链接库列表可以看出,单算子 API 调用需要同时依赖 ACL 基础库与自定义算子 API 库cust_opapi,这也解释了为何必须先部署自定义算子包、再编译调用程序。
4.3 一键编译运行
参考 run.sh 脚本执行编译与运行:
bash run.shrun.sh内部做了四件事:
- 确定 CANN 安装路径:按
ASCEND_INSTALL_PATH→ASCEND_HOME_PATH→$HOME/Ascend/ascend-toolkit/latest→/usr/local/Ascend/ascend-toolkit/latest的优先级自动探测; - 导入环境:
source $_ASCEND_INSTALL_PATH/bin/setenv.bash,并显式导出DDK_PATH(供 CMake 使用)与NPU_HOST_LIB(指向${arch}-${os}/lib64运行库目录); - 编译:
cmake -B build -DCMAKE_SKIP_RPATH=TRUE后cmake --build build -j,产物输出到工程根目录; - 运行:在
build目录下设置LD_LIBRARY_PATH为$_ASCEND_INSTALL_PATH/opp/vendors/customize/op_api/lib(确保能找到libcust_opapi.so),随后执行./execute_reduce_sum_op。
由于run.sh已自动完成环境变量探测与导出,大多数场景下直接执行bash run.sh即可;仅当 CMake 默认路径与实际安装路径不一致时才需要手工修改 CMakeLists.txt。
4.4 运行结果验证
程序运行后将打印输出前 10 个元素(均为8.0),并输出判定结果:
result is: 8.0 8.0 8.0 8.0 8.0 8.0 8.0 8.0 8.0 8.0 test pass输出test pass即代表ReduceSumCustom算子在8×2048 → 1×2048、dim=0 的规格下正确完成了归约求和,验证了 ATVC 框架开发的算子通过单算子 API 可被正常调用。
五、ATVC 在算子实现侧的配合
AclNNInvocationNaive 之所以能如此简洁地完成调用,离不开 ATVC 对算子 host/kernel 侧的模板化封装。在本样例依赖的 op_host/reduce_sum_custom.cpp 中:
- 通过
ATVC::OpTraits<ATVC::OpInputs<float>, ATVC::OpOutputs<float>>声明编译态算子描述,并在TilingFunc中调用ATVC::Host::CalcReduceTiling<ReduceOpTraitsFloat>(shapeIn, dim, &policy, tiling)自动完成 tiling 计算,随后context->SetBlockDim(tiling->tilingData.coreNum)设置核数; - 归约策略由
ATVC::ReducePolicy自动推导,tiling->policyId = policy.getID()写入运行态参数,供 kernel 侧分支。
在 op_kernel/reduce_sum_custom.cpp 中,核函数依据param.policyId选择不同的ATVC::Kernel::ReduceOpTemplate<ATVC::ReduceSumCompute<ReduceOpTraits>, ATVC::REDUCE_POLICYx>模板实例执行。这里没有使用TILING_KEY_IS做分支判断,而是用policyId运行时判断,是因为 Reduce 的 policy 分支较多,使用tilingKey判断存在爆栈风险。
由此可见,ATVC 把 tiling 计算、数据搬运、核间调度等固定逻辑封装在模板内部,开发者只需聚焦归约计算本身(ReduceSumCompute),即可快速产出可被 aclnn 单算子 API 调用的高质量自定义算子。更多 ATVC 的模板能力(Elementwise/Reduce/Broadcast/Pool 及CalcReduceTiling、ReduceOpTemplate等接口)可参考 docs/02_developer_guide.md 与 docs/01_quick_start.md。
六、小结
AclNNInvocationNaive 样例以最小化工程呈现了 ATVC 自定义算子的单算子 API 调用全流程:先通过两段式接口(GetWorkspaceSize + Execute)在宿主程序中驱动算子在 Device 上执行,再以同步、拷回、校验三个收尾步骤完成闭环验证。其工程结构清晰、代码可直接套用,是开发者快速验证"基于 ATVC 开发的 Vector 算子是否可被上层应用正确调用"的首选模板——只需替换输入输出张量、attr 参数与接口名称,即可推广到其他 ATVC 算子(如 Add、Broadcast 等,参见 examples/ops_aclnn/README.md)。
【免费下载链接】atvcATVC(Ascend C Templates for Vector Compute),是为基于Ascend C开发的典型Vector算子封装的一系列模板头文件的集合,可帮助用户快速开发典型Vector算子。项目地址: https://gitcode.com/cann/atvc
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考