4. 二维脉动阵列:矩形阵列拓扑,矩阵乘法映射,数据广播与收缩
好,我们进入二维脉动阵列。一维的 systolic array 说白了就是一条流水线,数据从左往右流,或者从右往左流。但到了矩阵乘法这种二维运算,一维结构就有点力不从心了。你想想看,两个 N×N 矩阵相乘,每个输出元素需要 N 次乘累加,如果只用一维阵列,要么数据反复进出,要么控制逻辑变得极其复杂。
二维阵列的思路很直接:把 PE 排成矩形网格,让数据在行和列两个方向同时流动。我最早接触这个结构是在做图像处理加速器的时候,当时需要在 FPGA 上实现一个 3×3 的卷积核。用二维阵列映射卷积,数据流天然匹配,效果出奇的好。
4.1 矩形阵列拓扑
二维脉动阵列最常见的拓扑就是矩形网格。每个 PE 只和上下左右四个邻居相连。数据从左边进入,从上边进入,结果从右边或下边输出。
核心特征:
- PE 排列成 R 行 × C 列的网格
- 每个 PE 有 4 个数据端口:北、南、西、东
- 数据在行方向水平流动,在列方向垂直流动
- 控制信号通常从左上角广播到整个阵列
我习惯把这种拓扑叫做「全连接网格」。为什么?因为每个 PE 和邻居之间都有物理连线,数据可以同时向两个维度传播。相比一维阵列,二维阵列的带宽利用率高得多。
下面这张图展示了一个 3×3 的矩形阵列拓扑结构:
设计小技巧: 矩形阵列的规模选择很讲究。我一般建议让阵列的行数和列数等于矩阵维度 N 的约数。比如 4×4 矩阵用 2×2 阵列,每个 PE 处理 2×2 子块。这样数据局部性最好,外部带宽压力最小。
4.2 矩阵乘法映射
好了,拓扑搭好了,怎么把矩阵乘法映射上去?这是核心问题。我们以 C = A × B 为例,其中 A、B、C 都是 N×N 矩阵。
映射策略其实就一句话:每个 PE 负责计算 C 矩阵的一个元素。PE(i,j) 计算 C(i,j),需要 A 的第 i 行和 B 的第 j 列。
具体怎么做?数据按以下方式注入:
- A 矩阵:按行从左到右流入阵列。第 i 行的数据从阵列左侧进入第 i 行 PE。
- B 矩阵:按列从上到下流入阵列。第 j 列的数据从阵列顶部进入第 j 列 PE。
- 每个 PE:收到 A 值和 B 值后,做一次乘累加:C += A × B。然后把 A 传给右边的邻居,把 B 传给下边的邻居。
我举个例子。假设 2×2 矩阵乘法:
A = [a11 a12; a21 a22]
B = [b11 b12; b21 b22]
C = [c11 c12; c21 c22]
时间步 1:
PE(0,0): 收到 a11, b11 → c11 += a11*b11
PE(0,1): 收到 a12, b21 → c12 += a12*b21
PE(1,0): 收到 a21, b12 → c21 += a21*b12
PE(1,1): 收到 a22, b22 → c22 += a22*b22
时间步 2:
PE(0,0): 收到 a12(从左边来), b21(从上边来) → c11 += a12*b21
PE(0,1): 收到 a11(从左边来), b22(从上边来) → c12 += a11*b22
PE(1,0): 收到 a22(从左边来), b11(从上边来) → c21 += a22*b11
PE(1,1): 收到 a21(从左边来), b12(从上边来) → c22 += a21*b12
两个时间步后,每个 PE 内部累加的结果就是 C 矩阵的对应元素。完美。
注意: 数据注入时序必须精确对齐。A 矩阵的第 i 行和 B 矩阵的第 j 列必须在同一时刻到达 PE(i,j)。我曾经在项目中因为数据延迟没算对,导致结果全错。排查了整整两天才发现是数据注入的起始时间差了 1 个时钟周期。
4.3 数据广播与收缩
二维阵列里有两个重要的数据流动模式:广播和收缩。这两个概念容易混淆,我分开讲。
4.3.1 数据广播
广播,就是一个数据源同时发送数据到多个目标。在二维阵列中,广播通常发生在以下场景:
- 控制信号广播:全局使能、复位、模式选择等信号,从控制器同时发给所有 PE
- 权重数据广播:某些算法中,同一个权重需要被多个 PE 使用,可以广播到整行或整列
- 常数广播:比如偏置值、缩放因子等
广播的优点是简单,缺点是扇出大、功耗高。我做过一个 16×16 的阵列,全局复位信号的扇出达到 256 个 PE,如果不做缓冲树,信号边沿会变得很慢。
经验之谈: 广播信号一定要做时钟树综合(CTS)级别的处理。我习惯在阵列的每行和每列插入一级缓冲,形成 H 树结构。这样既能控制扇出,又能保证信号到达每个 PE 的时间差在 0.5ns 以内。
4.3.2 数据收缩
收缩(Systolic)是脉动阵列的核心概念。数据像血液一样在 PE 之间「收缩」流动——每个时钟周期移动一个 PE 位置。收缩的特点:
- 节奏固定:每个时钟周期数据移动一步
- 局部通信:数据只和邻居交换,没有全局连线
- 流水线满:阵列填满后,每个时钟周期都能产生一个结果
我拿矩阵乘法举例。A 矩阵的数据从左边进入,每拍向右移动一个 PE。B 矩阵的数据从上边进入,每拍向下移动一个 PE。这种「收缩」流动保证了每个 PE 在正确的时间拿到正确的数据。
下面这个表格对比了广播和收缩的区别:
| 特性 | 数据广播 | 数据收缩 |
|---|---|---|
| 通信范围 | 一对多(全局) | 点对点(局部) |
| 扇出 | 高(随阵列规模线性增长) | 低(固定为 1~4) |
| 时序约束 | 需要时钟树平衡 | 天然流水线友好 |
| 功耗 | 高(大量翻转) | 低(局部翻转) |
| 适用场景 | 控制信号、常数、权重 | 激活值、中间结果 |
4.4 实际设计中的权衡
二维阵列设计不是非黑即白。我遇到过很多场景需要混合使用广播和收缩。比如在卷积神经网络加速器中:
- 权重:通常广播到整行 PE,因为同一行 PE 共享同一个卷积核
- 输入特征图:采用收缩方式,在行方向滑动窗口
- 部分和:在列方向收缩累加
这种混合模式能最大化数据复用率。我做过一个实验:纯广播方案的数据复用率只有 40%,纯收缩方案是 60%,而混合方案能达到 85% 以上。
设计准则:
- 数据量小、使用次数多 → 用广播
- 数据量大、使用次数少 → 用收缩
- 控制信号 → 必须用广播(但要做好缓冲)
- 中间结果 → 必须用收缩(保持流水线节奏)