1. 从一次面试翻车聊起为什么脉动阵列值得单独拎出来讲几年前我去面试一家做AI加速器的公司技术面聊得都挺顺直到面试官在白板上画了一个二维网格问我“如果让你设计一个矩阵乘法加速器数据怎么在PE阵列里流动最省带宽”我当时脑子里第一反应是“每个周期从内存取数、算完写回”结果面试官笑了笑说“那你这个设计算力还没跑满带宽先炸了。”那次翻车之后我才真正把脉动阵列这个东西从教科书里翻出来一行一行地啃。现在回头看脉动阵列Systolic Array几乎是所有主流AI芯片绕不开的一个关键词。从谷歌TPU第一代开始到后面一堆做推理加速的NPU、DSA架构脉动阵列基本成了矩阵乘法单元的标配。它解决的问题非常具体在算力、带宽、功耗这三者之间找一个能落地的平衡点。尤其是矩阵乘法这种在神经网络里占比超过80%的运算如果数据搬运的开销压不下去再高的峰值算力都是纸面数字。这篇文章我打算把脉动阵列从原理到实现、从参数计算到踩坑经验完整地捋一遍。适合谁看如果你是做芯片架构、FPGA加速、或者对AI芯片底层感兴趣但一直没搞明白“数据到底怎么在阵列里流动”的这篇应该能帮你把这块拼图补上。我会尽量用生活化的类比把原理讲透同时把参数计算、时序设计、常见问题这些实操层面的东西写清楚让你看完能自己动手推一遍。2. 脉动阵列到底在解决什么问题核心思路与方案选型2.1 从“取数-计算-写回”的痛点说起先说说最朴素的矩阵乘法加速器是怎么做的。假设要算 C A × BA是M×KB是K×N。最直接的做法是从内存里读A的一行和B的一列做乘加写回C的一个元素。这个过程中每算一个输出元素就要访问一次内存。对于大矩阵来说内存带宽根本扛不住。我举个具体数字。假设一个256×256的矩阵乘256×256的矩阵输出是256×256。如果每个输出元素都独立取数那需要读取的数据量是256×256×256×2A和B各一次≈ 3300万次读取。而实际上A和B各自只有65536个元素。也就是说超过99%的读取是重复的。这就是典型的“算力被带宽卡脖子”。脉动阵列的核心思路就是让数据在计算单元之间流动起来而不是每次都回内存取。数据像血液在血管里脉动一样从一个PEProcessing Element处理单元流向下一个PE每个PE在数据经过时完成一次乘加然后把部分和继续往下传。这样数据被复用的次数大幅提升内存访问次数能降一到两个数量级。2.2 为什么是“脉动”而不是“广播”你可能会问那为什么不直接把数据广播给所有PE广播确实简单一个数据发给所有需要它的PE大家同时算。但广播的问题在于扇出fan-out太大。一个数据要驱动几百上千个PE连线延迟和功耗都会飙升。而且广播模式下每个PE还是需要从总线取数总线的带宽压力并没有真正缓解。脉动阵列不一样它更像是流水线邻居传递。每个PE只和相邻的PE通信数据一步步“脉动”过去。这样做的好处是连线短、扇出小、功耗低而且数据在流动过程中被反复使用。用个类比广播像是老师用大喇叭给全校广播所有人都能听到但喇叭很费电脉动阵列像是传纸条每个人看完传给下一个人纸条上的信息被沿途所有人利用但传递过程很安静。2.3 权重固定 vs 输入固定两种数据流的取舍脉动阵列在具体实现上通常有两种数据流方式权重固定Weight Stationary和输入固定Input Stationary。这两个名字听起来有点绕我用一个2×2的阵列来举例说明。权重固定的意思是把B矩阵权重的元素预先加载到每个PE里然后A矩阵的元素从左边流入部分和从上往下流动。每个PE拿自己存的权重和流进来的输入做乘加结果往下传。这种方式适合权重复用率高的场景比如推理阶段权重是固定的输入是变化的。输入固定的意思是A矩阵的元素固定在PE里B矩阵从上方流入。这种方式适合输入复用率高的场景。实际芯片里权重固定用得更多因为推理场景下权重不变可以提前加载好运行时只需要灌输入数据。谷歌TPU用的就是权重固定的脉动阵列。这个选择背后的逻辑是推理任务中权重的访问频率远低于输入把权重“钉”在PE里能省掉大量权重读取的带宽。2.4 方案选型的几个关键考量选脉动阵列不是拍脑袋决定的我总结下来主要看这几个维度考量维度脉动阵列广播式阵列结论数据复用率高数据在阵列内多次复用低每次都要从总线取脉动阵列胜连线复杂度低只连邻居高全局广播脉动阵列胜功耗低短连线高长连线驱动脉动阵列胜灵活性低适合规则运算高适合不规则运算广播式胜设计复杂度中时序要仔细调低逻辑简单广播式胜所以脉动阵列不是万能的。它最适合规则、密集、可预测的运算比如卷积、矩阵乘。如果你要做的是稀疏运算或者控制流复杂的任务脉动阵列反而会拖后腿。这也是为什么很多芯片会同时集成脉动阵列和通用计算单元各干各擅长的活。3. 核心细节拆解PE、数据流与时序设计3.1 PE内部到底长什么样PE是脉动阵列的基本单元它的结构其实不复杂。一个典型的PE包含一个乘法器Multiplier一个累加器Adder/Accumulator若干寄存器用于暂存输入、权重、部分和控制逻辑控制数据何时流入、何时锁存以权重固定的PE为例它的工作流程是权重存在PE内部的寄存器里输入数据从左边进来和权重相乘结果加上从上方传来的部分和然后往下方输出。同时输入数据还会往右边传给下一个PE。也就是说每个PE每个周期做一次乘加同时把输入往右传、把部分和往下传。这里有个细节很多人会忽略PE里的寄存器不是随便加的它决定了数据的流动节奏。如果寄存器加多了延迟变大加少了时序可能不满足。我见过一个设计因为PE里少加了一级流水寄存器导致关键路径太长频率上不去最后不得不返工。所以PE的微架构设计本质上是在延迟、频率、面积之间做权衡。3.2 数据在阵列里怎么“脉动”我用一个3×3的脉动阵列来演示数据流动。假设要算C A × BA是3×3B是3×3阵列也是3×3。权重固定模式下第一步把B矩阵的元素加载到每个PE里。PE(i,j)存B[i][j]。第二步A矩阵的元素从左边流入。具体来说A的第i行元素从第i行的最左边PE流入每个周期往右移动一格。第三步部分和从上往下流动。最上面一行的PE部分和初始为0算完往下传。关键点在于数据的错位对齐。因为数据在阵列里流动需要时间所以A的元素不是同时灌进去的而是要有一定的斜排skew。具体来说A的第i行第j列元素要在第(ij)个周期进入阵列。这个斜排的设计是脉动阵列能正确工作的核心。我画个简单的时序表来说明周期PE(0,0)输入PE(0,1)输入PE(0,2)输入PE(1,0)输入PE(1,1)输入PE(1,2)输入0A00-----1A01A00-A10--2A02A01A00A11A10-3-A02A01A12A11A104--A02-A12A115-----A12可以看到每个PE在正确的周期拿到正确的数据这就是斜排的作用。如果不对齐算出来的结果就是错的。这个斜排逻辑在硬件里通常用一个输入缓冲FIFO来实现每个行通道的FIFO深度不同越往下的行延迟越大。3.3 部分和的累加与输出部分和的流动方向是从上到下。最上面一行的PE部分和初始为0算完往下传。第二行的PE收到上面传来的部分和加上自己的乘加结果再往下传。最下面一行的PE算完之后结果就是最终的C矩阵元素。这里有个容易踩的坑部分和的位宽。如果输入是8位权重是8位乘积是16位。但累加多次之后位宽会增长。比如累加256次位宽至少要16824位。如果位宽不够会溢出结果就错了。我见过一个设计部分和只用了16位结果跑大矩阵的时候精度崩了排查了半天才发现是位宽问题。所以部分和的位宽要根据累加次数来算宁可多留几位也别省。3.4 时序收敛的几个关键点脉动阵列的时序收敛是个细致活。我总结下来主要卡在三个地方第一PE之间的连线延迟。虽然只连邻居但如果阵列很大比如256×256最远端的连线还是有延迟的。解决办法是加流水寄存器把长连线打断。第二乘法器的关键路径。乘法器本身延迟就大如果再加上累加器关键路径可能就超了。常见的做法是把乘法和累加分到两个周期中间加一级寄存器。第三控制信号的扇出。虽然数据是邻居传递但控制信号比如使能、复位通常是全局的扇出很大。解决办法是用局部的控制逻辑每个PE自己根据状态判断减少全局信号的依赖。提示时序收敛不要等到后端再做前端设计的时候就要把流水级数规划好。我一般会在PE里预留一级可选的流水寄存器综合的时候根据时序报告决定插不插。4. 实操过程从参数计算到阵列搭建4.1 先算清楚阵列规模阵列规模不是越大越好。我见过有人一上来就说要做512×512的阵列结果一算面积和功耗直接超预算。阵列规模的选择要从目标算力和带宽反推。假设目标算力是10 TOPSINT8每个PE每个周期做一次乘加算两次操作乘和加。那么需要的PE数量是PE数量 目标算力 / (频率 × 2)如果频率是1GHz那么PE数量 10e12 / (1e9 × 2) 5000个。开方之后阵列大概是70×70。实际设计里会取整到64×64或者128×128。但这里还有个约束带宽。阵列越大需要的输入带宽越高。如果带宽跟不上PE就会饿死。所以阵列规模还要和内存带宽匹配。我一般会算一个计算强度Operational Intensity即每字节数据能支撑多少次运算。如果计算强度不够说明带宽是瓶颈阵列再大也没用。4.2 数据位宽与精度设计位宽设计是个权衡。位宽越大精度越高但面积和功耗也越大。INT8是目前推理的主流因为神经网络对精度不那么敏感INT8基本够用。但有些场景需要INT16甚至FP16。我的经验是输入和权重用INT8部分和用INT32。这样既能保证精度又不会太浪费面积。如果要做混合精度可以在PE里加一个模式选择支持INT8和INT16切换。但这样PE面积会增加大概30%要看是否值得。4.3 输入缓冲与斜排逻辑的实现输入缓冲的设计直接决定了数据能不能按时送到PE。我通常会用行缓冲Line Buffer加FIFO的方式。每一行有一个独立的FIFO深度根据斜排的延迟来定。第i行的FIFO深度是i个周期。具体实现的时候可以用一个移位寄存器链每个周期把数据往右移一格。但移位寄存器的功耗比较高因为每个周期都在翻转。更省电的做法是用双端口RAM加读写指针但控制逻辑会复杂一些。我一般会先用移位寄存器做原型验证功能之后再换成RAM优化功耗。4.4 一个4×4阵列的Verilog核心代码下面是我写的一个简化版4×4脉动阵列的PE核心代码权重固定模式INT8输入INT32部分和。这个代码可以直接综合适合做原型验证。module pe #( parameter DATA_WIDTH 8, parameter ACC_WIDTH 32 )( input wire clk, input wire rst_n, input wire en, input wire [DATA_WIDTH-1:0] in_data, input wire [DATA_WIDTH-1:0] weight, input wire [ACC_WIDTH-1:0] in_acc, output reg [DATA_WIDTH-1:0] out_data, output reg [ACC_WIDTH-1:0] out_acc ); reg [DATA_WIDTH-1:0] data_reg; reg [ACC_WIDTH-1:0] acc_reg; wire [2*DATA_WIDTH-1:0] mult_result; assign mult_result data_reg * weight; always (posedge clk or negedge rst_n) begin if (!rst_n) begin data_reg 0; acc_reg 0; out_data 0; out_acc 0; end else if (en) begin data_reg in_data; acc_reg in_acc mult_result; out_data data_reg; out_acc acc_reg; end end endmodule这个PE的逻辑很简单每个周期锁存输入数据做乘加把数据和部分和往下传。注意这里我加了一级寄存器所以延迟是一个周期。实际设计里乘法器和加法器可能需要再打一拍那就变成两级延迟。阵列的顶层模块就是把PE按二维网格连起来行方向连数据列方向连部分和。斜排逻辑在输入端口做用一个FIFO链实现。4.5 仿真验证的关键场景写完代码之后仿真验证是必须的。我一般会跑这几个场景小矩阵正确性用4×4的矩阵手动算一遍结果和仿真对比。边界情况全0矩阵、全1矩阵、最大值矩阵看有没有溢出。连续运算连续灌多组数据看流水线会不会卡住。复位测试中途复位看状态能不能正确清除。仿真的时候我习惯把每个PE的输出都打出来画成波形图一眼就能看出数据流动对不对。如果某个PE的输出不对顺着数据流往回查很快就能定位问题。5. 常见问题与排查技巧实录5.1 结果不对先查斜排脉动阵列结果不对十有八九是斜排逻辑出了问题。我遇到过一次仿真结果整体偏移了一个周期查了半天发现是第0行的FIFO深度设成了1应该是0。斜排的规律是第i行的延迟是i个周期第0行不延迟。这个很容易搞错建议写个断言assertion在仿真里自动检查。5.2 部分和溢出怎么发现部分和溢出比较隐蔽因为仿真的时候如果数据量不大可能不会触发。我的做法是在PE里加一个溢出标志位一旦累加结果超过阈值就置位。仿真的时候监控这个标志如果有置位就说明位宽不够。另外跑真实网络的时候用实际数据测一遍比随机数据更容易暴露溢出问题。5.3 时序不收敛的常见原因时序不收敛我总结下来主要是三个原因乘法器路径太长、连线延迟太大、控制信号扇出太高。对应的解决办法是乘法器加流水、长连线加寄存器、控制信号局部化。如果这些都做了还是不行那就得降频或者缩小阵列规模。别硬扛降频有时候比返工划算。5.4 常见问题速查表问题现象可能原因排查方法解决办法结果整体偏移斜排延迟不对检查每行FIFO深度第i行延迟i个周期结果部分错误部分和位宽不够加溢出标志监控增加部分和位宽时序不收敛关键路径太长看综合时序报告加流水寄存器功耗过高移位寄存器翻转多看功耗分析报告换成RAM缓冲数据卡住FIFO满或空看FIFO状态信号调整读写速率5.5 几个独家避坑技巧第一个技巧先用小阵列验证功能再扩展到大阵列。我见过有人直接写256×256的阵列仿真跑一次要几个小时出了问题根本没法调。先用4×4验证逻辑确认没问题之后再参数化扩展效率高很多。第二个技巧权重加载和计算分开做。权重固定模式下权重加载需要额外的周期。如果权重加载和计算混在一起控制逻辑会复杂很多。我一般会设计一个独立的加载阶段加载完成之后再开始计算。第三个技巧部分和的初始值要清零。这个听起来是废话但我真的见过有人忘了清零结果第一次计算的结果是错的后面才对。原因是部分和寄存器上电之后是随机值不清零就会带入错误。注意脉动阵列的验证一定要覆盖连续多组数据的场景。单组数据正确不代表连续运行时也正确流水线冲突往往在连续运行时才暴露。6. 脉动阵列的边界与扩展方向脉动阵列不是银弹它有明确的适用边界。它最适合规则、密集、数据复用率高的运算。如果你要做的是稀疏矩阵、动态形状的运算脉动阵列的效率会大打折扣。这也是为什么现在很多芯片会在脉动阵列之外再配一个通用的向量单元处理那些不规则的运算。从扩展方向来看脉动阵列本身也在进化。比如可重构脉动阵列可以根据不同的运算模式动态调整数据流方向兼顾灵活性和效率。还有混合精度脉动阵列支持INT8、INT16、FP16多种精度适应不同的任务需求。另外三维脉动阵列也是一个研究方向把数据流扩展到第三个维度进一步提升复用率。我在实际项目里的体会是脉动阵列的设计核心不在于阵列本身而在于数据流的规划。把数据流想清楚了PE和阵列都是水到渠成的事。反过来如果数据流没想明白PE设计得再精巧也没用。所以每次开始一个新设计我都会先在纸上把数据流动画一遍确认每个周期每个PE在做什么然后再动手写代码。这个习惯帮我省了很多返工的时间。最后分享一个小技巧如果你在调脉动阵列的时候卡住了不妨把阵列缩小到2×2把每个周期的数据都打出来手工推一遍。很多时候问题就藏在你以为“肯定对”的那个细节里。