4. 脉动阵列原理

脉动阵列(Systolic Array)之所以在 GEMM 加速中占据核心地位,根本原因在于它把“计算”从控制逻辑中解放出来,交给数据流动本身来驱动。传统处理器执行矩阵乘加时,需要反复取指、译码、访存、写回,控制开销往往超过乘加本身;而脉动阵列让权重、激活和部分和像心跳一样在 PE(Processing Element)之间同步推进,每个周期每个 PE 只做一次乘加并把结果传给邻居,控制路径极短,数据复用率极高。

一、数据流驱动计算:为什么“流动”比“调度”更高效

脉动阵列的基本思想是:把矩阵乘加拆成大量规则的小运算,让数据在二维网格中按固定方向逐拍移动。以 C = A × B 为例,A 的行元素从左向右流动,B 的列元素从上向下流动,每个 PE 在本地完成一次乘加,并把部分和沿对角线方向向下或向右传递。这样,同一个 A 元素可以被同一行所有 PE 复用,同一个 B 元素可以被同一列所有 PE 复用,而部分和不需要频繁写回全局内存。

这种结构的本质是“空间换时间”与“局部性换带宽”。PE 之间只有短距离的寄存器到寄存器连接,没有长线跨片通信;每个周期所有 PE 同步工作,形成类似心脏搏动的节律,因此得名“脉动”。对于 FPGA 而言,这种规则结构非常契合 DSP48 切片和 BRAM 的布局,便于做流水线平衡和时序收敛。

核心结论:脉动阵列的性能上限不取决于单个 PE 的算力,而取决于数据供给能否让每个 PE 在每个周期都保持忙碌。数据流设计的目标就是消除气泡、减少停顿、提高 PE 利用率。

二、PE 单元:脉动阵列的最小计算细胞

PE 单元通常由乘法器、累加器、若干寄存器以及局部互连组成。一个典型的 PE 在每个时钟周期执行:

// 简化 PE 行为模型
always @(posedge clk) begin
    if (en) begin
        acc     <= acc + a_in * b_in;  // 乘加
        a_out   <= a_in;               // 激活向右传递
        b_out   <= b_in;               // 权重向下传递
    end
end

其中 a_in 和 b_in 分别来自左方和上方,a_out 和 b_out 分别送往右方和下方,acc 是本地部分和。根据权重是否驻留、部分和是否移动,PE 可以演化出多种变体。FPGA 实现时,通常把乘法映射到 DSP48E2,把累加和延时映射到寄存器,把权重缓存映射到分布式 RAM 或 BRAM。

三、典型脉动结构:权重固定、输出固定与行固定

根据“谁固定、谁流动”,脉动阵列可分为三类经典结构。它们没有绝对优劣,只有与算法形状、数据复用模式和硬件资源是否匹配。

结构类型固定对象流动对象适用场景FPGA 实现特点
权重固定(Weight Stationary)权重 B激活 A、部分和权重可长期驻留、批量推理权重存 BRAM,PE 复用率高,适合大 K 维
输出固定(Output Stationary)部分和 C激活 A、权重 B输出块较小、需要频繁累加累加器驻留 PE,减少部分和搬运
行固定(Row Stationary)一行权重与部分和激活按行流动卷积/GEMM 行复用明显行内广播,行间流水,适合规则卷积

权重固定结构把 B 矩阵的每个元素预先加载到对应 PE 的本地寄存器中,A 矩阵从左向右流动,部分和沿对角线向下累加。它的优势是权重访问次数最少,适合 K 维很大、权重可复用的场景。输出固定结构则让每个 PE 持有一个输出部分和,A 和 B 同时流动,适合输出块较小但需要多次累加的情况。行固定结构介于两者之间,通常把一行权重固定在一行 PE 中,激活按行广播,部分和在行内累加后向下传递,在卷积神经网络中非常常见。

脉动阵列典型数据流结构对比 权重固定 B 驻留 PE,A 向右流动 PE B00 PE B01 PE B10 PE B11 A psum 权重访问少,适合大 K PE 复用率高 输出固定 C 驻留 PE,A/B 同时流动 PE C00 PE C01 PE C10 PE C11 A B 部分和驻留,减少搬运 适合输出块较小场景 行固定 一行权重固定,激活按行广播 PE W0 PE W1 PE W2 PE W3 A行 行内广播,行间流水 卷积/GEMM 行复用明显 蓝色:激活 A 流动方向 橙色:权重 B 或部分和流动方向 绿色:行广播方向

四、案例:8×8 权重固定脉动阵列做 8×8×8 GEMM

假设我们要在 FPGA 上实现一个 8×8×8 的 GEMM,即 C[8][8] = A[8][8] × B[8][8]。采用权重固定结构时,可以这样组织:

  • 把 B 的 64 个元素按行列位置预加载到 8×8 个 PE 的本地权重寄存器中,每个 PE 持有 B[row][col]。
  • A 的每一行从左侧输入,每个周期向右推进一格;A[row][k] 在第 k 个周期到达第 k 列 PE。
  • 每个 PE 在收到 A 元素后,与本地权重相乘,并把乘积加到沿对角线向下传递的部分和中。
  • 经过 8 个周期后,第一行部分和到达底部,形成 C 的一行结果;再经过若干流水周期,全部 64 个输出完成。

这个例子的关键参数是:PE 数量 64,权重加载周期 64,计算流水深度约 8+8=16 个周期,理论峰值算力为 64 次乘加/周期。若时钟为 200 MHz,则峰值约 12.8 GMAC/s。实际性能取决于 A 的供给带宽和部分和的读出带宽。

实操提示:在 FPGA 中实现权重固定脉动阵列时,优先把权重放在 BRAM 中按 PE 列组织,避免每个 PE 单独使用 LUTRAM 导致布线拥塞。A 的输入建议使用移位寄存器链,部分和则用 DSP48 的级联端口向下传递,减少通用布线资源占用。

五、误区与复盘:脉动阵列不是“越大越好”

初学者常认为 PE 阵列越大,算力越高。实际上,阵列规模受限于三个因素:第一,数据供给带宽。如果 A 和 B 的加载速度跟不上 PE 消耗速度,PE 就会空转。第二,部分和位宽与累加精度。K 维增大时,部分和位宽必须增加,否则会溢出或精度不足。第三,时序收敛。PE 之间的短连线虽然快,但大规模阵列的时钟树和复位网络会变得复杂,容易产生时序违例。

另一个常见误区是忽视“填充”和“边界”处理。当矩阵维度不是阵列尺寸的整数倍时,需要补零或做边界屏蔽,否则会引入错误结果。复盘时应重点检查:PE 利用率是否达到预期、权重加载是否与计算重叠、部分和读出是否成为瓶颈、以及是否存在气泡周期。

排障要点:如果综合后 PE 利用率低于 60%,优先检查 A/B 的输入 FIFO 是否频繁空满、权重加载是否阻塞计算、以及部分和是否在 PE 之间产生了反压。脉动阵列的调试应从数据流时序图入手,而不是只看最终结果。

总结来看,脉动阵列的精髓在于用规则的数据流动替代复杂的控制调度。权重固定、输出固定和行固定三种结构分别对应不同的复用策略和硬件约束。理解它们的差异,才能在 FPGA 上为具体 GEMM 形状选择最合适的阵列组织方式,并为后续的 tiling、双缓冲和流水线优化打下基础。