第三章 脉动阵列(Systolic Array)详解

各位同学,今天我们来聊聊脉动阵列。说实话,这是深度学习加速器里最经典、也最容易被误解的结构之一。我当年刚接触这个名词时,以为是什么高深莫测的数学变换,后来才发现——说白了,就是一种让数据像心跳一样有节奏地在计算单元间流动的架构。

3.1 经典脉动阵列原理

脉动阵列的概念最早由卡内基梅隆大学的孔祥重(H.T. Kung)教授在1982年提出。你想想看,四十多年前的想法,到现在还是AI芯片的基石,这本身就说明了很多问题。

它的核心思想很简单:让数据在多个处理单元(PE)之间规律地流动,每个PE只做最简单的乘加运算。这样做的好处是什么?我举个例子你就明白了。

假设我们要做两个3×3矩阵的乘法:

C[i][j] = Σ A[i][k] × B[k][j]

传统做法是:取A的一行,取B的一列,做点积。但脉动阵列不一样,它把A矩阵的每一行从左到右推,把B矩阵的每一列从上到下推,数据在PE阵列中像脉搏一样跳动。

关键洞察:脉动阵列的本质是用空间换时间。通过增加并行度,把O(n³)的时间复杂度降到O(n),代价是O(n²)的硬件资源。

我在项目中遇到过这样一个坑:刚开始设计时,我天真地以为PE越多越好。结果发现,当阵列规模超过某个阈值后,布线延迟和时钟歪斜会严重拖累性能。嗯,这里要注意——不是所有问题都能靠堆PE解决

3.2 权重固定与数据固定流

脉动阵列有两种主流的数据流模式,我分别说说。

3.2.1 权重固定(Weight Stationary)

这种模式下,权重数据预先加载到每个PE的寄存器中,然后保持不变。输入特征图的数据在阵列中流动,每个PE计算部分和,最后累加得到结果。

我个人习惯在卷积层使用权重固定模式。为什么?因为卷积核的权重是固定的,加载一次就可以反复使用,省去了频繁搬运权重的功耗。

// 权重固定模式的伪代码
for (int i = 0; i < ROWS; i++) {
    for (int j = 0; j < COLS; j++) {
        PE[i][j].weight = W[i][j];  // 预加载权重
    }
}
// 数据流动
for (int t = 0; t < TIME_STEPS; t++) {
    for (int i = 0; i < ROWS; i++) {
        for (int j = 0; j < COLS; j++) {
            PE[i][j].compute(input[i][t], PE[i][j].weight);
            // 部分和向右传递
            if (j < COLS-1) PE[i][j+1].psum += PE[i][j].psum;
        }
    }
}

3.2.2 数据固定(Output Stationary)

这种模式反过来——输入数据固定不动,权重在阵列中流动。每个PE负责计算输出矩阵中的一个元素,累加来自不同权重的部分结果。

我记得有一次做全连接层加速时,发现数据固定模式更合适。因为全连接层的输入特征图通常较大,固定输入可以减少数据搬运次数。

特性 权重固定 数据固定
适用场景 卷积层(权重复用度高) 全连接层(输入复用度高)
数据搬运量 输入数据搬运多 权重数据搬运多
PE利用率 高(权重常驻) 中等(权重流动)
控制复杂度 中等

我的建议:实际项目中不要死板地只用一种模式。我做过一个混合方案——卷积层用权重固定,全连接层用数据固定,中间加一个数据重排模块。效果比单一模式好15%左右。

3.3 性能建模

做加速器设计,光会画架构图不行,你得会算性能。我见过太多人拍脑袋定参数,结果流片回来发现性能差一大截。

脉动阵列的性能模型主要考虑三个因素:

  • 计算延迟:PE完成一次乘加需要几个时钟周期
  • 数据加载延迟:从片外DRAM搬运数据到PE阵列的时间
  • 流水线填充/排空时间:阵列启动和结束时的额外开销

假设我们有一个N×N的脉动阵列,每个PE的乘加延迟为T_pe,数据加载延迟为T_load,那么完成一次M×K×N的矩阵乘法所需的总时钟周期数大约为:

T_total = T_load + (M + N + K - 2) × T_pe + T_drain

其中T_drain是排空时间,大约等于N个时钟周期。

我曾经犯过一个错误:忽略了数据加载延迟。当时设计了一个32×32的阵列,算下来理论吞吐量很高,结果实际测试时发现,大部分时间都在等数据从DDR搬过来。说白了,计算单元再快,喂不饱也是白搭

避坑指南:我曾经在项目里把阵列规模从16×16直接翻到32×32,以为性能能翻4倍。结果因为数据带宽没跟上,实际只提升了1.8倍。所以做性能建模时,一定要把存储带宽作为约束条件加进去。

下面我用一张图来展示脉动阵列的核心逻辑:

脉动阵列数据流示意图 输入数据流 PE00 PE01 PE02 PE10 PE11 PE12 PE20 PE21 PE22 输出部分和 ← 权重预加载(权重固定模式) ↓ 输入数据流动(数据固定模式) 图例 水平数据流(输入特征图) 垂直数据流(权重/部分和) 输出数据流 PE处理单元

这张图展示了一个3×3的脉动阵列。红色箭头表示输入数据从左向右流动,绿色箭头表示权重或部分和从上向下流动。每个PE接收来自左边和上边的数据,做一次乘加后,把结果继续向右和向下传递。

你想想看,这种结构天然适合矩阵乘法——数据流动有规律,控制逻辑简单,而且每个PE的硬件设计可以高度统一。这也是为什么几乎所有AI芯片(Google TPU、NVIDIA Tensor Core等)都采用了脉动阵列的变体。

性能建模小技巧:我一般会用Roofline模型来评估脉动阵列的瓶颈。把计算峰值画成一条水平线,把存储带宽画成一条斜线,你的实际性能点落在哪里,瓶颈就在哪里。这个方法帮我快速定位过好几个项目的性能问题。

好了,关于脉动阵列的核心内容就讲到这里。记住三点:数据流动要有节奏、权重固定和数据固定各有适用场景、性能建模一定要考虑存储带宽。这些经验都是我用实际项目换来的,希望能帮你少走弯路。

专注资料整理