矩阵乘法加速硬件汇总详解
矩阵乘法加速硬件汇总详解
矩阵乘法是深度学习和机器学习中最重要的计算操作之一。为了加速矩阵乘法的计算,各种硬件加速器应运而生,包括CPU、GPU、NPU、TPU等。本文将详细介绍这些硬件加速器的矩阵计算原理,以及类脑芯片、忆阻器等前沿技术的介绍。
CPU中的矩阵计算
在CPU中,矩阵计算通常采用三重循环的方式实现,如下所示:
for (unsigned int i = 0; i < hA; ++i) {
for (unsigned int j = 0; j < wB; ++j) {
float C[i][j] = 0;
for (unsigned int k = 0; k < wA; ++k) {
C[i][j] += A[i][k] * B[k][j];
}
C[i][j] = Cij;
}
}
这种计算方式遵循冯·诺依曼计算架构,通过内存访问和计算的分离来实现矩阵乘法。CPU支持矩阵相乘的库包括OpenBLAS和Intel MKL。
GPU中的矩阵计算
GPU为了实现并行运算,主要的思想是将矩阵分为多个子矩阵,然后同时计算它们的乘积。GPU支持矩阵相乘的库包括cuBLAS和cuDNN。
AI芯片中的矩阵计算
AI芯片主要用于处理深度学习任务,其核心指标包括OPS(每秒操作数)、MACs(乘积累加操作数)、FLOPs(浮点运算数)和MAC(内存访问成本)。在AI芯片中,卷积操作通常被转换为矩阵相乘的形式,如下图所示:
Nvidia TensorCore矩阵相乘过程
TensorCore的核心思想是在矩阵相乘的过程中,先分别计算需要相乘的部分,然后再进行相乘部分的相加。下图展示了AB+C的过程,其中1-3步解释的是AB,第四步是相加:
Google TPU矩阵相乘过程
TPU使用专为执行机器学习算法中常见的大型矩阵运算而设计的硬件,具有芯片上高带宽内存(HBM),可以使用较大的模型和批量大小。TPU可以组成Pod组,这样无需更改代码即可纵向扩容工作负载。
类脑芯片
类脑芯片采用存算一体的方式进行计算和处理数据,突破了冯·诺依曼架构的限制。目前发展的类脑芯片包括TrueNorth(IBM-2014年)、英特尔Loihi(2018年)、天机Tianjic(清华大学-2019年)、达尔文Darwin(浙江大学和之江实验室-2020年)和苏轼SHSHI(中科院-2023年)。
忆阻器
忆阻器是一种存算一体电子突触,受流过的电荷影响而变化的元件。忆阻器阵列是一种利用物理规律完成的模拟计算。例如,可以通过在忆阻器阵列上加电压来实现矩阵和向量的相乘。
真北TrueNorth芯片
TrueNorth芯片具有100万个神经元,每个神经元有256个轴突输入。计算过程如下图所示:
苏轼SUSHI芯片
苏轼芯片采用超导神经形态芯片,需要极低的超导温度。其创新之处在于设计了状态控制器,完成了数据的存储和状态的翻转,绕开了传统的权重计算过程。
NVIDIA Turing架构的Tensor Core
Tensor Core是一种新型处理核心,它执行一种专门的矩阵数学运算,适用于深度学习和某些类型的HPC。Tensor Core执行融合乘法加法,其中两个44 FP16矩阵相乘,然后将结果添加到44 FP16或FP32矩阵中,最终输出新的4*4 FP16或FP32矩阵。
Tensor Core虽然在GPU里是全新的运算单元,但其实它与标准的ALU流水线并没有太大差别,只不过Tensor Core处理的是大型矩阵运算,而不是简单地单指令流多数据流标量运算。Tensor Core是灵活性和吞吐量权衡的选择,它在执行标量运算时的表现很糟糕,但它可以将更多的操作打包到同一个芯片区域。
NPU架构设计
CPU发射指令 vs NPU自己取指
两种方案:
- 类似传统的协处理器(如早期的FPU),是CPU发射指令,FPU负责执行单条(或线性顺序)指令和写回,而其他流水线的调度和控制(取指 Load/Store bypassing等)是CPU在做。
- 类似GPU,因为GPU自己的流水线很复杂,必须自己来独立取指,控制自己的运行。更像是个独立的CPU,只是共享主存,其他的和CPU的交互如同我们接着调试器的MCU。
这类NPU内部都会有一个小的cpu,用于指令预取、控制(分支 跳转)、流水线。
矩阵计算单元 标量 vs 向量
还是两种方案:
- spatial型,如脉动阵列(systolic array)。也叫标量MAC。
- 向量MAC,时间型(一组向量点积 一组向量点积..),如H树阵列,。
前者:TPU(Google) 众多初创公司(CGRA Wave Inferentia Samba-Nova) 等
后者: GPU(nVidia) MatrixCore(AMD) 昇腾 寒武纪 等
第一种,我个人认为属于另类。其基本的逻辑是,一个运算之后,结果不用写回寄存器或cache,而是直接送给邻近单元做下一个运算:
这一波热潮的开头者,还是google的TPU(上图),衍生了众多变种,架构都同下图
一个标量运算单元称作一个PE,构造都长这样:
采用这种方式唯一的动机,在于抚平处理速度和内存速度的不平衡。
第二种方法用流水线,一样可以实现同样的效果。只是除了运算单元要n倍之外,解码和寄存器读写单元也要n倍,占用资源更多。
而第一种方法的代价,则是不够灵活,只适合做特定运算。
比如做卷积:
做矩阵乘法:
在卷积的例子中,一个权值固定在一个PE中,输入值流动,叫做Weight Stationary数据流。这是主流方式。
除此之外,还有一个输出的部分和固定在一个PE中,权重和输入值流动,叫做Output Stationary数据流。
显然,二维卷积和全连接层的数据复用特点不一样,前者权重被重复使用,后者一个权重只用一次,而输入的使用次数更多。
这也是这种空间方法要兼容不同网络计算时遇到的困难。
我们看第二种:
这时候最小单元PE不再是标量计算,而是向量计算。
C=A*B+D
输出是C矩阵里的一个数,等于A的行向量乘以B的列向量加上D的一位。
不要被下面这个图骗了,nVidia并没有公开tensor core的任何设计。
也并非一个clock这个矩阵mac就算完了。于是很多人在外围去测试破解。
这篇文章想看,tensorcore究竟多长时间算完一次。(流水线有多长)
结果是24个cycles,完成一次HMMA16816指令,也就是算一次:A=168 B=816 D=16*16
一次HMMA16816指令,tensorcore要算几次呢?如下图所示(下图是算1616的,自己换成168的)16次。
似乎可以猜测,之所以HMMA一次要算更大的矩阵,就是因为要掩盖流水线,减少数据依赖竞争。而一次tensor core计算的latency可能是8(流水线阶段数)。
最终对比,来自陈云霁老师的书,
总结一下,第一种标量单元方式,最大的优点是减少访存(32vs512),相应的缺点是灵活性差,只能支持提前定义好的特定算子。
1-D 向量计算单元
NPU有三种情况:
- 全是Vector Unit,矩阵计算也靠它;
- 没有Vector Unit,向量计算也靠Tensor Core;
- 有Vector Unit,作为Tensor Core补充。
目前主流的是第三种。
因为现在的CPU DSP都已经有SIMD了,512bit数据长度,所以协处理器模式的都支持向量计算。
Vector计算单元占用的面积只是张量计算的1/n(n是矩阵维度),所以NPU也不差这点。
向量计算单元用来做pooling, activation, normalization这些,理论上来说,性价比更高,让张量计算单元专心去做矩阵和卷积操作。
但其实,tensor core用来做这些reduction和element-wise操作也不见得慢:
着篇文章直接把一个1D向量,填满了两个矩阵,用来做reduction操作。最终取得了比CUB库(使用CUDA)更好的结果。
但是,我们依然容易理解增加一个向量计算单元的好处
- 适合更小的向量计算,尤其是国内普遍大核的现状下;
- 相当于multi-issue,增加了指令并行性。目前国内车载NPU,用于一体机的普遍是只用1个tensor-core,这时增加一个向量core就显得能大幅提速了。
最后提一下Reduction Tree:
Reduction Tree原本就是朴素的在做MLP,追求最少的乘法器和加法器。后来因为MLP不流行了,Transfomer开始流行,所以比较尴尬。
但是,用Reduction Tree可以实现动态配置,有研究用于解决稀疏计算的问题。见上图,挺复杂,需要专门得稀疏检查,建立索引,建立映射,固件算控制寄存器得配置。
目前稀疏问题的处理,小核处理稀疏问题有天然优势(特别稀疏的,可以全0);不太稀疏的(比如一半是0),用Compressed Sparse Row (CSR) 非Reduction Tree得效率可以追平Reduction Tree。
所以,上面这类复杂的Reduction Tree方案,尚无商用产品。
存储 gather-scatter
计算单元和存储单元速度的巨大不一致,是所有计算机体系架构的核心问题,NPU也不例外。
CPU和NPC共享一块主存地址,和CPU自己用的地址隔离;在GPU里是独立的显存芯片。一般是CPU维护堆,NPU维护栈。(对于3.1里的第一种类型,个人理解没有主动操作主存的能力,只能接收CPU发来的配置信息,完成既定动作,写入既定地址。)
指令、权重、输入 是相互独立的数据通道。考虑到权重和输入数据量较大,显然指令要提前预取很久,这样才能在exe阶段,准备好数据。并且写回cache和主存,也需要很多pipeline stage,不能让计算stall。
在编译阶段,就要静态指定哪些数据要写回cache和ram,哪些数据直接进入下一层计算。后者动态再打个tag,当一个数据再也不需要使用时,就可以丢弃了,否则一层cache满时要逐层write back。
继续详细分析之前,我们先看一个目前很有代表性的算子:
gather就是张量向量数据按index“查表”,它看上去很简单,但问题在于需要随机的去读Rin,并不是地址连续的去读Rin。
Scatter是随机的去写Rout,这更难一些,因为写就有cache一致性维护的问题。
稀疏运算,可以通过gather来把非0元素提取出来。
gather在CV最近很常见,因为BEV流行的原因(把2D的featuremap映射到BEVmap,就是对featuremap的index查表),下面只讲gather。
如果Rin维度很小,全在reg或L1 cache里,那理论上就没有额外的开销。可实际上,Rin是网络输入data,一般最多在片上sram。
我们知道顺序取数时,可以又dcache做预取,一次取很多等着慢慢用。
可是不停的随机去很大的sram里面取一个数,是很耗能量的,如果miss了,要去ddr里面去取,那计算就卡住了,慢200-1000倍,满足不了部署要求。看下图右侧时间柱的高度。
我们看一下nvidia的解决办法,业界楷模,当很多家都不知道存在这个问题时,nvidia已经实现了完美方案:
随机读才是常态,顺序读硬件加了一个合并器(Coalescer),统一处理可能的打包优化。反其道而行之。可见nvidia最初做GPGPU就是瞄着最通用的场景去的。
我们再来看高通的解决办法:
增加了一个TCM,可以软件取data,更大的带宽。注意到这是独立于原有dcache通道外新加的一路通道。
“ A worst-case gather from a hypothetical 512 KB, 8-way L2 could require 128 * 8 = 1024 tag comparisons. Since the TCM isn’t a cache, it avoids the overhead of tag checks. Hexagon doesn’t even try to do scatter and gather operations on cacheable memory, and only does them on the TCM.”
TCM不是cache,不用比较tag(判断是否hit)。从中我们也可以看出,没有经过特殊硬件设计的话,scatter-gather是很难实现的。
比如,三星第四代NPU,去年年底才预告支持scatter-gather。
同样是TCM方案
这次先讲到这里。
后面会更细致的做一些NPU内存响应和算力负荷的定量分析,对一些具体的模型。
我的计划,是讲完 NPU <> AI编译 <> 模型 <> 应用 完整图景。
每个部分都还有不少值得写的内容,比如 存算一体,多面体理论,PAC理论,一些CV里的理论等等。
当这些东西串起来,有可能会有新的发现。和排序被研究的如此透彻相比,目前CV NLP的领域还大部分未被发掘。
最后聊个轻松的话题。
经常看见的场景:
算法工程师说某NPU算子支持的不好。
当然如果是pytorch都没匹配好,那是NPU的问题。或者性能严重偏离,是编译参数优化的问题。这些之前的文章都讲过。
但有一种情况,就是
要用某个NPU就不支持的模型。
我们从本文看到,NPU很可能设计之初,就不支持一些算子,一些数据操作,一些输入大小,稀疏,控制指令;或只能以固定的权重或输入复用方式进行计算。
这是去麦当劳非要点卤煮。
先天局限。