打开微信,使用扫一扫进入页面后,点击右上角菜单,
点击“发送给朋友”或“分享到朋友圈”完成分享
本篇文章面向对寒武纪BANG C编程感兴趣的开发者,你只要有C语言的基础就能够很容易看懂这篇文章。整篇文章会先介绍BANG C开发的整个大致流程,随后会以具体例子来详细介绍BANG C的开发和优化,最后展示每一步优化的性能情况。
概述
BANG C语言是寒武纪针对MLU硬件提出的编程语言,它基于C语言扩展而来。BANG C采用异构编程,一个完整的BANG C程序包括host端和MLU端,host端和MLU端分别进行编程、编译,最后链接成一个可执行程序。host端使用c/c++语言进行编写,会调用寒武纪的CNRT接口执行控制部分和串行任务;MLU端使用BANG C特定的语法规则执行计算部分和并行任务。用户可以在host端输入数据,做一定的处理后,通过一个kernel启动函数将相应输入数据传给MLU端,MLU端进行计算后,再将计算结果拷回host端。
接下来就以矩阵乘的程序示例具体介绍BANG C的编程过程,以及如何利用MLU硬件架构优势去优化该程序。
Host端
在整个矩阵乘demo执行过程中,用户先输入参数m,k,n代表要计算的左右矩阵分别为m*k和k*n大小,随后host端对这两个矩阵进行随机赋值,将输入矩阵以及大小相应的参数传入MLU端进行矩阵运算,然后将运算结果传回host端,在host端打印矩阵乘的硬件处理时间。
host端关键代码如下:
① 输入左右矩阵初始化
② 准备相关参数,启动kernel,将参数传入MLU端
在优化的过程中,host端的代码基本不变,我们重点关注MLU端代码的开发和优化过程。
MLU端
在整个demo中,我们分成6步来逐一优化这个任务,希望帮助大家理解BANG C的使用和优化方法,具体介绍如下:
第一步是直接在GDRAM上使用循环和标量操作进行计算。无须对输入的矩阵作任何处理,使用矩阵乘公式直接计算,完全没有利用到MLU硬件架构的优势,所以整个计算时间很长。MLU端关键代码如下:
第二步是在第一步的基础上引入NRAM/WRAM的使用,每个core都有自己的NRAM和WRAM,虽然相比于GDRAM空间小,但是可以获得更高的读写带宽和更低的访问时延。所以在此种方式中,我们将输入的左右矩阵全部从GDRAM拷入NRAM中,在NRAM中进行计算,然后再拷回GDRAM。需要注意的是,为了展示方便,在这个例子中我们是假设输入的左右矩阵规模都为256*256,来保证输入的矩阵可以一次性拷入NRAM/WRAM。一旦输入矩阵规模超过NRAM/WRAM的空间大小时,则需要对NRAM/WRAM复用进行多次拷入和拷出。MLU端关键代码如下:
第三步是在第二步的基础上,使用BANG C提供的向量计算指令完成矩阵乘的计算,采用向量计算指令可以更好地发挥MLU硬件性能,减少计算时间。
我们先介绍下接下来几步要解决的矩阵乘中的矩阵规模大小问题,为了方便展示和读者理解,我们假设的是左矩阵规模大小为256*256,右矩阵规模大小为256*N(N可被256整除)。图示如下:
所以在此种方式中,我们可以将输入的左矩阵一次性拷入NRAM,同时由于MLU架构的特点,在执行卷积指令操作时,需要将输入的右矩阵拷入WRAM中,并且在向WRAM拷入前需要对数据进行量化处理和摆放成特定要求的数据摆放格式,然后使用__bang_conv指令进行计算,另外由于右矩阵规模较大,我们在代码中将右矩阵分批次拷入WRAM进行计算。MLU端关键代码如下,读者可能对下面截图中的一些参数感到疑惑,为读者简单介绍一下。all_round表示计算的循环次数,这和右矩阵规模大小相关;dst_stride和src_stride代表调整右矩阵数据摆放格式过程中的步长;total_times表示调整右矩阵数据格式需要的次数,因为目前MLU270上有64个卷积计算单元,所以需要将原本顺序摆放的数据按照64个为一组间隔摆放。
在第三步的计算中,我们只使用了1个MLU core进行计算,而第四步可以进一步采用16个core进行并行运算,根据输入矩阵规模的大小,将输入矩阵拆分成多份并分配给不同的core进行计算,最后再对计算结果进行合并。这就将原本由1个core承担的计算量分摊给了16个core,大大提高了计算速度。
MLU端关键代码如下,在实现过程中,我们会利用到与并行相关的内置变量,大概介绍一下下面代码截图中使用到的内置变量,taskDim表示任务规模,taskId表示程序运行时所分配的任务ID,在这步的方法中taskDim=16,taskId范围为[0,15]。更多关于taskDim和taskId的介绍,读者可以参考我们论坛的BANG C用户手册第5章的内容:http://www.cambricon.com/docs/bangc/developer_guide_html/4ProgrammingModel/index.html
第五步是在第四步的基础上引入SRAM,在MLU中,每个cluster中的4个core共享一个SRAM。在第四步中,因为使用了4个cluster的16个core进行并行计算,而同1个cluster上的4个core在从GDRAM上拷贝数据到各自的NRAM/WRAM时,会争抢这个cluster到GDRAM的带宽,从而导致数据读取速度降低。所以我们将数据先从GDRAM拷贝到SRAM,再从SRAM分发到NRAM/WRAM中,避免了调度争抢问题,提高了数据读取速度。特别注意的是,因为从GDRAM拷入数据到SRAM和从SRAM拷入数据到NRAM这两个操作是由两种不同功能的core执行(这个会在下一步的方法中解释),所以这两个操作是并行的关系。为了避免数据冲突,故我们要设置一个同步,保证数据从GDRAM拷入到SRAM之后,才能执行从SRAM拷入到NRAM的过程,在BANG C中我们可以使用内置的__sync_cluster()函数完成同步功能。图示如下:
整个执行过程如下图所示:
MLU端关键代码如下,其中clusterId表示此时执行任务的是哪个cluster,范围为[0,3]:
对于MLU270上的每个cluster除了4个一般的计算core之外,还有专门用以管理片上总线和SRAM的memory core。这就是上一步提到的两种不同功能的core。这同时也为我们使用流水线的优化手段创造了条件。
所以第六步在上面的基础上,实现了4个cluster并行,且每个cluster中的memory core和其他4个 MLU core构成流水线的计算模式。在每个cluster中,memory core只负责将数据从GDRAM拷入SRAM,其余的每个MLU core则负责从SRAM拷入数据、矩阵乘计算、将数据拷回GDRAM。在此优化方法中,我们设置了在SRAM上的两个变量input2SRAM1,inputSRAM2,初始时,memory core从GDRAM上拷入数据到input2SRAM1,当数据拷入完成后,4个core开始工作,它们将自己需要的数据部分从input2SRAM1拷入进行计算,在MLU core工作的同时,memory core不会停止工作,它会将下一次需要计算的数据从GDRAM拷入inputSRAM2,供给4个MLU core在下一次使用,减少了core下一次的等待时间,inputSRAM1和inputSRAM2交替读写重复上述过程直至所有数据计算完成。我们从中可以发现耗时很长的从GDRAM到SRAM的这一步被“藏起来”了。和原来相比,在相同的时间内,我们搬运了更多的GDRAM数据到片上并且完成了计算。那么为什么会使用两个SRAM变量对GDRAM上的数据进行拷贝呢?因为在上述过程中,MLU core在从SRAM读取数据的同时,SRAM也会从GDRAM写入数据,如果只使用一个SRAM变量,则很有可能导致MLU core应该读取的数据在读取前被写入覆盖。
有经验的开发者可能已经发现,这里使用的是一种常用的数据流控制的处理技巧,乒乓操作。
整个过程如下图所示:
MLU端关键代码如下:
测试性能
由于循环操作计算矩阵乘性能太差,计算时间太长,没有实际意义,故不在此向读者展示了,下图只罗列出后四步在相同规模下的硬件执行时间的比较。
m=256 k=256 n=327680规模下: | ||
耗时 (ms) | 提升幅度 | |
NRAM+conv+单核 | 83.637 | |
NRAM+conv+16核 | 14.142 | 491.40% |
SRAM+conv+16核 | 13.026 | 8.56% |
SRAM+conv+16核+流水 | 12.375 | 5.26% |
热门帖子
精华帖子