1. 从矩阵乘法说起:为什么AI芯片偏偏看上了脉动阵列
搞AI芯片的人,绕不开一个词——矩阵乘法。不管你是做CNN卷积、Transformer注意力,还是全连接层推理,底层拆开来看,八成以上的计算量都压在矩阵乘加这一件事上。问题在于,矩阵乘法这东西,计算量大、数据复用率高、对内存带宽极度饥渴。你用CPU去跑,算力还没喂饱,内存带宽先跪了;你用GPU去跑,虽然并行度高,但功耗和面积又上去了。
所以做专用AI加速器的人,脑子里想的都是同一件事:怎么用最少的功耗、最小的面积,把矩阵乘法的吞吐量拉满。脉动阵列(Systolic Array)就是在这个背景下被反复拿出来讨论的方案。它最早不是为AI设计的,上世纪七八十年代H.T. Kung提出这个概念的时候,目标是用规整的PE(Processing Element)阵列做高效的信号处理和矩阵运算。但有意思的是,几十年后,TPU把它推到了AI芯片舞台的中央。
脉动阵列的核心思路,用一句话概括:让数据像心跳一样有节奏地在PE阵列中流动,每个PE只做最简单的乘加,数据从一边进、从另一边出,中间不做全局广播,不做复杂的地址计算。这种设计的好处是,数据复用率极高,控制逻辑极简,功耗效率非常好。你可以把它想象成一条流水线:每个工位只负责拧一颗螺丝,零件从上一站传过来,拧完传给下一站,不需要每个工位都跑去仓库取零件。
但脉动阵列也不是万能药。它的规整性意味着灵活性差,遇到稀疏矩阵、非规则计算、动态shape的时候,效率会打折扣。而且阵列尺寸一旦定下来,就很难改,这对算法快速迭代的AI领域来说,是个不小的风险。所以理解脉动阵列,不能只看它“快”,还要看它“为什么快”、“在什么条件下快”、“什么情况下会变慢”。
这篇文章我会从基本原理讲起,拆解PE的工作机制、数据流的组织方式、权重 stationary 和 output stationary 的区别,然后聊到实际芯片设计中的取舍,包括阵列尺寸怎么定、数据位宽怎么选、如何应对稀疏性,最后分享一些在仿真和RTL实现中容易踩的坑。适合做AI加速器架构、数字IC设计、以及想搞清楚TPU内部到底怎么跑的人。
2. 脉动阵列的底层工作机制:PE、数据流与节拍控制
2.1 一个PE到底在做什么
脉动阵列的基本单元叫PE,Processing Element。别看名字唬人,它干的事极其简单:一个乘法器加一个累加器,再加几个寄存器。每个时钟周期,PE从左边或上边接收数据,做一次乘加,把结果往右边或下边传。就这么简单。
但正是这种简单,让它能跑得很快。因为PE内部没有复杂的控制逻辑,没有指令译码,没有分支预测,关键路径短,时钟频率可以拉得很高。而且所有PE结构一模一样,布局布线非常规整,后端实现的时候面积和功耗都好控制。
一个典型的PE有三个端口:输入数据、输入权重、部分和输出。在权重stationary模式下,权重提前加载到PE内部寄存器里,输入数据从左侧流入,部分和从上往下流动。每个周期,PE执行:
partial_sum_out = partial_sum_in + (data_in * weight_reg)数据进来,乘一下,加上从上面来的部分和,再往下面送。权重不动,数据横向流动,部分和纵向流动。这就是最经典的脉动阵列数据流。
2.2 数据流动的“节拍感”从哪来
脉动阵列之所以叫“脉动”,是因为数据在阵列中的流动是有节奏的,像心跳一样。每个PE在每个周期都做一次操作,没有空闲,没有等待。这种节奏感来自于数据的错位排列。
举个例子,假设你要算一个2x2矩阵乘以2x2矩阵。输入矩阵A的行从左向右流动,矩阵B的列从上向下流动。为了让每个PE在正确的时刻拿到正确的数据,你需要在数据进入阵列之前做skew,也就是错开排列。第一行数据第0周期进,第二行数据第1周期进,第三行第2周期进,以此类推。这样当数据流到某个PE的时候,正好和从另一个方向流过来的权重相遇,完成乘加。
这个skew操作看起来简单,但在硬件实现的时候很关键。如果skew的周期数算错了,整个阵列的计算结果就全乱了。而且skew的深度和阵列尺寸直接相关,NxN的阵列,skew深度就是N-1个周期。
2.3 权重stationary vs 输出stationary
脉动阵列的数据流组织方式,决定了它的适用场景。最常见的两种是权重stationary和输出stationary。
权重stationary的意思是,权重提前加载到PE里,在整个计算过程中不动,输入数据流动,部分和流动。这种方式适合权重复用率高的场景,比如卷积神经网络里,同一个卷积核要在输入特征图上滑动很多次,权重不变,输入变。TPU用的就是这种方式。
输出stationary的意思是,部分和固定在PE里不动,输入数据和权重都流动。这种方式适合输出复用率高的场景,比如全连接层里,输出神经元的累加结果需要保留很久。但这种方式对PE的寄存器压力更大,因为部分和要一直存着。
实际芯片设计里,很少纯用一种模式,通常是混合的。比如Google TPU的MXU(Matrix Multiply Unit)就是权重stationary,但它在系统层面做了很多优化,让数据复用最大化。
| 数据流类型 | 固定不动 | 流动数据 | 适用场景 | 寄存器压力 |
|---|---|---|---|---|
| 权重stationary | 权重 | 输入、部分和 | 卷积、权重复用高 | 中 |
| 输出stationary | 部分和 | 输入、权重 | 全连接、输出复用高 | 高 |
| 输入stationary | 输入 | 权重、部分和 | 输入复用高 | 低 |
2.4 为什么脉动阵列能省带宽
脉动阵列最吸引人的地方,是它的数据复用能力。在一个NxN的阵列里,每个输入数据被复用N次,每个权重被复用N次。这意味着你从内存里读一次数据,就能在阵列里用N次。对于矩阵乘法这种计算密度极高的操作,这能大幅降低对内存带宽的需求。
举个例子,一个256x256的脉动阵列,做一次矩阵乘法,输入数据只需要从内存读256次,权重读256次,但实际完成了256x256x256次乘加。计算量是内存访问量的几万倍。这就是为什么TPU能用相对较低的内存带宽跑出很高的吞吐量。
但这里有个前提:数据必须能规整地流入阵列。如果矩阵是稀疏的,或者shape不规整,很多PE就会空转,复用率下降,带宽优势就没了。所以脉动阵列的高效率,是建立在“稠密、规整、大矩阵”这个假设上的。
3. 阵列尺寸、位宽与稀疏性:设计中的三个硬约束
3.1 阵列尺寸怎么定:不是越大越好
做架构设计的时候,第一个要回答的问题是:阵列做多大?128x128?256x256?还是512x512?
阵列越大,理论峰值算力越高,因为算力是N^2级别的。但阵列越大,面积也越大,功耗越高,而且良率会下降。更重要的是,阵列越大,对数据供给的要求越高。如果内存带宽喂不饱,大阵列就是浪费。
实际设计里,阵列尺寸的确定要综合考虑几个因素:目标模型的矩阵维度、内存带宽、功耗预算、面积预算。比如TPU v1用的是256x256的MXU,因为当时的目标模型主要是CNN,卷积核和特征图的维度大概在这个量级。如果做的是Transformer加速器,矩阵维度可能更大,但也要考虑注意力矩阵的动态性。
还有一个容易被忽略的点:阵列尺寸和skew深度直接相关。NxN的阵列,skew深度是N-1。如果N很大,skew的寄存器开销也不小。而且skew逻辑会增加控制复杂度,影响时序收敛。
实操心得:在架构探索阶段,不要一上来就定死阵列尺寸。先用仿真模型跑一遍目标负载,看看不同尺寸下的利用率和带宽需求,再结合面积和功耗估算做取舍。我见过不少项目,阵列做大了,结果发现大部分时间利用率不到30%,算力全浪费了。
3.2 数据位宽:8bit、16bit还是混合精度
AI芯片的位宽选择,直接影响到算力、功耗和精度。早期TPU用的是8bit整数,因为推理场景对精度要求不高,8bit够用,而且面积和功耗都小。后来训练场景需要更高的精度,16bit浮点和bfloat16开始流行。
脉动阵列的位宽设计有个特点:乘法器的面积和位宽的平方成正比。8bit乘法器面积大概是16bit的四分之一。所以位宽翻倍,算力不一定翻倍,但面积肯定翻倍。这就是为什么很多AI芯片主打8bit或4bit推理,用低位宽换高吞吐。
但低位宽有个问题:累加器的位宽不能太低。即使输入是8bit,累加结果也可能很大,需要32bit甚至更宽的累加器。所以PE内部通常是“窄乘法、宽累加”的结构。乘法器做8x8,累加器做32bit,这样既能保证精度,又能控制面积。
混合精度是另一个趋势。有些芯片支持多种位宽模式,比如8bit模式下阵列跑满,16bit模式下阵列拆成两半用。这种灵活性对算法迭代很友好,但控制逻辑会复杂不少。
| 位宽模式 | 乘法器面积 | 典型算力 | 适用场景 | 精度风险 |
|---|---|---|---|---|
| 4bit | 最小 | 最高 | 量化推理 | 高 |
| 8bit | 小 | 高 | 主流推理 | 中 |
| 16bit | 中 | 中 | 训练、高精度推理 | 低 |
| 32bit | 大 | 低 | 科学计算 | 极低 |
3.3 稀疏性:脉动阵列的“天敌”
脉动阵列最怕稀疏矩阵。因为它的设计前提是每个PE每个周期都有活干,一旦矩阵稀疏,很多PE就在做无用功,乘零加零,功耗照跑,算力浪费。
稀疏性在AI模型里其实很常见。剪枝后的模型、ReLU后的激活、注意力矩阵,都有大量零。如果直接丢进稠密脉动阵列,效率会掉得很厉害。有研究显示,50%稀疏度的矩阵,在稠密阵列上利用率可能只有50%甚至更低。
应对稀疏性有几种思路。一种是硬件上支持稀疏跳过,PE检测到零输入就跳过计算,省功耗。但这需要额外的控制逻辑,而且会破坏脉动阵列的规整性。另一种是在数据流层面做压缩,把稀疏矩阵压缩成稠密格式再送进阵列,但解压缩的开销也不小。
实际产品里,纯稀疏加速器很少见,更多是在稠密阵列基础上做一定程度的稀疏优化。比如支持结构化稀疏(2:4稀疏),每四个元素里有两个零,硬件可以固定跳过。这种方式对阵列改动小,收益也比较确定。
4. 从RTL到系统:脉动阵列落地时的真实挑战
4.1 数据供给:阵列再快,喂不饱也是白搭
脉动阵列的算力是N^2级别的,但数据供给是N级别的。这意味着阵列越大,对数据带宽的要求越高。一个256x256的阵列,每个周期需要256个输入数据和256个权重,如果时钟跑1GHz,带宽需求就是256x2x4Bytex1GHz,大概2TB/s。这个带宽在芯片内部还能靠SRAM撑一撑,但要从DRAM读,基本不可能。
所以实际设计里,脉动阵列前面通常有一级甚至多级缓存。权重先加载到片上SRAM,输入数据也先缓存在SRAM里,然后以阵列需要的节奏喂进去。这个缓存的设计很关键,缓存太小,阵列会饿;缓存太大,面积和功耗又上去了。
还有一个细节:权重的加载时间。权重stationary模式下,权重加载是一次性的,但加载本身需要时间。如果矩阵很小,加载时间可能比计算时间还长,这时候阵列利用率就很低。所以有些设计会做权重预加载,或者支持权重流式加载,减少等待。
4.2 部分和的累加与溢出处理
脉动阵列的部分和是纵向流动的,从阵列顶部流到底部,最后输出。这个过程中,部分和的位宽会逐渐增加。如果累加器位宽不够,就会溢出,结果就错了。
假设输入是8bit,权重是8bit,乘积是16bit。一个256x256的阵列,最底下的PE要累加256个乘积,结果可能达到16+8=24bit。所以累加器至少要24bit,通常做32bit留余量。但32bit累加器面积不小,每个PE都要一个,256x256就是65536个,面积很可观。
有些设计会用分段累加,比如每16个PE做一次局部累加,然后再把局部结果加起来。这样每个PE的累加器可以小一点,但需要额外的加法树。取舍点在于面积和时序的平衡。
注意:部分和的溢出是静默错误,不会报错,但结果全错。仿真的时候一定要用边界数据测,比如全1输入、全最大值输入,看看累加器会不会溢出。
4.3 控制逻辑:简单不等于没有
脉动阵列的控制逻辑确实比CPU/GPU简单得多,但也不是没有。你需要控制权重的加载、数据的skew、阵列的启动和排空、部分和的输出。这些控制信号要精确到周期,错一个周期结果就错。
特别是排空阶段。阵列算完之后,部分和还在阵列里流动,需要额外的时间排空。排空的周期数也是N-1。如果控制逻辑没算对排空时间,最后几个结果就丢了。
还有一个容易踩的坑:阵列的启动和停止。启动的时候,数据要逐渐填满阵列,前N-1个周期阵列是没满的,算力在爬坡。停止的时候,阵列逐渐排空,后N-1个周期算力在下降。对于大阵列,这个爬坡和排空的开销占比不大;但对于小阵列,可能占很大比例。所以小阵列适合小矩阵,大阵列适合大矩阵,不能一概而论。
4.4 时序收敛与物理实现
脉动阵列的规整性对后端实现是好事,但也不是没有挑战。最大的挑战是时钟树。NxN的阵列,时钟要送到每个PE,时钟树很长,skew很难控制。如果时钟skew太大,PE之间的数据传递就会出错。
另一个挑战是布线拥塞。PE之间的数据线是横向和纵向的,如果阵列很大,布线资源会很紧张。特别是部分和的纵向连线,每个PE都要往下传,线宽和间距都要仔细规划。
实际项目中,脉动阵列的物理实现通常会用定制布局,PE做硬核,阵列做规整排列,电源和时钟网格专门设计。这也是为什么脉动阵列适合ASIC,不太适合FPGA。FPGA上做脉动阵列,布线资源和时钟资源都受限,效率会打折扣。
5. 脉动阵列在AI芯片中的实际表现与边界
5.1 TPU的实战数据说明了什么
Google TPU v1的MXU是256x256的8bit脉动阵列,峰值算力92 TOPS,功耗大概40W。这个能效比在当时是非常惊人的。但实际跑模型的时候,利用率并不是100%。根据公开资料,TPU v1在跑CNN推理的时候,MXU利用率大概在20%到60%之间,取决于模型和batch size。
为什么利用率上不去?主要原因是数据供给和模型shape的匹配问题。如果卷积核太小,或者特征图通道数不是256的倍数,阵列就会有空转。还有batch size太小的时候,矩阵维度不够,阵列填不满。
TPU v2之后做了很多改进,比如支持更大的batch、更好的数据复用、更高的带宽。但脉动阵列的基本架构没变,说明这个方向是对的,只是需要系统层面的配合。
5.2 脉动阵列不适合什么
脉动阵列不是万能的。以下几种情况,它表现不好:
- 稀疏矩阵:前面说过,稀疏性会让PE空转,效率骤降。
- 动态shape:脉动阵列的skew和控制逻辑是固定的,如果矩阵维度动态变化,需要额外的控制逻辑来适配,灵活性差。
- 小矩阵:矩阵太小,阵列填不满,启动和排空开销占比高。
- 非矩阵运算:脉动阵列是为矩阵乘加设计的,遇到激活函数、归一化、池化这些操作,需要额外的硬件单元。
所以实际AI芯片里,脉动阵列通常只是其中一个模块,周围还有向量单元、标量单元、特殊函数单元。脉动阵列负责最重的矩阵乘法,其他操作交给别的模块。
5.3 和其他架构的对比
和脉动阵列竞争的主要是SIMD/SIMT架构(比如GPU)和可重构架构(比如FPGA)。
GPU的优点是灵活,什么都能算,但能效比不如专用阵列。FPGA的优点是可重构,适合算法快速迭代,但峰值算力低,功耗高。脉动阵列的优点是能效比高,适合稠密矩阵乘法,但灵活性差。
实际选择的时候,要看目标场景。如果是云端推理,模型固定、batch大、追求能效比,脉动阵列是很好的选择。如果是边缘设备,模型多变、功耗受限,可能需要更灵活的架构。如果是训练,对精度和灵活性要求高,GPU可能更合适。
| 架构类型 | 能效比 | 灵活性 | 适用场景 | 典型代表 |
|---|---|---|---|---|
| 脉动阵列 | 高 | 低 | 稠密矩阵推理 | TPU |
| SIMT | 中 | 高 | 训练、通用计算 | GPU |
| 可重构 | 低 | 高 | 算法迭代、小批量 | FPGA |
| 存内计算 | 极高 | 低 | 低功耗推理 | 新型AI芯片 |
6. 仿真与验证:脉动阵列开发中的踩坑记录
6.1 功能仿真:skew逻辑最容易出错
我第一次做脉动阵列仿真的时候,skew逻辑写错了,结果整个阵列的输出全是乱的。排查了半天,发现是skew的周期数算错了。NxN的阵列,第i行数据应该在第i个周期进入,我写成了第i+1个周期,导致所有数据都错位了一拍。
这个坑很典型。skew逻辑看起来简单,但周期数、数据排列、控制信号的配合,任何一个细节错了,结果就不对。而且这种错误在仿真波形上不容易看出来,因为数据都在动,只是错位了。最好的排查方法是:用小矩阵(比如2x2)做仿真,手动算一遍预期结果,然后对比波形。如果2x2对了,再放大到4x4、8x8,逐步验证。
6.2 时序仿真:部分和的建立时间
部分和的纵向流动是组合逻辑路径,从上面的PE传到下面的PE,中间可能经过多个PE。如果阵列很大,这条路径会很长,时序可能不收敛。
解决办法是在PE之间插入寄存器,把部分和的传递做成流水线。但这样会增加延迟,而且控制逻辑要相应调整。另一种办法是分段累加,每几个PE做一次寄存器打拍,减少关键路径长度。
实际项目中,时序收敛通常要迭代好几轮。建议在架构设计阶段就考虑时序,不要等到RTL写完再改。比如阵列尺寸不要一味求大,位宽不要一味求宽,给时序留余量。
6.3 验证覆盖率:怎么确保测全了
脉动阵列的验证,最难的是覆盖率。因为数据流是并行的,状态空间很大。你不能像验证CPU那样,跑几个指令序列就完了。你需要验证各种数据模式、各种矩阵维度、各种边界条件。
我的经验是,验证要分层次。先验证单个PE的功能,再验证一行PE的联动,再验证整个阵列。数据模式要覆盖:全零、全一、最大值、最小值、随机值、稀疏值。矩阵维度要覆盖:等于阵列尺寸、小于阵列尺寸、大于阵列尺寸、非方阵。
还有一个容易漏的点:阵列的启动和排空。很多验证只测稳态,不测启动和排空,结果流片后发现边界情况出错。启动和排空的测试用例一定要单独写,确保控制逻辑正确。
6.4 功耗仿真:动态功耗的估算
脉动阵列的功耗主要来自动态功耗,也就是PE的翻转。每个周期,PE的乘法器和累加器都在翻转,功耗和阵列尺寸、时钟频率、数据翻转率都相关。
估算功耗的时候,不能只看峰值。实际跑模型的时候,数据翻转率是变化的。比如全零输入的时候,翻转率很低,功耗也低。随机输入的时候,翻转率大概50%,功耗最高。所以功耗估算要用实际负载的翻转率,不能用理论峰值。
还有一个经验:脉动阵列的时钟树功耗占比很高。大阵列的时钟树可能占整个阵列功耗的30%以上。所以时钟门控很重要,空闲的PE要把时钟关掉,省功耗。
7. 写在最后:一些个人体会
脉动阵列这个东西,原理不复杂,但做好不容易。它的优势在于规整和高效,劣势在于死板和局限。做AI芯片架构,不能因为TPU用了脉动阵列就盲目跟风,要看自己的目标场景和数据特征。
我在实际项目里最大的体会是:脉动阵列的效率,不取决于阵列本身,而取决于数据供给和系统配合。阵列做得再大,数据喂不饱就是浪费。所以架构设计要从系统层面考虑,内存、缓存、数据流、控制逻辑,每一环都要匹配。
还有一个建议:如果你刚开始做脉动阵列,先用Python或C++写一个周期精确的仿真模型,把数据流和控制逻辑跑通,再写RTL。这样能省很多调试时间。RTL仿真慢,波形难看,先用高级语言验证逻辑,效率高得多。
最后,脉动阵列只是AI芯片的一种选择,不是唯一选择。存内计算、近存计算、可重构架构,都在快速发展。保持开放心态,根据实际需求选方案,比迷信某一种架构更重要。