Tandem Processor是一种神经网络加速器,其考虑了GEMM与非GEMM计算的协同优化。
论文全称:《Tandem Processor: Grappling with Emerging Operators in Neural Networks》
1. 背景
1.1 非GEMM计算优化的必要性
作者认为,当前存在许多针对GEMM(通用矩阵乘法)的优化设计,但却鲜少考虑非GEMM的设计。主要是因为之前神经网络中GEMM计算占比较大,但是现在的新型神经网络中非GEMM的占比越来越大。
非GEMM计算包括以下五类:数学运算(元素加乘、求幂、比较等等)、激活函数、Reduction操作、数据shape转换、精度转换。

显然,这些计算都是神经网络中的常见运算,作者研究了它们在一些经典模型中的使用次数(不知道是不是精心挑选的结果,后面实验环节也是测试对比这些模型的计算性能)。总体来看,新的神经网络模型非GEMM使用次数越来越多:

当然,使用次数多并不代表其迫切需要优化,还需要对比GEMM和非GEMM的计算用时:

Baseline(1)和Baseline(2)是两种计算单元,后面会提到。这里主要是表明了非GEMM计算用时越来越多,逐渐超过GEMM的计算用时。另外,传输延迟(PCIe)的占比也不容小觑。
1.2 非GEMM计算的特点
非GEMM计算通常穿插在GEMM计算之中,这就导致GEMM单元和非GEMM单元之间的频繁数据交换:

此外,非GEMM计算通常是内存受限的,而GEMM计算是计算受限的(GEMM的数据重用性较强)。下面的Roofline模型展示了这一点,除了Softmax和GELU外(包含指数、除法等,计算强度较高),其余都位于虚线左边,它们加载每字节的计算强度较低,所以性能被内存带宽限制:

1.3 现有的处理非GEMM层计算的方法
后面实验环节也是将Tandem处理器和上面这几种处理器进行对比。
2. Tandem处理器设计
2.1 串联执行
Tandem相比于片上通用向量处理单元更加细粒度。传统的都是以层为单位,先计算GEMM层再计算非GEMM层,然而一层的数据较多,寄存器文件放不下一整个矩阵,只能让GEMM单元先将计算结果store到缓存中,然后非GEMM单元再根据计算需要load到regfile中进行计算。
而Tandem以分块矩阵(tile)为单位,不需要regfile,没有LD/ST延迟。如下图所示,GEMM单元计算完一个分块后就可以直接交给非GEMM单元来运行了,同时GEMM单元可以处理下一个分块,如此串联执行。

注意到有一个Inst.Buf用于存放指令,如下图所示,指令分为GEMM指令和Tandem指令,GEMM指令通常比较简单比如matmul、conv,一条指令即可启动运算。Tandem处理器完成非GEMM计算则需要多条指令。在指令分发阶段(Inst.Dispatch),会将GEMM指令分派给GEMM单元,其余非GEMM计算指令仍然保留在Inst.Buf中。
当FSM为Tandem(只有非GEMM计算),或者为GEMM-Tandem(GEMM和非GEMM串联计算)且GEMM计算完一个分块时,Tandem就会去Inst.Buf中读取指令并运行。

OBUF存放GEMM的输出结果,因为OBUF存在读后写相关(只有Tandem将OBUF数据读出之后GEMM单元才能写入新的数据),所以OBUF使用双缓冲机制,这样GEMM单元即可连续地运行。
作者还另外使用了同步指令,当Tandem处理器读取完OBUF中的数据后,会通过OBUF_done告知GEMM单元OBUF空闲。同步指令可能是在GEMM单元运行较快而Tandem执行较慢时,GEMM单元已经计算出一个分块结果并放入OBUF1中,但是Tandem还在处理OBUF2的分块,此时如果等待Tandem处理完再执行GEMM计算就慢了一点,只需要Tandem将OBUF2中数据读出后就可以启动GEMM单元了。

同步指令如上图所示,func字段包括⟨GEMM/SIMD,START/END,EXEC/BUF,X⟩(X代表未使用),⟨GEMM/SIMD, START/END⟩可用于标识代码区域属于GEMM还是SIMD(Tandem),方便指令分发器进行指令分发。
EXEC用于通知FSM Tandem已经处理完一个分块,处于空闲状态。
BUF通知FSM产生OBUF_done信号允许GEMM单元向OBUF写入新的数据。
2.2 内存子系统
前面提到,GEMM和Tandem之间通过OBUF传递数据,但是数据并不完全来自GEMM单元,也可能来自片外,或者是立即数。
作者将Tandem处理器的scratchpads(片上暂存器)称为命名空间(Namespace),其中:

Tandem处理计算采用多周期流水线,分为取值、计算、写回三个步骤,可以看到写回有一个箭头指向前面的Interim BUF 1&2,可能是因为从哪个bank取出的数据,最后计算完又需要写回到对应的bank中。
2.3 取值
从命名空间中取出数据需要先得到数据所在的地址,作者做了一个表:

可以通过Offset(可以理解为基地址)和Stride(步长)得到地址(后面再介绍得到地址的方法),但是Offset和Stride所需位数较多,不方便放入指令中。那么就通过Scratchpad ID(命名空间ID,前面提到,有三个命名空间,每个命名空间各自维护一个子表)和Iterator Index(迭代索引)来获取这两个值,Scratchpad ID和Iterator Index使用的位数较少,可以放入指令中。
那么这个表如何构建呢?其实也是通过指令:

ns id和iter idx就是Scratchpad ID和Iterator Index。通过func决定写入的是Offset还是Stride。另外,还可以通过该指令配置IMM BUF,也是通过func字段来确定。
然后在计算的时候就可以在源和目的字段指定ns和iter idx即可,其中func字段指定操作类型(ADD等计算操作、MOVE等移动类操作、CALCULUS计算绝对值和符号等、COMPARISON逻辑比较):

2.4 嵌套循环
前面提到,可以通过ns和iter idx得到offset和stride,进而得到地址。
这个地址的计算与循环相关,假设执行一个ADD操作,它包含五重循环:

最外层循环是c,然后二三重循环是矩阵的分块数(每列有h/br个分块,每行有w/bc个分块),最内层两重循环则是分块的行列数(br和bc)。假如该三维矩阵在内存上的是这样保存的:

那么对于最内层循环,只需要不断+1即可获得下一个元素的位置,所以它的stride为1。
倒数第二层循环,需要获得下一行元素的位置,所以它的stride为w(矩阵列大小)。
依此类推,可以看到,不同循环层级它的stride是不一样的,所以iter idx即是用来表明这是第几层循环,然后再配置该层循环的offset和stride。
不过从这个图来看,为每个循环层级设置offset似乎没有必要,因为offset都是一样的。
文中的offset应该指的是每一个分块的基地址,因为Tandem以分块为单位计算,知道了分块基址后,通过列stride和行stride即可得到分块中每个元素的位置。
此外,由于循环会带来分支开销,作者这里使用Code Repeater,Code Repeater会记录每一层循环的迭代次数,以及当前的迭代次数,当内层循环完成后,外层循环的迭代次数-1,通过这种方式来代替分支。另外,还会记录循环的ns和idx。同样,这通过指令来指定:

loop id指定第几层循环,因为它只有3位,所以循环嵌套不能大于8,func指定写入的是迭代次数、<ns,idx>、还是循环体的指令数等等。
综合这几个指令:
首先通过Loop指令为每一层循环设置ns(数据所在的命名空间编号)和idx(循环层次的编号)。
然后通过Configuration指令为每层循环建立<ns,idx>到<offset,stride>的转换表。
最后调用Compute指令开始计算,此时可以通过<ns,idx>得到<offset,stride>进而得到每一个操作数的地址,有专门的计算地址部件。执行完一次计算后,Code Repeater判断循环是否结束,否则继续+stride得到下一次要计算的操作数的地址。
2.5 片上与片外数据传输
图9还有个Data Access Engine,它用于片上与片外数据的传输,同样通过多条Off-chip Data Movement指令来确定:

func2用于确定存放到片内的Interim BUF 1还是2。这里也需要基地址(offset)以及各循环层数的stride得到写入的目标地址,所以func1指定写入到Data Access Engine中的是基地址、loop iter、还是stride。
2.6 数据转换
数据转换比如Reshape、精度转换通过Permute Engine完成。由下面指令确定:

src/dst以及dim idx指定源和目标维数,维数决定循环的层数。比如从<c,h,w>reshape到<c,h*w>,源的维数为3,循环层数也是3,而目标的维数是2,循环层数也是2。
同样每个循环层级,都需要指定Base addr、loop iter、stride。
另外,Imm字段的低位(LSB)用于决定是否进行数据shuffle,因为有些reshape只需更改Base addr、loop iter、stride即可,比如从<c,h,w>reshape到<c,h*w>,reshape前后对访存影响不大,此时可以不shuffle。
但是如果从<c,h,w>reshape到<h,w,c>,此时就需要shuffle,否则数据不连续,缓存不命中。shuffle过程会产生数据的搬移。
2.7 编译过程

编译过程文中描述较少,总体流程是从ONNX Model到中间模板,最后生成指令。
上面这个图可能有问题,比如应该是“for m1(M1)”,而且”c1=ADD(a1,b1)”也不太对,因为这个只需要执行一次就行,不需要内层两个循环,如果是“c1[m1][n1]=ADD(a1[m1][n1],b1[m1][n1])”那才需要内层循环。
汇编代码也只有两重循环,SET ITER 0和SET ITER1,不过这个汇编也不是很能看懂,所以这一章节有理解错误的地方也不奇怪。
3. 实验
3.1 与第(1)和第(2)类处理器对比
前面提到第(1)类是非GEMM在CPU上算,第(2)类支持部分非GEMM计算单元,不支持的部分仍然在CPU上算。

图14以第(1)类为基准来计算加速比,图中显示Tandem比(1)/(2)类要快(平均分别提高了3.5×和2.7×),主要是没有PCIe通信开销,而且片上串联运行减少了数据访问开销(LD/ST到regfile)。图15显示耗电情况,Tandem更加省电,因为片外CPU比较耗电,其TDP(散热设计功耗)为165W,而Tandem的为2.7W。
3.2 与第(3)类处理器对比
第(3)类处理器的例子是Gemmini,带有片上RISCV的处理器,以及专用计算单元。


图16以Gemmini的单核片上RISCV作为基准来计算加速比,将Gemmini的片上RISCV扩展为32核(与Tandem的SIMD通道一致)后,多核Gemmini计算比单核Gemmini要快,但还是比Tandem慢。
图17以Gemmini的单核片上RISCV为例探讨其计算时间的分布,对于MobileNet-V2和EfficientNet来说,其卡在专用计算单元上(im2col操作较为耗时)。不过更值得关注的是ResNet-50这些,RISCV内核计算用时太长,甚至可能不如第(2)类使用片外高性能CPU,虽然多了PCIe的通信时间,但计算时间比单核RISCV快得多。
3.3 与第(4)类处理器对比
第(4)类是GEMM+SIMD,虽然Tandem也是GEMM+SIMD,不过更加细粒度,而且还有其它方面优化。
3.3.1 谷歌TPU+VPU

图18以谷歌TPU计算速度为基准,对比了不同优化方法对于性能的提升程度。提升程度最大的是在串联处理器中支持专 门循环执行,平均为 2.1×,尤其是对于MobileNet-v2 和 EfficientNet(具有五个嵌套循环的操作)而言提升非常明显。
其次是取消片上暂存器到寄存器文件的LD/ST延迟,平均为1.4×。
Tandem可直接访问OBUF,VPU需要FIFO来接收来自GEMM的数据,这一点平均提速1.1×。
最后VPU为特殊函数(exp、sqrt、clip)提供硬件支持和专用指令,这一点比Tandem强,所以考虑了这一点后,Tandem的Speedup下降了一些。不过提供这些支持也意味着VPU的面积和设计复杂度增加。

图19是能耗对比,Tandem通过消除RegFile及其LD/ST开销可带来最大的能耗降低,平均为 1.2×。另外需要注意的是VPU的专用硬件支持节省了指令数,会使其能耗降低。
3.3.2 GPU

图20以Jetson Xavier为基准来计算每瓦特性能优势,与Jetson Xavier相比,RTX 2080 TI的能效较低(平均低20%),而NPU-Tandem的能效相比于基准提高了4.8×。
不过可以看到在MobileNet-v2和EfficientNet中,RTX 2080 TI的能效比Jetson Xavier高,因为与使用线程数量相对较少的Jetson Xavier相比,RTX 2080 TI可以更好地在丰富的线程间并行深度分解。
3.3.3 A100
作者将NPU-Tandem的GEMM(GEMM单元的MAC单元)和非GEMM(串联处理器中的ALU)资源都放大了216×,以匹配A100在GEMM和非GEMM运算方面的TOPs,使得两种设计在GEMM和非GEMM操作中使用的资源量相同。

图21以A100执行CUDA时的速度为基准来计算加速比。平均而言,等TOPS条件下,NPU-Tandem的性能与执行TensorRT的A100GPU相似(提高了2.5%),与执行CUDA的A100相比,NPU-Tandem的速度提高了4.0×。VGG-16和Yolov3因为有大量GEMM计算,所以A100性能会好一点。
图22和图23都是为了说明NPU-Tandem在非GEMM计算上的性能提升,不过图22似乎不好说明这点。
3.4 其余实验

图24分析了Tandem处理器在运算时的各操作的时间占比,对于VGG-16、Yolov3,非GEMM计算用时很少,不过前面也提到,因为这两个模型主要是GEMM计算,本身非GEMM计算就少,所以不清楚作者想用这个图表达什么,可能就是单纯放一个图。
图25分析了能量开销的情况,最多的是循环控制以及地址计算,占40%左右的能耗。
图26是面积占比,ALU逻辑占用的面积最大(56.6%),其次是Interim BUF1&2(29.2%),第三是permute逻辑(12.0%)。其余面积主要用于复用逻辑、流水线寄存器、代码分发器和解码逻辑。
关于Tandem的设计,原文写得比较分散,尤其是地址计算,原文3、4、5章都讲了一些,所以这里我只能凭借自己的理解来写,将这几章的内容合并,可能理解有不到位的地方,仅供参考。