第四章:NPU宏观架构概览

好,我们进入正题。这一章讲的是NPU的宏观架构,说白了就是整个芯片的骨架。你想想看,一个NPU要跑神经网络,它得有四样东西:算力来源、数据暂存、搬运方式、调度控制。这四样分别对应:计算单元(PE阵列)、存储层次(SRAM/DRAM)、数据流(Dataflow)、控制单元(Controller)。

我在做第一代NPU的时候,犯过一个低级错误——光顾着堆算力,结果数据喂不进去,PE阵列大部分时间在空转。嗯,那感觉就像给F1赛车配了个自行车链条。所以这一章,我会把每个模块的坑和心得都讲透。

4.1 计算单元:PE阵列

PE,Processing Element,处理单元。它是NPU的最小计算节点。一个PE通常包含一个乘法器和一个累加器,也就是MAC(Multiply-Accumulate)。

PE阵列就是把这些PE排成矩阵。为什么是矩阵?因为矩阵乘法是神经网络的核心操作。你想想看,一个卷积层,本质上就是输入特征图和卷积核做矩阵乘加。

核心要点:PE阵列的规模决定了NPU的峰值算力。一个256x256的PE阵列,每个PE做一次MAC,就是65536次操作。但注意,这是理论值,实际能跑多少,取决于数据能不能喂饱它。

PE阵列的组织方式,我见过三种主流方案:

  • 脉动阵列(Systolic Array):数据像流水一样在PE间传递。Google TPU用的就是这种。优点是控制简单,缺点是灵活性差。
  • 网格阵列(Mesh Array):每个PE只和邻居通信。适合做数据重用,但全局通信慢。
  • 广播阵列(Broadcast Array):一个数据同时发给所有PE。带宽要求高,但适合某些特定层。

我个人习惯用脉动阵列。为什么?因为它的数据流最规整,硬件实现起来不容易出错。我曾经在网格阵列上吃过亏——邻居通信的布线太复杂,后端物理设计跑了好几个月才收敛。

设计技巧:PE阵列的大小不是越大越好。要考虑片上存储的容量。我建议PE阵列的总MAC数,和SRAM带宽的比例控制在1:2左右。也就是说,每1个MAC操作,要有2个数据能同时从SRAM读出来。

4.2 存储层次:SRAM/DRAM

存储层次,说白了就是数据放哪的问题。NPU的存储分三层:

层级 容量 带宽 延迟 用途
寄存器(Reg) 几十字节 极高 1周期 PE内部临时变量
片上SRAM 几MB到几十MB 几个周期 权重、激活值、中间结果
片外DRAM 几GB 几百周期 模型参数、输入输出数据

这里有个关键点:SRAM是NPU的命脉。为什么?因为DRAM太慢了。你算一笔账:一个MAC操作只要1个周期,但从DRAM读数据要几百个周期。如果每次计算都要等数据从DRAM来,那算力再高也没用。

我记得有个项目,客户要求跑ResNet-50,我们的PE阵列算力绰绰有余,但SRAM只有2MB。结果呢?权重放不下,频繁从DRAM搬运,实际性能只有理论值的30%。后来我们把SRAM加到8MB,性能直接翻了三倍。

避坑指南:我曾经在SRAM的bank划分上犯过错。如果bank数量不够,多个PE同时访问同一个bank,就会产生冲突。解决办法是:bank数量至少是PE阵列行数的两倍,并且地址要交错映射。

SRAM的带宽怎么算?很简单:假设PE阵列有N个PE,每个PE每周期需要读两个数据(一个权重,一个激活),写一个结果。那么SRAM的总带宽就是3N个数据/周期。如果数据位宽是16位,那就是48N bit/周期。

4.3 数据流:Dataflow

数据流,就是数据怎么在存储和计算单元之间流动。这决定了NPU的效率和灵活性。

主流的数据流有三种:

  • 权重固定(Weight Stationary):权重留在PE里不动,激活值流进来。适合全连接层。
  • 激活固定(Input Stationary):激活值留在PE里,权重流进来。适合卷积层。
  • 输出固定(Output Stationary):部分和留在PE里,不断累加。适合深度可分离卷积。

你可能会问:哪种最好?其实没有绝对的好坏。我在项目中遇到过,同一个NPU跑不同模型,最优数据流完全不同。所以现在的NPU大多支持可配置的数据流。

举个例子,我设计过一个支持三种数据流的PE阵列。控制逻辑稍微复杂了点,但换来的是对各种网络层的良好适配。跑MobileNet时用激活固定,跑BERT时用权重固定,效果都不错。

关键设计点:数据流的选择直接影响数据重用率。重用率越高,对DRAM的访问越少,功耗越低。我建议在设计初期,用Roofline模型分析一下,看看你的NPU是计算受限还是带宽受限。

4.4 控制单元:Controller

控制单元,就是NPU的大脑。它负责:

  • 指令译码:把上层发来的指令翻译成微操作
  • 数据调度:决定什么时候从哪读数据,写到哪
  • PE控制:控制PE阵列的计算模式和数据流
  • 同步管理:确保所有PE步调一致

控制单元的设计,我见过两种流派:

  1. 集中式控制器:一个中央控制器管所有。简单,但扩展性差。PE阵列一大,控制信号的路由就成问题。
  2. 分布式控制器:每个PE或每组PE有自己的小控制器。灵活,但设计复杂,同步是个难题。

我个人偏向混合方案。中央控制器负责宏观调度,比如加载哪一层、用哪种数据流。每个PE行有一个本地控制器,负责行内的微操作。这样既保证了全局一致性,又减少了控制信号的布线压力。

嗯,这里要注意:控制器的指令集设计很关键。指令太细,代码量爆炸;指令太粗,灵活性不够。我建议用VLIW(超长指令字)风格,一条指令包含多个并行操作。比如:

// 一条VLIW指令示例
// 同时执行:从SRAM读数据、PE计算、写结果回SRAM
LOAD  R0, SRAM_ADDR_WEIGHT   // 读权重
LOAD  R1, SRAM_ADDR_ACT      // 读激活
MAC   PEOUT, R0, R1          // 乘加计算
STORE PEOUT, SRAM_ADDR_RESULT // 写结果

这样一条指令,就完成了数据读取、计算、写回的全流程。效率很高。

设计技巧:控制器的流水线深度要适中。太深了,分支预测出错时惩罚大;太浅了,频率上不去。我一般控制在3-5级流水线,既能跑高频,又不会因为控制冒险浪费太多周期。

4.5 宏观架构的权衡

讲完了四个模块,我们来聊聊它们之间的权衡。NPU设计,说白了就是在面积、功耗、性能之间找平衡。

举个例子:PE阵列大了,算力高,但SRAM也得跟着大,否则数据喂不饱。SRAM大了,面积和功耗就上去了。DRAM接口宽了,带宽高,但封装和PCB成本也高了。

我做过一个项目,目标是在5W功耗内跑YOLOv5。最后权衡下来,PE阵列用了128x128,SRAM用了4MB,DRAM接口用了LPDDR4 32-bit。这个配置刚好卡在功耗和性能的平衡点上。

避坑指南:我曾经在架构设计阶段忽略了数据路径的带宽匹配。PE阵列的输入带宽是256bit/周期,但SRAM的输出带宽只有128bit/周期。结果PE阵列有一半时间在等数据。后来不得不重新设计SRAM的bank划分和交叉开关,浪费了三个月。

最后,我建议你在做宏观架构设计时,先画一张数据流图。把每个模块的带宽、延迟、容量标出来。然后跑几个典型模型,看看瓶颈在哪。这个习惯,我从第一代NPU项目开始就一直保持,帮我避了不少坑。

好,这一章就到这里。下一章我们会深入PE阵列的微架构设计,讲讲每个PE内部到底是怎么工作的。