如果你在AI芯片相关的岗位工作,或者刚翻过几篇加速器论文,你一定绕不开脉动阵列这个词。Google的TPU、多家初创公司的NPU、还有各种边缘推理芯片,内部的运算核心几乎都跟它有关系。但很多人对脉动阵列的理解停留在"一个二维的PE阵列,数据像波浪一样流过"这种模糊印象上——听起来很美,真到自己写RTL或者搭硬件架构的时候,又不知道从何下手。
这篇文章我打算彻底讲透脉动阵列。不绕弯子,直接从它为什么叫"脉动"、数据到底怎么流动、三种主流数据流各自适合什么场景,再到一个3x3矩阵乘法的完整逐周期推演,最后聊聊我在实际项目里踩过的坑。不管你是在做芯片验证、体系结构设计,还是只想搞明白TPU到底牛在哪,这篇都值得你花十五分钟读一读。
1. 脉动阵列到底是什么
1.1 名字的来历:数据不是乱跑,是按节拍跑的
最早提出脉动阵列概念的是卡内基梅隆大学的H.T. Kung,那还是上世纪八十年代的事。他当时想解决一个很朴素的问题:处理单元(PE,Processing Element)越多,芯片越强,但数据搬运成了瓶颈——片外DRAM的速度根本喂不饱一堆计算单元。他的思路是把一堆简单的PE排成规则阵列,让数据在PE之间像血液在血管里一样,随着每个时钟节拍向前推进一步。这个"每个节拍推进一步"的特性,就是它叫"Systolic"(心脏收缩式)的原因——不是数据随机涌动,而是沿着固定方向被同步"泵"出去。
这个思路放到今天依然成立,因为本质矛盾没变:计算越来越便宜,访存越来越贵。一个MAC(乘累加)操作在28nm工艺下大约消耗0.2到0.3皮焦的能量,但从DRAM里读一个数据要消耗几十倍甚至上百倍的能量。脉动阵列的价值就在于把"计算密集"和"数据复用"结合起来,用结构代替控制,让每个数据从片外进来之后尽量多被利用几次,减少对片外存储的依赖。
1.2 一个经典的类比:工厂流水线和共享单车
理解脉动阵列最直观的方式是把它想成一条汽车装配线。工位(PE)固定不动,每个工位只干一件事,零件(数据)在工位之间传动,每个节拍往前挪一个工位。这种方式的优势很明显:工位之间只需传递半成品,不需要每个工位都去仓库领所有零件。
另一个更贴近底层的类比是共享单车调度。假设你有一条单向街道,每隔100米一个停车点,调度员骑着一辆三轮车沿着街道往前走,每到一个点放下一辆车、收走一辆车。三轮车就是流动的数据,停车点就是PE,每个PE只跟旁边两个邻居通信,不需要全局广播总线。这个"只跟邻居说话"的特性,决定了脉动阵列的物理实现可以做得非常规整——没有长线互联,没有复杂的交叉开关,时钟频率能跑得上去,功耗也容易控住。
2. 核心结构拆解:PE、数据复用与三种数据流
2.1 PE内部到底干了什么
一个PE本质上就是一个带寄存器的乘累加单元。正常情况下,它能完成的操作就是y += a * b。四个关键部分组成:乘法器、加法器、累加寄存器、以及输入输出寄存器。
- 乘法器做
a * b,得到一个部分积。 - 加法器把部分积累加到累加寄存器里。
- 输入寄存器缓存来自上游/左侧的数据,输出寄存器准备送给下游/右侧的数据。
- 控制信号决定是把累加结果输出走,还是继续留在PE内部做累计。
这就是整个阵列的最小单元,你把它复制成8x8、16x16、32x32,就构成了脉动阵列。很多人会问:PE这么大,count已经很高了,乘法器不是浪费吗?其实这里有个设计选择:脉动阵列的PE里乘法器必然是标配,因为它的目标就是高吞吐、低访存,而不是省面积。有些低功耗芯片会把稀疏场景下的PE简化成累加器或跳零器,但这是后话。
2.2 三种经典数据流:权重固定、输入固定、输出固定
脉动阵列之所以让人觉得乱,是因为数据可以"流"的方式有很多种。业界把三种最常见的拓扑总结为:权重固定(Weight Stationary, WS)、输入固定(Input Stationary, IS)、输出固定(Output Stationary, OS)。三种模式对应三种不同的数据复用策略。
权重固定的思路是:先把权重矩阵加载进PE里的寄存器,然后让输入激活值流过阵列,输出部分和被下游PE一步步累加。Google TPU用的就是这种风格。它的好处是权重一旦载入,就可以反复使用——这对推理场景太友好了,因为推理时权重是不变的,而数据batch可以一直送。缺点也很明显,权重复用的前提是权重要能塞进PE阵列的本地存储里,如果模型很大,权重就得频繁换入换出,这时候优势就没了。
输入固定的思路反过来:输入激活值被固定在PE里,权重从一侧流过,输出部分和逐级向下累加。这个模式适合激活值复用,因为卷积计算时同一个输入特征图会被多个卷积核共用,如果能把输入固定在片上,功耗就下来了。
输出固定则是让部分和留在PE里,权重和输入都流过去。这种模式对输出的累加过程最友好,因为不会出现一个输出由多个PE算完再汇总的局面,减少了对额外加法树的需求。
三种数据流没有绝对的优劣,只看你的场景匹配度。做推理芯片、权重相对小、batch吞吐大,WS是首选;做训练芯片、梯度回传频繁,OS和IS可能更适合。实际芯片里还经常混着用。
2.3 一张表说清楚差异
| 数据流模式 | 固定数据 | 流动数据 | 数据复用优势 | 典型代表 |
|---|---|---|---|---|
| 权重固定(WS) | 权重 | 输入激活值、部分和 | 权重复用,推理场景友好 | Google TPU v1 |
| 输入固定(IS) | 输入激活值 | 权重、部分和 | 激活值复用,卷积友好 | Eyeriss v1 |
| 输出固定(OS) | 输出部分和 | 权重、输入激活值 | 输出累加路径清晰,训练友好 | 部分高能效训练芯片 |
选型的时候还有另一个维度——阵列里数据是单向流动还是双向流动。经典论文里常见的是单向脉动,性能可控;但某些实际芯片为了减少加载延迟,会采用双向交替脉动,让数据从两侧进、中间出。这种设计会带来PE间布线密度上升,时序约束变紧,但是换来的是更短的加载空档期。
3. 实操视角:一个3x3矩阵乘法的完整推演
3.1 搭建拓扑和初始化
接下来我手把手把脉动阵列的运算流程走一遍。为了说明而不失一般性,选一个3x3的脉动阵列,让权重矩阵W(3x3)乘以输入向量X(3x1),得到一个3x1的输出向量Y。
首先把权重矩阵预加载到每个PE的寄存器里。PE[i][j]里存的是W[i][j],i表示行,j表示列。加载过程在芯片里通常是从片外DRAM按行写入,通过一个加载端口串行或并行地填充到阵列里。这一步在WS模式下只发生一次,后面算不同输入时都复用。
然后你把输入向量X的3个元素按照时间先后,从阵列左侧依次送入。注意,输入不是一次性全部进去的,而是每个周期推入一个元素。这是脉动阵列和普通并行阵列的显著区别——数据被"泵"进去,像节流阀一样一个个放。
3.2 逐周期推演:数据是怎么"滑"过去的
为了说清楚,我展开一下时间步(Cycle)的概念:
- Cycle 0:X[0]进入PE[0][0]的输入端。此时PE[0][0]做一个乘法:W[0][0] * X[0],并把结果暂存到累加寄存器。这个结果不是最终答案,因为Y[0]还需要加上W[0][1]*X[1]和W[0][2]*X[2]。
- Cycle 1:X[0]被移入PE[0][1],同时新的X[1]进入PE[0][0]。PE[0][0]计算W[0][0] * X[1],PE[0][1]计算W[0][1] * X[0]。两个PE都在工作,没有空闲。
- Cycle 2:X[0]进入PE[0][2],X[1]进入PE[0][1],X[2]进入PE[0][0]。三个PE同时做计算。此时PE[0][2]算的是W[0][2]*X[0],这是Y[0]的最后一个部分积。
- Cycle 3:按照WS模式的设计,部分和会向下累加流动。具体来说,PE[0][0]里算出来的Y0_part0在Cycle 3时沿垂直方向传到PE[1][0],同时PE[1][0]会计算W[1][0]*X[0]并把两者相加。然后继续往下游传递,每经过一行就累加一个部分乘积。
- 依此类推,到Cycle 5时,第一个输出Y[0]就会在阵列底部出来。
你会发现,这个过程中PE没有一刻闲着(边界上的空档期除外),每个周期所有PE都在做乘加。这就是"脉动"最舒服的状态——计算节奏紧密,数据利用率高。
3.3 为什么权重按对角线加载是常态
实际操作中,你还会发现一个别扭的点:权重加载进PE的寄存器的时候,如果不做任何预处理,数据流的开始阶段会出现某些PE空转。因为在Cycle 0,只有PE[0][0]有输入,其他PE都在等数据。
解决办法是对权重做对角线偏移加载(Diagonal Loading)。也就是说,在物理加载的时候,W[0][1]不要放在PE[0][1],而是放在PE[0][0]的下一拍能到达的位置。这样安排后,输入数据流一进入阵列,就能天然地和对应权重"碰上"。这个细节在写RTL或者配编译器映射的时候特别容易忽略,但直接决定了阵列的利用效率。很多论文里提到"利用律"低,就是没做对角线映射。
4. 为什么AI芯片都爱用脉动阵列
4.1 数据复用是最核心的账
算一笔账你就明白了。假设一个64x64的脉动阵列,权重矩阵64x64,输入batch有32个向量。如果是朴素SIMD阵列,每个数据从DRAM读一次用一次,你需要读326464*2个数(权重和输入都算上)。而对于脉动阵列,权重加载一次之后就在PE里住了下来,输入数据在PE间依次传递,每个输入数据会被64个PE通过不同的权重多次使用——精确地说,一个输入值会在阵列中沿行方向被传播64步,每一步都被一个PE用对应的权重乘了一次。这样一来,从片外读入的数据量降低了O(N)倍(N是阵列维度)。
这正好对应了前面说的那个核心矛盾:片内计算几乎不耗能,片外访存是能耗大头。减少数据搬运,就是减少能耗。脉动阵列的能效优势不是靠什么黑科技,只是单纯地把数据用得更狠。
4.2 硬件上的"规整"是另一个隐藏优势
脉动阵列的PE之间的连线只有相邻两点,这比你想象的重要得多。芯片物理设计的时候,全局广播总线是时序收敛的头号敌人——一条总线要驱动一大堆负载,线延迟和功耗都受不住。而脉动阵列的局部连接使得每根线的电容负载都很小,时钟频率可以拉得很高,设计也容易收敛。
另外,阵列的控制逻辑非常简单,基本上就是一个主状态机加上几组计数器,不需要复杂的指令译码和调度器。对芯片验证来说,这也降低了形式化验证的难度,因为PE都是同构的,只要验证一个PE的行为,再验证互联拓扑,整个阵列的行为就确定了。
4.3 高并行度下的同步计算
GPU用的是大量线程并行加缓存优化,CPU用SIMD加乱序执行,脉动阵列则是纯数据流驱动下的同步计算。它天然没有控制冒险和数据冒险,因为每个PE只依赖邻居的状态,不需要像乱序执行那样做复杂的冒险检测。这类同步计算架构在深度学习里特别好用,因为神经网络的计算图绝大部分都是规则的矩阵乘法加激活函数,几乎不需要分支跳转。
脉动阵列的高并行度是在空间上展开的,不是靠时间上的多发射或流水线深度挖出来的。你可以把它理解为"空间上的SIMD"——每条数据流的执行路径是固定的,不占用调度器资源。
4.4 局限性要说清楚
脉动阵列不是银弹。第一个明显的痛点是它擅长规则计算,面对稀疏矩阵时的利用率会骤降。比如一个90%稀疏的矩阵,你照样得把零权重塞进PE里做无意义乘加,白白浪费功耗。这也是后面很多稀疏加速器在脉动阵列基础上加跳跃逻辑、加位图掩码的原因。
第二个痛点是加载空档(Loading Bubble)。每次切换权重的时候,整个阵列都要重新填充,这个过程中PE都在空转。如果模型很大、权重频繁换,空档期的开销会抵消掉数据复用的收益。设计者通常会采用双缓冲机制屏蔽这个延迟:一个缓冲在做计算,另一个缓冲在预加载下一组权重。
5. 实际项目中的常见问题与排查技巧
5.1 时序跑不上去?先看PE间互联
我做过一次基于FPGA的脉动阵列原型验证,最初时序只能到100MHz,距离目标150MHz差很远。用时序报告一查,关键路径全都集中在PE间传递的路径上——因为我在每个PE输出端加了两级流水寄存器,但中间的组合逻辑(累加器和进位链)没有被均匀切分。
解决办法很简单:把累加路径上的寄存器再做一次平衡,让每级流水线只承担乘法器或加法器的一部分延迟。另外,脉动方向上的数据路径宽度也值得检查,如果数据位宽是从32位扩到32位,累加过程可能会有额外的进位链延迟,选好进位加法器结构(比如进位保存加法器)能明显改善时序。
5.2 算出来的结果对不上?八成是坐标对齐错了
我在仿真中遇到过一个经典错误:权重加载之后没有做对角线偏移,导致输出端的数据总是错位。具体表现是,Y[0]在某个周期出来后,Y[1]却早了一个周期,整个结果序列的顺序全乱了。这种问题在波形上看不出来,因为每个PE的计算结果都是对的,只是一一对应关系错了。
排查方法是把输入向量做成单位阵,直接把权重在PE里的位置暴露出来。也就是输入X是单位矩阵时,输出Y应该就是权重本身的转置,你只需要去每个PE里dump出来比对,很快就能定位是哪条路径错位了。
5.3 边界PE的空转怎么处理
任何一个有限大小的脉动阵列都有边界效应。最左边一列的PE在等待输入到来的最初几个周期是空转的,最右边一列的PE在输出送走后也是空转的。阵列越大,这种边界浪费占比越小,所以设计大阵列时这个问题不明显;但当芯片面积受限只能做小阵列(比如8x8)的时候,边界空转可能占掉15%到20%的有效周期。
常用手段是数组分区(Array Partitioning)加多路复用。把小阵列按照时间片轮流服务不同任务,让某个任务在做权重加载的时候,另一个任务在计算。这样边界空转可以被任务切换掩盖掉,整体利用率能上去。我实测下来,两个任务轮转的效果就很不错,再多任务调度复杂度就上来了,收益未必划算。
5.4 稀疏化改造的坑
给脉动阵列加稀疏支持是现在很多芯片在做的事。常见的方案是在每个PE里加一个跳零逻辑:如果发现某个权重为零,就不做乘法,直接把下游数据透传过去。听起来很完美,但在实现上贼坑。因为你跳过了一个PE的计算,数据到达下游的时间就变了,这意味着整个脉动节奏被打乱。如果你不做对齐处理,后面所有PE的累加顺序就全错了。
解决办法是让跳过的PE输出零而不是数据直通,保留流水节拍。这样一来,计算量确实减少了(乘法器没有翻转),但PE间的数据传递路径还保持原样,时序不会乱。还有一种更激进的做法是把稀疏矩阵在软件层做重排,让零元素集中在一行,然后整行跳过,这种方案对算法层的改动比较大,但硬件上更省。
6. 设计脉动阵列时的工具链与验证思路
6.1 从算法到RTL的一百种路
你现在想自己搭一个脉动阵列,最顺手的路径是先用Python或NumPy写一个行为级模型,把数据流和时间周期都模拟清楚。然后在这个模型上做算法到阵列的映射,验证映射器的逻辑是否正确。接着才进入RTL阶段。
我在实际项目里用的是三步走策略:
- 用行为级模型做功能验证,专门让人在Python上算矩阵乘法,再对比脉动阵列逐周期输出的结果。
- 写一个参数化的Chisel或Verilog RTL模板,阵列大小、数据位宽、累加位宽都做成参数,方便跑不同配置。
- 做FPGA原型验证时,把真实模型的中间层数据dump下来对比,而不是只看最终结果——这样能快速发现坐标错位、累加时序等问题。
6.2 验证重点清单
脉动阵列的验证重点和普通ALU完全不同。普通ALU只要测指令和数据的组合,脉动阵列则要测数据流的时间关系。下面这份清单是我每次做脉动阵列design review必对的项:
- 权重加载阶段有没有产生气泡(Bubble),如果产生了,后续输出的时间戳对不对。
- 累加位宽是否足够——中间累加和的最大值是N(N是阵列维度)倍的单精度乘积,如果位宽不够,会有溢出截断。
- 多时钟域设计下,输入数据的节拍和阵列时钟是否完全对齐。
- 权重切换时,旧权重是否已经被完全冲刷出新计算区域,避免新旧权重混在一起。
6.3 我踩过最深的一个坑
有次我做一个16x16阵列,仿真一切正常,跑到FPGA上却偶尔出现计算结果错位。一开始以为是时序问题,后来加了很多探针才发现是复位信号没有做全局同步,有些PE复位早一个周期,有些复位晚一个周期,相当于初始状态就没对齐。
这个坑教训很深刻:脉动阵列对"初始状态一致性"极度敏感。解决方法是所有PE的复位信号都用同一级同步器打一拍,然后通过一条平衡树分发到每个PE。这不算什么高深技术,但在硬件里它是一个非常容易忽略的工程细节,只要阵列大一点、复位树长一点,就容易出问题。
7. 扩展思路:脉动阵列还能往哪个方向演进
7.1 近存计算与脉动阵列的结合
现在很多AI芯片把SRAM直接堆在阵列旁边,目的就是让权重加载的路径变短。如果你做设计时面积有余量,完全可以参考这种思路,加一个Double Buffer的SRAM Bank挂载在阵列边缘,让片外DRAM到SRAM的搬运和计算重叠,能再省掉不少加载气泡。这个方案对推理芯片特别实用,因为推理模型的权重可以在片内常驻。
7.2 多阵列互联:从芯片到芯片阵列
当市场份额变大之后,单阵列解决不了问题,就要把多个脉动阵列拼起来。横向拼和纵向拼各有各的讲究。横向拼适合做大批量数据流的切分,纵向拼适合做模型并行中的层间流水。这个层面的设计会很早就进入规划期,因为它直接决定了芯片的die面积利用率和通信带宽。
7.3 面向动态稀疏的脉动阵列
另一条路线是让阵列适应动态稀疏结构,而不是只适配静态稀疏权重。这类设计通常会加入某种形式的预测器或提前分析器,判断哪些区域大概率是稀疏的,提前把计算绕开。这个方向还很前沿,但值得持续关注。
就我个人经验而言,脉动阵列不是那种"看完一篇论文就能闭眼写"的架构,它真正的难点在数据流的精确控制和物理实现的配合上。如果你已经开始动手做自己的RTL实现,这里有一个最直接的建议:一定到最细的时间周期上去画数据流图,把每个PE在每个Cycle做什么、传什么、存什么都列出来,这个过程虽然繁琐,但能帮你省掉后面大量的调试时间。多画几遍,你就能找到感觉了。