本文分享自华为云社区《昇腾Ascend C编程入门教程》,作者:昇腾CANN 。

2023年5月6日,在昇腾AI开发者峰会上,华为正式发布了面向算子开发场景的昇腾Ascend C编程语言。Ascend C原生支持C/C++编程规范,通过多层接口抽象、并行编程范式、孪生调试等技术,极大提高了算子的开发效率,帮助AI开发者低成本完成算子开发和模型调优部署。

昇腾AI软硬件基础

和CUDA开发的算子运行在GPU上一样,基于Ascend C开发的算子,可以通过异构计算架构CANN(Compute Architecture for Neural Networks)运行在昇腾AI处理器(可简称NPU)上。CANN是使能昇腾AI处理器的一个软件栈,通过软硬件协同优化,能够充分发挥昇腾AI处理器的强大算力。从下面的架构图可以清楚的看到,使用Ascend C编程语言开发的算子通过编译器编译和运行时调度,最终运行在昇腾AI处理器上。

我们知道,通用计算就是我们常写的一些在CPU上运行的计算,它擅长逻辑控制和串行计算,而AI计算相对通用计算来说,更擅长并行计算,可支持大规模的计算密集型任务。如下面左图所示,做一个矩阵乘,使用CPU计算需要三层for循环,而右图在昇腾AI处理器上使用vector计算单元,只需要两层for循环,最小计算代码能同时计算多个数据的乘加,更近一步,如果使用Cube计算单元,只需要一条语句就能完成一个矩阵乘的计算,这就是我们所说的SIMD(单指令多数据)。因此,我们通常使用AI处理器来进行大量的并行计算。

NPU不能独立运行,需要与CPU协同工作,可以看成是CPU的协处理器,CPU负责整个操作系统运行,管理各类资源并进行复杂的逻辑控制,而NPU主要负责并行计算任务。在基于CPU+NPU的异构计算架构中,NPU与CPU通过PCIe总线连接在一起来协同工作,CPU所在位置称为主机端(host),而NPU所在位置称为设备端(device),示意图如下:

这里再详细介绍一下昇腾AI处理器。昇腾AI处理器有不同的型号和产品形态,小到模块、加速卡,大到服务器、集群。昇腾AI处理器里面最核心的部件是AI Core,有多个,是神经网络加速的计算核心,每一个AI Core就相当于我们大家平时理解的多核cpu里的每个核,使用Ascend C编程语言开发的算子就运行在AI Core上,因为核心的神经网络计算的加速都来源于AI Core的算力。

AI Core内部的并行计算架构抽象如下图所示:

这个并行计算架构抽象核心包含了几个大的部件,AI Core外面有一个Gobal Memory,是多个AI Core共享的,在AI Core内部有一块本地内存Local Memory,因为靠近计算单元,所以它的带宽会非常高,相对的容量就会很小,比如一般是几百K到1M。AI Core内部的核心组件有三个计算单元,标量计算单元、向量计算单元,矩阵计算单元。另外还有一个DMA搬运单元,DMA搬运单元负责在Global Memory和Local Memory之间搬运数据。

AI Core内部的异步并行计算过程:Scalar计算单元读取指令序列,并把向量计算、矩阵计算、数据搬运指令发射给对应单元的指令队列,向量计算单元、矩阵计算单元、数据搬运单元异步并行执行接收到的指令。该过程可以参考上图中蓝色箭头所示的指令流。不同的指令间有可能存在依赖关系,为了保证不同指令队列间的指令按照正确的逻辑关系执行,Scalar计算单元也会给对应单元下发同步指令。各单元之间的同步过程可以参考上图中的橙色箭头所示的同步信号流。

AI Core内部数据处理的基本过程:DMA搬入单元把数据搬运到Local Memory,Vector/Cube计算单元完成数据,并把计算结果写回Local Memory,DMA搬出单元把处理好的数据搬运回Global Memory。该过程可以参考上图中的红色箭头所示的数据流。

Ascend C编程模型基础

Ascend C编程范式

Ascend C编程范式是一种流水线式的编程范式,把算子核内的处理程序,分成多个流水任务,通过队列(Queue)完成任务间通信和同步,并通过统一的内存管理模块(Pipe)管理任务间通信内存。流水编程范式应用了流水线并行计算方法。

若n=3,即待处理的数据被切分成3片,则上图中的流水任务运行起来的示意图如下,从运行图中可以看出,对于同一片数据,Stage1、Stage2、Stage3之间的处理具有依赖关系,需要串行处理;不同的数据切片,同一时间点,可以有多个任务在并行处理,由此达到任务并行、提升性能的目的。

Ascend C分别针对Vector、Cube编程设计了不同的流水任务。开发者只需要完成基本任务的代码实现即可,底层的指令同步和并行调度由Ascend C框架实现,开发者无需关注。

矢量编程范式

矢量编程范式把算子的实现流程分为3个基本任务:CopyIn,Compute,CopyOut。CopyIn负责搬入操作,Compute负责矢量计算操作,CopyOut负责搬出操作。

我们只需要根据编程范式完成基本任务的代码实现就可以了,底层的指令同步和并行调度由Ascend C框架来实现。

那Ascend C是怎么完成不同任务之间的数据通信和同步的呢?这里Ascend C提供了Queue队列管理的API,主要就是两个队列操作API EnQue、DeQue以及内存的逻辑抽象。

矢量编程中使用到的逻辑位置(QuePosition)定义如下:

  • 搬入数据的存放位置:VECIN;
  • 计算中间变量的位置:VECCALC;
  • 搬出数据的存放位置:VECOUT。

从前面可以看到,矢量编程主要分为CopyIn、Compute、CopyOut三个任务。CopyIn任务中将输入数据从Global内存搬运至Local内存后,需要使用EnQue将LocalTensor放入VECIN的Queue中;Compute任务等待VECIN的Queue中LocalTensor出队之后才可以完成矢量计算,计算完成后使用EnQue将计算结果LocalTensor放入到VECOUT的Queue中;CopyOut任务等待VECOUT的Queue中LocalTensor出队,再将其拷贝到Global内存。这样 ,Queue队列就完成了三个任务间的数据通信和同步。具体流程和流程图如下:

  • Stage1:CopyIn任务。

使用DataCopy接口将GlobalTensor数据拷贝到LocalTensor。

使用EnQue接口将LocalTensor放入VECIN的Queue中。

  • Stage2:Compute任务。

使用DeQue接口从VECIN中取出LocalTensor。

使用Ascend C接口完成矢量计算。

使用EnQue接口将计算结果LocalTensor放入到VECOUT的Queue中。

  • Stage3:CopyOut任务。

使用DeQue接口从VECOUT的Queue中去除LocalTensor。

使用DataCopy接口将LocalTensor拷贝到GlobalTensor上。

这样我们的kernel实现代码就很清晰了。先初始化内存和队列,然后通过编程范式实现CopyIn、Compute、CopyOut三个Stage就可以了。

SPMD并行编程-多核

最前面介绍昇腾AI处理器的时候,有介绍过AI Core是有多个的,那我们怎么把多个AI Core充分利用起来呢?常用的并行计算方法中,有一种SPMD(Single-Program Multiple-Data)数据并行的方法,简单说就是将数据分片,每片数据经过完整的一个数据处理流程。这个就能和昇腾AI处理器的多核匹配上了,我们将数据分成多份,每份数据的处理运行在一个核上,这样每份数据并行处理完成,整个数据也就处理完了。Ascend C是SPMD(Single-Program Multiple-Data)编程,多个AI Core共享相同的指令代码,每个核上的运行实例唯一的区别是就是block_idx(内置变量)不同,这样我们就可以通过block_idx来区分不同的核,只要对Global Memory上的数据地址进行切分偏移,就可以让每个核处理自己对应的那部分数据了。

算子被调用时,所有的计算核心都执行相同的实现代码,入口函数的入参也是相同的。每个核上处理的数据地址需要在起始地址上增加block_idx*BLOCK_LENGTH(每个block处理的数据长度)的偏移来获取。这样也就实现了多核并行计算的数据切分。

  1. class KernelAdd {
  2.  
  3. public:
  4.  
  5. __aicore__ inline KernelAdd() {}
  6.  
  7. __aicore__ inline void Init(GM_ADDR x, GM_ADDR y, GM_ADDR z)
  8.  
  9. {
  10.  
  11. // get start index for current core, core parallel
  12.  
  13. GM_ADDR xGmOffset = x + BLOCK_LENGTH * GetBlockIdx();
  14.  
  15. GM_ADDR yGmOffset = y + BLOCK_LENGTH * GetBlockIdx();
  16.  
  17. GM_ADDR zGmOffset = z + BLOCK_LENGTH * GetBlockIdx();
  18.  
  19. xGm.SetGlobalBuffer((__gm__ half*)xGmOffset, BLOCK_LENGTH);
  20.  
  21. yGm.SetGlobalBuffer((__gm__ half*)yGmOffset, BLOCK_LENGTH);
  22.  
  23. zGm.SetGlobalBuffer((__gm__ half*)zGmOffset, BLOCK_LENGTH);
  24.  
  25. ……
  26.  
  27. }
  28.  
  29. ……
  30.  
  31. }

Ascend C API介绍

在整个kernel实现中,最最核心的代码就是Add(zLocal, xLocal, yLocal, TILE_LENGTH);通过一个Ascend C提供的API接口完成了所有数据的加法计算,对,没看错,就是这个接口完成了计算。

接下来就介绍下Ascend C提供的API。Ascend C算子采用标准C++语法和一组类库API进行编程,类库API主要包含以下几种,大家可以在核函数的实现中根据自己的需求选择合适的API:

  • 计算类API,包括标量计算API、向量计算API、矩阵计算API,分别实现调用Scalar计算单元、Vector计算单元、Cube计算单元执行计算的功能。
  • 数据搬运API,上述计算API基于Local Memory数据进行计算,所以数据需要先从Global Memory搬运至Local Memory,再使用计算接口完成计算,最后从Local Memory搬出至Global Memory。执行搬运过程的接口称之为数据搬移接口,比如DataCopy接口。
  • 内存管理API,用于分配管理内存,比如AllocTensor、FreeTensor接口。
  • 任务同步API,完成任务间的通信和同步,比如EnQue、DeQue接口。

Ascend C API的计算操作数都是Tensor类型:GlobalTensor和LocalTensor。

介绍完Ascend C API种类后,下面来解释下为什么一个Add接口就可以计算所有的数。原来Ascend C编程模型是基于SIMD(单指令多数据)架构的,单条指令可以完成多个数据操作,同时在API内部封装了一些指令的高级功能。

算子执行基本流程

前面有提到,在异构计算架构中,NPU与CPU是协同工作的,在Ascend C编程模型中,我们需要实现NPU侧的代码和CPU侧的代码。在NPU侧的代码我们通常叫做Kernel实现代码,CPU侧的代码我们一般叫做Host实现代码,一份完整的Ascend C代码,通常包括Host侧实现代码和Kernel侧实现代码。Ascend C算子执行的基本流程如下:

  1. 初始化Device设备;
  2. 创建Context绑定设备;
  3. 分配Host内存,并进行数据初始化;
  4. 分配Device内存,并将数据从Host上拷贝到Device上;
  5. 用内核调用符<<<>>>调用核函数完成指定的运算;
  6. 将Device上的运算结果拷贝回Host;
  7. 释放申请的资源。

核函数介绍

上面的流程中,最重要的一步就是调用核函数来进行并行计算任务。核函数(Kernel Function)是Ascend C算子Device侧实现的入口。在核函数中,需要为在AI核上执行的代码规定要进行的数据访问和计算操作。

  1. extern "C" __global__ __aicore__ void add_custom(__gm__ uint8_t* x, __gm__ uint8_t* y, __gm__ uint8_t* z);

上面这个是一个核函数声明的示例,extern "C"表示核函数按照类C的编译和连接规约来编译和连接,__global__函数类型限定符表示它是一个核函数, __aicore__函数类型限定符表示该核函数在device侧的AI Core上执行。参数列表中的变量类型限定符__gm__,表明该指针变量指向Global Memory上某处内存地址,注意这里的入参只能支持指针或C/C++内置数据类型,样例里指针使用的类型为uint8_t,在后续的使用中需要将其转化为实际的指针类型。

Ascend C编程模型中的核函数采用内核调用符<<<...>>>来调用,样例如下:

  1. kernel_name<<<blockDim, l2ctrl, stream>>>(argument list);

kernel_name即为上面讲的核函数名称,argument list是核函数的函数入参,在<<<>>>中间,有3个参数:

  • blockDim,规定了核函数将会在几个核上执行,我们可以先设置为1;
  • l2ctrl,保留参数,暂时设置为固定值nullptr,我们不用关注;
  • stream,使用aclrtCreateStream创建,用于多线程调度。

样例开发讲解

样例代码结构

  1. kernel_name<<<blockDim, l2ctrl, stream>>>(argument list);

主要文件

输入数据和真值数据生成脚本文件:KERNEL_NAME.py。

根据算子的输入输出编写生成输入数据和真值数据的脚本。

本例子生成8 * 200 * 1024大小的fp16数据:

  1. ……
  2.  
  3. def gen_golden_data_simple():
  4.  
  5. total_length_imm = 8 * 200 * 1024
  6.  
  7. tile_num_imm = 8
  8.  
  9. //生成tilling的bin文件
  10.  
  11. total_length = np.array(total_length_imm, dtype=np.uint32)
  12.  
  13. tile_num = np.array(tile_num_imm, dtype=np.uint32)
  14.  
  15. scalar = np.array(0.1, dtype=np.float32)
  16.  
  17. tiling = (total_length, tile_num, scalar)
  18.  
  19. tiling_data = b''.join(x.tobytes() for x in tiling)
  20.  
  21. with os.fdopen(os.open('./input/tiling.bin', WRITE_FILE_FLAGS, PEN_FILE_MODES_640), 'wb') as f:
  22.  
  23. f.write(tiling_data)
  24.  
  25. //生成输入数据
  26.  
  27. input_x = np.random.uniform(-100, 100, [8, 200, 1024]).astype(np.float16)
  28.  
  29. //生成golden数据,功能和LeakyRelu相同
  30.  
  31. golden = np.where(input_x > 0, input_x, input_x * scalar).astype(np.float16)
  32.  
  33. input_x.tofile("./input/input_x.bin")
  34.  
  35. golden.tofile("./output/golden.bin")

编译工程文件:CMakeLists.txt

用于编译cpu侧或npu侧运行的Ascend C算子。主要关注CMakeLists.txt中源文件是否全部列全。

调用算子的应用程序:main.cpp

主要是内存申请,数据拷贝和文件读写等操作,并最终调用算子,相关API的介绍如下:

1.AscendCL初始化接口aclInit,用于运行时接口AscendCL的初始化,是程序最先调用的接口;aclrtCreateContext和aclrtCreateStream用于创建Context和Stream,主要用于线程相关的资源管理。

2.aclrtMallocHost接口,用于在Host上申请内存:

aclError aclrtMallocHost(void **hostPtr, size_t size)

这个函数和C语言中的malloc类似,用于在Host上申请一定字节大小的内存,其中hostPtr是指向所分配内存的指针,size是申请的内存大小,如果需要释放这块内存的话,使用aclrtFreeHost接口释放,这和C语言中的free函数对应。

3.aclrtMalloc接口,用于在Device上申请内存:

aclError aclrtMalloc(void **devPtr, size_t size, aclrtMemMallocPolicy policy)

和Host上的内存申请接口相比,多了一个policy参数,用于设置内存分配规则,一般设置成ACL_MEM_MALLOC_HUGE_FIRST就可以了。使用完毕后可以用对应的aclrtFree接口释放内存。

4.aclrtMemcpy接口,用于Host和Device之间数据拷贝:

前面申请的内存区分了Host内存和Device内存,那就会涉及到数据同步的问题,aclrtMemcpy就是用于Host和Device之间数据通信的接口:

aclError aclrtMemcpy(void *dst, size_t destMax, const void *src, size_t count, aclrtMemcpyKind kind)

其中src指向数据源,而dst是目标内存地址,destMax 是目的内存地址的最大内存长度,count是拷贝的字节数,其中aclrtMemcpyKind控制复制的方向:ACL_MEMCPY_HOST_TO_HOST、ACL_MEMCPY_HOST_TO_DEVICE、ACL_MEMCPY_DEVICE_TO_HOST和ACL_MEMCPY_DEVICE_TO_DEVICE,像ACL_MEMCPY_HOST_TO_DEVICE就是将Host上数据拷贝到Device上。

5.核心函数为CPU侧的调用kernel函数

  1. ICPU_RUN_KF(leakyrelu_custom, blockDim, x, y, usrWorkSpace, tiling);

和NPU侧调用的

  1. leakyrelu_custom_do(blockDim, nullptr, stream, xDevice, yDevice, workspaceDevice, tilingDevice);

完整代码如下:

  1. //This file constains code of cpu debug and npu code.We read data from bin file and write result to file.
  2.  
  3. #include "data_utils.h"
  4.  
  5. #include "leakyrelu_custom_tiling.h"
  6.  
  7. #ifndef __CCE_KT_TEST__
  8.  
  9. #include "acl/acl.h"
  10.  
  11. extern void leakyrelu_custom_do(uint32_t coreDim, void* l2ctrl, void* stream, uint8_t* x, uint8_t* y,
  12.  
  13. uint8_t* workspace, uint8_t* tiling);
  14.  
  15. #else
  16.  
  17. #include "tikicpulib.h"
  18.  
  19. extern "C" __global__ __aicore__ void leakyrelu_custom(GM_ADDR x, GM_ADDR y, GM_ADDR workspace, GM_ADDR tiling);
  20.  
  21. #endif
  22.  
  23. int32_t main(int32_t argc, char* argv[])
  24.  
  25. {
  26.  
  27. size_t tilingSize = sizeof(LeakyReluCustomTilingData);
  28.  
  29. size_t usrWorkspaceSize = 4096;
  30.  
  31. size_t sysWorkspaceSize = 16 * 1024 * 1024;
  32.  
  33. uint32_t blockDim = 8;
  34.  
  35. #ifdef __CCE_KT_TEST__ //CPU侧调用
  36.  
  37. //申请内存用于存放workspace和tilling数据
  38.  
  39. uint8_t* usrWorkSpace = (uint8_t*)AscendC::GmAlloc(usrWorkspaceSize);
  40.  
  41. uint8_t* tiling = (uint8_t*)AscendC::GmAlloc(tilingSize);
  42.  
  43. ReadFile("./input/tiling.bin", tilingSize, tiling, tilingSize);
  44.  
  45. size_t inputByteSize = blockDim * 200 * 1024 * sizeof(uint16_t); // uint16_t represent half
  46.  
  47. size_t outputByteSize = blockDim * 200 * 1024 * sizeof(uint16_t); // uint16_t represent half
  48.  
  49. //申请内存用于存放输入和输出数据
  50.  
  51. uint8_t* x = (uint8_t*)AscendC::GmAlloc(inputByteSize);
  52.  
  53. uint8_t* y = (uint8_t*)AscendC::GmAlloc(inputByteSize);
  54.  
  55. //获取输入数据
  56.  
  57. ReadFile("./input/input_x.bin", inputByteSize, x, inputByteSize);
  58.  
  59. // PrintData(x, 16, printDataType::HALF);
  60.  
  61. //在AIV上执行
  62.  
  63. AscendC::SetKernelMode(KernelMode::AIV_MODE);
  64.  
  65. //调用kernel函数
  66.  
  67. ICPU_RUN_KF(leakyrelu_custom, blockDim, x, y, usrWorkSpace, tiling); // use this macro for cpu debug
  68.  
  69. // PrintData(y, 16, printDataType::HALF);
  70.  
  71. WriteFile("./output/output_y.bin", y, outputByteSize);
  72.  
  73. AscendC::GmFree((void *)x);
  74.  
  75. AscendC::GmFree((void *)y);
  76.  
  77. AscendC::GmFree((void *)usrWorkSpace);
  78.  
  79. AscendC::GmFree((void *)tiling);
  80.  
  81. #else //NPU侧调用
  82.  
  83. CHECK_ACL(aclInit(nullptr));
  84.  
  85. aclrtContext context;
  86.  
  87. int32_t deviceId = 0;
  88.  
  89. CHECK_ACL(aclrtSetDevice(deviceId));
  90.  
  91. CHECK_ACL(aclrtCreateContext(&context, deviceId));
  92.  
  93. aclrtStream stream = nullptr;
  94.  
  95. CHECK_ACL(aclrtCreateStream(&stream));
  96.  
  97. uint8_t *xHost, *yHost, *tilingHost, *workspaceHost;
  98.  
  99. uint8_t *xDevice, *yDevice, *tilingDevice, *workspaceDevice;
  100.  
  101. //申请host上tilling内存并读入tilling数据
  102.  
  103. CHECK_ACL(aclrtMallocHost((void**)(&tilingHost), tilingSize));
  104.  
  105. ReadFile("./input/tiling.bin", tilingSize, tilingHost, tilingSize);
  106.  
  107. //申请host上workspace内存
  108.  
  109. CHECK_ACL(aclrtMallocHost((void**)(&workspaceHost), tilingSize));
  110.  
  111. size_t inputByteSize = blockDim * 200 * 1024 * sizeof(uint16_t); // uint16_t represent half
  112.  
  113. size_t outputByteSize = blockDim * 200 * 1024 * sizeof(uint16_t); // uint16_t represent half
  114.  
  115. size_t workspaceByteSize = sysWorkspaceSize + usrWorkspaceSize;
  116.  
  117. //申请host和device上的输入输出内存和device上的workspace和tilling内存
  118.  
  119. CHECK_ACL(aclrtMallocHost((void**)(&xHost), inputByteSize));
  120.  
  121. CHECK_ACL(aclrtMallocHost((void**)(&yHost), inputByteSize));
  122.  
  123. CHECK_ACL(aclrtMallocHost((void**)(&workspaceHost), workspaceByteSize));
  124.  
  125. CHECK_ACL(aclrtMalloc((void**)&xDevice, inputByteSize, ACL_MEM_MALLOC_HUGE_FIRST));
  126.  
  127. CHECK_ACL(aclrtMalloc((void**)&yDevice, inputByteSize, ACL_MEM_MALLOC_HUGE_FIRST));
  128.  
  129. CHECK_ACL(aclrtMalloc((void**)&tilingDevice, tilingSize, ACL_MEM_MALLOC_HUGE_FIRST));
  130.  
  131. CHECK_ACL(aclrtMalloc((void**)&workspaceDevice, workspaceByteSize, ACL_MEM_MALLOC_HUGE_FIRST));
  132.  
  133. ReadFile("./input/input_x.bin", inputByteSize, xHost, inputByteSize);
  134.  
  135. // PrintData(xHost, 16, printDataType::HALF);
  136.  
  137. //从host上拷贝输入数据和tilling数据到device
  138.  
  139. CHECK_ACL(aclrtMemcpy(xDevice, inputByteSize, xHost, inputByteSize, ACL_MEMCPY_HOST_TO_DEVICE));
  140.  
  141. CHECK_ACL(aclrtMemcpy(tilingDevice, tilingSize, tilingHost, tilingSize, ACL_MEMCPY_HOST_TO_DEVICE));
  142.  
  143. //调用核函数
  144.  
  145. leakyrelu_custom_do(blockDim, nullptr, stream, xDevice, yDevice, workspaceDevice, tilingDevice);
  146.  
  147. //等待核函数运行完成
  148.  
  149. CHECK_ACL(aclrtSynchronizeStream(stream));
  150.  
  151. //拷回运行结果到host
  152.  
  153. CHECK_ACL(aclrtMemcpy(yHost, outputByteSize, yDevice, outputByteSize, ACL_MEMCPY_DEVICE_TO_HOST));
  154.  
  155. // PrintData(yHost, 16, printDataType::HALF);
  156.  
  157. WriteFile("./output/output_y.bin", yHost, outputByteSize);
  158.  
  159. //释放资源
  160.  
  161. CHECK_ACL(aclrtFree(xDevice));
  162.  
  163. CHECK_ACL(aclrtFree(yDevice));
  164.  
  165. CHECK_ACL(aclrtFree(workspaceDevice));
  166.  
  167. CHECK_ACL(aclrtFree(tilingDevice));
  168.  
  169. CHECK_ACL(aclrtFreeHost(xHost));
  170.  
  171. CHECK_ACL(aclrtFreeHost(yHost));
  172.  
  173. CHECK_ACL(aclrtFreeHost(workspaceHost));
  174.  
  175. CHECK_ACL(aclrtFreeHost(tilingHost));
  176.  
  177. CHECK_ACL(aclrtDestroyStream(stream));
  178.  
  179. CHECK_ACL(aclrtDestroyContext(context));
  180.  
  181. CHECK_ACL(aclrtResetDevice(deviceId));
  182.  
  183. CHECK_ACL(aclFinalize());
  184.  
  185. #endif
  186.  
  187. return 0;
  188.  
  189. }

一键式编译运行脚本run.sh

编译和运行应用程序。

cpu侧运行命令:

  1. bash run.sh leakyrelu_custom ascend910B1 VectorCore cpu

npu侧运行命令:

  1. bash run.sh leakyrelu_custom ascend910B1 VectorCore npu

参数含义如下:

  1. bash run.sh <kernel_name> <soc_version> <core_type> <run_mode>

<kernel_name>表示需要运行的算子。

<soc_version>表示算子运行的AI处理器型号。

<core_type>表示在AI Core上或者Vector Core上运行,参数取值为AiCore/VectorCore。

<run_mode>表示算子以cpu模式或npu模式运行,参数取值为cpu/npu。

kernel实现

函数原型定义

本样例中,函数名为leakyrelu_custom,根据对算子输入输出的分析,确定有2个参数x,y,其中x为输入内存,y为输出内存。核函数原型定义如下所示:

  1. extern "C" __global__ __aicore__ void leakyrelu_custom(GM_ADDR x, GM_ADDR y, GM_ADDR workspace, GM_ADDR tiling){ }

使用__global__函数类型限定符来标识它是一个核函数,可以被<<<...>>>调用;使用__aicore__函数类型限定符来标识该核函数在设备端AI Core上执行;为方便起见,统一使用GM_ADDR宏修饰入参,GM_ADDR宏定义:

  1. #define GM_ADDR __gm__ uint8_t* __restrict__

获取tilling数据,并调用算子类的Init和Process函数。

算子类的Init函数,完成内存初始化相关工作,Process函数完成算子实现的核心逻辑。

  1. extern "C" __global__ __aicore__ void leakyrelu_custom(GM_ADDR x, GM_ADDR y, GM_ADDR workspace, GM_ADDR tiling)
  2.  
  3. {
  4.  
  5. GET_TILING_DATA(tilingData, tiling);
  6.  
  7. KernelLeakyRelu op;
  8.  
  9. op.Init(x, y, tilingData.totalLength, tilingData.tileNum, tilingData.scalar);
  10.  
  11. op.Process();
  12.  
  13. }

对核函数的调用进行封装

封装后得到leakyrelu_custom_do函数,便于主程序调用。#ifndef __CCE_KT_TEST__表示该封装函数仅在编译运行NPU侧的算子时会用到,编译运行CPU侧的算子时,可以直接调用add_custom函数。调用核函数时,除了需要传入输入输出参数x,y,切分相关参数tiling,还需要传入blockDim(核函数执行的核数), l2ctrl(保留参数,设置为nullptr), stream(应用程序中维护异步操作执行顺序的stream)来规定核函数的执行配置。

  1. extern "C" __global__ __aicore__ void leakyrelu_custom(GM_ADDR x, GM_ADDR y, GM_ADDR workspace, GM_ADDR tiling)
  2.  
  3. {
  4.  
  5. GET_TILING_DATA(tilingData, tiling);
  6.  
  7. KernelLeakyRelu op;
  8.  
  9. op.Init(x, y, tilingData.totalLength, tilingData.tileNum, tilingData.scalar);
  10.  
  11. op.Process();
  12.  
  13. }

获取tiling参数

主要从tilingPointer中获取tiling的参数totalLength(总长度)、tileNum(切分个数,单核循环处理数据次数)和scalar(LeakyRelu计算标量)。

  1. #define GET_TILING_DATA(tilingData, tilingPointer) \
  2.  
  3. LeakyReluCustomTilingData tilingData; \
  4.  
  5. INIT_TILING_DATA(LeakyReluCustomTilingData, tilingDataPointer, tilingPointer); \
  6.  
  7. (tilingData).totalLength = tilingDataPointer->totalLength; \
  8.  
  9. (tilingData).tileNum = tilingDataPointer->tileNum; \
  10.  
  11. (tilingData).scalar = tilingDataPointer->scalar;
  12.  
  13. #endif // LEAKYRELU_CUSTOM_TILING_H

Init函数

主要获取tiling数据后,设置单核上gm的地址和Buffer的初始化。

  1. __aicore__ inline void Init(GM_ADDR x, GM_ADDR y, uint32_t totalLength, uint32_t tileNum, float scalar)
  2.  
  3. {
  4.  
  5. ASSERT(GetBlockNum() != 0 && "block dim can not be zero!");
  6.  
  7. this->blockLength = totalLength / GetBlockNum();
  8.  
  9. this->tileNum = tileNum;
  10.  
  11. this->scalar = static_cast<half>(scalar);
  12.  
  13. ASSERT(tileNum != 0 && "tile num can not be zero!");
  14.  
  15. this->tileLength = this->blockLength / tileNum / BUFFER_NUM;
  16.  
  17. // get start index for current core, core parallel
  18.  
  19. xGm.SetGlobalBuffer((__gm__ half*)x + this->blockLength * get_block_idx(), this->blockLength);
  20.  
  21. yGm.SetGlobalBuffer((__gm__ half*)y + this->blockLength * get_block_idx(), this->blockLength);
  22.  
  23. // pipe alloc memory to queue, the unit is Bytes
  24.  
  25. pipe.InitBuffer(inQueueX, BUFFER_NUM, this->tileLength * sizeof(half));
  26.  
  27. pipe.InitBuffer(outQueueY, BUFFER_NUM, this->tileLength * sizeof(half));
  28.  
  29. }

Process函数

主要实现三个CopyIn、Compute、CopyOut这三stage。

  1. __aicore__ inline void Process()
  2.  
  3. {
  4.  
  5. // loop count need to be doubled, due to double buffer
  6.  
  7. int32_t loopCount = this->tileNum * BUFFER_NUM;
  8.  
  9. // tiling strategy, pipeline parallel
  10.  
  11. for (int32_t i = 0; i < loopCount; i++) {
  12.  
  13. CopyIn(i);
  14.  
  15. Compute(i);
  16.  
  17. CopyOut(i);
  18.  
  19. }
  20.  
  21. }

CopyIn函数

负责从Global Memory拷贝数据到Local Memory,并将数据加入Queue

  1. __aicore__ inline void CopyIn(int32_t progress)
  2.  
  3. {
  4.  
  5. // alloc tensor from queue memory
  6.  
  7. LocalTensor<half> xLocal = inQueueX.AllocTensor<half>();
  8.  
  9. // copy progress_th tile from global tensor to local tensor
  10.  
  11. DataCopy(xLocal, xGm[progress * tileLength], tileLength);
  12.  
  13. // enque input tensors to VECIN queue
  14.  
  15. inQueueX.EnQue(xLocal);
  16.  
  17. }

Compute函数

负责从Queue中取出数据,进行计算,并将结果放入Queue

  1. __aicore__ inline void Compute(int32_t progress)
  2.  
  3. {
  4.  
  5. // deque input tensors from VECIN queue
  6.  
  7. LocalTensor<half> xLocal = inQueueX.DeQue<half>();
  8.  
  9. LocalTensor<half> yLocal = outQueueY.AllocTensor<half>();
  10.  
  11. // call LeakyRelu instr for computation
  12.  
  13. LeakyRelu(yLocal, xLocal, scalar, tileLength);
  14.  
  15. // enque the output tensor to VECOUT queue
  16.  
  17. outQueueY.EnQue<half>(yLocal);
  18.  
  19. // free input tensors for reuse
  20.  
  21. inQueueX.FreeTensor(xLocal);
  22.  
  23. }

CopyOut函数

负责从Queue中将数据取出,并将数据从Local Memory拷贝到Global Memory。

  1. __aicore__ inline void CopyOut(int32_t progress)
  2.  
  3. {
  4.  
  5. // deque output tensor from VECOUT queue
  6.  
  7. LocalTensor<half> yLocal = outQueueY.DeQue<half>();
  8.  
  9. // copy progress_th tile from local tensor to global tensor
  10.  
  11. DataCopy(yGm[progress * tileLength], yLocal, tileLength);
  12.  
  13. // free output tensor for reuse
  14.  
  15. outQueueY.FreeTensor(yLocal);
  16.  
  17. }

编译和执行

在CPU侧执行

执行结果如下:

可以看到最后的输出结果output_y.bin和标杆数据golden.bin的MD5值相同,说明计算结果相同。

执行完成后,在input下存放输入数据和tiling数据,在output下面存放了输出数据和标杆数据,npuchk目录下是每个核的npu_check执行结果

在当前目录还有一个可执行二进制文件leakyrelu_custom_cpu,如果执行报错,可以通过gdb调试这个可执行文件,具体调试可参考文末官方教程。

在NPU侧执行

在NPU侧执行有两种方式:仿真执行和上板运行,命令都相同,只是编译选项不同,我们可以通过修改编译选项-DASCEND_RUN_MODE为SIMULATOR运行CAModel仿真,设置为 ONBOARD是上板运行。

  1. function compile_and_execute() {
  2.  
  3. # 使用cmake编译cpu侧或者npu侧算子, SIMULATOR or ONBOARD
  4.  
  5. mkdir -p build; cd build; \
  6.  
  7. cmake .. \
  8.  
  9. -Dsmoke_testcase=$1 \
  10.  
  11. -DASCEND_PRODUCT_TYPE=$2 \
  12.  
  13. -DASCEND_CORE_TYPE=$3 \
  14.  
  15. -DASCEND_RUN_MODE="SIMULATOR" \
  16.  
  17. -DASCEND_INSTALL_PATH=$ASCEND_HOME_DIR
  18.  
  19. VERBOSE=1 cmake --build . --target ${1}_${4}
  20.  
  21. ……
  22.  
  23. }

参考资料

总之,学习Ascend C,仅需了解C++编程、理解对列通信与内存申请释放机制、通过调用相应的计算接口与搬运接口,就可以写出运行在昇腾AI处理器上的高性能算子。

了解更多Ascend C学习资源,请访问官方教程:Ascend C编程指南(官方教程)

号外!

华为将于2023年9月20-22日,在上海世博展览馆和上海世博中心举办第八届华为全联接大会(HUAWEICONNECT 2023)。本次大会以“加速行业智能化”为主题,邀请思想领袖、商业精英、技术专家、合作伙伴、开发者等业界同仁,从商业、产业、生态等方面探讨如何加速行业智能化。

我们诚邀您莅临现场,分享智能化的机遇和挑战,共商智能化的关键举措,体验智能化技术的创新和应用。您可以:

  • 在100+场主题演讲、峰会、论坛中,碰撞加速行业智能化的观点
  • 参观17000平米展区,近距离感受智能化技术在行业中的创新和应用
  • 与技术专家面对面交流,了解最新的解决方案、开发工具并动手实践
  • 与客户和伙伴共寻商机

感谢您一如既往的支持和信赖,我们热忱期待与您在上海见面。

大会官网:https://www.huawei.com/cn/events/huaweiconnect

欢迎关注“华为云开发者联盟”公众号,获取大会议程、精彩活动和前沿干货。

点击关注,第一时间了解华为云新鲜技术~

纯干货!一文get昇腾Ascend C编程入门全部知识点的更多相关文章

  1. HTML+CSS纯干货就业前基础到精通系统学习2016/9/3

    1:HTML纯干货学习后的达到的效果 (1):会使用HTML的基本结构,创建网页 (2):会使用文本字体相关标签,实现文字修饰和布局 (3):会使用图像.超链接相关标签,实现图文并茂的页面 (4):会 ...

  2. mongoDB 学习笔记纯干货(mongoose、增删改查、聚合、索引、连接、备份与恢复、监控等等)

    最后更新时间:2017-07-13 11:10:49 原始文章链接:http://www.lovebxm.com/2017/07/13/mongodb_primer/ MongoDB - 简介 官网: ...

  3. IT技术学习指导之Linux系统入门的4个阶段(纯干货带图)

    IT技术学习指导之Linux系统入门的4个阶段(纯干货带图) 全世界60%的人都在使用Linux.几乎没有人没有受到Linux系统的"恩惠",我们享受的大量服务(包括网页服务.聊天 ...

  4. Java程序员的日常——经验贴(纯干货)

    工作当中遇到的事情比较杂,因此涉及的知识点也很多.这里暂且记录一下,今天遇到的知识点,纯干货~ 关于文件的解压和压缩 如果你的系统不支持tar -z命令 如果是古老的Unix系统,可能并不认识tar ...

  5. 360手机助手内部资料曝光,63张PPT纯干货

    360手机助手内部资料曝光,63张PPT纯干货 日前,国内最大的安卓应用商店360手机助手发布了<2016年手机软件行业趋势绿皮书>,这份绿皮书对2015年以来移动互联网的趋势做了总结,展 ...

  6. (纯干货)最新WEB前端学习路线汇总初学者必看

    Web前端好学吗?这是很多web学习者常问的问题,想要学习一门自己从未接触过的领域,事先有些了解并知道要学的内容,对接下来的学习会有事半功倍的效果.在当下来说web前端开发工程师可谓是高福利.高薪水的 ...

  7. 纯干货:深度学习实现之空间变换网络-part2

    https://www.jianshu.com/p/854d111670b6 纯干货:深度学习实现之空间变换网络-part1 在第一部分中,我们主要介绍了两个非常重要的概念:仿射变换和双线性插值,并了 ...

  8. 【转】纯干货:PS高手完全自学宝典(原创文章)

    文章版权:Tommy子言  原创 一. 一切从基础开始 强大的PS虽然功能众多,但归纳起来主要有三大功能: 修图——主要包括纯图片的修饰.合成.3D合成等等: 画图——主要是指用PS来绘画.广告插画, ...

  9. 纯干货:Linux抓包命令集锦

    /******************************************************************************************* 版权声明* 本 ...

  10. 【转】mongoDB 学习笔记纯干货(mongoose、增删改查、聚合、索引、连接、备份与恢复、监控等等)

    mongoDB 学习笔记纯干货(mongoose.增删改查.聚合.索引.连接.备份与恢复.监控等等) http://www.cnblogs.com/bxm0927/p/7159556.html

随机推荐

  1. GPT大语言模型Alpaca-lora本地化部署实践【大语言模型实践一】

    模型介绍 Alpaca模型是斯坦福大学研发的LLM(Large Language Model,大语言)开源模型,是一个在52K指令上从LLaMA 7B(Meta公司开源的7B)模型微调而来,具有70亿 ...

  2. vue 一键导出数据为excel文件并附带样式 十分简单

    自入行以来我就一直疑惑一个问题,导出excel为什么总是搞的很复杂,包括网上的教程,屎里淘金,非常耗费精力.今天刚好业务需要,整理一个简单明了的由vue前端导出的版本出来. 开始: #1.添加xlsx ...

  3. es mysql 适用场景对比

    es mysql 适用场景对比 问题一 全文检索毫无疑问直接上es,那么除了这种场景,什么时候该选es?为啥mysql不行? 对枚举字段的搜索 mysql创建索引的原则是对于那些区别度高字段建立索引, ...

  4. 我在 vscode 插件里接入了 ChatGPT,解决了代码变量命名的难题

    lowcode 插件 已经迭代了差不多3年.作为我的生产力工具,平常一些不需要动脑的搬砖活基本上都是用 lowcode 去完成,比如管理脚手架,生成 CURD 页面,根据接口文档生成 TS 类型,生成 ...

  5. vivo 帐号服务稳定性建设之路-平台产品系列06

    作者:vivo 互联网平台产品研发团队- Shi Jianhua.Sun Song 帐号是一个核心的基础服务,对于基础服务而言稳定性就是生命线.在这篇文章中,将与大家分享我们在帐号稳定性建设方面的经验 ...

  6. Nashorn引擎导致metaspace oom

          从报错内容很清楚是Metaspace区域oom了 大部分情况下,程序运行中不会出现过多的类加载数量的变动,先导入dump文件检查是否有异常的classLoader或者有异常动态生成的cla ...

  7. Linux Nacos2.2.0版本集群搭建,常见报错问题解决

    准备: 服务器,nacos,mysql,nginx,java,maven Nacos 官网:https://nacos.io 下载地址github:https://github.com/alibaba ...

  8. Auto.js食用指南

    Auto.js食用指南 控件点击是autojs特有的一项功能,基于安卓的无障碍功能的,在软件上有很好的支持,常用于办公软件等...... 前言: 软件选择: auto.js 8.0pro版本(对比4. ...

  9. 基于Sa-Token实现微服务之前的单点登录

    修改配置文件,准备好四个域名 127.0.0.1 auth.server.com 127.0.0.1 user.server.com 127.0.0.1 third.server.com 127.0. ...

  10. CSRF与SSRF

    CSRF与SSRF CSRF(跨站请求伪造) 跨站请求伪造(Cross-site request forgery,CSRF),它强制终端用户在当前对其进行身份 验证后的Web应用程序上执行非本意的操作 ...