从算力焦虑到脉动阵列:AI芯片如何破解数据搬运与能效难题
1. 从算力焦虑说起为什么AI芯片绕不开脉动阵列这两年只要聊AI芯片不管是做加速卡的创业公司还是在做端侧NPU的团队绕不开的一个词就是“脉动阵列”。我最早接触这个概念是在看Google TPU那篇论文的时候。当时第一反应是这不就是把一堆乘法器排成棋盘然后让数据像流水线一样流过去吗原理好像很简单。但真正深入进去发现自己根本没看懂因为脉动阵列背后牵涉的是AI芯片设计里最核心的一笔账——算力、带宽和功耗之间的三角博弈。先说个背景感受。咱们都知道AI推理和训练本质上是极其大量的矩阵乘法。一个Transformer模型跑一次推理几乎全部时间都耗在矩阵乘法和注意力权重的乘法上。而这类计算有个鲜明的特点计算密度极高数据复用率极高。同一个权重会被成百上千个不同输入反复使用同一个输入也会和成百上千个权重相乘。如果CPU或者GPU按照传统方式把数据从内存搬进寄存器、算完再搬回去那大部分功耗和延迟都耗费在“搬运”而不是“计算”上。脉动阵列就是为了解决“搬运”问题而生的一个经典架构思路。它把一大堆处理单元PEProcessing 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 StationaryWS权重预先装在PE里不动输入数据流经阵列部分和随输入一起流动。优点是权重复用极好同一权重可以匹配所有输入适合推理场景权重是固定常量。输出固定Output StationaryOS每个PE负责算输出矩阵的一个元素输入和权重都从外面流进来乘累加结果在PE里累积到最终结果再输出。优点是每个PE只写一次结果减少数据搬移适合训练场景反向传播时权重经常更新。输入固定Input StationaryIS输入数据固定在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里的矩阵乘法算子翻译成对脉动阵列的指令流。这里面包含几个步骤算子分解把大算子切分成基本操作GEMM、向量操作、激活等。循环优化决定tiling块大小、循环顺序目标是最小化片外数据传输。数据布局转换把张量的内存布局从行主序转成脉动阵列注入需要的格式。这一步在NPU芯片上极其关键因为经常需要“转置”和“padding”。指令调度生成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里最常见的GEMMTransformer中最典型的算子是 (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][j1]), .weight_out(), // 简化不级联 .acc_out (acc_net[i1][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 第四步控制状态机和数据注入阵列本身只是计算引擎真正让它动起来的是外部控制逻辑。核心控制器需要解决两件事装载阶段把权重按照正确位置灌入对应PE。多数设计按列扫描装载一列一列写入。计算阶段把输入数据按波前顺序注入。第一个周期只给第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计算里最本质的问题——数据移动才是真正的主战场。搞清楚这一点再看任何加速器眼界都会不太一样。