CANN内置算子调用
基本介绍
CANN算子库提供了一系列丰富且经过深度优化、硬件亲和的高性能内置算子,可直接应用于AI业务。算子包括Device侧Kernel实现和Host侧调用两部分,为方便调用,每个算子都提供了相应的C语言API,方便用户在Host侧调用,下文简称单算子API(或者算子Host API)。
1 2 |
aclnnStatus aclnnXxxGetWorkspaceSize(const aclTensor *src, ..., aclTensor *out, ..., uint64_t *workspaceSize, aclOpExecutor **executor) aclnnStatus aclnnXxx(void *workspace, uint64_t workspaceSize, aclOpExecutor *executor, aclrtStream stream) |
其中“Xxx”表示对应的算子类型,如Add算子。两段式接口具体作用如下:
- 第一段接口aclnnXxxGetWorkspaceSize:完成入参校验、动态Shape场景下推导输出Shape、数据切块(Tiling)以及计算执行过程中算子所需的workspace内存大小等任务。
- 第二段接口aclnnXxx:executor执行器执行计算,框架自动调用DFX(例如Dump、溢出检测等)、Runtime提供的LaunchKernel接口等。
算子分类
CANN算子主要包含如下几类:
- Math类算子:数值计算类算子,提供不同维度的数值处理与计算算子,例如Add、Abs等,覆盖张量形态变换、基础数学运算、随机数生成等场景。
- NN类算子:神经网络类算子(Neural Network),提供深度学习模型中常见的计算算子,例如卷积、矩阵乘、激活函数、归一化等。
- CV类算子:计算机视觉类算子(Computer Vision),提供图像处理和目标检测算子,例如GridSample等。
- Transformer类算子:大模型计算类算子,提供Transformer核心算子,例如Attention类、LayerNorm类、通算融合类(简称MC2)等。
对于集合通信和MatMul计算融合、并行的算子,统称为通算融合算子(简称MC2算子),如AllGatherMatmul、AlltoAllAllGatherBatchMatMul、BatchMatMulReduceScatterAlltoAll、MatMulAllReduce、MatMulAllReduceAddRmsNorm、MatMulReduceScatter等。
关于算子IR(Intermediate Representation)规格介绍,请参见《算子库》中“Ascend IR算子规格说明”;关于算子对应的API文档介绍,请参见《算子库》中。
调用算子API时,请include依赖必要的头文件和库文件,其中${INSTALL_DIR}请替换为CANN软件安装后文件存储路径。以root用户安装为例,安装后文件默认存储路径为:/usr/local/Ascend/cann。
- 头文件路径:一般默认在“${INSTALL_DIR}/include/”目录,可引用每类算子总头文件aclnnop/aclnn_ops_math.h、aclnnop/aclnn_ops_nn.h、aclnnop/aclnn_ops_cv.h、aclnnop/aclnn_ops_transformer.h;也可以引用单个算子头文件
- 库文件路径:一般默认在“${INSTALL_DIR}/lib64/”目录,可引用每类算子总库文件libopapi_math.so、libopapi_nn.so、libopapi_cv.so、libopapi_transformer.so。注意,从CANN 8.5.0版本开始全量算子总库文件libopapi.so要废弃,请勿使用。
单算子API调用流程
-
运行时资源申请:调用aclrtSetDevice、aclrtCreateStream等接口,依次申请运行时资源即Device、Context、Stream。
- 数据内存申请和传输。
- 调用aclrtMalloc接口申请Device上的内存,存放待执行算子的输入、输出数据。
- 调用aclCreateTensor、aclCreateIntArray等构造算子输入、输出参数,相关接口详见《算子库》中“公共接口”。
如果需要将Host上数据传输到Device,则需要调用aclrtMemcpy接口(同步接口)或aclrtMemcpyAsync接口(异步接口)通过内存复制的方式实现数据传输。
- 计算workspace并执行算子。
- 调用aclxxXxxGetWorkspaceSize接口获取算子入参,并计算该算子执行时需要的workspace大小。
- 调用aclrtMalloc接口,根据workspace大小申请Device侧内存。
- 调用aclxxXxx接口执行计算并得到结果。
- 调用aclrtSynchronizeStream接口阻塞应用运行,直到指定Stream中的所有任务都完成。
- 调用aclrtFree接口释放内存。
如果需要将Device上的算子执行结果数据传输到Host,则需要调用aclrtMemcpy接口(同步接口)或aclrtMemcpyAsync接口(异步接口)通过内存复制的方式实现数据传输,然后再释放内存。
- 运行时资源释放。
- 去初始化:调用aclFinalize接口实现去初始化。
示例代码
注意,算子调用必须在支持的产品型号上进行编译运行,否则会调用失败。
以Add算子API调用为例,介绍算子两段式接口调用的基本逻辑,其他算子调用过程类似,请根据实际情况自行修改。
已知Add算子实现了张量加法运算,计算公式为:y=x1+αxx2。您可以获取如下示例代码,并将文件命名为“test_add.cpp”,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 |
#include <iostream> #include <vector> #include "acl/acl.h" #include "aclnnop/aclnn_add.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 main() { // 1. (固定写法)device/stream初始化 // 根据自己的实际device填写deviceId int32_t deviceId = 0; aclrtStream stream; auto ret = Init(deviceId, &stream); // check根据自己的需要处理 CHECK_RET(ret == 0, LOG_PRINT("Init acl failed. ERROR: %d\n", ret); return ret); // 2. 构造输入与输出,需要根据API的接口自定义构造 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; aclScalar* alpha = nullptr; aclTensor* out = nullptr; std::vector<float> selfHostData = {0, 1, 2, 3, 4, 5, 6, 7}; std::vector<float> otherHostData = {1, 1, 1, 2, 2, 2, 3, 3}; std::vector<float> outHostData = {0, 0, 0, 0, 0, 0, 0, 0}; float alphaValue = 1.2f; // 创建self aclTensor ret = CreateAclTensor(selfHostData, selfShape, &selfDeviceAddr, aclDataType::ACL_FLOAT, &self); CHECK_RET(ret == ACL_SUCCESS, return ret); // 创建other aclTensor ret = CreateAclTensor(otherHostData, otherShape, &otherDeviceAddr, aclDataType::ACL_FLOAT, &other); CHECK_RET(ret == ACL_SUCCESS, return ret); // 创建alpha aclScalar alpha = aclCreateScalar(&alphaValue, aclDataType::ACL_FLOAT); CHECK_RET(alpha != nullptr, return ret); // 创建out aclTensor 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; // 调用aclnnAdd第一段接口 ret = aclnnAddGetWorkspaceSize(self, other, alpha, out, &workspaceSize, &executor); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclnnAddGetWorkspaceSize 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;); } // 调用aclnnAdd第二段接口 ret = aclnnAdd(workspaceAddr, workspaceSize, executor, stream); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclnnAdd 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侧,需要根据具体API的接口定义修改 auto size = GetShapeSize(outShape); std::vector<float> resultData(size, 0); ret = aclrtMemcpy(resultData.data(), resultData.size() * sizeof(resultData[0]), outDeviceAddr, size * sizeof(float), 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,需要根据具体API的接口定义修改 aclDestroyTensor(self); aclDestroyTensor(other); aclDestroyScalar(alpha); aclDestroyTensor(out); // 7. 释放device资源,需要根据具体API的接口定义修改 aclrtFree(selfDeviceAddr); aclrtFree(otherDeviceAddr); aclrtFree(outDeviceAddr); if (workspaceSize > 0) { aclrtFree(workspaceAddr); } aclrtDestroyStream(stream); aclrtResetDevice(deviceId); aclFinalize(); return 0; } |
CMakeLists文件
以Add算子编译为例,CMake文件定义如下,请根据实际情况自行修改。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
# CMake lowest version requirement cmake_minimum_required(VERSION 3.14) # 设置工程名 project(ACLNN_EXAMPLE) # Compile options 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") # 设置可执行文件名(如opapi_test),并指定待运行算子文件*.cpp所在目录 add_executable(opapi_test test_add.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_${ops_project}.so) # 可执行文件在CMakeLists文件所在目录的bin目录下 install(TARGETS opapi_test DESTINATION ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}) |
其中${ops_project}表示不同类型算子库,请按需引用:
- Math类算子:引用对应libopapi_math.so。
- NN类算子:引用libopapi_math.so和libopapi_nn.so。
- CV类算子:引用libopapi_math.so和libopapi_cv.so。
- Transformer类算子:引用libopapi_math.so和libopapi_transformer.so。
调用MC2算子API时,一般会涉及多线程和HCCL(Huawei Collective Communication Library,集合通信库),因此CMake文件需要额外导入如下内容,否则无法成功编译。
1 2 3 4 5 6 7 8 |
# 设置链接的库文件路径 find_package(Threads REQUIRED) target_link_libraries(opapi_test PRIVATE ${ASCEND_PATH}/lib64/libascendcl.so ${ASCEND_PATH}/lib64/libnnopbase.so ${ASCEND_PATH}/lib64/libopapi_${ops_project}.so ${ASCEND_PATH}/lib64/libhccl.so # 集合通信库文件 ${CMAKE_THREAD_LIBS_INIT}) # 多线程依赖的库文件 |
“find_package(Threads REQUIRED)”是CMake用于查找线程库的命令,可自动链接线程库依赖的头文件和间接依赖的其它库文件。
编译与运行
本节以开发和运行环境合设场景为例,即带AI处理器的机器既作为开发环境又作为运行环境。该场景下,代码开发与代码运行在同一台机器上。
- 根据前文提供的示例代码、CMakeLists文件,提前准备好算子的调用代码(*.cpp)和编译脚本(CMakeLists.txt)。
- 配置环境变量。
安装CANN软件后,使用CANN运行用户登录环境,执行如下命令生效环境变量。
source /usr/local/Ascend/cann/set_env.sh - 编译并运行。
- 进入CMakeLists.txt所在目录,执行如下命令,新建build目录存放生成的编译文件。
mkdir -p build
- 进入build所在目录,执行cmake命令编译,再执行make命令生成可执行文件。
1 2 3
cd build cmake ../ -DCMAKE_CXX_COMPILER=g++ -DCMAKE_SKIP_RPATH=TRUE make
编译成功后,会在build目录的bin文件夹下生成opapi_test可执行文件。
- 进入bin目录,运行可执行文件opapi_test。
cd bin ./opapi_test
以Add算子的运行结果为例,运行后的结果如下:

- 进入CMakeLists.txt所在目录,执行如下命令,新建build目录存放生成的编译文件。