【2023 · CANN训练营第一季】TIK C++算子开发入门第二章---TIK C++算子编程范式

87 阅读3分钟

 一、编程范式的概念

        TIK C++编程范式把算子内部的处理程序,分成多个流水任务(Stage),以张量(Tensor)为数据载体,以队列(Queue)进行任务之间的通信与同步,以内存管理模块(Pipe)管理任务间的通信内存。

二、TIK C++中的流水任务

        流水任务(Stage)指的是单核处理程序中主程序调度的并行任务。 在核函数内部,可以通过流水任务实现数据的并行处理来提升性能。

        举例来说,单核处理程序的功能可以被拆分成3个流水任务:Stage1、Stage2、Stage3,每个任务专注于完成单一功能;需要处理的数据被切分成n片,使用Progress1~n表示,每个任务需要依次完成n个数据切片的处理。Stage间的箭头表达数据间的依赖关系,比如Stage1处理完Progress1之后,Stage2才能对Progress1进行处理。

​编辑

         如果Progress的n=3,待处理的数据被切分成3片,对于同一片数据,Stage1、Stage2、Stage3之间的处理具有依赖关系,需要串行处理;不同的数据切片,同一时间点,可以有多个流水任务Stage在并行处理,由此达到任务并行、提升性能的目的。

​编辑

流水任务设计——矢量编程

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

编辑

三、TIK C++的任务通信与同步

3.1、任务间通信和同步

数据通信与同步的管理者

        不同的流水任务之间存在数据依赖,需要进行数据传递 TIK C++中使用Queue队列完成任务之间的数据通信和同步,Queue提供了EnQue、DeQue等基础API Queue队列管理NPU上不同层级的物理内存时,用一种抽象的逻辑位置(QuePosition)来表达各个级别的存储(Storage Scope),代替了片上物理存储的概念,开发者无需感知硬件架构 矢量编程中Queue类型(逻辑位置)包括:VECIN、VECOUT。

数据的载体

        TIK C++使用GlobalTensor和LocalTensor作为数据的基本操作单元,它是各种指令API直接调用的对象,也是数据的载体。

3.2、矢量编程

矢量编程中的逻辑位置(QuePosition):搬入数据的存放位置:VECIN、搬出数据的存放位置:VECOUT。

矢量编程主要分为CopyIn、Compute、CopyOut三个任务:

        CopyIn任务中将输入数据从GlobalTensor搬运至LocalTensor后,需要使用EnQue将LocalTensor放入VECIN的Queue中

        Compute任务等待VECIN的Queue中LocalTensor出队之后才可以进行矢量计算,计算完成后使用EnQue将计算结果LocalTensor放入到VECOUT的Queue中

        CopyOut任务等待VECOUT的Queue中LocalTensor出队,再将其拷贝到GlobalTensor

Stage1: CopyIn任务

        使用DataCopy接口将GlobalTensor拷贝到LocalTensor

        使用EnQue将LocalTensor放入VECIN的Queue中

Stage2: Compute任务

        使用DeQue从VECIN中取出LocalTensor

        使用TIK C++指令API完成矢量计算:Add 使用EnQue将结果LocalTensor放入VECOUT的Queue中

Stage3: CopyOut任务

        使用DeQue接口从VECOUT的Queue中取出LocalTensor

        使用DataCopy接口将LocalTensor拷贝到GlobalTensor

​编辑

四、TIK C++的内存管理机制

4.1、内存管理

任务间数据传递使用到的内存统一由内存管理模块Pipe进行管理

        Pipe作为片上内存管理者,通过InitBuffer接口对外提供Queue内存初始化功能,开发者可以通过该接口为指定的Queue分配内存

        Queue队列内存初始化完成后,需要使用内存时,通过调用AllocTensor来为LocalTensor分配内存给Tensor,当创建的LocalTensor完成相关计算无需再使用时,再调用FreeTensor来回收LocalTensor的内存。

InitBuffer、AllocTensor和 FreeTensor的示例:

// 使用AllocTensor分配Tensor
TPipe pipe;
TQue<TPosition::VECOUT, 2> que;
int num = 4;
int len = 1024;
// InitBuffer分配内存块数为4,每块大小为1024Bytes
pipe.InitBuffer(que, num, len);
// AllocTensor分配Tensor长度为1024Bytes
LocalTensor<half> tensor1 = que.AllocTensor();
// 使用FreeTensor释放通过AllocTensor分配的Tensor,注意配对使用
que.FreeTensor<half>(tensor1);

​编辑

 内存管理示意图

4.2、临时变量内存管理

        编程过程中使用到的临时变量内存同样通过Pipe进行管理。临时变量可以使用TBuf数据结构来申请指定QuePosition上的存储空间,并使用Get()来将分配到的存储空间分配给新的LocalTensor 从TBuf上获取全部长度,或者获取指定长度的LocalTensor。

注意:使用TBuf申请的内存空间只能参与计算,无法执行Queue队列的入队出队操作

// 为TBuf初始化分配内存,分配内存长度为1024字节
TPipe pipe;
TBuf<TPosition::VECIN> calcBuf; // 模板参数为QuePosition中的VECIN类型
uint32_t byteLen = 1024;
pipe.InitBuffer(calcBuf, byteLen);
// 从calcBuf获取Tensor,Tensor为pipe分配的所有内存大小,为1024字节
LocalTensor<int32_t> tempTensor1 = calcBuf.Get<int32_t>();
// 从calcBuf获取Tensor,Tensor为128个int32_t类型元素的内存大小,为512字节
LocalTensor<int32_t> tempTensor1 = calcBuf.Get<int32_t>(128);