1. 脉动阵列是什么:从一条流水线说起
大家好,我是你们的芯片架构课讲师。今天咱们聊一个听起来很唬人的东西——脉动阵列。别被名字吓到,其实它的核心思想特别朴素。
我先问个问题:你洗过盘子吗?
一个人洗、一个人擦、一个人摆。三个人站成一排,盘子从第一个人传到第三个人。这就是流水线。每个环节只做一件事,做完就传给下一个人。吞吐量上去了,延迟也没增加太多。
芯片里的流水线,道理一模一样。
1.1 从一条简单的流水线开始
假设我们要计算一个向量点积:
// 两个长度为4的向量
A = [a0, a1, a2, a3]
B = [b0, b1, b2, b3]
// 点积 = a0*b0 + a1*b1 + a2*b2 + a3*b3
result = 0
for i in 0..3:
result += A[i] * B[i]
用单核处理器跑,就是4次乘法和3次加法。每次乘法等数据从内存搬过来,加法等乘法结果回来。大部分时间花在等数据上,计算单元闲着。
我早年做视频编码器时就吃过这个亏。算法算好了,仿真也过了,结果流片回来帧率死活上不去。一查,ALU利用率不到30%。剩下的时间都在等数据。
那怎么办?把计算拆成多个阶段,让每个阶段同时干活。
流水线的核心思想:把一个大任务拆成多个小步骤,每个步骤由独立的硬件单元完成。数据像流水一样流过这些单元,每个时钟周期都能产出一个结果。
拿点积来说,我们可以拆成这样:
- Stage 0: 读取 a0, b0,计算乘加
- Stage 1: 读取 a1, b1,计算乘加,同时把 Stage 0 的结果传过来
- Stage 2: 读取 a2, b2,计算乘加,同时累加前面的结果
- Stage 3: 读取 a3, b3,计算乘加,输出最终结果
每个时钟周期,四个阶段都在干活。虽然单个结果还是需要4个周期,但每周期都能吐出一个结果。吞吐量翻了4倍。
1.2 流水线的瓶颈:数据搬运
流水线好是好,但有个问题——数据怎么喂进去?
你想想看,每个阶段都要读数据。如果所有数据都存在片外DRAM里,每次读取都要几十甚至上百个周期。流水线再快,也快不过内存带宽的瓶颈。
这就是所谓的「内存墙」。
我做过一个神经网络加速器项目,当时把卷积层拆成了9级流水线。仿真时吞吐量漂亮得很,结果一上FPGA实测,性能只有仿真的1/5。为什么?数据从DDR搬不过来,流水线大部分时间在空转。
避坑指南:我曾经天真地以为流水线级数越多越好。后来发现,如果数据供给跟不上,多级流水线只会让控制逻辑更复杂,性能反而下降。流水线的深度要和内存带宽匹配,不是越深越好。
1.3 脉动阵列的诞生:让数据动起来
好,问题清楚了:计算单元很快,但数据搬运太慢。怎么办?
换个思路——不让数据来回搬,而是让数据在计算单元之间流动。
这就是脉动阵列的核心思想:数据流驱动计算。
想象一下:你有一排处理单元(PE),排成阵列。数据从左边流进来,经过每个PE时,PE顺手做一次计算,然后把数据传给下一个PE。数据像血液一样在阵列里「脉动」,每个PE收到数据就干活,干完就传走。
这样做的好处是什么?
- 局部性:每个PE只和邻居通信,不需要全局总线
- 高吞吐:所有PE同时工作,数据流不停,计算就不停
- 低功耗:没有长距离走线,没有频繁的片外访问
说白了,就是用空间换时间。用更多的计算单元,换来数据搬运的零开销。
下面这张图展示了脉动阵列的基本结构:
1.4 为什么叫「脉动」?
这个名字其实很形象。你想想心脏是怎么工作的——收缩一下,血液被泵到全身;再收缩一下,又泵一次。数据在阵列里的流动也是这样,每个时钟周期,数据向前「跳动」一次。
H.T. Kung 在1982年提出这个概念时,就是借鉴了人体血液循环的灵感。数据像血液一样,在计算单元这个「心脏」之间规律地流动。
我个人觉得,这个名字起得特别好。它抓住了这个架构最本质的特征——节奏感。每个PE都在同一个时钟节拍下工作,数据同步地向前推进。
1.5 脉动阵列 vs 传统架构
咱们用个表格对比一下,一目了然:
| 特性 | 传统处理器 | 脉动阵列 |
|---|---|---|
| 数据搬运 | 频繁访问全局存储/缓存 | 只在相邻PE间传递 |
| 控制方式 | 指令驱动(取指→译码→执行) | 数据流驱动(数据到了就算) |
| 并行粒度 | 指令级并行(ILP) | 数据级并行(DLP) |
| 时钟频率 | 受全局走线限制 | 局部走线,可跑更高频率 |
| 功耗效率 | 控制逻辑和缓存占大头 | 大部分功耗花在计算上 |
| 适用场景 | 通用计算,分支多 | 规律计算,如矩阵乘、卷积 |
看到没?脉动阵列不是万能的。它特别适合那种数据访问模式很规律的计算——比如矩阵乘法、卷积、FIR滤波器。但遇到分支多、数据依赖复杂的程序,它就抓瞎了。
我的经验:判断一个算法适不适合用脉动阵列,就看它的数据流图是不是规则的。如果每个计算节点只和邻居通信,而且数据流向是单向的,那基本就是脉动阵列的菜。我在做图像处理加速时,Sobel边缘检测、高斯滤波这类卷积操作,用脉动阵列实现效果特别好。
1.6 一个简单的例子:一维脉动阵列做卷积
光说不练假把式。咱们看个具体例子。
假设输入序列 x = [x₀, x₁, x₂, x₃, x₄],卷积核 w = [w₀, w₁, w₂]。输出 y[n] = x[n]*w₀ + x[n+1]*w₁ + x[n+2]*w₂。
用一维脉动阵列实现:
// 3个PE,每个PE存一个权重
PE0: 存 w₀
PE1: 存 w₁
PE2: 存 w₂
// 时钟周期0
x₀ 进入 PE0 → PE0 计算 x₀ * w₀ → 结果传给 PE1
PE1 收到结果,但还没数据,等待
// 时钟周期1
x₁ 进入 PE0 → PE0 计算 x₁ * w₀ → 结果传给 PE1
PE1 收到 x₀*w₀,加上 x₁*w₁ → 结果传给 PE2
// 时钟周期2
x₂ 进入 PE0 → PE0 计算 x₂ * w₀ → 传给 PE1
PE1 收到 x₁*w₀,加上 x₂*w₁ → 传给 PE2
PE2 收到 x₀*w₀ + x₁*w₁,加上 x₂*w₂ → 输出 y₀
// 之后每个时钟周期,PE2 输出一个结果
看到了吗?经过3个周期的「预热」后,每个周期都能产出一个卷积结果。这就是脉动阵列的威力——一旦流水线填满,吞吐量就是每个周期一个结果。
1.7 小结:脉动阵列的核心思想
说了这么多,总结一下:
- 从流水线出发:把计算拆成多个阶段,每个阶段独立工作
- 遇到瓶颈:数据搬运太慢,内存带宽跟不上计算速度
- 解决方案:让数据在计算单元之间流动,而不是来回搬运
- 核心思想:数据流驱动计算——数据到了,计算就自动触发
嗯,这就是脉动阵列的底层逻辑。听起来复杂,说白了就是「让数据自己跑起来,别让计算单元闲着等数据」。