1. 为什么AI芯片都在聊脉动阵列如果你最近翻过几篇AI加速器的论文或者拆解过几款主流推理芯片的架构图大概率会反复撞见一个词——脉动阵列Systolic Array。我第一次认真研究它是因为手里一块边缘推理板子跑矩阵乘法时功耗高得离谱算力利用率却只有三成出头查来查去发现瓶颈根本不在主频而在数据搬运。那之后我把脉动阵列从原理到RTL实现啃了一遍也自己写过简化版的仿真模型才算真正理解为什么谷歌TPU、不少国产NPU都把它当作矩阵计算的核心引擎。这篇文章我想聊的不是教科书式的定义而是作为一个实际做过加速器相关工作的人把脉动阵列这件事讲透它到底是什么、为什么AI芯片偏爱它、内部数据是怎么流动的、自己动手实现时要注意哪些坑。内容会覆盖原理拆解、结构选型、参数计算、实操建模和常见问题排查适合做芯片架构、FPGA加速、算子优化的朋友参考也适合刚接触AI硬件、想搞明白“矩阵乘法到底怎么在硬件上跑起来”的读者。整篇我会尽量用生活化的类比配合具体数字让你看完能自己推一遍数据流甚至能动手搭一个简化模型。先说结论性的判断脉动阵列不是万能药它是一套用规整的硬件结构换取极高数据复用率的设计哲学。理解了这句话后面所有的细节都是它的展开。2. 脉动阵列到底解决了什么问题2.1 从矩阵乘法的数据搬运说起AI推理和训练里最核心的运算就是矩阵乘法卷积也可以展开成矩阵乘法im2col。一个 M×K 的矩阵乘 K×N 的矩阵理论上有 M×N×K 次乘加运算。假设 MNK256那就是1600多万次乘加。问题在于如果每次乘加都要从内存里重新取两个操作数那数据搬运量会是运算量的好几倍而搬运的能耗远高于计算本身。我做过一个粗略的估算在典型的CMOS工艺下一次32位浮点乘加的能耗大约是一次片上寄存器访问的几倍到十几倍而一次片外DRAM访问的能耗又是片上访问的几十上百倍。也就是说算力不是被计算单元卡住的而是被数据供给卡住的。这就是所谓的“内存墙”。脉动阵列的核心思路就是让数据在计算单元之间“流动”起来每个数据被取进来之后尽可能多地参与运算而不是用完就扔。这就像工厂流水线原材料不是每个工位都重新领一次而是沿着传送带依次经过各个工位每个工位顺手加工一下再传给下一个。2.2 复用率是衡量加速器效率的关键指标衡量一个矩阵加速器好不好我通常看三个复用维度权重复用同一个权重能不能被多个输入激活值共用激活复用同一个输入激活值能不能被多个权重共用部分和复用累加结果能不能在本地持续累积而不是频繁写回脉动阵列的精妙之处在于它通过让权重预先驻留在PEProcessing Element处理单元里让激活值沿一个方向脉动、部分和沿另一个方向脉动同时实现了这三个维度的复用。一个权重在它驻留期间可以服务成百上千次乘加激活值在穿过阵列的过程中被反复使用部分和则在阵列内部一路累加到底几乎不需要中间写回。对比一下另一种常见结构——二维SIMD阵列每个周期从寄存器堆里取操作数算完写回。它的灵活性更高但寄存器堆的读写带宽会成为瓶颈复用率上不去。脉动阵列牺牲了一部分灵活性换来了接近理论峰值的能效比。这就是为什么在矩阵乘法这种规整度极高的负载上脉动阵列几乎是默认选择。2.3 什么样的负载适合脉动阵列不是所有AI运算都适合脉动阵列。我的经验判断标准是运算是否规整、数据依赖是否规则、维度是否足够大。矩阵乘法、标准卷积、全连接层这些都非常适合因为它们的计算模式高度规整数据流可以预先编排。但像注意力机制里的动态稀疏、非规则卷积、图神经网络里的邻居聚合这些负载的访存模式不规则硬塞进脉动阵列反而会大量空转利用率暴跌。所以实际芯片设计里往往是脉动阵列负责密集矩阵运算再配一套灵活的向量单元或标量单元处理非规则部分。TPU早期版本就是这种思路矩阵单元加向量单元的组合。理解这个边界比盲目崇拜脉动阵列更重要。3. 脉动阵列的内部结构拆解3.1 处理单元PE里到底有什么脉动阵列的基本单元是PE一个最简的PE通常包含一个乘法器一个累加器加法器加寄存器若干用于数据传递的寄存器控制信号通路以经典的输出驻留Output Stationary数据流为例每个PE负责计算输出矩阵中的一个元素。权重预先加载并驻留在PE的寄存器里激活值从左侧流入部分和从上往下流动。每个周期PE做一次乘加把激活值传给右边的邻居把部分和传给下面的邻居。这里有个关键细节PE之间的连线是单向的、规则的。这种规则性对硬件实现极其友好布线短、时钟树好做、可以大规模复制。我见过一些新手设计为了追求灵活性把PE之间的互联做成可配置的全连接结果布线拥塞、时序根本收敛不了最后不得不推倒重来。3.2 三种主流数据流的取舍脉动阵列按数据驻留方式主要分三类我整理成表格方便对比数据流类型驻留对象激活流动方向部分和流动方向适用场景权重驻留WS权重水平脉动垂直脉动权重可预加载、批量推理输出驻留OS部分和水平垂直本地累积通用矩阵乘、卷积行驻留RS激活行对角脉动水平脉动特定卷积实现我实际做项目时权重驻留用得最多因为推理场景下权重是固定的可以提前加载好激活值流过去就行控制逻辑最简单。但如果是训练场景权重需要频繁更新输出驻留或者混合数据流会更合适。选哪种数据流本质是在问哪份数据最稳定、最值得驻留驻留那份数据可以省掉反复加载的开销但代价是其他数据必须流动起来对带宽和调度提出要求。3.3 阵列尺寸怎么定阵列尺寸比如16×16、32×32、128×128不是越大越好。我踩过的坑是一开始觉得阵列越大算力越强结果做出来发现大部分时间阵列利用率不到20%。尺寸选择要考虑几个约束片上存储容量权重驻留需要寄存器或SRAM阵列越大驻留权重占用的存储越多数据供给带宽阵列边缘每个周期要喂进多少数据取决于阵列边长负载的实际维度如果实际矩阵维度远小于阵列尺寸大量PE会闲置时序和功耗大阵列的时钟树和布线延迟更棘手一个实用的经验公式阵列边长大致取实际负载典型维度的平方根量级。比如你的模型里矩阵乘的典型维度是256到512那16×16到32×32的阵列往往比128×128更划算因为小阵列的利用率更高调度更灵活。大阵列适合维度上千、批量很大的场景。4. 手把手推一遍数据流4.1 用一个4×4阵列算8×8矩阵乘光看结构图容易懵我带你用一个具体例子走一遍。假设我们要算 C A × BA是8×8B是8×8用一个4×4的脉动阵列分块计算。采用权重驻留数据流把B矩阵切成4×4的块A也按行切成4行的块。计算过程分几个阶段权重加载阶段把B的一个4×4块加载进阵列每个PE存一个权重元素。这一步需要4个周期按对角线错开加载。激活流入阶段A的对应4行数据从左侧流入每行错开一个周期进入形成脉动。部分和累积每个PE做乘加部分和沿垂直方向向下传递最下面一行PE输出最终结果。结果写出底部PE把累加完成的结果写出到输出缓冲。整个过程A的每个元素进入阵列后会依次经过一行PE被复用4次B的每个权重驻留期间会被4个不同的激活值使用。这就是复用的来源。4.2 关键参数的计算过程假设阵列是N×N时钟频率f那么理论峰值算力是峰值算力 2 × N² × f 乘加算两次操作比如N32f1GHz峰值就是 2 × 1024 × 1e9 2.048 TOPS。注意这是理论峰值实际算力要乘以利用率。数据供给带宽的需求每个周期阵列左侧需要流入N个激活值上方需要流入N个部分和如果是输出驻留。所以边缘带宽至少是 N × 数据位宽 × f。N32、位宽16bit、f1GHz时左侧带宽需求是 32×2×1e9 64 GB/s。这个数字很关键如果你的片上存储带宽喂不上阵列就会饿死。我在做设计时会先算这个带宽需求再反推片上SRAM的位宽和bank划分。很多新手只盯着算力忽略了带宽匹配最后实测性能只有峰值的一两成。4.3 一个简化仿真模型的搭建思路想真正理解数据流最好的办法是自己写一个周期级的仿真模型。我用Python写过一个简化版核心逻辑大概是这样class PE: def __init__(self): self.weight 0 self.psum 0 self.act_reg 0 def cycle(self, act_in, psum_in): # 乘加 self.psum psum_in self.act_reg * self.weight # 激活值传给右边 act_out self.act_reg self.act_reg act_in return act_out, self.psum def systolic_array(activations, weights, size): array [[PE() for _ in range(size)] for _ in range(size)] # 加载权重 for i in range(size): for j in range(size): array[i][j].weight weights[i][j] # 逐周期推进 results [] for t in range(len(activations) size): # 构造输入处理边界 # 每个PE接收左邻居的act和上邻居的psum # 收集底部输出 pass return results这个模型跑起来之后你可以打印每个周期每个PE的状态直观看到激活值怎么斜着流进去、部分和怎么往下走。我建议每个想搞懂脉动阵列的人都动手写一遍比看十张架构图都管用。5. 实操中的坑与排查技巧5.1 阵列利用率低的常见原因实测下来阵列利用率低通常不是阵列本身的问题而是外围没配合好。我整理了一份排查清单现象可能原因排查方向利用率低于30%数据供给带宽不足检查SRAM位宽、bank冲突阵列周期性空转负载维度不匹配统计实际矩阵维度分布部分PE长期闲置数据流调度不合理检查权重加载和激活流入的时序功耗异常高无效翻转过多加时钟门控、数据门控我遇到最典型的一次是激活值流入的节奏和权重加载没对齐导致前几个周期阵列在算垃圾数据。后来加了一个简单的状态机把加载和计算分成明确的阶段利用率立刻从40%提到75%。5.2 边界处理与padding的坑矩阵维度往往不是阵列尺寸的整数倍这时候需要padding。padding本身不难难的是padding带来的额外计算和存储开销。我的经验是尽量让padding后的维度接近阵列尺寸的整数倍减少浪费。比如阵列是32×32实际维度是250那padding到256比padding到288划算得多。另外padding的数据要保证不污染结果。通常做法是padding零因为零乘任何数都是零不会影响累加。但要注意如果用了带偏置的累加padding部分可能会引入偏差需要在最后结果里裁掉。5.3 权重加载的时序设计权重加载阶段阵列是不干活的这段时间纯属开销。减少这个开销的办法有两个一是双缓冲一边算当前块一边加载下一块的权重二是权重压缩利用稀疏性或量化减少加载量。我做过一个对比不加双缓冲时权重加载占了总时间的15%左右加了双缓冲之后降到3%以下。这个优化在批量小、切换频繁的场景下收益特别明显。5.4 量化对脉动阵列的影响现在AI芯片普遍用INT8甚至INT4量化这对脉动阵列是利好因为乘法器面积和功耗都大幅下降。但量化也带来新问题累加器的位宽要足够否则会溢出。INT8乘INT8的结果是INT16累加K次后位宽还要增加log2(K)位。K256时累加器至少要24位才安全。我见过有人为了省面积把累加器做成16位结果大K值下频繁溢出精度崩了。这个位宽计算一定要在架构阶段就算清楚别等到RTL阶段才发现。6. 脉动阵列之外的一点延伸思考脉动阵列火了这么多年但它不是终点。我观察到几个演进方向值得关注一是可重构数据流让阵列能在权重驻留和输出驻留之间切换适应不同负载二是稀疏感知跳过零权重对应的计算提升有效利用率三是近存计算把阵列和存储做得更近进一步降低搬运开销。但不管怎么演进脉动阵列背后的核心思想不会变让数据流动起来让复用最大化让硬件结构匹配运算的规整性。理解了这一点你再看任何一款AI加速器的架构都能快速抓住它的设计取舍。我个人在实际项目里的体会是脉动阵列的难点从来不在阵列本身而在它和存储、调度、量化这些外围模块的配合。把数据供给的带宽算清楚把时序对齐做扎实把边界情况处理干净阵列的利用率自然就上去了。反过来如果只盯着阵列尺寸和峰值算力做出来的东西大概率是纸面参数好看、实测拉胯。
阅读完成 · 觉得有帮助?