第四章:脉动阵列架构(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 同时处理输入数据和部分和,数据在两个维度上流动。
我画了一张图,帮你理解二维阵列的数据流:
看到没?输入数据从左往右流,部分和从上往下流。每个 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 实际项目中的设计考量
最后,我想聊聊在实际项目中设计脉动阵列时,需要关注的几个点:
- PE 的微架构:每个 PE 内部需要乘法器、加法器、寄存器。乘法器的位宽决定了精度,我一般用 8 位或 16 位定点数。
- 数据广播 vs 数据流水:权重是广播到所有 PE,还是像流水线一样传递?广播快但布线压力大,流水线慢但省布线资源。
- 部分和的累加:是每个 PE 内部累加,还是传到阵列底部统一累加?前者省面积,后者省时间。
- 边界处理:特征图边缘的 padding 怎么处理?我习惯在阵列外围加一圈 padding 逻辑,自动补零。
嗯,关于脉动阵列的原理和数据流,今天就聊到这里。这些内容是我在实际项目中反复验证过的,希望能帮你少走弯路。