Ascend C异构并行程序设计:昇腾算子编程指南
上QQ阅读APP看书,第一时间看更新

1.3.1 计算单元

计算单元是AI Core中提供强大算力的核心单元,相当于AI Core的“主力军”。AI Core的计算单元主要包含标量计算单元(Scalar ALU)、向量计算单元(VEC ALU)、矩阵计算单元(CUBE ALU)和存储计算临时变量的通用寄存器(General Register),如图1-8中虚线框包含的区域所示。矩阵计算单元主要完成矩阵计算,向量计算单元负责执行向量计算,标量计算单元主要用于各类型的标量数据计算和程序的流程控制。

图1-8 AI Core的计算单元

1.标量计算单元

标量计算单元负责完成AI Core中与标量相关的计算。它相当于一个微型CPU,控制整个AI Core的运行。标量计算单元可以对程序中的循环进行控制,实现分支判断,其结果可以通过在事件同步模块中插入同步信号的方式来控制AI Core中其他功能性单元的执行流水线。此外,它可以将不属于标量计算单元执行的指令发射到对应执行单元的执行队列中。它还为矩阵计算单元或向量计算单元提供数据地址和相关参数的计算结果,并且能够实现基本的算术运算。复杂度较高的标量运算则由专门的AI CPU通过算子完成。

在标量计算单元周围配备了多个通用寄存器。这些通用寄存器可以用于变量或地址的寄存,为算术逻辑运算提供源操作数并存储中间计算结果,还支持指令集中一些指令的特殊功能。通用寄存器一般不可以直接访问,只有部分任务可以通过 MOV 指令读写通用寄存器。AI Core 中具有代表性的专用寄存器包括 Core ID(用于标识不同的 AI Core)、VA(向量地址寄存器)及STATUS(AI Core运行状态寄存器)等。软件可以通过监视这些专用寄存器来控制和改变AI Core的运行状态和模式。

由于达·芬奇架构在设计中规定了标量计算单元不能直接通过DDR或HBM访问内存,且自身配给的通用寄存器数量有限,所以在程序运行过程中往往需要在堆栈空间存放一些通用寄存器的值。只有需要使用这些值时,才会将其从堆栈空间中取出来存入通用寄存器。为此将UB的一部分作为标量计算单元的堆栈空间,专门用作标量计算单元的编程。

2.向量计算单元

AI Core中的向量计算单元主要负责完成与向量相关的计算,能够实现单向量或双向量之间的计算,其功能覆盖基本的和定制的计算类型,主要包括FP32、FP16、int32和int8等数据类型的计算。向量计算单元可以快速完成两个FP16类型的向量加法或乘法计算,如图1-9所示。向量计算单元的源操作数和目的操作数通常保存在UB中,一般需要以32字节为基本单位对齐。对向量计算单元而言,输入的数据可以不连续,这取决于输入数据的寻址模式。向量计算单元支持的寻址模式包括向量连续寻址和固定间隔寻址。

图1-9 向量加法和乘法计算

向量计算单元可以完成深度神经网络中的向量函数运算,尤其是卷积神经网络计算中常用的ReLU(Rectified Linear Unit)激活函数、池化(Pooling)、批归一化(BatchNorm)等功能。经过向量计算单元处理后的数据可以被写回UB中,以等待下一次运算。上述所有操作都可以通过软件配合相应的向量计算单元指令来实现。向量计算单元除了提供丰富的计算功能,还可以实现很多特殊的计算函数,从而和矩阵计算单元形成功能互补,全面提升 AI Core对非矩阵类型数据计算的能力。

3.矩阵计算单元

(1)矩阵乘法

由于常见的深度神经网络算法中大量使用了矩阵计算,达·芬奇架构中特意对矩阵计算进行了深度优化,并定制了相应的矩阵计算单元来支持高吞吐量的矩阵处理。图1-10表示一个矩阵A和另一个矩阵B之间的乘法计算A×B=C,其中M表示矩阵A的行数,K表示矩阵A的列数及矩阵B的行数,N表示矩阵B的列数。

图1-10 矩阵乘法

在传统CPU中,计算矩阵乘法的典型代码如下。

for (m=0; m<M, m++){
     for (n=0; n<N, n++){
          for (k=0; k<K, k++){
               C[m][n] += A[m][k]*B[k][n]}}}

该代码需要用到3个循环进行一次完整的矩阵乘法计算。如果在一个单发射的CPU上执行此代码,总共需要M×K×N个时钟周期才能完成,当矩阵非常庞大时,这个执行过程极为耗时。在CPU计算过程中,矩阵A按行扫描,矩阵B按列扫描。考虑到典型的矩阵存储方式,无论矩阵A还是矩阵B,都会按行存放,也就是所谓的行主序(row-major order)的方式。而读取内存的方式具有极强的数据局部性特征,也就是说,当读取内存中某个数的时候,会打开内存中相应的一整行并且把同一行中所有的数都读取出来。这种读取方式对矩阵A是非常高效的,但是对矩阵B却显得非常不友好,因为代码中矩阵B需要按列读取。为此,需要将矩阵B的存储方式转成按列存储,也就是所谓的列主序(column-major order),如图1-11所示,这样才能够符合内存读取的高效率模式。因此,在矩阵计算中往往通过改变某个矩阵的存储方式来提升矩阵计算的效率。

图1-11 矩阵B存储方式从行存储转成列存储

(2)矩阵计算单元的计算方式

在深度神经网络实现计算卷积的过程中,关键的步骤是将卷积运算转换为矩阵运算。在CPU中,大规模的矩阵计算往往成为性能瓶颈,而矩阵计算在深度学习算法中又极为重要。为了解决这个矛盾,GPU采用了通用矩阵乘法(General Matrix Multiplication,GEMM)的方法来实现矩阵乘法。例如要实现一个16×16矩阵与另一个16×16矩阵的乘法,需要安排256个并行的线程,并且每个线程都可以独立计算完成结果矩阵中的1个输出点。假设每个线程在1个时钟周期内可以完成1次乘加计算,则GPU完成整个矩阵计算需要16个时钟周期,这个延迟是GPU无法避免的重大瓶颈。而昇腾AI处理器芯片针对这个问题做了深度的优化,AI Core对矩阵乘法计算的高效性为昇腾AI处理器芯片作为深度神经网络的加速器提供了强大的性能保障。

达·芬奇架构在AI Core中特意设计了矩阵计算单元作为昇腾AI处理器芯片的核心计算模块,意在高效解决矩阵计算的瓶颈。矩阵计算单元提供超强的并行乘加计算能力,使AI Core能够高速处理矩阵计算问题。通过设计精巧的定制电路和极致的后端优化手段,矩阵计算单元可以快速完成两个16×16矩阵的乘法计算(标记为163,也是Cube这一名称的来历),等同于在极短时间内进行163=4096个乘加计算,并且可以达到FP16的计算精度。矩阵计算单元在完成图1-12所示的A×B=C的矩阵计算时,会事先将矩阵A按行存放在矩阵计算单元的L0A缓冲区中,同时将矩阵B按列存放在矩阵计算单元的L0B缓冲区中,通过矩阵计算单元计算后得到的结果矩阵C按行存放在L0C缓冲区中。

图1-12 AI Core的矩阵计算单元计算

矩阵乘法计算的具体执行由软硬件协同完成。假设矩阵AB都是16×16的方阵,那么矩阵C的256个元素可以由256个矩阵乘法子电路硬件在1个时钟周期完成计算。图1-13给出了矩阵A的第一行与矩阵B的第一列如何借助归约算法由矩阵乘法子电路完成点积操作,从而计算出矩阵C的第一行第一列元素的过程。当矩阵的尺寸超过16×16,则需要通过软件实现特定格式的数据存储和分块读取。

图1-13 矩阵计算单元电路

4.数据存储格式

在深度学习中,同一个张量存在很多不同的存储格式。尽管存储的数据相同,但不同的存储顺序会导致数据的访问特性不同,因此即便进行同样的运算,相应的计算性能也会不同。在本小节中,我们首先介绍ND存储格式,随后介绍达·芬奇架构为了更高效地搬运和计算所采用的Nz存储格式。

(1)ND格式

一般来说,在计算机内存中,由于只能线性地存储数据,多维数组可以被看作一维数组的扁平化表示。

ND(N-Dimension)格式是深度学习网络中最常见、最基本的张量存储格式,代表N维度的张量数据。以一个矩阵为例,矩阵是二维张量,在这种格式下按照行主序进行存储,即张量中每一行(每一维)被依次存储在内存中,这意味着相邻的元素在内存中的地址是连续的。这种存储格式是目前很多编程语言中默认的方式,例如C++语言。

以下述矩阵为例:

其按照ND格式进行存储,结果如下:[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15]。

实际网络中会要求按照一定顺序存储多个矩阵数据,如图1-14所示。在这里可以按照深度学习领域中的惯例做出的抽象,其中N代表矩阵的个数,W代表矩阵的宽度,H代表矩阵的高度。

图1-14 存储多个矩阵的排布

上述数据按照ND格式进行存储的物理排布如下:[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,…]。

先对一个矩阵进行行主序存储,接着再存储相邻的下一个矩阵,同样是进行行主序存储,直到存储完所有的数据。即此种方式先按W方向存储,再按H方向存储,最后按N方向存储,直到存储完所有数据。我们也称这种数据排布格式为NHW格式。

(2)Nz格式

为了更高效地搬运和进行矩阵计算,达·芬奇架构引入一种特殊的数据分形格式——Nz格式。

Nz格式的分形操作如下:整个矩阵被分为(H1×W1)个分形,按照列主序排布,即类比行主序的存储方式,列主序是先存储一列,再存储相邻的下一列,这样整体存储形状如N字形;每个分形内部有(H0×W0)个元素,按照行主序排布,形状如z字形。Nz格式的物理排列方式如图1-15所示。

图1-15 Nz格式的物理排列方式

下面展示一个分形内部有2×2个元素的实际数据样例,借此让读者更加直观地理解这种数据存储格式。如图1-16所示,这是一个按照Nz格式进行数据存储的样例,图中虚线箭头表示数据存储的顺序。

图1-16 Nz格式存储实际数据样例

上述数据按照Nz格式进行存储的物理排布如下:[0,1,4,5,8,9,12,13,2,3,6,7,10,11,14,15,16,17,20,21,…]。

首先对一个分形内部进行行主序存储,然后在一个完整矩阵中以分形宽度划分,进行列主序存储,最后依次对相邻的下一个矩阵进行存储。此方式先按W0方向存储,再按H0方向存储,接着按照H1方向存储,随后按照W1方向存储,最后按N方向存储,直到存储完所有数据。我们也称Nz数据排布格式为NW1H1H0W0格式,且由于其存储顺序形象展示为先“N”后“z”,故简称为Nz格式。

从ND格式转换为Nz格式,需要进行的操作是平铺、拆分及转置,这个转换过程示例如图1-17所示。

图1-17 ND格式转换为Nz格式的过程

读者也可以通过图1-18来直观感受从ND格式转换为Nz格式的过程。

图1-18 ND格式转换为Nz格式的直观流程