☰
从算力焦虑到脉动阵列:AI芯片如何破解数据搬运与能效难题
2026/10/26 16:33:13 网站建设 项目流程

1. 从算力焦虑说起:为什么AI芯片绕不开脉动阵列

这两年只要聊AI芯片,不管是做加速卡的创业公司,还是在做端侧NPU的团队,绕不开的一个词就是“脉动阵列”。我最早接触这个概念,是在看Google TPU那篇论文的时候。当时第一反应是:这不就是把一堆乘法器排成棋盘,然后让数据像流水线一样流过去吗?原理好像很简单。但真正深入进去,发现自己根本没看懂,因为脉动阵列背后牵涉的,是AI芯片设计里最核心的一笔账——算力、带宽和功耗之间的三角博弈。

先说个背景感受。咱们都知道,AI推理和训练本质上是极其大量的矩阵乘法。一个Transformer模型跑一次推理,几乎全部时间都耗在矩阵乘法和注意力权重的乘法上。而这类计算有个鲜明的特点:计算密度极高,数据复用率极高。同一个权重,会被成百上千个不同输入反复使用;同一个输入,也会和成百上千个权重相乘。如果CPU或者GPU按照传统方式,把数据从内存搬进寄存器、算完再搬回去,那大部分功耗和延迟都耗费在“搬运”而不是“计算”上。

脉动阵列就是为了解决“搬运”问题而生的一个经典架构思路。它把一大堆处理单元(PE,Processing Element)排成二维阵列,数据按节奏从阵列边缘流入,在每个PE里完成一次乘累加,然后直接传给下一个PE。数据流像心脏泵血一样,一拍拍地往前走。这样做的好处是:权重可以预先装载在各个PE里,输入数据只需要从一边灌入,算出的部分和从一个方向流动出去,最终汇成完整的矩阵乘结果。整个过程中,数据不用来回进出全局内存,总线带宽的压力被压到最低。

这些年我接触过不少芯片项目,凡是做AI加速器的,第一版架构大多会考虑脉动阵列。原因很简单:它结构规整、易于VLSI实现、面积可控、功耗效率理论上可以做到非常好看。它没有复杂的分布式调度逻辑,没有缓存一致性问题,只要把数据流管好,效率就能稳定发挥。这也是为什么从Google TPU到寒武纪,再到各种端侧NPU,都能看到脉动阵列或其变体的影子。

适合看这篇文章的人,我觉得有三类:一是做芯片架构设计的工程师,想搞懂脉动阵列的设计权衡;二是做AI加速器软件栈的开发者,因为硬件是脉动阵列,你会面临映射和调度的很多坑;三是还在学校里读体系结构方向的学生,想建立直觉,知道脉动阵列为什么能赢、赢在哪里、边界又在哪。我希望能把原理和经验写到能让这三类人都觉得有用的程度。

2. 抛开抽象概念:脉动阵列到底是怎么流起来的

很多人看脉动阵列的图,第一眼都会觉得眼晕:一格格方块,一堆箭头,数据朝哪边走的都有。我建议你把脑子里那些花花绿绿的图先扔掉,回到最朴素的视角——它不过是一群乘法器组成的流水线结构。

2.1 最小单元:PE里发生了什么

先看单个PE。一个最基本的PE,内部其实只做了三件事:取一个输入、取一个权重、做一个乘累加。伪代码大概是这样:

acc = 0 for each time step: input_data = receive_from_left_peer() // 从左侧PE收到输入 weight_data = receive_from_up_peer() // 从上方PE收到权重(或预先装载) acc += input_data * weight_data pass_input_to_right_peer(input_data) // 输入继续向右流 pass_weight_to_down_peer(weight_data) // 权重继续向下流

注意看这个循环:每个PE在每个周期里只做一次乘累加,然后把数据往后传。数据不是在内存里,而是在“流动”中。这就把一个很长的矩阵乘法,拆成了无数个微小步流水。

这里有个最容易误解的点:PE里的acc寄存器保存的是部分和,并不是最终结果。最终结果是所有部分和沿着阵列流动、逐级累积之后才得到的。所以你看任何一个脉动阵列的数据流图,会发现它的部分和通常是沿着对角方向流动的,因为它要把同一行(或者同一列)的计算结果累在一起。

2.2 用矩阵乘法看全流程

拿一个具体的例子,假设我们要算 (C = A \times B),A是4×4的矩阵,B也是4×4的矩阵。为了直观一点,我们把A当输入矩阵,B当权重矩阵。一个常见做法是:权重矩阵B预先加载到每个PE里,输入矩阵A从左侧流入,部分和从上方流入、下方流出。

第一步,权重装载:把B矩阵的第j列装到第j列的所有PE上。第0列PE里全是B[0][j]的权重,第1列PE里全是B[1][j]的权重,以此类推。这个过程可以在启动计算之前完成,也可以在计算过程中通过上方的权重通道逐步灌入。

第二步,输入注入:A矩阵从左上角开始,按“对角波前”的方式注入阵列。第一个周期,只有最左上角的PE收到A[0][0];第二个周期,A[0][1]进入第一行第二列PE,同时A[1][0]进入第二行第一列PE;第三个周期,第三列的PE收到A[0][2],第二列收到A[1][1],第一列收到A[2][0]……直到整个A矩阵全部流入。

第三步,部分和累积:C矩阵的结果,是让部分和从阵列顶部流入、底部流出。在(0,0)PE处,A[0][0]乘B[0][0],得到的是C[0][0]的第一个部分和,然后这个部分和往正下方传;下一个周期到达(1,0)PE,这时候正好A[1][0]从左边流进来,乘上B[1][0],加到上一个部分和上,形成C[0][0]的第二个部分和……这样一路累积下去,当部分和从阵列底部流出时,C[0][0]就已经是全部四项乘累加的完整结果了。

这个过程你可以用纸笔画一下坐标,很快就明白了。关键点在于:输入矩阵A的每一行、每一列都严格按时间步错开,错开的距离刚好等于它在二维阵列中要走的路径长度。错位没对齐,算出来的结果就是乱的。

2.3 三种经典数据流:权重固定、输出固定和输入固定

了解脉动阵列的都知道,ArrayNet和TPU用的数据流方式不完全一样,各有各的优缺点。我整理一下三种最经典的数据流,方便对比理解:

  • 权重固定(Weight Stationary,WS):权重预先装在PE里不动,输入数据流经阵列,部分和随输入一起流动。优点是权重复用极好,同一权重可以匹配所有输入,适合推理场景(权重是固定常量)。
  • 输出固定(Output Stationary,OS):每个PE负责算输出矩阵的一个元素,输入和权重都从外面流进来,乘累加结果在PE里累积到最终结果再输出。优点是每个PE只写一次结果,减少数据搬移,适合训练场景(反向传播时权重经常更新)。
  • 输入固定(Input Stationary,IS):输入数据固定在PE里,权重在阵列中流动。这个在稀疏场景或特殊矩阵分解时有用,但工程上用得少一些。

这三种名字经常把人绕晕。我的记忆方法是:看谁“不动”——权重不动是WS,输出不动是OS,输入不动是IS。不动的东西就是需要被最大程度复用的那个操作数,因为“不动”意味着没有反复从内存搬它。

2.4 为什么叫“脉动”——一个心跳的比喻

为什么叫“脉动”呢?我自己的理解是:数据在每个PE之间的流动,由全局时钟统一驱动,就像心脏跳动一下,血液就往前推进一格。整个阵列里所有PE同时动作,数据流稳定地、周期性地向前推进。正因为这种节奏感,它才叫Systolic Array。

这个比喻也揭示了脉动阵列的一个内在约束:全局同步。所有PE都按同一时钟工作,只要有一个PE没算完,整条流水线就得空转。这也是脉动阵列和异步众核架构最本质的区别——它牺牲灵活性换取了极致的规整性和确定性。

3. 效率背后的筹码:脉动阵列到底赢在哪

聊完原理,接下来就是工程决策最关心的部分:脉动阵列到底值不值得用?它的效率优势是怎么换算成实际性能的?

3.1 算力公式:时钟频率 × PE数量 × 使用率

先看最直观的算力公式:理论算力 = 时钟频率 × PE数量 × 每个PE每周期执行的乘累加数。比如一个128×128的阵列,每个PE一次乘累加,跑1GHz时钟,那么理论算力就是128×128×1e9 ≈ 16.4 TOPS(每秒万亿次乘累加操作)。

这个公式看起来简单,但工程上有三个变量都在被制约。时钟频率不能无限高,因为PE之间的数据传递有物理距离限制,频率太高时序收敛困难;PE数量受芯片面积和功耗预算制约;而使用率才是最容易翻车的地方——矩阵形状不匹配阵列尺寸时,大量PE是空转的。

我见过不少团队在第一版流片后,拿着RTL仿真去测实际算力,发现只有理论值的50%。原因就是PE空载。脉动阵列效率高,是指它在“数据恰好能填满阵列”时效率高。一旦遇到很小的矩阵,或者非方阵,空转不可避免。

3.2 能效的账:搬运成本 vs 计算成本

在传统处理器里,一次乘累加的“计算能耗”可能只占很小一部分,大头全耗在数据搬运上。有一个经常被引用的粗略数据:从DRAM读一次数据的能耗,大概是做一次乘累加运算能耗的一百到几百倍。所以只要你能把同一个数据在片上多用几次,能效就会有质变。

脉动阵列的厉害之处,就是让数据在PE之间流动,而不是来回进出全局内存。以TPU为例,它的能效之所以当年能碾压同时代的GPU,很大程度不是因为它晶体管先进,而是因为脉动阵列带来的片上数据复用率极高。同样一个权重,在传统架构里可能要从DRAM读很多次,在脉动阵列里面只需要在PE里装一次,然后匹配无数个输入。

3.3 带宽微积分:为什么常规架构算力上不去

我们做个简单的估算。假设一块加速器需要做到100 TOPS,每个乘累加至少需要两个操作数(一个输入、一个权重),那么每秒钟需要200×10^12个操作数被送到计算单元。如果这些操作数全部来自片外DRAM,那需要的带宽是:假设每个操作数4字节,就是800TB/s。而现实中的HBM带宽也就几百GB/s到1~2TB/s,差了整整两个数量级以上。

这也就是为什么GPU虽然FLOPS很高,但在某些小batch推理场景下效率上不去——算力不是瓶颈,喂数据的带宽才是。脉动阵列通过阵列内数据流转,把“需要片外带宽的数据量”降了几个量级:权重复用率是阵列宽度,输入复用率是阵列高度,片外带宽需求自然降下来。这是脉动阵列在AI芯片界站稳脚跟的根本原因。

3.4 真实产品和架构对照

我自己研究过的几款芯片,放在一起看更有感觉:

芯片 / 架构阵列规模数据流应用侧重典型特征
Google TPU v1256×256Weight Stationary推理权重驻留SRAM,输入波前注入
TPU v3+大阵列分块混合训练/推理多核互联,数据流可配置
寒武纪 MLU多组中小规模阵列可切换云端训练/推理支持更多算子的非脉动扩展
各种NPU IP8×8 ~ 32×32WS为主端侧推理小阵列加精细调度

TPU v1是最经典的脉动阵列案例。当年谷歌声称它的能效比同代GPU高几个量级,尽管制程并不先进。核心就在于:TPU把整个推理过程中的权重都放在片上,输入以波前方式流过阵列,片外带宽需求降到令人发指的低。寒武纪的架构里,脉动阵列只是核心之一,旁边还有大量可编程逻辑来处理非矩阵类算子——这也是一个重要的工程洞察:纯脉动阵列不是万能的,编译器至少要配套支持普通计算单元。

4. 纸上谈兵容易,落地才有坑:脉动阵列的工程化设计要点

原理清楚了,但真正到做芯片的阶段,你会遇到一堆不上手根本意识不到的问题。我按照自己接触过的设计流程,把工程化的要点从头到尾捋一遍。

4.1 阵列规模怎么定:面积、频率和数据级的三角权衡

阵列规模不是拍脑袋定的。它直接决定SRAM大小、功耗墙和可映射的最大矩阵尺寸。

  • 面积预算:一个PE大概由乘法器、加法器、累加寄存器和数据通路寄存器组成,折算下来大约几千个晶体管。128×128的PE阵列光PE本身的面积,在一颗先进制程芯片上可能就要占到几十平方毫米。面积大了,成本自然上去。
  • 功耗预算:所有PE同时翻转,动态功耗很猛。所以很多芯片会用降低时钟频率的方式来控制功耗,而不是让阵列跑满最高频率。
  • 数据带宽匹配:阵列边缘的数据注入/抽出速度,必须和阵列内部的计算速度匹配。一个128宽的阵列,如果输入必须每个周期都全宽更新,那就需要持续128个数据的带宽。这里的数据通道位宽设计,往往决定了整个系统的瓶颈。

我给个实际建议:端侧或者中低算力场景,32×32到64×64是一个比较平衡的选择。云端大算力场景,宁可做多组中小阵列,不要盲目追求单阵列超大。原因很简单:矩阵乘法本身经常是分块执行的(tiling),太大阵列匹配不上实际矩阵的形状,反而浪费。

4.2 数据暂存:SRAM和片上buffer怎么放

脉动阵列和普通计算单元最大的不同,是它周围必须有一整套数据缓冲。我记得自己第一次画架构图时,只画了一个大大的PE阵列,然后被导师批了一顿:“数据从哪来?”脉动阵列再省带宽,也不可能完全杜绝片外内存访问。所以数据缓冲的设计几乎是和阵列同等重要的:

  • 权重缓冲:在WS模式下,权重需要预装载并留驻。这个buffer一般采用SRAM,容量要能装下一个大权重张量。
  • 输入片缓冲:输入数据流在注入阵列前,需要按波前顺序重新排列,所以要做重排逻辑。
  • 输出缓冲:部分和离开阵列后,还要经过累加、激活函数、量化等操作,才能写回主存。

我见过一个真实的例子:某团队开发端侧芯片,PE阵列只有16×16,理论上很小,但因为输入重排buffer设计得太小,导致数据每次都要绕外部Pad走一圈,最终实际吞吐只有设计值的60%。这种案例太常见了——阵列小但buffer设计不合理,照样跑不满。

4.3 分块映射(Tiling):大矩阵塞不进阵列怎么办

真实的矩阵乘法动辄上千维,而阵列只有几十乘几十。那怎么办?只能分块。分块策略也是编译器要解决的核心问题。

把大矩阵切成符合阵列尺寸的块,然后逐块计算,最后把部分和累加并写回。这一过程中,映射顺序直接决定数据重用率和带宽需求。

我举一个具体例子。假设PE阵列是16×16,我们现在要算两个256×256矩阵的乘法。如果按比较粗暴的方式,把A矩阵切成16×16的块,每块都单独从片外读进来,那是灾难性的——因为要读很多次。合理的做法,是在片上设置足够大的A块缓冲和B块缓冲,让一个A块在片上驻留的同时,匹配多个B块,再把部分和累积到一个输出块里。这个“片上驻留多次重用”的策略,编译器里叫loop tiling + loop permutation,做的好不好,直接影响实际算力。

4.4 调度和数据对齐:一条数据线的差都不能有

脉动阵列对时序对齐是零容忍的。输入必须按精确的周期错开,权重必须按正确的周期到达对应PE,部分和必须在正确的位置被累加。一旦对齐错了,结果就是错数,而且这种错数不会在仿真里轻易暴露——它依赖所有PE协同工作。

所以工程上一定要做周期精确仿真(cycle-accurate simulation)。我们在这个阶段就吃过亏:硬件RTL仿真过了,结果软件栈里调度发了一组特殊形状的矩阵,导致有一行的数据提前一个周期注入,仿真器却没检查出来。后来加了断言检查,每个周期核对PE内数据流的期望值,才把这类问题兜住。

4.5 精度处理:INT8要不要,量化放哪里

大部分AI芯片为了让算力数字好看,都会用INT8甚至INT4。脉动阵列的ALU设计,需要考虑乘累加操作是否会溢出。通常做法是:乘法结果先到32位或者高比特位宽累加,最后再做一次量化。这块看似小,实际对面积和时序影响很大。累加寄存器位宽加倍,意味着每个PE里寄存器翻倍,整个阵列的面积就跟着涨。

另一个容易忽略的点是:偏置和激活在哪里加。如果偏置在每个PE里都加一次,那就浪费了。合理做法是在部分和流出阵列后,额外一个Vector Unit统一做偏置、激活和量化。这套“脉动阵列 + Vector Unit”的配套设计,几乎成了AI加速器的事实标准。

5. 编译器视角:怎么把高级语言变成流入阵列的指令

搞硬件的人有时候会轻视软件栈,觉得“我把硬件做好了就行”。但真实情况是:脉动阵列好不好用,百分之六十取决于编译器能不能把它喂饱。没有好的编译器,再大的阵列也是摆设。

5.1 从计算图到硬件指令

编译器的主要工作,是把深度学习框架的计算图(比如PyTorch里的矩阵乘法算子)翻译成对脉动阵列的指令流。这里面包含几个步骤:

  1. 算子分解:把大算子切分成基本操作(GEMM、向量操作、激活等)。
  2. 循环优化:决定tiling块大小、循环顺序,目标是最小化片外数据传输。
  3. 数据布局转换:把张量的内存布局从行主序转成脉动阵列注入需要的格式。这一步在NPU芯片上极其关键,因为经常需要“转置”和“padding”。
  4. 指令调度:生成PE阵列的控制信号,确保数据流对齐。

需要强调的是,数据布局转换往往是性能杀手。很多Tensor在框架里是NCHW格式,但脉动阵列希望数据按它自己的数据流格式排布。如果每次推理都花很多时间做布局转换,那转换本身可能比计算还贵。所以高级一点的编译器会想办法在模型编译阶段就固定好布局,或者在计算图层面做layout optimization。

5.2 数据流映射:WS、OS到底怎么选

在做编译器调度时,数据流策略的选择是第一个决策点。我给一个经验法则:

  • 推理场景、权重固定:优先选WS。权重驻留片上,输入波前不断流入,阵列利用率高。
  • 训练场景、梯度频繁更新:OS往往更合理,因为每个PE独立维持输出累积,梯度计算不需要像WS那样频繁重新装载权重。
  • 矩阵规模很小:与其用大阵列不如用小阵列加高频率。大阵列空转率太高,反而是浪费。

5.3 阵列利用率的优化技巧:Padding、切分和融合

编译器优化脉动阵列利用率,核心思路就一句话:尽可能让每个PE在每个周期都有活干。具体有几个实操技巧:

  • Padding补齐:如果矩阵维度不是阵列尺寸的整数倍,补零到最近的可分块尺寸。代价是额外算一些无效的乘累加,但换来的是数据流规整和利用率提升。
  • Batch合并:把多个小矩阵合并成一个大矩阵(比如将batch维和sequence维合起来当一个大矩阵的一维)。这在Transformer推理时特别有用,因为注意力计算经常是很多小矩阵乘法。
  • 算子融合:把矩阵乘法后面的偏置加法、激活函数、LayerNorm等融合成一条流水线指令,避免数据为了中间结果反复进出外部内存。

这些优化在传统GPU优化里也有,但脉动阵列把它们的优先级提到了极其重要的位置——因为硬件本身灵活性低,编译器必须在图级别就把事情安排好。

5.4 一个实际调度案例:Transformer里最常见的GEMM

Transformer中最典型的算子是 (Q = XW_Q),其中X形状是[batch, seq_len, hidden],W_Q是[hidden, hidden]。如果把X按batch_slice切块,每个slice是[seq_len, hidden],送入脉动阵列时就需要把shape转成适合阵列输入的格式。

我实际做调度的习惯是:先把X的这两个维度合并映射成一层循环,即把[seq_len, hidden]矩阵乘[hidden, hidden]当成一个大的GEMM。然后按阵列尺寸切块,先切k维,再切n维,尽量保证A块在片上驻留期间能对应多个B块,减少重复搬运。实测下来,这种“k维外循环、n维内循环”的切分方式,在WS模式下的利用率通常比反过来的切分方式高10%~25%。

6. 局限性必须认清:脉动阵列不是银弹

把脉动阵列吹了这么多,也该给它泼盆冷水。我在工程上踩过的坑,足以说明这东西的边界在哪里。

6.1 稀疏性利用不起来

现代AI模型里很多权重是稀疏的(剪枝后大量为0)。传统计算架构可以通过跳过零值来省算力,但脉动阵列是固定节奏的流水线——数据流到哪个PE就是哪个PE,你没法让特定PE跳过某个周期。于是稀疏模型在脉动阵列上的加速效果很有限。

业界现在有几条路在走:结构化稀疏(让稀疏模式规整,便于硬件跳过)、稀疏感知映射(把非零权重提前重排)。但这些都牺牲了脉动阵列的规整性,还没有理想方案成型。我在实际项目里见过为了硬吃稀疏加速,把脉动阵列拆成一大堆小模块,结果面积、功耗全部失控。如果不是模型剪枝已经固定且稀疏模式规整,我建议别为了稀疏性去魔改阵列。

6.2 动态形状模型的调度复杂度

NLP模型有变长输入,CV模型有不同分辨率,这就导致实际矩阵形状变化极大。脉动阵列是固定结构,形状一变,映射策略就得重新算。如果编译器没有非常快速的映射搜索能力,动态形状就会带来严重延迟。

我之前做过一个端到端推理系统的性能分析:同一个模型,静态batch跑,利用率接近90%;动态batch跑,利用率掉到60%以下,而且调度时间比计算时间还长。后来解决办法是把动态形状聚成几个“bucket”,每个bucket预编译好调度方案,运行时按形状查表。这个方法强烈推荐给做在线推理服务的同学。

6.3 不是所有算子都适合脉动阵列

卷积、矩阵乘法、注意力里的QK^T和AV这些都是矩阵乘类算子,脉动阵列很合适。但ReLU、LayerNorm、Softmax、量化、GELU这些就是典型的非矩阵算子。它们要么是element-wise,要么是归约操作。所以在任何现代AI芯片里,脉动阵列都只是子系统之一。你得在旁边配一个Vector Unit或者通用ALU来处理这些算子。这个“异构计算核心”的思路,是所有真实商用芯片的共同答案。

6.4 与其他架构的对比视野

把脉动阵列放在更大的架构坐标系里看,它并不孤独。GPU的Tensor Core本质上也是一种收缩阵列,只是形态略有不同。Cerebras的晶圆级引擎则是把超大二维阵列做在整片晶圆上,脉动思想贯穿始终。Graphcore的IPU则是采用大量小型处理器加分布式内存,放弃了同步脉动,换取更大的灵活性。这些不同方向,几乎都在“规整高效”和“灵活易用”之间做取舍。脉动阵列选的极端高效一端,代价是灵活性差。

7. 手把手走一遍:从零设计一个8×8脉动阵列的全流程

学了原理,接下来会是不少读者更关心的问题:如果在FPGA上自己撸一个最小可跑的脉动阵列,到底要经历哪些步骤?我这里用Verilog/SystemVerilog的思路,把设计流程走一遍。这个流程我在学校带的项目里反复讲过,跟着做基本能跑通。

7.1 第一步:定架构参数

先固定三个参数:阵列大小8×8,数据位宽8-bit,累加位宽32-bit。权重采用WS模式预装载,输入从左侧流入,部分和从上往下流。这是最标准的模型,也最好在FPGA上验证。

7.2 第二步:PE的RTL实现

一个PE的SystemVerilog代码模型可以这样写:

module pe #( parameter DW = 8, parameter ACCW = 32 )( input logic clk, input logic rst_n, input logic [DW-1:0] data_in, // 从左侧或上方来的输入 input logic [DW-1:0] weight_in, // 权重(预装载模式下只在装载阶段有效) input logic [ACCW-1:0] acc_in, // 从上方来的部分和 output logic [DW-1:0] data_out, // 流向右侧或下方 output logic [DW-1:0] weight_out, output logic [ACCW-1:0] acc_out ); logic [DW-1:0] weight_reg; logic [ACCW-1:0] acc_reg; always_ff @(posedge clk or negedge rst_n) begin if (!rst_n) begin weight_reg <= '0; acc_reg <= '0; end else begin weight_reg <= weight_in; // 权重始终保持(WS) acc_reg <= acc_in + $signed(data_in) * $signed(weight_reg); end end assign data_out = data_in; // 输入直通 assign weight_out = weight_reg; // 如果需要级联权重,可在load模式传出 assign acc_out = acc_reg; endmodule

这个模块比实际项目要简化,但核心逻辑已经完整:每个时钟周期把从上方来的部分和与本地权重乘积累加,再把输入原样传给右边。权重装载和计算模式没有真正分开,实际工程里需要加一个mode信号来控制是load还是compute。

7.3 第三步:二维阵列连接

8×8的阵列,最直接的做法是用generate语句例化64个PE,并按照行列把相邻PE的数据口连起来:

module systolic_array_8x8 #( parameter DW = 8, parameter ACCW = 32 )( input logic clk, input logic rst_n, input logic [DW-1:0] data_in [0:7], // 左侧输入,8个 input logic [DW-1:0] weight_in [0:7][0:7], // 权重装载 input logic [ACCW-1:0] acc_in [0:7], // 顶部部分和输入 output logic [DW-1:0] data_out [0:7], output logic [ACCW-1:0] acc_out [0:7] ); logic [DW-1:0] data_net [0:8][0:7]; // 第0列是外部输入 logic [ACCW-1:0] acc_net [0:8][0:7]; // 第0行是外部输入 genvar i, j; generate for (i = 0; i < 8; i++) begin : row for (j = 0; j < 8; j++) begin : col pe #(.DW(DW), .ACCW(ACCW)) u_pe ( .clk(clk), .rst_n(rst_n), .data_in (data_net[i][j]), .weight_in(weight_in[i][j]), .acc_in (acc_net[i][j]), .data_out (data_net[i][j+1]), .weight_out(), // 简化,不级联 .acc_out (acc_net[i+1][j]) ); end end endgenerate // tie boundary nets for (genvar i = 0; i < 8; i++) begin : input_assign assign data_net[i][0] = data_in[i]; assign acc_net[0][i] = acc_in[i]; end for (genvar j = 0; j < 8; j++) begin : output_assign assign data_out[j] = data_net[j][8]; assign acc_out[j] = acc_net[8][j]; end endmodule

这里我故意把权重装载写得简化:每个PE的权重端口直接绑到了top module的输入。实际项目中权重会存储在SRAM中,通过地址译码器按列或按行灌入,并且在load模式下data/acc通路要处于不计算状态。

7.4 第四步:控制状态机和数据注入

阵列本身只是计算引擎,真正让它动起来的是外部控制逻辑。核心控制器需要解决两件事:

  1. 装载阶段:把权重按照正确位置灌入对应PE。多数设计按列扫描装载,一列一列写入。
  2. 计算阶段:把输入数据按波前顺序注入。第一个周期只给第0行第0列发数据,第二个周期同时给(0,1)和(1,0)发……具体方式是给每个输入端口接一个延迟链,或者直接根据周期计数生成注入使能。

我建议在FPGA验证时,先用单周期注入两个输入、检查一个输出,手推一下结果对不对,再让控制器全速跑。纯靠仿真器看结果,一旦数据对齐错了,排查极其痛苦。

7.5 第五步:上板验证和常见坑

上板验证时最容易遇到的问题,我列几个下来:

  • 时序违例:8×8的小阵列一般不会太严重,但数据通路和累加通路都带寄存,如果综合频率设置太高,布局布线后时序可能不收敛。建议先跑100MHz,确认功能正确后再慢慢拉高频率。
  • 权重装载时序:很多FPGA开发板提供了JTAG调试,你可能很想在计算时动态改变权重。这个可以做,但要确保改变权重时计算暂停,否则会导致部分PE用新权重、部分PE用旧权重,算出来的C矩阵对不齐。
  • 数据类型:输入、权重用有符号数还是无符号数,决定了乘法律减法的行为。我建议统一用有符号定点数,避免训好的模型权重在量化后出现符号错误。

8. 写在最后的一些实际经验

从原理到设计流程,这篇文章基本把脉动阵列的核心东西都过了一遍。最后我再分享几点自己实操中沉淀下来的体会,算不上什么高深结论,但对刚接触这块的朋友应该有点帮助。

第一,多画图,少看论文公式。脉动阵列的数据流,用纸笔把坐标标一遍,比看十篇论文都管用。我建议你拿一个4×4的小阵列,自己手推一遍那三种数据流的周期表,一定会有全新的理解。

第二,编译器早做,别等硬件定稿再写。脉动阵列的性能上限,很大程度是在调度器手里。硬件RTL冻结以后再改软件栈,通常只能跪着优化,很多架构层面的问题想改都改不了。我们做第二个版本时,让RTL工程师和编译器工程师从第一天就坐在一起,性能比第一版提升非常明显。

第三,矩阵太小的时候,别硬上阵列。如果输入矩阵尺寸只有个位数,直接上脉动阵列不如用几个并行的MAC单元。有一个经验阈值:当单个矩阵的行数或列数小于阵列规模的一半时,利用率已经开始难看。此时应该是混合调度:小算子走通用ALU,大算子走阵列。

第四,能效数字要以真实模型为准,不要用理想矩阵跑分。很多芯片公布的理论TOPS很吓人,但你拿一个真实Transformer跑一遍,实际利用率可能只有百分之三五十。所以我每次做性能评估,都会用真实模型跑端到端,再看Roofline模型里资源消耗在哪里冒尖。测试矩阵故意选规整的方阵,那叫自我安慰。

脉动阵列从1980年代H.T. Kung的学术论文,到今天AI芯片的基础架构,走了几十年。它能在这么多架构流派里活下来,靠的不是花哨,而是它把“数据复用”这个核心问题解决得足够彻底。理解它,不只是为了看懂某一块芯片,更是为了理解AI计算里最本质的问题——数据移动才是真正的主战场。搞清楚这一点,再看任何加速器,眼界都会不太一样。

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

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

立即咨询