☰
脉动阵列:AI芯片如何用数据流动破解算力与带宽困局
2026/10/28 21:13:38 网站建设 项目流程

1. 算力与带宽的矛盾:脉动阵列诞生的现实逻辑

第一次听到"脉动阵列"这个名字,是在一次AI芯片的架构评审会上。说实话,刚接触这个概念的人很容易被名字唬住——"脉动"?"阵列"?听起来像是某种流体力学的模型。但搞了几年芯片设计之后你会发现,这个名字起得相当直白:它就是让数据像血液在血管里一样,有节奏地流过一个计算单元阵列,每一个计算单元(PE,Processing Element)只跟邻居打交道,数据流到哪儿,计算就在哪儿完成。

要理解脉动阵列,先得理解AI芯片面临的核心矛盾:算力越来越贵,但把数据搬进搬出的带宽更贵。

传统CPU的思路是"数据放到寄存器,指令控制ALU去算"。这个模型在通用计算里没有问题,因为你要处理的分支、跳转、访存模式完全不可预测,必须靠通用的取指-译码-执行流水线来兜底。但在神经网络推理里,计算模式是极度规律的:卷积、矩阵乘、全连接,本质上就是"乘累加"的堆叠。你让CPU去做这件事,绝大部分晶体管和功耗都消耗在指令调度、缓存替换、分支预测上,真正干活的乘法器反而闲得很。更致命的是,每做一次乘法,数据要从内存里搬到寄存器,算完又要搬回去——这个过程中数据通路的功耗,比乘法器本身的功耗高一个数量级。

脉动阵列换了一个思路:既然数据搬运的代价远高于计算,那我们就让数据少搬家,让乘法器密集地排列在一起,每个乘法器只跟上下左右四个邻居交换数据,数据顺着阵列像流水一样一步步流过去。没有总线,没有多级缓存,没有复杂的调度器,每个PE就是一个乘法器加一个累加器,再加上一小块寄存器。你用不着从内存取两次数、做一次运算、再写回内存,你只要把矩阵的数据一次性喂到阵列的边界上,它们自己就会在阵列里流动,边流动边把乘累加做完。

这个思路最早来自20世纪80年代卡内基梅隆大学的H. T. Kung团队,他们当时提出来是为了解决信号处理里的矩阵运算问题。真正让脉动阵列大放异彩是Google的TPU——第一代TPU里就是一个256×256的脉动阵列,专门跑推理场景的矩阵乘法。后来的寒武纪、地平线、各种NPU/IPU,几乎都采用了脉动阵列或它的变体作为矩阵运算的核心单元。

所以脉动阵列解决的根本问题是"数据搬运成本"。它不是靠把时钟频率拉高来提升算力,也不是靠堆更多的缓存来缓解带宽压力,而是靠数据在整个计算阵列中有节奏地流动,让每一个数据在生命周期内尽可能多地被复用。

提示:读到这里你可以先记住一个关键判断——脉动阵列不是某种算法,而是一种硬件数据流组织方式。它牺牲了通用性,换来了极高的计算密度和能效比。这个"通用性换能效"的取舍,是所有AI芯片设计者绕不开的起点。

2. 三种数据流模式与PE内部的微架构

2.1 权重固定(Weight Stationary):TPU的选择

脉动阵列最关心的一个问题是:矩阵计算中,谁应该留在阵列里不动,谁应该在里面流动?

以经典的矩阵乘法 C = A × B 为例,A矩阵(输入特征图)和B矩阵(权重)都需要参与运算。如果让权重B预先加载到每个PE里,保持不动,输入数据A像流水一样从阵列中流过,每个PE将自己持有的权重和流过的输入做乘累加,最后得到部分的输出C——这就是Weight Stationary(权重固定)模式。

这个模式最大的好处是权重被复用得非常充分。一个权重一旦加载进PE,就可以连续参与多次乘法运算,完全不用重复搬运。在卷积网络中,权重本来就是参数总量最大、最需要复用的数据,所以Google TPU v1选择了这种模式——它把权重从DDR搬到片上SRAM,再从SRAM搬到每一个PE的寄存器里,然后权重就"钉"在那里不动了,接下来输入数据的每一行都会从这256个PE上流过去做一次乘累加。

沿着一列PE往下看数据的流动路径:同一列的所有PE共享同一个输入数据流,每一时刻,输入到达PE (i, j),该PE里的权重w(i, j)和输入a做乘法,结果累加到本地寄存器。下一时刻,相同路径上的输入a又流到PE (i+1, j),与那里的权重相乘。与此同时,整个阵列按照节拍同步推进,部分和也可以选择纵向流动或留在本地累加。

你会问,这个过程中输出C在哪里算出来的?如果每个PE只做一次乘法就结束,那输出C(i, j)还需要对多个输入维度求和。所以权重固定模式下,部分和是在PE内部不断累加的:同一个PE会被多次不同的输入值"访问",每访问一次就做一次乘加,最终在某个时刻PE内部寄存器里的值就是完整的输出C(i, j)。这也是为什么每个PE里必须累加器比乘法器还要关键——它才是真正"计算"发生的地方。

2.2 输入固定(Input Stationary)与输出固定(Output Stationary)

不是所有场景都适合让权重固定。比如在推理中,如果输入特征图非常大,但每次推理的权重都一样,也许你想把输入钉在PE里、让权重流动,这样可以减少输入数据的反复装载,这就是Input Stationary(输入固定)。

输入固定模式特别适合某种"批量归一化+卷积"联合运算的场景:多个卷积核共享同一块输入特征图,输入数据被加载到PE上后不用动,让不同的卷积核权重依次流过来,做乘累加然后输出。这种方式对带宽压力最小的恰恰是输入侧。

还有一种模式叫Output Stationary(输出固定),它把输出数据当作"固定不动"的一方。完整矩阵乘法的输出C需要通过多轮乘累加才能完成,Output Stationary的思路是把一个输出C(i, j)的所有累加项全部算完之后再写回——这样片上的写回次数最少,输出数据不会在片上反复搬进搬出。如果你的算法对片上存储总量比较敏感,输出固定模式通常能减少中间结果的存储需求,代价是输入数据流和权重数据流的组织会变得更复杂。

三种数据流模式各有取舍,没有哪一个在所有情况下都占优。我在实际引入脉动阵列时通常会先做一遍数据复用度的建模:

  • 推理场景,权重已经固定且复用度高 → Weight Stationary
  • 多数是不断变化的动态输入 → Input Stationary 或灵活切换
  • 输出侧的数据量极其庞大、片内存储吃紧 → Output Stationary

2.3 PE内部到底有什么:乘法器、累加器、寄存器还是SRAM

很多人以为脉动阵列里的PE是一个完整的处理器,其实不是。从第一代TPU的公开资料来看,一个PE内部只包含三个关键部分:

  • 一个低精度乘法器(比如8bit×8bit整数乘法)
  • 一个累加器,专门负责把乘法的结果累加起来
  • 一小块寄存器,用来临时寄存流经本PE的数据(权重、输入或部分和)

就这么点东西,构成了整个阵列的"细胞"。

PE里几乎不会放SRAM,更不会放指令。因为整个阵列的调度逻辑是集中式的外部控制器负责的,PE自身只需同步接收时钟节拍,按固定的延迟把数据从左边传到右边或从上边传下来。没有指令来打扰每个周期该怎么算,这让PE变得异常精简——你能在同样面积上塞下更多的乘法器,计算密度就上去了。

我见过一个刚开始做脉动阵列的团队,第一版设计里每个PE都塞了一个小的多周期乘法器,结果整个阵列的频率根本提不上去,而且面积翻了整整两倍。后来换成组合逻辑的8bit乘法器加定点累加器,面积和功耗都骤降。记住,脉动阵列是吃"简单PE"的红利,PE越简单,排列越密集,数据流动的效率就越高。如果你非要每个PE都做成可以独立编程的微处理器,那就不是脉动阵列了,是一个二维的CPU簇。

3. 从公式到硬件:脉动阵列的装载与空泡问题

3.1 矩阵怎么塞进阵列:分块、装载、对角线调度

这里有个非常重要但又经常被忽略的问题:矩阵乘法公式里的"整块矩阵",怎么映射到一个物理尺寸固定的阵列上?

假设你的脉动阵列是4×4的PE阵列,但你要算的矩阵是1000×1000的。不可能把1000列都摆在阵列上,所以必须分块(tiling)。如图所示的经典做法是:把大矩阵切成K个小块,每次在阵上完成一块的乘累加,部分和留在PE里,循环把K个小块都算完,最后一次性把输出写回。

分块大小不是拍脑袋定的。一个经验法则:你的分块的大小最好能适配片上的权重SRAM容量和输入数据的复用关系。块切得太小,装载频率就高,带宽会被消耗在装载上;块切得太大,片上SRAM装不下,你得频繁做DMA换出换入,出现"数据倒灌"。

装载时序是另一个坑。脉动阵列的装载不是"一下子全部填进去"。准确地说,权重数据需要按照列的节奏逐拍打入:第0拍,第一列?不对,往往是一条对角线。

拿2×2阵列举例,假设我们要算 C = A × B,其中A是2×2输入矩阵,B是2×2权重矩阵。你并不需要在第0周期让A[0][0]、A[0][1]同时到达PE(0,0)和PE(0,1)。由于乘法器只能处理单数据流,通常在硬件上你让输入矩阵A的第0行在时钟0到达第一行PE,而权重矩阵B按对角线方式先装载:B[0][0]在第0周期到达PE(0,0),B[1][1]在第1周期到达PE(1,1)……这样调度的结果是,数据在阵列中形成斜向的波前(wavefront),每一拍数据向下/向右流动一个PE,像波浪一样推进。

这种"对角线装载"或者叫"波前调度"是脉动阵列里最核心的时序设计。用软件术语来说,每一条对角线对应一个时钟周期,所有同时参与计算的数据恰好落在一条对角线上。这就是为什么脉动阵列的控制器往往复杂——它不是算乘法,而是管数据该在哪一拍、从哪条边、以什么顺序进入阵列。

3.2 空泡(Bubble)是怎么产生的,以及如何用软件抹掉

真实世界的矩阵不可能长得那么整齐。比如卷积网络的输入张量是 [N, C, H, W],你在做3×3卷积时,每个输出位置要滑动窗口取9个数据。如果数据流组织不当,某些PE在一个时钟周期里等不到有效数据,这个周期就被浪费了——这就是空泡(bubble)。

空泡的产生主要有两个原因:

第一,矩阵尺寸和物理阵列尺寸不匹配。300宽的矩阵放在256宽的阵列上,你只有256列在干活,剩下44列被闲置——严格说这不算空泡,算是利用率损失。更致命的是一些不规则操作,比如稀疏矩阵。现在很多AI芯片在搞稀疏化,权重矩阵里大量元素是0,如果你照样装载,乘法器做的全是"零乘有数"的无效运算,有效利用率会大幅下滑。

第二,卷积的滑动窗口天然会导致数据重复装载。同一块输入数据会被多个不同的卷积核复用,如果你没有安排好装载顺序,某个PE算完了手头的数据,下一个数据还没到,空泡就出现了。

解决空泡问题,软件和硬件要协同做。硬件层可以在PE数据通路上加"有效数据"标志位,空周期直接旁路不做乘法,降低功耗;软件层则通过数据重排和数据流调度来尽量减少空泡。我在做卷积映射时通常会把滑动窗口的重复利用率算进调度里,优先保证每个PE永远有活干。实测下来,对常见的3×3卷积,好的调度能把阵列利用率从不到50%拉到80%以上。

3.3 卷积的im2col与直接数据流:映射工程的真实取舍

既然脉动阵列本身只擅长乘累加,那卷积到底怎么送进去?业界有两个经典方案,各有取舍。

方法一叫im2col(Image to Column)。它把3×3卷积的每个窗口展开成一行向量,然后把所有窗口的行向量拼成一个大的矩阵,最后把卷积核也展开成矩阵,这样整个卷积就变成了一个标准的矩阵乘法。好处是你可以直接复用保证矩阵乘法的内核调度,软件生态成熟;坏处是特征图会被展开成原来几倍大的中间矩阵,内存占用暴增,而且展开操作本身也需要消耗额外的访存带宽。

方法二是直接在阵列上做"数据流卷积"(direct convolution)。它不让数据中间落地成大矩阵,而是利用脉动阵列的波前特性,让卷积窗口的数据在流动过程中自然重叠。这样一来省了中间矩阵,但对控制器的设计要求高得多——每次卷积核尺寸变化、步长变化、空洞率变化,都要重新生成一套调度方案。灵活性是它的代价。

从我的经验看,纯推理芯片如果追求极致能效,往往是混着来:主流3×3卷积走直接数据流,全连接和1×1卷积走im2col式的矩阵乘,两者的调度器共用一套存储和装载通路。真正把卷积全部做成im2col的,大多数是早期为了快速验证算法正确性的FPGA原型,不是最终量产方案。

4. 看懂一个真实的脉动阵列设计(以Google TPU v1为例)

4.1 256×256还是64×64?阵列规模的能耗与面积权衡

脉动阵列的规模怎么定,是架构师最头疼的问题之一。Google TPU v1公开的论文里提到,它使用了一个256×256的脉动阵列(实际有效是256×256的MAC计算),能在一个时钟周期内完成65536次乘累加运算,工作频率大约是700MHz,峰值算力一度达到了92 TOPS(8bit整数)。

你可能觉得,阵列越大越好,65536个PE的并行度多高啊。但大规模脉动阵列的物理实现代价非常具体:

面积:每个PE就算再精简,256×256个PE连在一起,布线资源就会变得非常紧张。PE之间的数据通路是逐格连接的,阵列规模翻一倍,面积至少翻三倍(PE本身+连线+重复的驱动buffer)。做后端物理设计的时候,你要在设定的功耗墙和时间收敛约束下保证每一列数据的延迟一致,这是非常头疼的。

功耗:PE虽然单个功耗极低,但六万多个PE同时在翻转,动态功耗非常可观。Google TPU v1的功耗非常大,很大一部分就烧在阵列的搬入搬出上。如果你没有足够的散热设计做支撑,阵列规模再大也只能降频使用。

装载效率:阵列越大,单次装载的浪费也越大。比如256×256的阵列去处理一个128×128的矩阵乘法,利用率只有25%,剩下的PE全在空转。所以现代很多芯片把阵列拆成多个中等规模的子阵列,比如4个64×64的小阵列,灵活性反而更好,可以根据实际矩阵大小动态调度不同的子阵列。

我在评估阵列规模时,一般先算一个"装载率曲线":把目标模型里所有矩阵乘法的维度分布统计出来,找出90%以上的矩阵长什么样,然后拿这个分布去试不同规模的阵列,选择一个在平均利用率上最优但面积不至于爆炸的尺寸。这个流程虽然听起来朴素,但能避免很多"买大求全"的架构决策失误。

4.2 为什么只做矩阵乘法,其他算子怎么办

第一代TPU最让人惊讶的一点:它没打算什么都算。激活函数ReLU、池化、甚至某些非矩阵算子,都会从阵列里拉出来,交给单独的硬件模块处理。

原因很现实:脉动阵列这个物理结构是为密集乘累加量身定做的,你让它在上面做ReLU(直接判断是否是0、取最大值)等于让一个重型卡车去送快递,既慢又浪费。激活函数和池化的计算量虽然不高,但其控制逻辑杂、访存模式随机,强行往阵列里塞,反而会打断数据的连续流动,制造大量空泡。

所以TPU的做法是"数据流分段":卷积层和矩阵乘下沉到脉动阵列处理,每个卷积层结束后,输出数据流经片上的"激活/池化单元"做非线性和降采样,再回到片上的卸载缓冲区,送去下一层。这种异构分工,让每个硬件模块都做自己最擅长的事。换成我家用的语言来说:把方钉放进方眼,圆钉放进圆眼,不要试图用一个万能插件解决所有问题。

这对普通开发者有什么启发?如果你要在FPGA或ASIC里做脉动阵列加速器,千万别一上来就规划"我要让阵列支持所有算子"。先梳理你算法里的算子占比,矩阵和卷积占了80%以上,就老老实实做矩阵路径,其他算子用独立的简单硬件单元配合流水线实现,效率会高得多。

4.3 脉动阵列如何配合流水线、量化、片外存储

用脉动阵列做推理加速,永远不是"阵列自己一家独大",它要跟周围的存储、控制、数据通路配合成一个完整系统。TPU v1片内SRAM只有28MB(其实最开始是8MB的权重SRAM加一些中间状态),和现在动辄上百MB的NPU相比少得可怜,但它通过优化数据流来把DDR带宽压到极限。

最关键的配合是权重SRAM的分块预装载。假设你要连续推理很多张图片,那么权重应该尽量常驻在片内。TPU的权重存储是按层预分配的,控制器在上一层计算还没结束时,就会把下一层需要的权重从DRAM提前拉到SRAM的ping-pong缓冲区内,以此隐藏装载延迟。这种双缓冲(double buffering)技巧是脉动阵列系统设计的老祖宗——没有它,装载时间会和计算时间串行叠加,再快的阵列也会被带宽拖垮。

量化在这里面扮演的角色同样重要。TPU v1主打8bit整数推理,而不是32bit浮点。8bit的乘法器面积大约是FP32乘法器的1/10,功耗更是小一个量级。脉冲阵列里65536个PE如果都放FP32乘法器,物理上根本做不出来功耗墙内的芯片。所以脉动阵列的锋芒,很大程度上是依托低精度量化才真正发挥出来的。

如果要做量化感知训练,你甚至可以把量化误差的分布反过来反馈给阵列的装载调度:哪些层容易出现精度下降,就在该层用更保守的量化策略,或者单独开一条高精度通路给敏感层。这个"精度-性能"联合设计思路,我在好几个项目里都验证过,很有用。

5. 如果你要在FPGA/RTL里手搓一个脉动阵列:实操与踩坑

5.1 从调度表到Verilog:先画时序不要先写代码

我在带团队做FPGA原型时,反复强调一句话:不要一上来就写Verilog/VHDL,先把调度图画出来。

脉动阵列的调度表是整个设计的地基。它的本质是:在每一个时钟周期,哪些数据从哪一侧进入哪个PE。你需要画一个时间轴,把PE阵列的坐标标上,然后标出每个周期、每个PE的输入来源(左邻居、上邻居、外部装载)和输出去向。这一步往往比写代码花两倍时间,但能帮你在仿真阶段省下二十倍的调试时间。

画调度表的实操步骤:

  1. 确定矩阵维度、阵列维度、PE间单向数据流方向(例如输入从左边流入,权重从上边流入)
  2. 列出乘累加时每个PE需要什么数据、在哪一拍需要
  3. 检查数据冲突:同一拍、同一输入端口,不能有两个数据源竞争
  4. 调整装载顺序:让二极管波前尽量覆盖所有有效PE
  5. 补空泡周期:在矩阵边缘数据还没有到达前,插入无操作,保证数据流整齐

调度表画清楚了,写代码时每个always块干什么就一目了然。我见过一个项目,跳过调度表直接写RTL,结果仿真波形一塌糊涂,最后花费了整整三周改模块间握手信号,比画调度表的时间长得多。

5.2 数据位宽、部分和进位与固定点溢出处理

脉动阵列最大的数值问题不是乘法器,而是累加器的溢出。

假设每个PE里权重和输入都是8bit整数,一次乘法结果最大是255×255=65025,需要17bit表示。如果一次矩阵乘法你需要累加10000个乘加结果(比如大矩阵的K维度是10000),那累加器的位宽至少需要17+14=31bit。很多人在这里犯了拍脑袋的错误:累加器用32bit确实安全,但如果你把位宽定小了,会出现NAN一样的乱值,而且这个问题在波形图上特别难捉。

经验做法是:先统计模型中最大K维累加次数,然后计算所需的累加器位宽,再加上4~8bit的安全余量。宁可面积多花一点,也不要让溢出成为"偶尔出现、无法复现"的幽灵 bug。

另外,部分和在多PE之间的流动还可能产生另一个问题:累积的时序路径太深导致时钟频率上不去。PE(0,0)的部分和流到PE(1,0)再流到PE(2,0),这条路径上每一级都要做加法,数据依赖链很长。如果综合工具自动布局布线后频率不够,你可能需要在数据流方向上加流水寄存器,让部分和晚几个节拍到达最终输出。

5.3 实测中常见的问题:时序跑不上去、测试bench对不上、空泡泄漏

挑几个我实际排查过的问题说:

一是时序跑不上去。脉动阵列的多bit数据线在PE之间级联时,驱动能力和布线延迟会累积,整个阵列的关键路径往往出现在PE的最底层。解决思路是给数据通路插入流水寄存器,把PE间传播路径拆短,或者用低摆幅信号优化功耗。做FPGA原型时还可以把大阵列拆成几个小阵列分频驱动,虽然逻辑上慢一点,但布线更容易收敛。

二是测试bench和你自己的调度表对不上。仿真时,输入矩阵的某一行应该在第几拍到达哪个PE,这不是拿眼睛看就能确认的。我习惯在testbench里设置"数据标签",每个数据包携带一个元数据ID(比如"行3列5"),然后断言它到达PE(2,4)的时刻和预期一致。凡是出现ID错位,肯定是调度表的数据路由画错了。这种断言式验证虽然无聊,但查错效率非常高。

三是"空泡泄漏"。就是本来该是空泡的周期,PE里还残留着上一轮计算的数据,被下一轮误当成有效输入。解决手段是每个数据包带1位valid标志,一旦某个PE的输入valid为低,后续就不执行乘累加而只是透传数据。这个设计几乎每版都要用到,千万别省。

5.4 用脉动阵列的替代方案:什么时候不要用脉动阵列

脉动阵列不是AI加速的唯一解法,甚至不是所有场景的最优解。

如果你的模型是高度稀疏的(比如剪枝到90%以上的稀疏度),密集脉动阵列会浪费大量乘法器在零值上。此时更适合的可能是稀疏感知的"跳过零值"架构,或者依赖稀疏矩阵的专用数据流引擎。

如果你的模型里动态形状占主导,比如自然语言处理里sequence长度变化极其剧烈,脉动阵列这种定长数据流的硬件往往利用率很低,因为每次填充到新的长度都要重新装载。GPU/TPU的"静态形状图"物理架构还能忍,但不规则的动态推理场景,CPU或可编程的向量处理器可能更合适。

另外,如果你的目标是极致灵活地落地到边缘端,一个只有4×4或者8×8的小型脉动阵列搭配向量ALU的组合,可能会比一整块大阵列更实用——小阵列填充方便、功耗可控、调度变化也快。所谓"小步快跑"反而往往是最稳的工程选择。

我在实际项目里选型时,不会一上来就画256×256的大饼,而是先做一轮工作负载分析,把矩阵尺寸分布、稀疏度、动态范围都统计出来,再决定用多大阵列、用哪种数据流。这样既有理论依据,又经得起后端实现的检验。

脉动阵列的潜力目前远没有挖尽。我之前用5nm工艺做仿真时,8bit脉动阵列在能效上依然能比同等面积的SIMT架构强3~5倍,但前提是你要把调度、存储和量化这三件事都做对。算力焦虑背后,真正决定产品体验的从来不是"峰值TOPS",而是数据能不能在阵列里顺畅地"脉动"起来。踩过这些坑之后,我现在的第一建议还是那句话:想清楚了再动手,调度表画明白了,脉动阵列就已经成功了一半。

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

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

立即咨询