ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

AI芯片脉动阵列原理:矩阵乘法加速与工程实现

AI芯片脉动阵列原理:矩阵乘法加速与工程实现

1. 从矩阵乘法说起:为什么AI芯片需要脉动阵列

聊AI芯片,绕不开一个核心问题:算力到底花在哪了。你去看任何一个主流神经网络,不管是CNN做图像识别,还是Transformer跑大语言模型,底层超过80%的计算量都集中在矩阵乘法和卷积上。卷积本质上也可以展开成矩阵乘法,所以归根结底,AI芯片的核心任务就是——把矩阵乘法算得又快又省。

那矩阵乘法到底有多吃资源?举个例子,一个1024×1024的矩阵A乘以1024×1024的矩阵B,输出也是1024×1024。这里面涉及的计算量是1024×1024×1024,也就是大约10亿次乘加运算。如果按传统CPU的做法,每个输出元素都要独立地从内存里把A的一整行和B的一整列读出来,算完再写回去。10亿次运算意味着几十亿次的数据搬运,而数据搬运消耗的能量远比计算本身大得多。有研究数据表明,在28nm工艺下,一次32位浮点乘加运算的能耗大约是3.7皮焦耳,但从DRAM里读一个32位数据的能耗是它的几十倍甚至上百倍。所以问题不在于“算不算得动”,而在于“喂不喂得起”。

这就是脉动阵列(Systolic Array)出场的背景。它的核心思路非常朴素:让数据像心跳一样有节奏地在计算单元之间流动,每个计算单元只跟邻居打交道,数据复用做到极致,从而大幅减少对主存的访问。我第一次接触这个概念的时候,觉得它跟工厂流水线特别像——原材料从一端进,经过一道道工序,每道工序只做一件小事,做完就传给下一个工位,成品从另一端出来。区别在于,脉动阵列里流动的不只是数据,还有“计算”本身。

脉动阵列最早是H.T. Kung在1982年提出的,那时候跟AI没什么关系,主要是为了解决VLSI(超大规模集成电路)里计算资源受限的问题。但几十年后,大家发现这个结构简直是为矩阵乘法量身定做的,于是Google在TPU里把它用了起来,一下子成了AI芯片的标配架构之一。你现在看到的很多AI加速器,不管是大厂的自研芯片还是创业公司的产品,多多少少都能看到脉动阵列的影子。

这篇文章我打算从工程实现的角度,把脉动阵列的来龙去脉讲清楚。不管你是刚入行的芯片设计工程师,还是想了解AI硬件底层原理的软件开发者,或者只是对“为什么TPU能跑那么快”感到好奇的技术爱好者,我都会尽量用大白话把关键点拆开。我会讲清楚它的工作原理、数据怎么流动、为什么能省带宽、实际设计中有哪些坑、以及它跟其他架构比到底强在哪、弱在哪。看完之后,你再看AI芯片的架构图,应该能一眼认出哪些是脉动阵列,以及它为什么长那样。

2. 脉动阵列的工作机制:数据怎么“脉动”起来

2.1 一个最简单的例子:3×3矩阵乘法怎么在阵列上跑

要理解脉动阵列,最好的办法就是拿一个小矩阵手动推一遍。假设我们要算C = A × B,其中A和B都是3×3的矩阵。传统做法是三重循环,一个一个算。脉动阵列的做法完全不同:它把计算单元排成一个3×3的网格,每个单元负责一个输出元素的部分累加。

具体怎么操作?A矩阵的元素从左侧流入,B矩阵的元素从上方流入。每个计算单元(Processing Element,简称PE)内部有一个乘法器和一个累加器。在每一个时钟周期,A的元素向右传,B的元素向下传,同时当前PE把收到的A和B相乘,累加到自己的寄存器里。

我拿第一行第一列的输出C[0][0]来举例。它需要A[0][0]×B[0][0] + A[0][1]×B[1][0] + A[0][2]×B[2][0]。在脉动阵列里,A[0][0]从左上角PE进入,B[0][0]也从左上角PE进入,第一个周期它们就在PE(0,0)相遇并相乘。第二个周期,A[0][0]向右传到PE(0,1),同时A[0][1]进入PE(0,0);B[0][0]向下传到PE(1,0),同时B[1][0]进入PE(0,0)。你看,PE(0,1)拿到的是A[0][0]和B[1][0],PE(1,0)拿到的是A[0][1]和B[0][0],这两个乘积恰好都是C[0][0]需要的项。到第三个周期,所有项都凑齐了,C[0][0]的累加完成。

这个过程的关键在于:每个PE只需要跟左右上下的邻居通信,不需要全局广播,也不需要从内存里反复读数据。A的每一行元素进入阵列后,会依次经过一整行的PE,被复用3次;B的每一列元素进入后,会依次经过一整列的PE,也被复用3次。对于N×N的矩阵乘法,每个数据元素被复用了N次,而传统做法里每个数据只被读一次就用完了。这就是脉动阵列省带宽的核心逻辑。

2.2 数据复用与带宽节省的量化分析

光说“省带宽”太虚了,我们拿数字说话。还是N×N的矩阵乘法,传统做法需要从内存读取A的N²个元素和B的N²个元素,总共2N²次读取。脉动阵列呢?A的每个元素只需要从内存读一次,进入阵列后就在PE之间流转;B也一样。所以总读取次数还是2N²,看起来没省?

别急,关键在于“读取”的层次。传统做法里,每次乘加运算都要从寄存器堆或者缓存里取数,而脉动阵列里,数据一旦进入PE,就在PE内部的寄存器里待着,下一个周期直接传给邻居,不需要再访问任何存储层次。换句话说,脉动阵列把“存储访问”变成了“寄存器间传递”,而后者的能耗比前者低一到两个数量级。

更准确地说,对于N×N的矩阵乘法,脉动阵列的PE数量是N²,每个PE在每个周期做一次乘加。完成整个矩阵乘法需要大约3N个周期(因为数据要流进流出)。总计算量是N³次乘加,总的数据加载量是2N²。计算访存比(Compute-to-Memory Ratio)是N³ / 2N² = N/2。当N=256的时候,这个比值是128,意味着每从内存加载一个数据,就能做128次运算。而传统CPU的访存比通常只有个位数。这就是为什么脉动阵列特别适合大矩阵——矩阵越大,数据复用率越高,带宽优势越明显。

我画个表对比一下不同规模下的情况:

矩阵规模NPE数量总计算量数据加载量计算访存比
8645121284
32102432768204816
1281638420971523276864
2566553616777216131072128
512262144134217728524288256

从表里能看出来,N越大,脉动阵列的优势越明显。但这里有个工程上的矛盾:PE数量是N²,N=512的时候需要26万个PE,芯片面积和功耗都扛不住。所以实际芯片里,脉动阵列的规模通常在128×128到256×256之间,再大的矩阵就分块(Tiling)处理,一块一块地喂给阵列。

2.3 权重固定 vs 输出固定:两种数据流的取舍

脉动阵列不是只有一种玩法。根据数据流动方式的不同,主要分为两种模式:权重固定(Weight Stationary)和输出固定(Output Stationary)。这两种模式在工程实现上差别很大,直接影响芯片的面积、功耗和灵活性。

权重固定模式,顾名思义,就是把权重(也就是模型参数)预先加载到PE里,然后让输入数据(激活值)在阵列里流动。这种模式的好处是权重只需要加载一次,之后可以反复使用,特别适合推理场景——因为推理时权重是固定的,只有输入数据在变。Google的TPU第一代用的就是这种模式,权重存在PE的寄存器里,输入数据从左侧流入,部分和从下方流出。

输出固定模式则是把部分和(Partial Sum)固定在PE里,让权重和输入数据都流动起来。这种模式适合训练场景,因为训练时权重需要不断更新,固定在PE里反而麻烦。但输出固定模式的控制逻辑更复杂,因为部分和需要在PE里累加,还要处理不同数据到达时间的对齐问题。

我个人的经验是,如果你做的是推理芯片,权重固定模式更简单、更高效;如果要做训练芯片,输出固定或者混合模式更合适。但不管哪种模式,核心思想都是一样的:让数据在PE之间流动,减少对主存的访问。

注意:权重固定模式下,如果模型太大,权重装不进阵列的寄存器,就需要分块加载,这时候权重复用的优势会打折扣。所以实际设计时,阵列规模要和目标模型的参数量匹配,不是越大越好。

3. 从TPU到边缘端:脉动阵列的工程实现细节

3.1 TPU v1的脉动阵列长什么样

Google在2016年公开TPU v1的架构时,很多人第一次看到了商用脉动阵列的细节。TPU v1的核心是一个256×256的脉动阵列,总共65536个PE,每个PE每个周期可以做一次8位整数的乘加运算。整个阵列的峰值算力是92 TOPS(万亿次运算每秒),功耗只有40瓦左右。这个能效比在当时是非常惊人的。

TPU v1的脉动阵列采用的是权重固定模式。权重从阵列的左侧加载,输入数据从上方流入,部分和从下方流出。每个PE内部有一个8位的乘法器、一个32位的累加器,以及一些控制逻辑。权重加载需要256个周期,之后就可以持续不断地处理输入数据。整个数据流是高度流水线化的,一旦流水线填满,每个周期都能产出一个完整的输出结果。

但TPU v1也有明显的局限。它只支持8位整数运算,不支持浮点,所以只能做推理,不能做训练。而且256×256的阵列对于小批量推理来说太大了,很多PE会闲置。Google后来在TPU v2和v3里做了改进,支持了浮点运算,还引入了“脉动阵列+向量单元”的混合架构,把灵活性补了上来。

3.2 边缘端芯片的缩小版脉动阵列

TPU是数据中心级别的芯片,功耗几十瓦,面积几百平方毫米。但脉动阵列的思想同样可以用在边缘端。比如一些做智能摄像头的AI芯片,功耗只有几瓦甚至几百毫瓦,它们也会用一个小规模的脉动阵列,比如16×16或者32×32。

边缘端的挑战跟数据中心完全不同。数据中心追求峰值算力,边缘端追求能效比和面积效率。一个16×16的脉动阵列只有256个PE,如果每个PE做8位乘加,峰值算力大概是几百GOPS(十亿次运算每秒),功耗可以控制在1瓦以内。但边缘端的模型通常比较小,比如MobileNet、YOLO-nano之类的,参数量在几百万到几千万之间。这时候脉动阵列的规模就要和模型匹配,太大了浪费面积,太小了算力不够。

我在实际项目中遇到过一个问题:边缘端芯片的片上存储通常很小,可能只有几百KB到几MB。脉动阵列虽然减少了主存访问,但权重加载和输入数据的缓冲仍然需要存储。如果存储不够,阵列就会经常停下来等数据,利用率大幅下降。所以边缘端设计脉动阵列时,存储带宽和阵列规模的匹配比数据中心更关键。

3.3 数据位宽与精度:8位、16位还是浮点

脉动阵列的PE里,乘法器和累加器的位宽直接决定了芯片的面积和功耗。8位整数的乘法器面积大约是16位的一半,32位浮点的四分之一甚至更少。所以早期AI芯片几乎都用8位整数做推理,因为精度够用,面积和功耗都省。

但事情没那么简单。8位整数的动态范围有限,做累加的时候很容易溢出。比如两个8位数相乘,结果是16位;但如果要累加256个这样的乘积,结果可能达到24位甚至更多。所以累加器通常要用32位,不然会溢出。TPU v1的PE里,乘法器是8位,但累加器是32位,就是这个原因。

到了训练场景,8位整数就不够用了。梯度更新需要更高的精度,通常要用16位浮点或者32位浮点。但浮点乘法器的面积和功耗远大于整数乘法器。所以很多训练芯片会采用混合精度:前向传播用16位,反向传播用32位,或者用bfloat16这种折中方案。bfloat16的指数位和32位浮点一样多,但尾数位只有7位,精度比16位浮点低,但动态范围大,不容易溢出。Google在TPU v2之后就开始支持bfloat16,现在很多AI芯片都跟进了。

提示:如果你在设计脉动阵列,位宽的选择一定要跟目标应用场景匹配。推理芯片优先考虑8位整数,训练芯片考虑bfloat16或混合精度。不要盲目追求高精度,面积和功耗的代价很大。

4. 脉动阵列不是万能药:局限性与其他架构的对比

4.1 稀疏性和不规则计算的天然短板

脉动阵列最大的问题是它太“规整”了。它的设计前提是矩阵乘法是稠密的、规则的,每个PE在每个周期都有活干。但现实中的神经网络越来越稀疏——剪枝后的模型有很多零权重,ReLU激活函数会让一半的激活值变成零。如果把这些零也送进脉动阵列,那就是在浪费算力。

更麻烦的是,稀疏性的模式通常是不规则的。哪些权重是零、哪些激活值是零,事先不知道,而且每次输入可能都不一样。脉动阵列的PE是固定连接的,数据流是预先编排好的,很难动态跳过零。所以很多芯片在处理稀疏模型时,实际算力利用率可能只有30%到50%。

为了解决这个问题,一些芯片开始引入“稀疏感知”的脉动阵列,比如在PE里加一个判断逻辑,如果输入是零就跳过乘法,只做累加。但这会增加PE的面积和复杂度,而且只能处理结构化的稀疏(比如整个块都是零),对于随机稀疏效果有限。

4.2 跟SIMD、GPU、FPGA的横向对比

脉动阵列不是唯一的AI加速架构。GPU用的是SIMD(单指令多数据)架构,成千上万个线程并行执行,灵活性高,适合各种不规则计算。FPGA可以动态重构,适合小批量、低延迟的场景。那脉动阵列到底强在哪?

架构优势劣势适用场景
脉动阵列能效比高,数据复用率高灵活性差,稀疏支持弱大规模稠密矩阵乘,推理为主
SIMD/GPU灵活,生态成熟能效比低,带宽压力大训练、通用计算
FPGA可重构,低延迟开发难度大,峰值算力低小批量、定制化场景
存内计算能效比极高工艺不成熟,精度受限未来方向,尚未大规模商用

从表里能看出来,脉动阵列的定位很明确:它就是为稠密矩阵乘法设计的专用加速器。如果你的模型是稠密的、规则的,脉动阵列的能效比是最好的。但如果模型稀疏、不规则,或者需要频繁变结构,GPU和FPGA可能更合适。

我个人的判断是,未来AI芯片不会只用一种架构,而是“脉动阵列+向量单元+可重构逻辑”的混合体。脉动阵列负责大头部的稠密计算,向量单元处理激活函数、归一化等逐元素操作,可重构逻辑应对稀疏和不规则计算。TPU v2之后的架构就是这个思路。

4.3 实际部署中遇到的坑:利用率为什么上不去

理论算力很美好,实际部署经常打脸。我见过不少项目,芯片标称算力几十TOPS,实际跑模型的时候利用率只有20%到30%。问题出在哪?

第一个坑是数据供给跟不上。脉动阵列虽然省带宽,但不是不耗带宽。如果片外存储的带宽不够,或者数据预取做得不好,阵列就会经常饿着。特别是大模型,权重动辄几百MB,片上是存不下的,必须从片外加载。这时候带宽就是瓶颈,阵列规模再大也没用。

第二个坑是矩阵维度不匹配。脉动阵列通常是N×N的,如果实际矩阵的维度不是N的整数倍,就需要补零,补零的部分就是浪费。比如一个256×256的阵列处理一个100×100的矩阵,实际利用率只有(100/256)² ≈ 15%。所以实际芯片通常会支持分块和动态调度,把小矩阵拼成大矩阵,或者用多个小阵列并行处理。

第三个坑是控制开销。脉动阵列的数据流需要精确的时序控制,每个PE什么时候收数据、什么时候传数据、什么时候累加,都要提前编排好。如果控制逻辑太复杂,或者编译器的调度不够优化,阵列的流水线就会经常断流。这也是为什么AI芯片的软件栈和编译器如此重要——硬件再好,软件跟不上也白搭。

5. 自己动手:用Python模拟一个脉动阵列

5.1 模拟器的设计思路

光看理论不过瘾,我们动手写一个简单的脉动阵列模拟器。用Python就够了,不需要任何硬件知识,目的是把数据流动的过程可视化,帮你建立直觉。

模拟器的核心是一个二维数组,每个元素代表一个PE。每个PE有两个输入队列(一个收A,一个收B),一个累加器,以及两个输出队列(一个往右传A,一个往下传B)。每个时钟周期,所有PE同时从输入队列取数、相乘、累加、然后把数据推到输出队列。为了简化,我们假设数据流入和流出的时序是理想化的,不考虑握手信号和反压。

代码大概长这样:

class PE: def __init__(self): self.acc = 0 self.a_in = None self.b_in = None self.a_out = None self.b_out = None def step(self): if self.a_in is not None and self.b_in is not None: self.acc += self.a_in * self.b_in self.a_out = self.a_in self.b_out = self.b_in self.a_in = None self.b_in = None class SystolicArray: def __init__(self, n): self.n = n self.pes = [[PE() for _ in range(n)] for _ in range(n)] def load_and_run(self, A, B): # 简化版:假设A和B已经按对角线对齐 cycles = 3 * self.n for t in range(cycles): # 注入数据 for i in range(self.n): if t - i >= 0 and t - i < self.n: self.pes[i][0].a_in = A[i][t - i] for j in range(self.n): if t - j >= 0 and t - j < self.n: self.pes[0][j].b_in = B[t - j][j] # 所有PE同步执行 for i in range(self.n): for j in range(self.n): self.pes[i][j].step() # 传递数据 for i in range(self.n): for j in range(self.n): if j + 1 < self.n: self.pes[i][j + 1].a_in = self.pes[i][j].a_out if i + 1 < self.n: self.pes[i + 1][j].b_in = self.pes[i][j].b_out # 收集结果 C = [[self.pes[i][j].acc for j in range(self.n)] for i in range(self.n)] return C

这段代码跑起来,你会看到A的元素从左边一列一列地进入,B的元素从上边一行一行地进入,然后在阵列里斜向传播。每个PE的累加器最终存的就是对应的输出元素。你可以拿一个3×3的矩阵手动验证一下,结果应该跟numpy的矩阵乘法一致。

5.2 从模拟结果看数据复用和时序

跑完模拟器,你可以打印每个周期每个PE的状态,观察数据是怎么流动的。你会发现几个有意思的现象:

第一,阵列的利用率是逐渐上升的。前几个周期,只有左上角的PE在工作,大部分PE是空闲的。这就是流水线填充(Pipeline Fill)的过程。对于N×N的阵列,填充需要大约N个周期,排空也需要N个周期。所以总周期数是3N,其中只有N个周期是满负荷的。当N很大的时候,填充和排空的开销可以忽略;但当N很小的时候,这个开销就很显著了。

第二,数据确实被复用了。A的每一行元素在阵列里横向传播,经过N个PE,被用了N次。B的每一列元素纵向传播,也被用了N次。你可以统计一下每个数据元素被读取的次数,跟传统三重循环对比,就能直观感受到复用率的差异。

第三,时序对齐非常关键。如果A和B的数据到达时间不对,PE就会在错误的周期拿到错误的数据,结果就全错了。实际硬件里,这个时序是由编译器或者控制器精确编排的,差一个周期都不行。这也是为什么脉动阵列的编译器如此复杂——它要解决的是一个大规模的时序调度问题。

5.3 模拟器能帮你验证的几个关键问题

这个模拟器虽然简单,但能帮你验证不少设计问题。比如:

  • 如果矩阵维度不是N的整数倍,补零之后利用率下降多少?
  • 如果数据位宽从8位增加到16位,累加器需要多少位才不会溢出?
  • 如果阵列规模从8×8增加到16×16,总周期数怎么变化?能效比的提升是否值得面积翻倍?
  • 如果输入数据有零,跳过零能不能减少周期数?怎么修改PE的逻辑?

我建议你在模拟器的基础上做几个实验,把结果记录下来。这些实验虽然简单,但能帮你建立对脉动阵列的“手感”。等你真正看硬件架构图的时候,脑子里就能自动跑起数据流,知道哪个模块在干什么。

提示:模拟器里没有考虑时钟频率、功耗、面积这些物理约束,所以它只能帮你理解逻辑行为,不能用来评估实际芯片的性能。真要评估,还是得上RTL仿真或者FPGA原型验证。

6. 脉动阵列的演进方向与我的几点判断

6.1 从固定阵列到可重构阵列

早期的脉动阵列是固定的,PE之间的连接是硬连线的,只能做矩阵乘法。但现在的研究方向是可重构脉动阵列——PE之间的连接可以通过配置改变,既能做矩阵乘法,也能做卷积、FFT、排序等其他操作。这种灵活性对于多模态AI芯片很重要,因为未来的模型不会只有矩阵乘法,还会有图神经网络、稀疏计算、动态路由等。

可重构的代价是面积和功耗。每个PE需要额外的配置寄存器和多路选择器,连接网络也更复杂。所以可重构脉动阵列通常用在需要灵活性的场景,比如边缘端的通用AI加速器。数据中心的专用芯片还是倾向于固定阵列,因为追求极致的能效比。

6.2 存内计算与脉动阵列的结合

存内计算(Compute-in-Memory)是另一个热门方向。它的核心思想是把计算单元嵌入到存储阵列里,让数据在存储单元里直接参与计算,省去数据搬运的能耗。这跟脉动阵列的思想是一脉相承的——都是减少数据搬运,只是存内计算走得更极端。

目前存内计算主要用模拟电路实现,精度和可靠性还有待验证。但已经有一些研究把脉动阵列和存内计算结合起来,用存内计算做乘法,用脉动阵列做累加和数据流控制。这种混合架构的能效比理论上可以比纯数字脉动阵列高一个数量级,但工艺和设计挑战也大得多。

6.3 给芯片设计新人的几点实在建议

如果你刚入行,想往AI芯片架构方向发展,我的建议是:

第一,先把脉动阵列吃透。它是AI芯片架构的“Hello World”,理解了它,再看其他架构就很容易上手。不要只停留在理论,一定要动手写模拟器、跑数据、看波形。

第二,不要迷信峰值算力。实际芯片的性能取决于算力、带宽、存储、编译器等多个因素的配合。我见过太多芯片标称算力很高,实际跑模型一塌糊涂。评估一个架构,要看它在目标工作负载下的端到端表现。

第三,关注软件栈。AI芯片的竞争不只是硬件,更是软件生态。一个易用的编译器、丰富的算子库、完善的调试工具,往往比多几个TOPS的算力更重要。如果你做硬件设计,也要懂软件在干什么,不然设计出来的东西没人会用。

第四,保持对新技术的好奇。脉动阵列是40年前的思想,但它在AI时代焕发了新生。未来还会有新的架构、新的器件、新的计算范式出现。保持学习,保持动手,才能跟上这个快速变化的领域。

我在实际项目中最大的体会是:架构设计没有银弹。脉动阵列在稠密矩阵乘法上很强,但它不是万能的。好的架构师知道在什么场景用什么工具,也知道什么时候该妥协、什么时候该坚持。希望这篇文章能帮你建立对脉动阵列的直觉,下次看到AI芯片的架构图时,你能一眼看出它的设计意图和取舍。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进