编程范式描述了算子实现的固定流程,基于编程范式进行编程,可以快速搭建算子实现的代码框架。
Ascend C编程范式就是这样一种流水线式的编程范式,把算子核内的处理程序,分成多个流水任务,通过队列(Queue)完成任务间通信和同步,并通过统一的资源管理模块(Pipe)来统一管理内存、事件等资源。
如上图所示,Vector编程范式把算子的实现流程分为3个基本任务:CopyIn,Compute,CopyOut。
从编程的角度来讲,具体流程(如下文的伪代码)和流程图如下:
Pipe pipe; //创建全局的资源管理 TQue<VecIn, 1> queIn; //创建CopyIn阶段的队列 TQue<VecOut, 1> queOut; //创建CopyOut阶段的队列 // Init 阶段: pipe.InitBuffer(queIn, 2, 1024); // 开启doublebuffer for-loop { //CopyIn 阶段{ auto tensor = queIn.AllocTensor<half>(); //从Que上申请资源, 长度1024 DataCopy(tensor, gm, len); //搬运数据从GM到VECIN queIn.EnQue(tensor); } //Compute阶段{ auto tensor = queIn.DeQue<half>(); auto tensorOut = queOut.AllocTensor<half>(); Abs(tensorOut, tensor, 1024); queIn.FreeTensor(tensor); queOut.EnQue(tensorOut); } //CopyOut 阶段{ auto tensor = queOut.DeQue<half>(); DataCopy(gmOut, tensor, 1024); queOut.FreeTensor(tensor); } }
任务间数据传递使用到的内存、事件等资源统一由管理模块Pipe进行管理。如下所示的内存管理示意图,TPipe通过InitBuffer接口对外提供Queue内存初始化功能,开发者可以通过该接口为指定的Queue分配内存。
Queue队列内存初始化完成后,需要使用内存时,通过调用AllocTensor来为LocalTensor分配内存,当创建的LocalTensor完成相关计算无需再使用时,再调用FreeTensor来回收LocalTensor的内存。
编程过程中使用到的临时变量内存同样通过Pipe进行管理。临时变量可以使用TBuf数据结构来申请指定TPosition上的存储空间。使用TBuf申请的内存空间只能参与计算,无法执行Queue队列的入队出队操作。具体的接口使用说明请参考TBuf。
按照上述编程范式进行编程即可实现单核上数据的并行处理。需要处理的数据被切分成n片,每个并行任务(Stage1、2、3)需要依次完成n个数据切片的处理。Stage间的箭头表达数据间的依赖关系,比如Stage1(CopyIn)处理完第一个数据分片之后,Stage2(Compute)才能对该分片进行处理。
上图中的流水任务运行起来的示意图如下,Progress1、2、3代表处理的数据分片,从运行图中可以看出,对于同一片数据,Stage1、Stage2、Stage3之间的处理具有依赖关系,需要串行处理;不同的数据切片,同一时间点,可以有多个任务在并行处理,由此达到任务并行、提升性能的目的。
Cube计算的典型数据流图如下所示:
上图中逻辑位置(QuePosition)定义如下:
Cube计算流程同样也可以理解为CopyIn、Compute、CopyOut这几个阶段,因为流程相对复杂,Matmul高阶API提供对此的高阶封装,编程范式如下:
创建Matmul对象的示例如下:
// 创建Matmul对象 创建对象时需要传入A、B、C、Bias的参数类型信息, 类型信息通过MatmulType来定义,包括:内存逻辑位置、数据格式、数据类型。 typedef MatmulType<TPosition::GM, CubeFormat::ND, half> aType; typedef MatmulType<TPosition::GM, CubeFormat::ND, half> bType; typedef MatmulType<TPosition::GM, CubeFormat::ND, float> cType; typedef MatmulType<TPosition::GM, CubeFormat::ND, float> biasType; Matmul<aType, bType, cType, biasType> mm; REGIST_MATMUL_OBJ(&pipe, GetSysWorkSpacePtr(), mm, &tiling); // 初始化 // CopyIn阶段:完成从GM到LocalMemory的搬运 mm.SetTensorA(gm_a); // 设置左矩阵A mm.SetTensorB(gm_b); // 设置右矩阵B mm.SetBias(gm_bias); // 设置Bias // Compute阶段:完成矩阵乘计算 while (mm.Iterate()) { // CopyOut阶段:完成从LocalMemory到GM的搬运 mm.GetTensorC(gm_c); } // 结束矩阵乘操作 mm.End();
支持Vector与Cube混合计算的算子称之为融合算子。Ascend C提供融合算子的编程范式,方便开发者基于该范式表达融合算子的数据流,快速实现自己的融合算子。
融合算子数据流指融合算子的输入输出在各存储位置间的流向。以一个典型的Cube和Vector融合算子为例,逻辑位置间的数据流向如下图所示(为了简化描述,没有列出bias):
整个过程的示例代码如下(伪代码):
template<typename aType, typename bType, typename cType, typename biasType> __aicore__ inline void MatmulLeakyKernel<aType, bType, cType, biasType>::Process() { // 步骤1:初始化一个MatMul对象,将输入数据从Global Memory搬运到Cube核上。 uint32_t computeRound = 0; REGIST_MATMUL_OBJ(&pipe, GetSysWorkSpacePtr(), matmulObj); matmulObj.Init(&tiling); matmulObj.SetTensorA(aGlobal); matmulObj.SetTensorB(bGlobal); matmulObj.SetBias(biasGlobal); while (matmulObj.template Iterate<true>()) { // 步骤2:进行MatMul内部的计算。 // 步骤3:将MatMul的计算结果搬运到Vector核上。 reluOutLocal = reluOutQueue_.AllocTensor<cType>(); matmulObj.template GetTensorC<true>(reluOutLocal, false, true); // 步骤4:进行Vector矢量计算。 AscendC::LeakyRelu(reluOutLocal, reluOutLocal, (cType)alpha, tiling.baseM * tiling.baseN); reluOutQueue_.EnQue(reluOutLocal); // 步骤5:将输出结果搬运到Global Memory上 reluOutQueue_.DeQue<cType>(); ... AscendC::DataCopy(cGlobal[startOffset], reluOutLocal, copyParam); reluOutQueue_.FreeTensor(reluOutLocal); computeRound++; } matmulObj.End(); }
由上文可知,Ascend C的并行编程范式核心要素是:一组并行计算任务、通过队列实现任务之间的通信和同步、开发者自主表达对并行计算任务和资源的调度。本节介绍编程模型的实现原理,作为扩展阅读,便于开发者更好的理解编程模型的设计思路和优势,对于后续的深度开发也会有所帮助。
每个并行任务Stage的编程范式如下:
以最简单的矢量编程范式为例,在调用上述接口时,实际上会给各执行单元下发一些指令,如下图所示:
由此可以看出,Enque/Deque主要解决了存在数据依赖时,并行执行单元的写后读同步控制问题。
由此可以看出,AllocTensor/FreeTensor主要解决了存在数据依赖时,并行执行单元的读后写同步控制问题。
通过上文的详细说明,可以看出异步并行程序需要考虑复杂的同步控制,而Ascend C编程模型将这些流程进行了封装,同时对外界面上通过Enque/Deque/AllocTensor/FreeTensor这种开发者熟悉的资源控制方式来体现,同时达到了简化编程和易于理解的目的。