第四节:脉动阵列架构(二):经典Systolic Array设计(Google TPU v1)
好,咱们接着聊脉动阵列。上一节我们把脉动阵列的基本概念和原理讲清楚了,这一节我带你看看一个真正在工业界大放异彩的经典案例——Google TPU v1 的脉动阵列设计。
说实话,TPU v1 刚出来那会儿,我第一反应是:这玩意儿也太「朴实无华」了吧?没有复杂的乱序执行,没有超标量,就是一个大号的矩阵乘法器。但后来仔细一琢磨,才明白什么叫「大道至简」。
TPU v1 的整体架构概览
TPU v1 是 Google 在 2015 年部署的专用加速器,专门用于推理任务。它的核心就是一个 256×256 的脉动阵列。你想想看,256×256 意味着什么?意味着一次可以完成 65536 个 MAC 操作。
我个人习惯把 TPU v1 的架构分成三个关键部分:
- 脉动阵列核心:256×256 的 MAC 单元矩阵
- 权重缓冲区:从片外 HBM 读取权重,然后「喂」给脉动阵列
- 累加器:处理部分和,完成最终的矩阵乘法结果
嗯,这里要注意,TPU v1 的脉动阵列是权重固定(Weight Stationary)的数据流方式。什么意思呢?就是权重数据待在 MAC 单元里不动,输入数据在阵列里流动,部分和也在流动。
数据复用与带宽优化
做芯片设计的人都知道,带宽永远是瓶颈。我曾经在一个项目里,计算单元利用率只有 30%,为什么?因为数据喂不进去。TPU v1 是怎么解决这个问题的?靠的就是数据复用。
咱们来算一笔账:
| 数据复用类型 | 复用次数 | 带宽节省比例 |
|---|---|---|
| 权重复用 | 256 次(一个权重被 256 个输入复用) | 256x |
| 输入复用 | 256 次(一个输入被 256 个权重复用) | 256x |
| 部分和复用 | 256 次(在累加过程中) | 256x |
说白了,TPU v1 的脉动阵列通过三种数据复用方式,把片外带宽需求降低了三个数量级。这就是为什么它可以用相对较低的带宽(DDR3-2133,约 34 GB/s)支撑起 92 TOPS 的算力。
关键洞察:TPU v1 的脉动阵列设计本质上是用「计算换带宽」——通过让数据在阵列内部多流动几次,减少对片外存储器的访问次数。
权重固定数据流详解
咱们具体看看权重固定是怎么工作的。假设我们有一个 3×3 的脉动阵列:
// 伪代码描述权重固定数据流
// 权重预先加载到每个 PE 中
for (int i = 0; i < 256; i++) {
for (int j = 0; j < 256; j++) {
// 输入数据从左侧流入,从上到下流动
PE[i][j].weight = W[i][j]; // 权重固定
PE[i][j].input = I[i][j]; // 输入流动
PE[i][j].partial_sum += PE[i][j].weight * PE[i][j].input;
}
}
你可能会问:为什么选择权重固定?我记得在 2018 年做的一个推理芯片项目里,我们对比过几种数据流方式。对于推理场景,权重是固定的(模型训练好后就不变了),所以把权重留在 PE 里是最合理的——加载一次,用到底。
实战技巧:如果你在做推理加速器,权重固定数据流是首选。但如果是训练场景,你可能需要考虑输出固定(Output Stationary)或者输入固定(Input Stationary)的数据流方式。
脉动阵列的时序设计
嗯,这里有个容易踩坑的地方——时序。脉动阵列的时序设计比想象中要复杂得多。
我曾经在一个项目里,把脉动阵列的时钟频率设得太高,结果数据在阵列里传播时,不同路径的延迟差异导致数据到达时间不一致。说白了就是「数据打架」了。
TPU v1 是怎么解决这个问题的?它采用了流水线寄存器的设计:
- 每个 PE 之间都有寄存器
- 数据在每个时钟周期只移动一个 PE
- 所有 PE 同步工作
这样做的好处是:时序收敛变得非常简单。你不需要担心长路径的延迟问题,因为每个 PE 之间的路径长度是固定的、可控的。
避坑指南:我曾经在 28nm 工艺下做过一个 128×128 的脉动阵列,时钟频率定在 700MHz。结果后仿发现,阵列边缘的 PE 和中心 PE 的时钟偏斜差了将近 200ps。最后不得不插入额外的时钟树缓冲器。所以,做脉动阵列时,时钟树的设计一定要提前规划。
带宽优化的具体策略
TPU v1 的带宽优化不仅仅是靠数据复用,还有几个关键设计:
- 双缓冲机制:权重缓冲区采用双缓冲设计,一个缓冲区在计算,另一个在加载下一批权重。这样计算和加载可以重叠。
- 数据广播:输入数据通过广播方式同时送到阵列的每一行,而不是逐行传输。
- 部分和累加:部分和在阵列内部累加,只有最终结果才写回片外存储器。
你想想看,如果没有这些优化,TPU v1 的带宽需求会是多少?我们来算一下:
// 假设矩阵乘法 C = A × B
// A 的大小:256 × 256
// B 的大小:256 × 256
// C 的大小:256 × 256
// 如果不做数据复用:
// 需要读取 A 的 256×256 = 65536 个元素
// 需要读取 B 的 256×256 = 65536 个元素
// 总共需要读取 131072 个元素
// 每个元素 8 位(INT8),总共 1 MB 数据
// 如果做数据复用(权重固定):
// 权重 B 只需要加载一次:65536 个元素
// 输入 A 按行流入:256 个元素/周期
// 总共只需要读取 65536 + 256 = 65792 个元素
// 节省了将近一半的带宽
这个计算虽然简化了,但道理是通的。数据复用带来的带宽节省是实实在在的。
实际设计中的权衡
做脉动阵列设计,说白了就是在做权衡。TPU v1 的设计团队做了几个关键决策:
| 设计决策 | 优点 | 缺点 |
|---|---|---|
| 256×256 阵列大小 | 算力高,适合大矩阵 | 小矩阵利用率低 |
| INT8 精度 | 推理场景足够,功耗低 | 训练场景精度不够 |
| 权重固定数据流 | 推理场景带宽效率高 | 训练场景需要频繁更新权重 |
| 无本地缓存 | 设计简单,面积小 | 灵活性差 |
我个人觉得,TPU v1 最聪明的地方在于:它知道自己要解决什么问题。它不是为了做一个通用的 AI 芯片,而是专门为 Google 的推理场景(比如 RankBrain、语音识别)设计的。所以它的设计决策都非常「极端」——把推理场景的性能做到极致,其他场景?抱歉,不是我的菜。
总结一下:TPU v1 的脉动阵列设计告诉我们,好的架构设计不是追求面面俱到,而是在特定场景下做到极致。数据复用、带宽优化、时序设计,这些都是围绕「推理场景」这个核心需求展开的。
下一节,我们会继续深入脉动阵列的变体设计,看看除了权重固定之外,还有哪些数据流方式,以及它们各自适用的场景。