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拍: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拍: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拍:数据继续流动,每个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阵列中,数据如何逐拍传播:
从这张图你能看到,数据像波浪一样从左上角向右下角传播。每个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加速芯片中广泛应用的原因。