4. 二维脉动阵列:矩阵乘法的经典映射

好,我们终于来到了脉动阵列最经典、也最实用的形态——二维脉动阵列。说白了,就是把一维的线性阵列在另一个维度上再复制一份,形成一个网格。每个网格点就是一个PE(处理单元),数据按行和列同时流动,PE内部完成乘加运算。

我个人觉得,理解二维脉动阵列的关键,不在于它有多复杂,而在于你能否在脑子里建立起那个“数据流动的画面”。我当年刚接触时,看了半天框图,总觉得数据会撞车。后来亲手画了几遍数据流时序图,才真正明白——原来数据流动是有节奏的,就像阅兵方阵一样整齐。

4.1 从一维到二维:为什么需要二维阵列?

先问个问题:一维脉动阵列能算矩阵乘法吗?

能,但有限制。一维阵列适合向量-矩阵乘法,或者矩阵-向量乘法。对于两个矩阵相乘 C = A × B,如果只用一维阵列,你需要把其中一个矩阵按行或按列串行送入,计算效率会打折扣。

二维阵列就不一样了。它让矩阵A的数据按行流动,矩阵B的数据按列流动,每个PE同时处理一个乘加操作。这样一来,整个矩阵乘法可以在一个“流水线式”的网格中完成,吞吐量大幅提升。

核心思想:二维脉动阵列把矩阵乘法映射到二维空间,数据沿两个维度同时流动,每个PE负责一个局部乘加。最终,所有PE并行工作,完成整个矩阵乘法。

4.2 经典映射:数据怎么流?

我们以两个 3×3 矩阵相乘为例。假设:

  • 矩阵 A 按行从左到右流动(水平方向)
  • 矩阵 B 按列从上到下流动(垂直方向)
  • 每个 PE 内部累加部分积

具体来说,PE(i,j) 负责计算 C(i,j) = Σ A(i,k) × B(k,j)。数据流动的节奏是这样的:

  1. 第1拍:A(0,0) 进入 PE(0,0),B(0,0) 也进入 PE(0,0)。PE(0,0) 计算 A(0,0)×B(0,0),累加到 C(0,0)。
  2. 第2拍:A(0,1) 向右流入 PE(0,1),A(0,0) 继续向右流入 PE(0,1)?不对,这里要注意——A(0,0) 在第2拍已经离开 PE(0,0) 了,它向右流入 PE(0,1)。同时 B(1,0) 向下流入 PE(1,0),B(0,0) 向下流入 PE(1,0)。
  3. 第3拍:数据继续流动,每个PE开始接收新的数据对,累加新的乘积。

嗯,这里要注意:数据不是同时到达所有PE的,而是像波浪一样逐拍推进。这就是“脉动”这个名字的由来——数据像心跳一样,一拍一拍地向前传播。

我的经验:刚开始设计二维阵列时,最容易犯的错误是数据同步。我曾经在一个项目中,因为数据到达PE的时间没对齐,导致累加结果全错了。后来我加了一组“延迟寄存器”,让数据在PE之间流动时保持固定的拍数差,问题就解决了。

4.3 PE内部结构:乘加单元

每个PE内部其实很简单,就是一个乘法器加一个累加器。我习惯把它画成下面这样:

PE(i,j) 内部结构:
  ┌─────────────────────┐
  │  A_in ──→ [REG] ──→ A_out  │
  │                     │
  │  B_in ──→ [REG] ──→ B_out  │
  │         ↓         ↓        │
  │      [MULT] ──→ [ACC] ──→ C_out │
  └─────────────────────┘

每个PE有三个输入:A_in(来自左边)、B_in(来自上边)、以及之前累加的部分和。它做一次乘加后,把A数据向右传,B数据向下传,同时把新的部分和传给下一个PE或者输出。

这里有个细节:累加器需要初始化为0。我见过有人忘记初始化,结果算出来的C矩阵全是错的。嗯,这种低级错误,我也犯过。

4.4 数据流时序图

为了让你更直观地理解,我画了一张数据流动的时序图。这张图展示了3×3阵列中,数据如何逐拍传播:

二维脉动阵列数据流时序图(3×3) 二维脉动阵列数据流时序图(3×3) 说明:每个PE在每拍接收A和B数据,完成乘加后,A向右传,B向下传。图中数字表示数据到达的拍数。 PE(0,0) 拍1 PE(0,1) 拍2 PE(0,2) 拍3 PE(1,0) 拍2 PE(1,1) 拍3 PE(1,2) 拍4 PE(2,0) 拍3 PE(2,1) 拍4 PE(2,2) 拍5 第0行PE 第1行PE 第2行PE → A数据流动方向 ↓ B数据流动方向

从这张图你能看到,数据像波浪一样从左上角向右下角传播。每个PE的启动时间不同,但一旦启动,就会持续工作直到所有数据流过。

4.5 性能分析:吞吐量与延迟

二维脉动阵列的性能,可以用两个关键指标衡量:

指标 公式 说明
吞吐量 每拍完成 N 个乘加 N×N 阵列,每拍可完成 N 个乘加(每个PE一个)
延迟 2N-1 拍 从第一个数据进入,到最后一个结果输出
计算效率 接近 100% 流水线填满后,所有PE每拍都在工作

举个例子,对于 3×3 阵列:

  • 吞吐量:每拍完成 3 个乘加(因为每行有3个PE同时工作)
  • 延迟:2×3 - 1 = 5 拍
  • 总计算量:3×3×3 = 27 次乘加,在5拍内完成

你想想看,如果用一个单PE的处理器,需要27拍才能算完。而二维阵列只需要5拍——这就是并行计算的威力。

注意:上面的分析假设阵列已经填满(流水线已建立)。在刚开始的几拍,阵列处于“填充阶段”,效率较低。同样,在最后几拍,阵列处于“排空阶段”,效率也会下降。对于大规模矩阵,填充和排空的时间占比很小,可以忽略。

4.6 实际设计中的避坑指南

我在项目中遇到过几个常见问题,分享给你:

  • 数据对齐:矩阵A和B的数据必须按正确的时序送入。我曾经因为数据提前或延迟了一拍,导致结果全乱。解决办法是加一组“对齐寄存器”,在数据进入阵列前先缓存一拍。
  • 边界处理:对于非方阵(比如 3×4 乘以 4×5),阵列的尺寸需要适配。我习惯的做法是,把阵列设计成最大支持的尺寸,然后用“掩码”控制哪些PE真正参与计算。
  • 累加器清零:每次开始新的矩阵乘法前,所有PE的累加器必须清零。我见过有人用“全局复位信号”来做,但更好的做法是让每个PE在接收到第一个数据时自动清零。

嗯,这些坑我都踩过。希望你能一次避开。

4.7 小结

二维脉动阵列是矩阵乘法加速的经典方案。它的核心思想很简单:数据按行和列流动,每个PE完成一次乘加。但真正用好它,需要理解数据流的时序、PE的内部结构,以及边界条件的处理。

我个人觉得,二维脉动阵列最迷人的地方在于,它把复杂的矩阵乘法,分解成了简单、规整、可扩展的硬件结构。你想想看,一个 3×3 的阵列只有9个PE,但通过增加阵列规模,你可以轻松扩展到 16×16、32×32,甚至更大。这种“可扩展性”,正是脉动阵列在AI加速芯片中广泛应用的原因。