第四章: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步调一致
控制单元的设计,我见过两种流派:
- 集中式控制器:一个中央控制器管所有。简单,但扩展性差。PE阵列一大,控制信号的路由就成问题。
- 分布式控制器:每个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内部到底是怎么工作的。