Cube编程范式把算子的实现流程分为5个基本任务:CopyIn,Split,Compute,Aggregate,CopyOut。CopyIn负责搬入操作,Split负责数据切分操作,Compute负责矩阵指令计算操作,Aggregate负责数据汇聚操作,CopyOut负责搬出操作。
具体任务之间的交互流程和流程图如下。
基于Ascend C方式实现矩阵算子的流程如下图所示。
下文将以Matmul算子为例对上述步骤进行详细介绍,Matmul算子的代码框架如下,完整代码请参见Mmad样例。
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 |
#include "kernel_operator.h" // 根据编程范式实现算子类 class KernelMatmul { public: __aicore__ inline void Init(GM_ADDR a, GM_ADDR b, GM_ADDR c) { // ... } __aicore__ inline void Process() { CopyIn(); SplitA(); AscendC::LocalTensor<half> b1Local = inQueueB1.DeQue<half>(); AscendC::LocalTensor<half> a2Local = inQueueA2.DeQue<half>(); AscendC::LocalTensor<float> c2Local = outQueueCO2.AllocTensor<float>(); // split matrix b into 2 parts, [32, 16] and [32, 16] for (int i = 0; i < 2; ++i) { SplitB(b1Local, i); Compute(a2Local); Aggregate(c2Local, i); } inQueueB1.FreeTensor(b1Local); inQueueA2.FreeTensor(a2Local); outQueueCO2.EnQue<float>(c2Local); CopyOut(); } private: __aicore__ inline void CopyIn() { // ... } __aicore__ inline void SplitA() { // ... } __aicore__ inline void SplitB(const LocalTensor<half>& b1Local, const int bSplitIdx) { // ... } __aicore__ inline void Compute(const LocalTensor<half>& a2Local) { // ... } __aicore__ inline void Aggregate(const LocalTensor<float>& c2Local, const int bSplitIdx) { // ... } __aicore__ inline void CopyOut() { // ... } private: // ... }; //核函数定义 extern "C" __global__ __aicore__ void matmul_custom(GM_ADDR a, GM_ADDR b, GM_ADDR c) { KernelMatmul op; op.Init(a, b, c); op.Process(); } |
在开发算子代码之前需要分析算子的数学表达式、输入、输出以及计算逻辑的实现,明确需要调用的Ascend C接口。
c = a * b
注意需要处理的数据过大时,需要对数据进行切分并分块搬运到A2、B2,分别计算后再进行汇聚。下文的计算逻辑为了展示Split和Aggregate阶段的样例,请您根据实际需要处理的数据大小决定是否需要切分和汇聚。
由于硬件架构对矩阵乘计算的输入输出有格式约束,需要在算子实现中增加格式转换的流程。
通过以上分析,得到Ascend C Matmul算子的计算流程图和设计规格如下:
算子类型(OpType) |
Matmul |
|||
---|---|---|---|---|
算子输入 |
name |
shape |
data type |
format |
a |
(m, k) = (32, 32) |
half |
ND |
|
b |
(k, n) = (32, 32) |
half |
ND |
|
算子输出 |
c |
(m, n) = (32, 32) |
float32 |
ND |
核函数名称 |
matmul_custom |
|||
使用的主要接口 |
DataCopy:数据搬移接口 |
|||
LoadData:矩阵数据格式转换接口 |
||||
Mmad:矩阵乘计算接口 |
||||
EnQue、DeQue等接口:Queue队列管理接口 |
||||
算子实现文件名称 |
matmul_custom.cpp |
根据核函数中介绍的规则进行核函数的定义。
本样例中,函数名为matmul_custom(核函数名称可自定义);根据算子分析中对算子输入输出的分析,确定有3个参数a,b,c,其中a,b都为输入内存,c为输出内存。根据核函数中核函数的规则介绍,函数原型定义如下所示:使用__global__函数类型限定符来标识它是一个核函数,可以被<<<>>>调用;使用__aicore__函数类型限定符来标识该核函数在设备端aicore上执行;为方便起见,统一使用GM_ADDR宏修饰入参,GM_ADDR宏定义请参考核函数。
1 2 3 |
extern "C" __global__ __aicore__ void matmul_custom(GM_ADDR a, GM_ADDR b, GM_ADDR c) { } |
1 2 3 4 5 6 |
extern "C" __global__ __aicore__ void matmul_custom(GM_ADDR a, GM_ADDR b, GM_ADDR c) { KernelMatmul op; op.Init(a, b, c); op.Process(); } |
1 2 3 4 5 6 7 |
#ifndef ASCENDC_CPU_DEBUG // call of kernel function void matmul_custom_do(uint32_t blockDim, void* l2ctrl, void* stream, uint8_t* a, uint8_t* b, uint8_t* c) { matmul_custom<<<blockDim, l2ctrl, stream>>>(a, b, c); } #endif |
根据上一章节介绍,核函数中会调用算子类的Init和Process函数,本章具体讲解基于编程范式实现算子类。矩阵编程范式请参考编程范式。
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 |
class KernelMatmul { public: __aicore__ inline KernelMatmul(){} // 初始化函数,完成内存初始化相关操作 __aicore__ inline void Init(GM_ADDR a, GM_ADDR b, GM_ADDR c){} // 核心处理函数,实现算子逻辑 // 调用私有成员函数CopyIn、SplitA、SplitB、Compute、Aggregate、CopyOut完成矩阵算子的五级流水操作 __aicore__ inline void Process(){} private: __aicore__ inline void CopyND2NZ(const LocalTensor<half>& dst, const GlobalTensor<half>& src, const uint16_t height, const uint16_t width){} // 搬进函数,完成编程范式中的CopyIn阶段的处理,由Process函数调用 __aicore__ inline void CopyIn(){} // 搬进函数,完成编程范式中的Split阶段的处理,由Process函数调用 __aicore__ inline void SplitA(){} // 搬进函数,完成编程范式中的Split阶段的处理,由Process函数循环调用两次,分别搬运b矩阵的两个part __aicore__ inline void SplitB(const LocalTensor<half>& b1Local, const int bSplitIdx){} // 计算函数,完成编程范式中的Compute阶段的处理,由Process函数循环调用两次,分别计算出矩阵c的两个part __aicore__ inline void Compute(const LocalTensor<half>& a2Local){} // 搬出函数,完成编程范式中的Aggregate阶段的处理,由Process函数循环调用两次,分别搬出矩阵c的两个part __aicore__ inline void Aggregate(const LocalTensor<float>& c2Local, const int bSplitIdx){} // 搬出函数,完成编程范式中的CopyOut阶段的处理,由Process函数调用 __aicore__ inline void CopyOut(){} private: AscendC::TPipe pipe; // Pipe内存管理对象,管理Queue队列的内存 AscendC::TQue<AscendC::TPosition::A1, 1> inQueueA1; // 输入数据的队列,TPosition为A1 AscendC::TQue<AscendC::TPosition::A2, 1> inQueueA2; // 输入数据的队列,TPosition为A2 AscendC::TQue<AscendC::TPosition::B1, 1> inQueueB1; // 输入数据的队列,TPosition为B1 AscendC::TQue<AscendC::TPosition::B2, 2> inQueueB2; // 输入数据的队列,TPosition为B2 AscendC::TQue<AscendC::TPosition::CO1, 2> outQueueCO1; // 输出数据的队列,TPosition为CO1 AscendC::TQue<AscendC::TPosition::CO2, 1> outQueueCO2; // 输出数据的队列,TPosition为CO2 // 管理输入输出Global Memory内存地址的对象,其中aGM,bGM为输入,cGM为输出 AscendC::GlobalTensor<half> aGM, bGM; AscendC::GlobalTensor<float> cGM; uint16_t m = 32; uint16_t n = 32; uint16_t k = 32; uint16_t aSize, bSize, cSize, mBlocks, nBlocks, kBlocks; }; |
KernelMatmul构造函数实现
构造函数中对私有成员变量进行初始化,具体代码如下:
1 2 3 4 5 6 7 8 9 |
__aicore__ inline KernelMatmul() { aSize = m * k; bSize = k * n; cSize = m * n; mBlocks = m / 16; nBlocks = n / 16; kBlocks = k / 16; } |
矩阵a的形状为[m, k],矩阵b的形状为[k, n],矩阵c的形状为[m,n],此样例中m、n、k均设置为32。
aSize、bSize、cSize分别为矩阵a、b、c的数值个数。
mBlocks、 nBlocks、 kBlocks为m、n、k所占分形数量,half类型一个分形长度为16,blocks计算公式为:
分形具体介绍可参考数据排布格式。
Init函数实现
Init函数主要完成以下内容:
注意,因为本样例中Init函数的入参统一设置为uint8_t*,这里需要强转成具体的数据类型(__gm__ half*),再进行偏移。
比如,为输入数据队列inQueueB2分配内存,可以通过如下代码段实现:
1
|
pipe.InitBuffer(inQueueB2, 2, bSize * sizeof(half) / 2); |
此样例中将b矩阵切分为两个part,为inQueueB2分配内存时需要申请两块内存空间,每一块的大小为b矩阵大小的一半,outQueueCO1的内存初始化同理。
具体的初始化函数代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 |
__aicore__ inline void Init(GM_ADDR a, GM_ADDR b, GM_ADDR c) { aGM.SetGlobalBuffer((__gm__ half*)a); bGM.SetGlobalBuffer((__gm__ half*)b); cGM.SetGlobalBuffer((__gm__ float*)c); pipe.InitBuffer(inQueueA1, 1, aSize * sizeof(half)); pipe.InitBuffer(inQueueA2, 1, aSize * sizeof(half)); pipe.InitBuffer(inQueueB1, 1, bSize * sizeof(half)); pipe.InitBuffer(inQueueB2, 2, bSize * sizeof(half) / 2); pipe.InitBuffer(outQueueCO1, 2, cSize * sizeof(float) / 2); pipe.InitBuffer(outQueueCO2, 1, cSize * sizeof(float)); } |
Process函数实现
基于矩阵编程范式,将核函数的实现分为5个基本阶段:CopyIn,Split,Compute,Aggregate,CopyOut。Split,Compute,Aggregate阶段需要区分a、b矩阵。Process函数中通过如下方式调用这几个函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
__aicore__ inline void Process() { CopyIn(); SplitA(); AscendC::LocalTensor<half> b1Local = inQueueB1.DeQue<half>(); AscendC::LocalTensor<half> a2Local = inQueueA2.DeQue<half>(); AscendC::LocalTensor<float> c2Local = outQueueCO2.AllocTensor<float>(); // split matrix b into 2 parts, [32, 16] and [32, 16] for (int i = 0; i < 2; ++i) { SplitB(b1Local, i); Compute(a2Local); Aggregate(c2Local, i); } inQueueB1.FreeTensor(b1Local); inQueueA2.FreeTensor(a2Local); outQueueCO2.EnQue<float>(c2Local); CopyOut(); } |
两次循环内,SplitB需要从inQueueB1中分别搬运两个part的b矩阵,Compute需要分别计算a矩阵和两个part b矩阵的乘法,Aggregate要分别搬运两个part的c矩阵,具体五个阶段数据流通示意图如下:
切分b矩阵,可以实现一部分的并行,本样例的流水并行示意图如下:
一次DataCopy指令搬运height*16个数,循环执行width/16次。DataCopy的参数设置如下:
格式转换示意图如下,第一次循环搬运蓝色部分,第二次循环搬运绿色部分;图中width为32,占两个分形,height为32,占两个分形,一共搬运4个16*16分形。
注意:上述ND到NZ的格式转换仅作为举例说明,开发者可根据实际情况选择合适的转换方式。
具体代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
__aicore__ inline void CopyND2NZ(const LocalTensor<half>& dst, const GlobalTensor<half>& src, const uint16_t height, const uint16_t width) { for (int i = 0; i < width / 16; ++i) { int srcOffset = i * 16; int dstOffset = i * 16 * height; AscendC::DataCopy(dst[dstOffset], src[srcOffset], { height, 1, uint16_t(width / 16 - 1), 0 }); } } __aicore__ inline void CopyIn() { AscendC::LocalTensor<half> a1Local = inQueueA1.AllocTensor<half>(); AscendC::LocalTensor<half> b1Local = inQueueB1.AllocTensor<half>(); CopyND2NZ(a1Local, aGM, m, k); CopyND2NZ(b1Local, bGM, k, n); inQueueA1.EnQue(a1Local); inQueueB1.EnQue(b1Local); } |
搬运及格式转换示意图如下:图中k为32,占kBlocks(k/16=2)个分形,m为32,占mBlocks(m/16=2)个分形,一共搬运4个16*16分形。本示例中,调用一次LoadData接口完成两个16*16分形的搬运,循环调用两次LoadData。第一次循环搬运蓝色部分两个分形,第二次循环搬运绿色部分两个分形。
具体代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
__aicore__ inline void SplitA() { int srcOffset = 0; int dstOffset = 0; AscendC::LocalTensor<half> a1Local = inQueueA1.DeQue<half>(); AscendC::LocalTensor<half> a2Local = inQueueA2.AllocTensor<half>(); // transform nz to zz for (int i = 0; i < mBlocks; ++i) { AscendC::LoadData2DParams loadDataParams; loadDataParams.repeatTimes = kBlocks; loadDataParams.srcStride = mBlocks; loadDataParams.ifTranspose = false; AscendC::LoadData(a2Local[dstOffset], a1Local[srcOffset], loadDataParams); srcOffset += 16 * 16; dstOffset += k * 16; } inQueueA2.EnQue<half>(a2Local); inQueueA1.FreeTensor(a1Local); } |
搬运及格式转换示意图如下:图中k为32,占kBlocks(k/16=2)个分形,n为32,占nBlocks(n/16=2)个分形,一共搬运4个16*16分形。本示例中,调用一次LoadData接口完成两个16*16分形的搬运,循环调用两次LoadData。第一次循环搬运蓝色部分两个分形,第二次循环搬运绿色部分两个分形。
具体代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
__aicore__ inline void SplitB(const AscendC::LocalTensor<half>& b1Local, const int bSplitIdx) { AscendC::LocalTensor<half> b2Local = inQueueB2.AllocTensor<half>(); // transform nz to zn AscendC::LoadData2DParams loadDataParams; loadDataParams.repeatTimes = kBlocks; loadDataParams.srcStride = 1; loadDataParams.ifTranspose = true; AscendC::LoadData(b2Local, b1Local[bSplitIdx * bSize / 2], loadDataParams); inQueueB2.EnQue<half>(b2Local); } |
具体代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
__aicore__ inline void Compute(const AscendC::LocalTensor<half>& a2Local) { AscendC::LocalTensor<half> b2Local = inQueueB2.DeQue<half>(); AscendC::LocalTensor<float> c1Local = outQueueCO1.AllocTensor<float>(); AscendC::MmadParams mmadParams; mmadParams.m = m; mmadParams.n = n / 2; mmadParams.k = k; AscendC::Mmad(c1Local, a2Local, b2Local, mmadParams); outQueueCO1.EnQue<float>(c1Local); inQueueB2.FreeTensor(b2Local); } |
具体代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
__aicore__ inline void Aggregate(const AscendC::LocalTensor<float>& c2Local, const int bSplitIdx) { AscendC::LocalTensor<float> c1Local = outQueueCO1.DeQue<float>(); AscendC::DataCopyParams dataCopyParams; dataCopyParams.blockCount = 1; dataCopyParams.blockLen = 2; AscendC::DataCopyEnhancedParams enhancedParams; enhancedParams.blockMode = AscendC::BlockMode::BLOCK_MODE_MATRIX; AscendC::DataCopy(c2Local[bSplitIdx * cSize / 2], c1Local, dataCopyParams, enhancedParams); outQueueCO1.FreeTensor(c1Local); } |
格式转换示意图如下,第一次循环搬运蓝色部分数据,第二次循环搬运绿色部分数据。
具体代码如下:
1 2 3 4 5 6 7 8 9 10 11 |
__aicore__ inline void CopyOut() { AscendC::LocalTensor<float> c2Local = outQueueCO2.DeQue<float>(); // transform nz to nd for (int i = 0; i < nBlocks; ++i) { AscendC::DataCopy(cGM[i * 16], c2Local[i * m * 16], { m, 2, 0, uint16_t((nBlocks - 1) * 2) }); } outQueueCO2.FreeTensor(c2Local); } |