第四章:脉动阵列架构(Systolic Array)原理:经典设计思想与数据流分析

各位同学,今天我们来聊聊脉动阵列。说实话,这玩意儿在卷积加速器里,就像心脏在人体里的地位。我当年刚接触这个架构时,觉得它不过是个二维的乘法器阵列,后来在项目中踩过坑才明白——它的数据流设计,才是真正的灵魂。

4.1 什么是脉动阵列?

脉动阵列,英文叫 Systolic Array。名字听着挺玄乎,其实说白了就是:一组处理单元(PE)排成阵列,数据像血液一样在阵列中有节奏地流动。每个 PE 只做一件事——乘加运算,然后把结果传给邻居。

为什么会叫「脉动」?你想想看,心脏收缩一次,血液就往前推一段。这里的时钟信号就是心脏,每个时钟周期,数据就往相邻的 PE 挪一步。嗯,就是这么形象。

核心思想:用简单的处理单元,通过规整的数据流,实现高吞吐的并行计算。代价是控制逻辑简单,但数据复用率高。

4.2 经典设计思想:从一维到二维

我记得最早接触脉动阵列是在读一篇 80 年代的,那时候的阵列还是一维的,用来做矩阵乘法。后来到了卷积加速器时代,大家发现二维阵列更适合处理特征图。

4.2.1 一维脉动阵列(1D Systolic Array)

一维阵列就是一条线。数据从左往右流,权重从上往下播。每个 PE 存一个权重,输入数据依次经过每个 PE,完成乘加。

举个例子,假设我们要计算 Y = W × X:

// 一维脉动阵列伪代码
// PE[i] 存储权重 W[i]
// 输入 X 从左向右流动

for each clock cycle:
    for each PE[i] (从左到右):
        if i == 0:
            PE[i].input = X[cycle]
        else:
            PE[i].input = PE[i-1].output
        PE[i].output = PE[i].input * W[i] + PE[i].partial_sum

这种结构简单,但有个问题——权重不能复用。每个 PE 只算一个权重,如果卷积核是 3×3,你就得用 9 个 PE 排成一排。我在项目中试过,对于大卷积核,一维阵列的面积效率其实不高。

4.2.2 二维脉动阵列(2D Systolic Array)

二维阵列才是主流。Google 的 TPU 用的就是 128×128 的二维脉动阵列。每个 PE 同时处理输入数据和部分和,数据在两个维度上流动。

我画了一张图,帮你理解二维阵列的数据流:

二维脉动阵列数据流示意图 权重行0 权重行1 权重行2 权重行3 输入列0 输入列1 输入列2 输入列3 PE (0,0) PE (0,1) PE (0,2) PE (0,3) PE (1,0) PE (1,1) PE (1,2) PE (1,3) PE (2,0) PE (2,1) PE (2,2) PE (2,3) PE (3,0) PE (3,1) PE (3,2) PE (3,3) 输入数据流(从左到右) 部分和流(从上到下)

看到没?输入数据从左往右流,部分和从上往下流。每个 PE 同时接收两个方向的数据,做一次乘加,然后把结果往右和往下传。这就是二维脉动阵列的经典数据流。

4.3 数据流分析:三种主流模式

在实际项目中,数据流的设计直接决定了加速器的性能。我总结下来,主要有三种模式:

数据流模式 输入数据 权重 部分和 典型应用
权重固定(Weight Stationary) 流动 固定 流动 卷积层(权重复用率高)
输入固定(Input Stationary) 固定 流动 流动 全连接层
输出固定(Output Stationary) 流动 流动 固定 矩阵乘法

4.3.1 权重固定模式(Weight Stationary)

这是我最常用的模式。权重预先加载到每个 PE 的寄存器里,然后就不动了。输入数据在阵列中流动,每个 PE 用自己存的权重去乘当前输入,结果累加到部分和里。

为什么好用?因为卷积的权重复用率极高。一个 3×3 的卷积核,在特征图上滑动时,同一个权重会被反复使用。把权重固定在 PE 里,就省去了反复读取权重的带宽开销。

我的经验:在 ResNet-50 的卷积层中,权重固定模式可以把权重读取次数降低 90% 以上。但要注意——权重需要提前广播到所有 PE,这需要额外的配置时间。

4.3.2 输入固定模式(Input Stationary)

这种模式把输入数据固定在 PE 里,权重在阵列中流动。适合全连接层,因为全连接层的每个输入只跟所有权重乘一次,没有复用。

我曾经在一个项目里试过用输入固定模式做卷积,结果发现效率很低。因为卷积的输入数据复用率其实不高,每个输入像素只参与几次乘加,固定它反而浪费了 PE 的存储资源。

4.3.3 输出固定模式(Output Stationary)

输出固定模式把部分和固定在 PE 里,输入和权重都在流动。每个 PE 负责计算输出特征图的一个像素,所有乘加结果累加到这个 PE 里。

这种模式的好处是——不需要额外的累加器。部分和就在 PE 内部累加,最后直接读出。但坏处是,如果输出特征图很大,需要的 PE 数量也很大。

4.4 脉动阵列的优缺点

说了这么多,我们来总结一下脉动阵列的优缺点。这也是面试时经常被问到的问题。

优点:

  • 高吞吐:所有 PE 同时工作,一个时钟周期完成大量乘加
  • 低功耗:数据流规整,没有复杂的控制逻辑,数据搬移少
  • 可扩展:增加阵列大小就能提升算力,设计思路简单
  • 数据复用好:权重或输入数据在阵列内被多次使用,减少片外带宽需求

缺点:

  • 灵活性差:只适合规整的计算模式,对不规则运算支持不好
  • 利用率问题:当卷积核小于阵列大小时,部分 PE 会闲置
  • 边界处理复杂:特征图边缘的 padding 需要特殊处理
  • 数据加载延迟:每次切换卷积核时,需要重新加载权重,有停顿

避坑指南:我曾经在一个项目中,为了追求高利用率,把阵列设计得跟卷积核一样大。结果换了网络模型后,卷积核尺寸变了,阵列利用率直接掉到 30%。后来我学乖了——阵列大小要取常见卷积核尺寸的最小公倍数,比如 16×16,既能适配 3×3,也能适配 5×5。

4.5 实际项目中的设计考量

最后,我想聊聊在实际项目中设计脉动阵列时,需要关注的几个点:

  1. PE 的微架构:每个 PE 内部需要乘法器、加法器、寄存器。乘法器的位宽决定了精度,我一般用 8 位或 16 位定点数。
  2. 数据广播 vs 数据流水:权重是广播到所有 PE,还是像流水线一样传递?广播快但布线压力大,流水线慢但省布线资源。
  3. 部分和的累加:是每个 PE 内部累加,还是传到阵列底部统一累加?前者省面积,后者省时间。
  4. 边界处理:特征图边缘的 padding 怎么处理?我习惯在阵列外围加一圈 padding 逻辑,自动补零。

嗯,关于脉动阵列的原理和数据流,今天就聊到这里。这些内容是我在实际项目中反复验证过的,希望能帮你少走弯路。

可可精品资料库