流水编程范式
编程范式描述了算子实现的固定流程,基于编程范式进行编程,可以快速搭建算子实现的代码框架。
Ascend C编程范式是一种流水线式的编程范式,把算子核内的处理程序,分成多个流水任务,通过队列(Queue)完成任务间通信和同步,并通过统一的内存管理模块(Pipe)管理任务间通信内存。流水编程范式应用了流水线并行计算方法,基本概念请参考并行计算常用方法。
下文将对流水任务设计、任务间通信和同步、内存管理展开详细介绍。
流水任务设计
流水任务指的是单核处理程序中主程序调度的并行任务。在核函数内部,可以通过流水任务实现数据的并行处理,进一步提升性能。下面举例来说明,流水任务如何进行并行调度。以下面的流水任务示意图为例,单核处理程序的功能被拆分成3个流水任务:Stage1、Stage2、Stage3,每个任务专注于完成单一功能;需要处理的数据被切分成n片,使用Progress1~n表示,每个任务需要依次完成n个数据切片的处理。Stage间的箭头表达数据间的依赖关系,比如Stage1处理完Progress1之后,Stage2才能对Progress1进行处理。
若n=3,即待处理的数据被切分成3片,则上图中的流水任务运行起来的示意图如下,从运行图中可以看出,对于同一片数据,Stage1、Stage2、Stage3之间的处理具有依赖关系,需要串行处理;不同的数据切片,同一时间点,可以有多个任务在并行处理,由此达到任务并行、提升性能的目的。
Ascend C分别针对Vector、Cube编程设计了不同的流水任务。开发者只需要完成基本任务的代码实现即可,底层的指令同步和并行调度由Ascend C框架实现,开发者无需关注。
Vector编程范式把算子的实现流程分为3个基本任务:CopyIn,Compute,CopyOut。CopyIn负责搬入操作,Compute负责矢量计算操作,CopyOut负责搬出操作。
Cube编程范式把算子的实现流程分为5个基本任务:CopyIn,Split,Compute,Aggregate,CopyOut。CopyIn负责搬入操作,Split负责数据切分操作,Compute负责矩阵指令计算操作,Aggregate负责数据汇聚操作,CopyOut负责搬出操作。
任务间通信和同步
上一章节描述了流水任务的基本概念及矢量编程和矩阵编程的几个基本任务,我们可以知道,不同的流水任务之间存在数据依赖,需要进行数据传递。本章节来介绍任务之间如何进行数据传递:Ascend C中使用Queue队列完成任务之间的数据通信和同步,提供EnQue、DeQue等基础API。
Queue队列管理不同层级的物理内存时,用一种抽象的逻辑位置(QuePosition)来表达各级别的存储,代替了片上物理存储的概念,开发者无需感知硬件架构。Queue类型包括:VECIN、VECCALC、VECOUT、A1、A2、B1、B2、CO1、CO2,其中VECIN、VECCALC、VECOUT主要用于矢量编程,A1、A2、B1、B2、CO1、CO2用于矩阵编程,具体说明参见下文详细介绍。
Ascend C使用GlobalTensor和LocalTensor作为数据的基本操作单元,它是各种指令API直接调用的对象,也是数据的载体。详见数据结构定义。
矢量编程中使用到的逻辑位置(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任务。
- Stage2:Compute任务。
- Stage3:CopyOut任务。
由流水任务设计可知,矩阵编程主要分为CopyIn,Split,Compute,Aggregate,CopyOut这5个任务。任务间进行数据传递时会使用到的逻辑位置示意图如下:
上图中逻辑位置(QuePosition)定义如下:
- 搬入数据的存放位置:A1,用于存放整块A矩阵,可类比CPU多级缓存中的二级缓存;
- 搬入数据的存放位置:B1,用于存放整块B矩阵,可类比CPU多级缓存中的二级缓存;
- 搬入数据的存放位置:A2,用于存放切分后的小块A矩阵,可类比CPU多级缓存中的一级缓存;
- 搬入数据的存放位置:B2,用于存放切分后的小块B矩阵,可类比CPU多级缓存中的一级缓存;
- 结果数据的存放位置:CO1,用于存放小块结果C矩阵,可理解为Cube Out;
- 结果数据的存放位置:CO2,用于存放整块结果C矩阵,可理解为Cube Out;
- 搬入数据的存放位置:VECIN,用于矢量计算,实际业务在数据搬入Vector计算单元时使用此位置;
- 搬入数据的存放位置:VECCALC,用于矢量计算,实际业务一般在计算需要临时变量时使用此位置;
- 搬出数据的存放位置:VECOUT,用于矢量计算,实际业务在将Vector计算单元结果搬出时使用此位置。
具体任务之间的交互流程和流程图如下。
- Stage1:CopyIn任务。
- Stage2:Split任务。
- Stage3:Compute任务。
- Stage4:Aggregate任务。
- Stage5:CopyOut任务。
内存管理
任务间数据传递使用到的内存统一由内存管理模块Pipe进行管理。如下图所示,Pipe作为片上内存管理者,通过InitBuffer接口对外提供Queue内存初始化功能,开发者可以通过该接口为指定的Queue分配内存。
Queue队列内存初始化完成后,需要使用内存时,通过调用AllocTensor来为LocalTensor分配内存,当创建的LocalTensor完成相关计算无需再使用时,再调用FreeTensor来回收LocalTensor的内存。
编程过程中使用到的临时变量内存同样通过Pipe进行管理。临时变量可以使用TBuf数据结构来申请指定QuePosition上的存储空间。使用TBuf申请的内存空间只能参与计算,无法执行Queue队列的入队出队操作。具体的接口使用说明请参考TBuf。