4. NPU硬件架构:脉动阵列、数据流与片上存储

做AI芯片这些年,我越来越觉得NPU的硬件架构就像一套精密的“数据舞蹈”编排。你想想看,神经网络计算本质就是大量乘加,怎么让数据在芯片上跑得又快又省电?核心就三个词:脉动阵列、数据流架构、存储层次。这一章咱们就把它们拆开揉碎聊透。

💡 核心认知: NPU不是把CPU或GPU改一改就完事。它专门为矩阵运算而生,脉动阵列是它的心脏,数据流决定了血液怎么流,存储层次决定了血液能存多少。

4.1 脉动阵列(Systolic Array)—— 计算的心脏

我第一次在芯片版图里看到脉动阵列时,心里就一个感觉:整齐得让人舒适。说白了,它就是一个由PE(处理单元)组成的二维网格。每个PE只和相邻的PE通信,数据像心跳一样有节奏地“泵”过整个阵列。

举个例子,一个8×8的脉动阵列,有64个PE。每个PE里有一个乘法器和一个累加器。权重从左边进来,激活值从上边进来,部分和在内部向下累加。整个过程没有复杂的全局互联,延迟极低。

脉动阵列数据流示意 (4×4) 权重 激活 PE: 乘加单元

实际芯片里,Google TPU v1用的是256×256的脉动阵列,也就是65536个PE。我当年第一次跑仿真时,看到数据像潮水一样流过阵列,吞吐量直接拉满,真的被震撼到了。

⚡ 我的经验: 脉动阵列最怕“气泡”——也就是某个PE空转。所以数据要提前排好流水,编译器得把卷积循环展开成适合阵列尺寸的块。我曾经因为没对齐数据,导致阵列利用率只有40%,后来调整了tile大小,直接干到85%以上。

4.2 数据流架构:Weight Stationary vs Output Stationary

脉动阵列是骨架,数据流就是血液流动的方式。不同的数据流策略,直接影响功耗和带宽。我习惯把数据流分成三类,但最常用的是两种:权重固定(Weight Stationary) 和 输出固定(Output Stationary)。

4.2.1 Weight Stationary (WS)

权重预先加载到PE里,然后激活值流过来。每个PE里的权重一直不动,直到算完一整块。这样做的好处是权重复用率极高,适合权重密集的卷积层。

我记得在某个边缘芯片项目中,我们用WS数据流,权重从DRAM搬到PE一次,就能被用几十次,带宽压力小了很多。但缺点也明显——激活值要广播或者逐行扫描,如果激活值太大,中间缓存容易爆。

4.2.2 Output Stationary (OS)

反过来,输出部分和固定在PE里,权重和激活都流进来。每个PE负责计算输出特征图的一个像素。这种架构对输出通道数大的层很友好,因为部分和不用频繁写回。

你想想看,如果输出是32通道,每个PE只算一个输出点,累加都在本地完成。我做过对比,OS在深度可分离卷积里比WS省电约20%。

数据流类型固定数据流动数据适合场景典型芯片
Weight Stationary权重激活值、部分和普通卷积、大kernelGoogle TPU v1
Output Stationary部分和权重、激活值深度可分离卷积、1×1卷积某些NPU加速器
Row Stationary (混合)行数据权重、激活、部分和平衡带宽与复用Eyeriss (MIT)
⚠️ 避坑指南: 我曾经在一个项目里无脑选了WS,结果激活值太大,片上SRAM装不下,不得不频繁从DRAM reload,功耗直接翻倍。后来改成OS+部分tiling才解决。所以没有万能的数据流,一定要根据模型层形状做仿真。

4.3 片上存储层次 —— 数据搬移的艺术

做异构计算,有一句话我特别认同:“计算是免费的,搬数据才要钱。” NPU的存储层次就是为了让数据尽量待在离PE近的地方。

典型的NPU存储层次长这样:

我个人的习惯是:把权重放在Global Buffer里,激活值按tile切分后放到Local Buffer,部分和尽量留在RF里累加。这样每层计算的数据搬移量能减少5~10倍。

📦 存储层次设计原则: 尽量让数据复用发生在离PE最近的那一层。权重固定架构就是典型——权重待在PE里不动,激活流过去。如果发现某个数据被反复从DRAM读取,那一定是你tile切分没做好。

4.4 三者如何协同?

脉动阵列、数据流、存储层次不是孤立的。我画了一张图帮你理解它们怎么配合:

NPU硬件架构协同关系 脉动阵列 PE网格 + 乘加 高吞吐、低延迟 数据流策略 WS / OS / RS 决定数据移动模式 存储层次 RF → Local → Global 减少DRAM访问 驱动 供给 编译器 + 运行时共同决定tile大小与数据流映射

实际项目中,我通常先用性能模型估算不同数据流下的DRAM访问量,再选存储层次配置。比如ResNet-50,用WS+256KB Local Buffer,DRAM访问能比朴素实现少4倍。

🔧 小技巧: 如果你在做NPU编译器,可以尝试“double buffering”——用两块Local Buffer交替加载和计算,把数据搬移和计算完全流水起来。我调过的一个算子,用了double buffering之后,阵列空闲时间几乎降为0。

嗯,这一章的内容差不多就是这些。脉动阵列、数据流、存储层次,这三板斧是理解NPU硬件的基石。下次你看到一款NPU的die photo,试着找找脉动阵列在哪,猜猜它用的什么数据流——会很有意思。