☰
AI芯片为何偏爱脉动阵列:原理、数据流与工程实践
2026/10/4 19:33:49 网站建设 项目流程

1. 为什么AI芯片都在聊脉动阵列

如果你最近翻过几篇AI加速器的论文或者拆解过几款主流推理芯片的架构图,大概率会反复撞见一个词——脉动阵列(Systolic Array)。我第一次认真研究它,是因为手里一块边缘推理板子跑矩阵乘法时功耗高得离谱,算力利用率却只有三成出头,查来查去发现瓶颈根本不在主频,而在数据搬运。那之后我把脉动阵列从原理到RTL实现啃了一遍,也自己写过简化版的仿真模型,才算真正理解为什么谷歌TPU、不少国产NPU都把它当作矩阵计算的核心引擎。

这篇文章我想聊的不是教科书式的定义,而是作为一个实际做过加速器相关工作的人,把脉动阵列这件事讲透:它到底是什么、为什么AI芯片偏爱它、内部数据是怎么流动的、自己动手实现时要注意哪些坑。内容会覆盖原理拆解、结构选型、参数计算、实操建模和常见问题排查,适合做芯片架构、FPGA加速、算子优化的朋友参考,也适合刚接触AI硬件、想搞明白“矩阵乘法到底怎么在硬件上跑起来”的读者。整篇我会尽量用生活化的类比配合具体数字,让你看完能自己推一遍数据流,甚至能动手搭一个简化模型。

先说结论性的判断:脉动阵列不是万能药,它是一套用规整的硬件结构换取极高数据复用率的设计哲学。理解了这句话,后面所有的细节都是它的展开。

2. 脉动阵列到底解决了什么问题

2.1 从矩阵乘法的数据搬运说起

AI推理和训练里最核心的运算就是矩阵乘法,卷积也可以展开成矩阵乘法(im2col)。一个 M×K 的矩阵乘 K×N 的矩阵,理论上有 M×N×K 次乘加运算。假设 M=N=K=256,那就是1600多万次乘加。问题在于,如果每次乘加都要从内存里重新取两个操作数,那数据搬运量会是运算量的好几倍,而搬运的能耗远高于计算本身。

我做过一个粗略的估算:在典型的CMOS工艺下,一次32位浮点乘加的能耗大约是一次片上寄存器访问的几倍到十几倍,而一次片外DRAM访问的能耗又是片上访问的几十上百倍。也就是说,算力不是被计算单元卡住的,而是被数据供给卡住的。这就是所谓的“内存墙”。

脉动阵列的核心思路,就是让数据在计算单元之间“流动”起来,每个数据被取进来之后,尽可能多地参与运算,而不是用完就扔。这就像工厂流水线:原材料不是每个工位都重新领一次,而是沿着传送带依次经过各个工位,每个工位顺手加工一下再传给下一个。

2.2 复用率是衡量加速器效率的关键指标

衡量一个矩阵加速器好不好,我通常看三个复用维度:

  • 权重复用:同一个权重能不能被多个输入激活值共用
  • 激活复用:同一个输入激活值能不能被多个权重共用
  • 部分和复用:累加结果能不能在本地持续累积,而不是频繁写回

脉动阵列的精妙之处在于,它通过让权重预先驻留在PE(Processing Element,处理单元)里,让激活值沿一个方向脉动、部分和沿另一个方向脉动,同时实现了这三个维度的复用。一个权重在它驻留期间可以服务成百上千次乘加,激活值在穿过阵列的过程中被反复使用,部分和则在阵列内部一路累加到底,几乎不需要中间写回。

对比一下另一种常见结构——二维SIMD阵列:每个周期从寄存器堆里取操作数,算完写回。它的灵活性更高,但寄存器堆的读写带宽会成为瓶颈,复用率上不去。脉动阵列牺牲了一部分灵活性,换来了接近理论峰值的能效比。这就是为什么在矩阵乘法这种规整度极高的负载上,脉动阵列几乎是默认选择。

2.3 什么样的负载适合脉动阵列

不是所有AI运算都适合脉动阵列。我的经验判断标准是:运算是否规整、数据依赖是否规则、维度是否足够大。

矩阵乘法、标准卷积、全连接层,这些都非常适合,因为它们的计算模式高度规整,数据流可以预先编排。但像注意力机制里的动态稀疏、非规则卷积、图神经网络里的邻居聚合,这些负载的访存模式不规则,硬塞进脉动阵列反而会大量空转,利用率暴跌。

所以实际芯片设计里,往往是脉动阵列负责密集矩阵运算,再配一套灵活的向量单元或标量单元处理非规则部分。TPU早期版本就是这种思路,矩阵单元加向量单元的组合。理解这个边界,比盲目崇拜脉动阵列更重要。

3. 脉动阵列的内部结构拆解

3.1 处理单元PE里到底有什么

脉动阵列的基本单元是PE,一个最简的PE通常包含:

  • 一个乘法器
  • 一个累加器(加法器加寄存器)
  • 若干用于数据传递的寄存器
  • 控制信号通路

以经典的输出驻留(Output Stationary)数据流为例,每个PE负责计算输出矩阵中的一个元素。权重预先加载并驻留在PE的寄存器里,激活值从左侧流入,部分和从上往下流动。每个周期,PE做一次乘加,把激活值传给右边的邻居,把部分和传给下面的邻居。

这里有个关键细节:PE之间的连线是单向的、规则的。这种规则性对硬件实现极其友好,布线短、时钟树好做、可以大规模复制。我见过一些新手设计,为了追求灵活性把PE之间的互联做成可配置的全连接,结果布线拥塞、时序根本收敛不了,最后不得不推倒重来。

3.2 三种主流数据流的取舍

脉动阵列按数据驻留方式,主要分三类,我整理成表格方便对比:

数据流类型驻留对象激活流动方向部分和流动方向适用场景
权重驻留(WS)权重水平脉动垂直脉动权重可预加载、批量推理
输出驻留(OS)部分和水平+垂直本地累积通用矩阵乘、卷积
行驻留(RS)激活行对角脉动水平脉动特定卷积实现

我实际做项目时,权重驻留用得最多,因为推理场景下权重是固定的,可以提前加载好,激活值流过去就行,控制逻辑最简单。但如果是训练场景,权重需要频繁更新,输出驻留或者混合数据流会更合适。

选哪种数据流,本质是在问:哪份数据最稳定、最值得驻留?驻留那份数据可以省掉反复加载的开销,但代价是其他数据必须流动起来,对带宽和调度提出要求。

3.3 阵列尺寸怎么定

阵列尺寸(比如16×16、32×32、128×128)不是越大越好。我踩过的坑是:一开始觉得阵列越大算力越强,结果做出来发现大部分时间阵列利用率不到20%。

尺寸选择要考虑几个约束:

  • 片上存储容量:权重驻留需要寄存器或SRAM,阵列越大,驻留权重占用的存储越多
  • 数据供给带宽:阵列边缘每个周期要喂进多少数据,取决于阵列边长
  • 负载的实际维度:如果实际矩阵维度远小于阵列尺寸,大量PE会闲置
  • 时序和功耗:大阵列的时钟树和布线延迟更棘手

一个实用的经验公式:阵列边长大致取实际负载典型维度的平方根量级。比如你的模型里矩阵乘的典型维度是256到512,那16×16到32×32的阵列往往比128×128更划算,因为小阵列的利用率更高,调度更灵活。大阵列适合维度上千、批量很大的场景。

4. 手把手推一遍数据流

4.1 用一个4×4阵列算8×8矩阵乘

光看结构图容易懵,我带你用一个具体例子走一遍。假设我们要算 C = A × B,A是8×8,B是8×8,用一个4×4的脉动阵列分块计算。

采用权重驻留数据流,把B矩阵切成4×4的块,A也按行切成4行的块。计算过程分几个阶段:

  1. 权重加载阶段:把B的一个4×4块加载进阵列,每个PE存一个权重元素。这一步需要4个周期(按对角线错开加载)。
  2. 激活流入阶段:A的对应4行数据从左侧流入,每行错开一个周期进入,形成脉动。
  3. 部分和累积:每个PE做乘加,部分和沿垂直方向向下传递,最下面一行PE输出最终结果。
  4. 结果写出:底部PE把累加完成的结果写出到输出缓冲。

整个过程,A的每个元素进入阵列后,会依次经过一行PE,被复用4次;B的每个权重驻留期间,会被4个不同的激活值使用。这就是复用的来源。

4.2 关键参数的计算过程

假设阵列是N×N,时钟频率f,那么理论峰值算力是:

峰值算力 = 2 × N² × f (乘加算两次操作)

比如N=32,f=1GHz,峰值就是 2 × 1024 × 1e9 = 2.048 TOPS。注意这是理论峰值,实际算力要乘以利用率。

数据供给带宽的需求:每个周期,阵列左侧需要流入N个激活值,上方需要流入N个部分和(如果是输出驻留)。所以边缘带宽至少是 N × 数据位宽 × f。N=32、位宽16bit、f=1GHz时,左侧带宽需求是 32×2×1e9 = 64 GB/s。这个数字很关键,如果你的片上存储带宽喂不上,阵列就会饿死。

我在做设计时,会先算这个带宽需求,再反推片上SRAM的位宽和bank划分。很多新手只盯着算力,忽略了带宽匹配,最后实测性能只有峰值的一两成。

4.3 一个简化仿真模型的搭建思路

想真正理解数据流,最好的办法是自己写一个周期级的仿真模型。我用Python写过一个简化版,核心逻辑大概是这样:

class PE: def __init__(self): self.weight = 0 self.psum = 0 self.act_reg = 0 def cycle(self, act_in, psum_in): # 乘加 self.psum = psum_in + self.act_reg * self.weight # 激活值传给右边 act_out = self.act_reg self.act_reg = act_in return act_out, self.psum def systolic_array(activations, weights, size): array = [[PE() for _ in range(size)] for _ in range(size)] # 加载权重 for i in range(size): for j in range(size): array[i][j].weight = weights[i][j] # 逐周期推进 results = [] for t in range(len(activations) + size): # 构造输入,处理边界 # 每个PE接收左邻居的act和上邻居的psum # 收集底部输出 pass return results

这个模型跑起来之后,你可以打印每个周期每个PE的状态,直观看到激活值怎么斜着流进去、部分和怎么往下走。我建议每个想搞懂脉动阵列的人都动手写一遍,比看十张架构图都管用。

5. 实操中的坑与排查技巧

5.1 阵列利用率低的常见原因

实测下来,阵列利用率低通常不是阵列本身的问题,而是外围没配合好。我整理了一份排查清单:

现象可能原因排查方向
利用率低于30%数据供给带宽不足检查SRAM位宽、bank冲突
阵列周期性空转负载维度不匹配统计实际矩阵维度分布
部分PE长期闲置数据流调度不合理检查权重加载和激活流入的时序
功耗异常高无效翻转过多加时钟门控、数据门控

我遇到最典型的一次,是激活值流入的节奏和权重加载没对齐,导致前几个周期阵列在算垃圾数据。后来加了一个简单的状态机,把加载和计算分成明确的阶段,利用率立刻从40%提到75%。

5.2 边界处理与padding的坑

矩阵维度往往不是阵列尺寸的整数倍,这时候需要padding。padding本身不难,难的是padding带来的额外计算和存储开销。我的经验是:尽量让padding后的维度接近阵列尺寸的整数倍,减少浪费。比如阵列是32×32,实际维度是250,那padding到256比padding到288划算得多。

另外,padding的数据要保证不污染结果。通常做法是padding零,因为零乘任何数都是零,不会影响累加。但要注意,如果用了带偏置的累加,padding部分可能会引入偏差,需要在最后结果里裁掉。

5.3 权重加载的时序设计

权重加载阶段阵列是不干活的,这段时间纯属开销。减少这个开销的办法有两个:一是双缓冲,一边算当前块一边加载下一块的权重;二是权重压缩,利用稀疏性或量化减少加载量。

我做过一个对比:不加双缓冲时,权重加载占了总时间的15%左右;加了双缓冲之后降到3%以下。这个优化在批量小、切换频繁的场景下收益特别明显。

5.4 量化对脉动阵列的影响

现在AI芯片普遍用INT8甚至INT4量化,这对脉动阵列是利好,因为乘法器面积和功耗都大幅下降。但量化也带来新问题:累加器的位宽要足够,否则会溢出。INT8乘INT8的结果是INT16,累加K次后位宽还要增加log2(K)位。K=256时,累加器至少要24位才安全。

我见过有人为了省面积把累加器做成16位,结果大K值下频繁溢出,精度崩了。这个位宽计算一定要在架构阶段就算清楚,别等到RTL阶段才发现。

6. 脉动阵列之外的一点延伸思考

脉动阵列火了这么多年,但它不是终点。我观察到几个演进方向值得关注:一是可重构数据流,让阵列能在权重驻留和输出驻留之间切换,适应不同负载;二是稀疏感知,跳过零权重对应的计算,提升有效利用率;三是近存计算,把阵列和存储做得更近,进一步降低搬运开销。

但不管怎么演进,脉动阵列背后的核心思想不会变:让数据流动起来,让复用最大化,让硬件结构匹配运算的规整性。理解了这一点,你再看任何一款AI加速器的架构,都能快速抓住它的设计取舍。

我个人在实际项目里的体会是,脉动阵列的难点从来不在阵列本身,而在它和存储、调度、量化这些外围模块的配合。把数据供给的带宽算清楚,把时序对齐做扎实,把边界情况处理干净,阵列的利用率自然就上去了。反过来,如果只盯着阵列尺寸和峰值算力,做出来的东西大概率是纸面参数好看、实测拉胯。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询