Kernel Launch算子工程

为了帮助开发者快速的完成算子的Kernel Launch,方便开发者调试调优,提供简易的算子工程,您可以基于该算子工程中的样例代码和工程框架进行算子开发。算子工程提供的功能如下:

环境准备

工程目录

算子工程样例所在路径为:CANN软件安装后文件存储路径中的“tools/ascendc_kernel_sample”。

样例目录结构如下所示:

ascendc_kernel_sample
├── CMakeLists.txt                                      // CMake编译配置文件
├── main.cpp                                            // NPU域算子调用应用程序
├── add_custom.cpp                                      // 矢量算子kernel实现
├── matmul_custom.cpp                                   // 矩阵算子kernel实现
├── matmul_leakyrelu_custom.cpp                         // 矢量+矩阵融合算子kernel实现
└── run.sh                                              // 编译运行算子的脚本

基于该算子工程,开发者进行算子开发的步骤如下:

算子kernel侧实现

请参考矢量编程和工程目录中的矩阵算子、融合算子的kernel实现完成Ascend C算子实现文件的编写。

算子调用应用程序

下面代码以固定shape的add_custom算子为例,介绍算子核函数调用的应用程序main.cpp如何编写。您在实现自己的应用程序时,需要关注由于算子核函数不同带来的修改,包括算子核函数名,入参出参的不同等,合理安排相应的内存分配、内存拷贝和文件读写等,相关API的调用方式直接复用即可。

  1. 按需包含头文件,需要注意的是,需要包含对应的核函数调用接口声明所在的头文件alcrtlaunch_{kernel_name}.h(该头文件为工程框架自动生成),kernelname为算子核函数的名称。

    #include <stdio.h>
    #include <string.h>
    #include <securec.h>
    #include "acl/acl.h"
    #include "aclrtlaunch_add_custom.h" 

  2. 算子调用程序编写,具体步骤如下:

    图1 算子调用程序编写步骤
    int32_t main(void)
    {
        constexpr int32_t loopValue = 16;
        constexpr uint32_t blockDim = 8;
        constexpr uint32_t rowDim = 1024;
        constexpr uint32_t colDim = 2048;
        size_t inputByteSize = rowDim * colDim * sizeof(aclFloat16);
        size_t outputByteSize = rowDim * colDim * sizeof(aclFloat16);
        aclFloat16 duplicateValue = aclFloatToFloat16(4.0f);
        // AscendCL初始化
        CHECK_ACL(aclInit(nullptr));
        // 运行管理资源申请
        aclrtContext context;
        int32_t deviceId = 0;
        CHECK_ACL(aclrtSetDevice(deviceId));
        CHECK_ACL(aclrtCreateContext(&context, deviceId));
        aclrtStream stream = nullptr;
        CHECK_ACL(aclrtCreateStream(&stream));
        // 分配Host内存
        uint8_t *xHost, *yHost, *zHost;
        uint8_t *xDevice, *yDevice, *zDevice;
        CHECK_ACL(aclrtMallocHost((void**)(&xHost), inputByteSize));
        CHECK_ACL(aclrtMallocHost((void**)(&yHost), inputByteSize));
        CHECK_ACL(aclrtMallocHost((void**)(&zHost), outputByteSize));
        // 分配Device内存
        CHECK_ACL(aclrtMalloc((void**)&xDevice, inputByteSize, ACL_MEM_MALLOC_HUGE_FIRST));
        CHECK_ACL(aclrtMalloc((void**)&yDevice, inputByteSize, ACL_MEM_MALLOC_HUGE_FIRST));
        CHECK_ACL(aclrtMalloc((void**)&zDevice, outputByteSize, ACL_MEM_MALLOC_HUGE_FIRST));
        // Host内存初始化
        for (int i = 0; i < rowDim * colDim; ++i) {
            xHost[i] = duplicateValue;
            yHost[i] = duplicateValue;
        }
        // 将数据从Host上拷贝到Device上
        CHECK_ACL(aclrtMemcpy(xDevice, inputByteSize, xHost, inputByteSize, ACL_MEMCPY_HOST_TO_DEVICE));
        CHECK_ACL(aclrtMemcpy(yDevice, inputByteSize, yHost, inputByteSize, ACL_MEMCPY_HOST_TO_DEVICE));
        // 用内核调用符ACLRT_LAUNCH_KERNEL调用核函数完成指定的运算
        CHECK_ACL(ACLRT_LAUNCH_KERNEL(add_custom)(blockDim, stream, xDevice, yDevice, zDevice));
        CHECK_ACL(aclrtSynchronizeStream(stream));
        // 将Device上的运算结果拷贝回Host
        CHECK_ACL(aclrtMemcpy(zHost, outputByteSize, zDevice, outputByteSize, ACL_MEMCPY_DEVICE_TO_HOST));
        // 打印数据结果,作为示例只打印前16个数
        printf("output of add_custom:\n");
        for (int i = 0; i < loopValue; i++) {
            printf("%f ", aclFloat16ToFloat(zHost[i]));
        }
        // 释放申请的资源
        CHECK_ACL(aclrtFree(xDevice));
        CHECK_ACL(aclrtFree(yDevice));
        CHECK_ACL(aclrtFree(zDevice));
        CHECK_ACL(aclrtFreeHost(xHost));
        CHECK_ACL(aclrtFreeHost(yHost));
        CHECK_ACL(aclrtFreeHost(zHost));
        // AscendCL去初始化
        CHECK_ACL(aclrtDestroyStream(stream));
        CHECK_ACL(aclrtDestroyContext(context));
        CHECK_ACL(aclrtResetDevice(deviceId));
        CHECK_ACL(aclFinalize());
        return 0;
    }

CMake编译配置文件编写

CMake编译配置文件的样例如下,开发者可基于该样例修改环境变量和Cmake命令参数的配置,配置参数的具体说明请参考表1表2

cmake_minimum_required(VERSION 3.16.0)
project(Ascend_C)

# user-defined configuration
// 配置AI处理器型号,请配置为实际的AI处理器型号
set(SOC_VERSION "ascendxxx" CACHE STRING "system on chip type")
// 配置CANN软件包安装后的实际路径
set(ASCEND_CANN_PACKAGE_PATH "/usr/local/Ascend/ascend-toolkit/latest/" CACHE PATH "ASCEND CANN package installation directory")
// 配置编译模式,"Release"或者"Debug"
set(CMAKE_BUILD_TYPE "Debug" CACHE STRING "Build type Release/Debug (default Debug)" FORCE) 
// 配置CMAKE执行install时,安装的路径前缀                
set(CMAKE_INSTALL_PREFIX "${CMAKE_CURRENT_LIST_DIR}/out" CACHE STRING "path for install()" FORCE) 
if(EXISTS ${ASCEND_CANN_PACKAGE_PATH}/compiler/tikcpp/ascendc_kernel_cmake)
    set(ASCENDC_CMAKE_DIR ${ASCEND_CANN_PACKAGE_PATH}/compiler/tikcpp/ascendc_kernel_cmake)
else()
    message(FATAL_ERROR "ascendc_kernel_cmake does not exist, please check whether the compiler package is installed.")
endif()
// 包含内部的编译文件
include(${ASCENDC_CMAKE_DIR}/ascendc.cmake)                                                                        
// 按需添加kernel实现文件
ascendc_library(kernels STATIC
    add_custom.cpp
)
// 配置编译宏,按需使能PRINTF/DumpTensor功能
ascendc_compile_definitions(kernels PRIVATE -DASCENDC_DUMP)
// 请替换为算子调用源文件 
add_executable(main main.cpp)                                                                                      
target_link_libraries(main PRIVATE
  kernels
)
表1 环境变量说明

环境变量

配置说明

SOC_VERSION

AI处理器的型号。

在安装昇腾AI处理器的服务器执行npu-smi info命令进行查询,查询到的“Name”前增加Ascend信息,例如“Name”对应取值为xxxyy,实际配置的SOC_VERSION值为Ascendxxxyy

ASCEND_CANN_PACKAGE_PATH

CANN软件包安装后的实际路径。

CMAKE_BUILD_TYPE

编译模式选项,可配置为:

  • “Release”,Release版本,不包含调试信息,编译最终发布的版本。
  • “Debug”,Debug版本,包含调试信息,便于开发者开发和调试。

CMAKE_INSTALL_PREFIX

用于指定CMAKE执行install时,安装的路径前缀,执行install后编译产物(ascendc_library中指定的target以及对应的头文件)会安装在该路径下。默认路径为当前目录的out目录下。

表2 Cmake命令参数说明

环境变量

配置说明

add_executable

使用指定的源文件将可执行文件添加到项目中。和Cmake通用的命令参数使用方法一致。

ascendc_library

使用指定的核函数源文件向项目(project)添加库。语法格式如下:

ascendc_library(<target_name> [STATIC | SHARED]
            [<source>...]) 

其中<target_name>表示库文件的名字,该库文件会根据命令里列出的源文件来建立。STATIC、SHARED的做用是指定生成的库文件的类型。STATIC库是目标文件的归档文件,在连接其它目标的时候使用。SHARED库会被动态连接(动态连接库),在运行时会被加载。<source>表示核函数源文件。

ascendc_compile_definitions

添加编译宏。可以添加Ascend C提供的编译宏和开发者自定义的编译宏。语法格式如下:

ascendc_compile_definitions(<target_name> [PRIVATE]
            [<xxx>...]) 

Ascend C提供的编译宏介绍如下:

-DASCENDC_DUMP :使用PRINTF或DumpTensor功能时需要添加该编译宏。

简化的编译流程图如下图所示:将算子核函数源文件编译生成kernel侧的库文件(*.so或*.a库文件);工程框架自动生成核函数调用接口声明头文件;编译main.cpp(算子调用应用程序)时依赖上述头文件,将编译应用程序生成的目标文件和kernel侧的库文件进行链接,生成最终的可执行文件。

图2 编译简化流程图

编译安装结束后在CMAKE_INSTALL_PREFIX目录下生成的编译产物示例如下;最终的可执行文件会生成在cmake命令的执行目录下。

out
├── lib 
│   ├── libkernels1.a
│   ├── libkernels2.so
├── include
│   ├── kernels1
│           ├── aclrtlaunch_matmul_custom.h
│           ├── aclrtlaunch_add_custom.h
│   ├── kernels2
│           ├── aclrtlaunch_xxx.h
│           ├── ...

修改并执行一键式编译运行脚本

您可以基于样例工程中提供的一键式编译运行脚本进行快速编译,在NPU侧执行Ascend C算子。同时可以按需完成性能数据的采集和分析。run.sh的代码如下:

set -e
rm -rf build out 
mkdir build
cmake -B build
cmake --build build -j
cmake --install build
#执行可执行文件,请替换为实际的应用程序二进制名称
./build/main 
#使能profiling功能
msprof --ai-core=on --ascendcl=on --model-execution=on --runtime-api=on --task-time=on --application="./build/main" 
完成上述文件的编写后,可以执行一键式编译运行脚本,编译和运行应用程序。脚本执行方式如下:
bash run.sh

运行脚本后可以看到结果输出示例如下:

output of add_custom:
8.000000 8.000000 8.000000 8.000000 8.000000 8.000000 8.000000 8.000000 8.000000 8.000000 8.000000 8.000000 8.000000 8.000000 8.000000 8.000000