This section describes how to use basic APIs to perform matrix multiplication in Coupled Mode.
The following sections do not support Atlas 350 Accelerator Card.
The cube programming paradigm divides the operator implementation process into five basic tasks: CopyIn, Split, Compute, Aggregate, and CopyOut. CopyIn moves in data, Split performs data tiling, Compute performs compute on matrix instructions, Aggregate performs data aggregation, and CopyOut moves out data.
The inter-task communication and the flow chart are explained below.
The following figure shows the process of implementing the cube operator based on Ascend C.
The following uses the Matmul operator as an example. The code framework of the Matmul operator is as follows. For details about the complete code, see Mmad sample.
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" // Implement the operator class based on the programming paradigm. 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: // ... }; // Kernel function definition 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(); } |
Before developing the operator code, analyze the mathematical expression, input, output, and compute logic implementation of the operator, and specify the Ascend C APIs to be called.
c = a x b
If the data to be processed is too large, the data needs to be tiled, moved to A2 and B2 by block, computed separately, and then aggregated. The following compute logic is used to show examples of the Split and Aggregate phases. Determine whether to tile and aggregate data based on the size of data to be processed.
The hardware architecture has format restrictions on the input and output of matrix multiplication. Therefore, the format conversion process needs to be added to the operator implementation.
Based on the analysis, the compute flowchart and design specifications of the Ascend C Matmul operator are as follows.
|
OpType |
Matmul |
|||
|---|---|---|---|---|
|
Operator Input |
Name |
Shape |
Data Type |
Format |
|
a |
(m, k) = (32, 32) |
half |
ND |
|
|
b |
(k, n) = (32, 32) |
half |
ND |
|
|
Operator Output |
c |
(m, n) = (32, 32) |
float32 |
ND |
|
Kernel Function Name |
matmul_custom |
|||
|
Main API |
DataCopy: data movement API |
|||
|
LoadData: matrix data format conversion API |
||||
|
Mmad: matrix multiplication API |
||||
|
EnQue, DeQue, and others: queue management APIs |
||||
|
Operator Implementation File |
matmul_custom.cpp |
|||
Define kernel functions based on the rules described in Kernel Function.
In this sample, the function name is matmul_custom (the kernel function name can be customized). Based on the operator input and output analysis in Operator Analysis, there are three parameters a, b, and c, where a and b indicate the input memory and c indicates the output memory. According to the rules of kernel functions described in Kernel Function, the function prototype is defined as follows: The __global__ function type qualifier is used to identify a kernel function that can be called by <<<>>>. The __aicore__ function type qualifier is used to identify that the kernel function is executed on the AI Core on the device. For convenience, the GM_ADDR macro is used to modify input parameters. For details about the definition of the GM_ADDR macro, see Kernel Function.
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 numBlocks, void* l2ctrl, void* stream, uint8_t* a, uint8_t* b, uint8_t* c) { matmul_custom<<<numBlocks, l2ctrl, stream>>>(a, b, c); } #endif |
The kernel function calls the Init and Process functions of the operator class. This section describes how to implement the operator class based on the programming paradigm. For details about the cube programming paradigm, see Programming Paradigm.
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(){} // Initialization function, which is used to initialize memory. __aicore__ inline void Init(GM_ADDR a, GM_ADDR b, GM_ADDR c){} // Core processing function, which implements the operator logic. // Call the private member functions CopyIn, SplitA, SplitB, Compute, Aggregate and CopyOut to complete the five-level pipeline operation of the cube operator. __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){} // Move-in function, which completes the processing in the CopyIn phase in the programming paradigm. This function is called by the Process function. __aicore__ inline void CopyIn(){} // Move-in function, which completes the processing in the Split phase in the programming paradigm. This function is called by the Process function. __aicore__ inline void SplitA(){} // Move-in function, which completes the processing in the Split phase in the programming paradigm. It is called by the Process function for twice cyclically to move the two parts of matrix b. __aicore__ inline void SplitB(const LocalTensor<half>& b1Local, const int bSplitIdx){} // Compute function, which completes the processing in the Compute phase in the programming paradigm. It is called by the Process function for twice cyclically to compute the two parts of matrix c. __aicore__ inline void Compute(const LocalTensor<half>& a2Local){} // Move-out function, which completes the processing in the Aggregate phase in the programming paradigm. It is called by the Process function for twice cyclically to move out the two parts of matrix c. __aicore__ inline void Aggregate(const LocalTensor<float>& c2Local, const int bSplitIdx){} // Move-out function, which completes the processing in the CopyOut phase in the programming paradigm. This function is called by the Process function. __aicore__ inline void CopyOut(){} private: AscendC::TPipe pipe; // Pipe memory management object, which manages the memory of the queue. AscendC::TQue<AscendC::TPosition::A1, 1> inQueueA1; // Input data queue. The value of TPosition is A1. AscendC::TQue<AscendC::TPosition::A2, 1> inQueueA2; // Input data queue. The value of TPosition is A2. AscendC::TQue<AscendC::TPosition::B1, 1> inQueueB1; // Input data queue. The value of TPosition is B1. AscendC::TQue<AscendC::TPosition::B2, 2> inQueueB2; // Input data queue. The value of TPosition is B2. AscendC::TQue<AscendC::TPosition::CO1, 2> outQueueCO1; // Output data queue. The value of TPosition is CO1. AscendC::TQue<AscendC::TPosition::CO2, 1> outQueueCO2; // Output data queue. The value of TPosition is CO2. // Object for managing the global memory addresses of the input and output data. aGM and bGM are input, and cGM is output. 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 Constructor Implementation
Initialize private member variables in the constructor function. The code is as follows:
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; } |
The shape of matrix a is [m, k], the shape of matrix b is [k, n], and the shape of matrix c is [m, n]. In this example, m, n, and k are all set to 32.
aSize, bSize, and cSize are the numbers of values of matrices a, b, and c, respectively.
mBlocks, nBlocks, and kBlocks are the number of fractals occupied by m, n, and k. For the half type, the shape of a fractal is 16 x 16. The formula for computing blocks is as follows:
For details about fractals, see Data Layout Formats.
Init Function Implementation
The Init function performs the following operations:
1
|
aGM.SetGlobalBuffer((__gm__ half*)a); |
In this example, the input parameter of the Init function is set to uint8_t*. Therefore, you need to forcibly convert the input parameter to a specific data type (__gm__ half*) and then perform offset.
For example, allocating memory to the inQueueB2 queue can be implemented by using the following code snippet:
1
|
pipe.InitBuffer(inQueueB2, 2, bSize * sizeof(half) / 2); |
In this example, matrix b is divided into two parts. When allocating memory to inQueueB2, you need to allocate two blocks of memory. The size of each memory block is half of the size of matrix b. The memory initialization of outQueueCO1 is similar.
The initialization function code is as follows:
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 Function Implementation
Based on the cube programming paradigm, the implementation of kernel functions is divided into five basic phases: CopyIn, Split, Compute, Aggregate, and CopyOut. Matrix a and matrix b need to be distinguished in the Split, Compute, and Aggregate phases. These functions are called in the Process function as follows:
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(); } |
In the two cycles, SplitB moves the two parts of matrix b from inQueueB1, Compute computes the multiplication of matrix a with the two parts of matrix b, and Aggregate outputs the two parts of matrix c. The following figure shows the data flow in the five phases.
Matrix b can be split to implement partial parallelism. The following figure shows the pipeline parallelism in this example.
The DataCopy command can move height x 16 elements at a time, so you need to run the command repeatedly for width/16 times. Set the parameters of DataCopy as follows:
The following figure shows the format conversion. The blue part is moved in the first cycle, and the green part is moved in the second cycle. In the figure, the width is 32, occupying two fractals, and the height is 32, occupying two fractals. A total of four 16 x 16 fractals are moved.
Note: The preceding format conversion from ND to NZ is only an example. You can select a proper conversion mode as required.
The code is as follows:
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); } |
The following figure shows the movement and format conversion. In the figure, k is 32, occupying kBlocks (k/16 = 2) fractals. m is 32, occupying mBlocks (m/16 = 2) fractals. A total of four 16 x 16 fractals are moved. In this example, the LoadData API is called once to move two 16 x 16 fractals, and the LoadData API is called twice. The first call moves two fractals in the blue part, and the second call moves two fractals in the green part.
The code is as follows:
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); } |
The following figure shows the movement and format conversion. In the figure, k is 32, occupying kBlocks (k/16 = 2) fractals. n is 32, occupying nBlocks (n/16 = 2) fractals. A total of four 16 x 16 fractals are moved. In this example, the LoadData API is called once to move two 16 x 16 fractals, and the LoadData API is called twice. The first call moves two fractals in the blue part, and the second call moves two fractals in the green part.
The code is as follows:
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); } |
The code is as follows:
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); } |
The code is as follows:
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); } |
The following figure shows the format conversion. The data in blue is moved in the first iteration, and the data in green is moved in the second iteration.
The code is as follows:
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); } |