2. NPU硬件架构基础
做算子适配,说白了就是跟NPU的硬件打交道。你得知道它肚子里装了什么,才能写出高效的算子。今天我就带你拆开NPU,看看里面到底是怎么运作的。
计算单元:NPU的心脏
NPU最核心的部分就是它的计算单元。跟CPU那种通用计算核心不一样,NPU的计算单元是为矩阵运算量身定做的。
- 矩阵乘法阵列(Systolic Array):这是NPU的招牌。一个典型的NPU里,会有一块由几百甚至几千个乘加器(MAC)组成的二维阵列。数据像血液一样在阵列里“脉动”流动,完成矩阵乘法。我见过一个项目,把矩阵乘法从CPU挪到NPU的Systolic Array上,推理速度直接翻了20倍。
- 向量处理单元(Vector Unit):除了矩阵乘法,还有向量运算。比如激活函数(ReLU、Sigmoid)、批量归一化、逐元素加法。这些操作在向量单元上跑,效率很高。
- 标量处理单元(Scalar Unit):处理一些零散的控制逻辑,比如循环计数、地址计算。它虽然算力不强,但不可或缺。
核心指标:MAC数量 × 频率 = 理论算力(TOPS)。但实际能跑多少,还得看数据能不能喂得饱。
存储层次:数据搬运的艺术
NPU的存储层次,说白了就是解决“算得快但取数据慢”这个矛盾。我刚开始做算子时,总以为算力是瓶颈,后来才发现,数据搬运才是大头。
| 存储层级 | 容量 | 带宽 | 延迟 | 用途 |
|---|---|---|---|---|
| 全局内存(Global Memory) | 大(GB级) | 高 | 高(几百周期) | 存放模型权重和输入数据 |
| 共享内存(Shared Memory) | 中(MB级) | 很高 | 中(几十周期) | 计算单元之间的数据交换 |
| 寄存器文件(Register File) | 小(KB级) | 极高 | 低(1-2周期) | 当前计算指令的操作数 |
嗯,这里要注意:数据局部性是性能的关键。你想想看,如果每次计算都去全局内存取数据,那计算单元大部分时间都在等数据。我建议在算子设计时,尽量把数据分块(Tiling),让数据在共享内存里多复用几次。
个人经验:我曾经优化一个卷积算子,发现全局内存访问占了总时间的70%。后来通过调整Tiling大小,把数据尽量留在共享内存里,性能直接提升了3倍。
数据通路:数据怎么跑起来
数据通路就是数据在NPU内部流动的路径。它决定了数据从哪来、到哪去、经过哪些单元。
- 加载通路(Load Path):从全局内存加载数据到共享内存或寄存器。通常有专门的DMA(直接内存访问)引擎来做这件事,不占用计算单元。
- 计算通路(Compute Path):数据从寄存器进入计算单元,算完后再写回寄存器。
- 存储通路(Store Path):把计算结果从寄存器写回全局内存。
我个人习惯把数据通路想象成一条流水线。加载、计算、存储可以重叠执行,这就是流水线并行。如果设计得好,计算单元可以一直满负荷运转。
避坑指南:我曾经遇到过一个问题,加载通路和计算通路争抢总线带宽,导致性能下降。后来通过调整DMA的优先级和分时策略,才解决了这个问题。
指令集概览:怎么指挥NPU干活
NPU的指令集跟CPU的指令集不太一样。它更偏向于数据并行和向量化操作。常见的指令类型包括:
- 矩阵运算指令:比如MATMUL、CONV2D。一条指令就能启动整个Systolic Array。
- 向量运算指令:比如VADD、VRELU、VBN(批量归一化)。
- 数据搬运指令:比如DMA_LOAD、DMA_STORE。负责在存储层次之间搬数据。
- 控制指令:比如JUMP、LOOP。用于控制程序流程。
举个例子,一个简单的卷积算子,在NPU上的指令序列可能是这样的:
// 伪代码示例
DMA_LOAD(input, shared_mem, size) // 加载输入数据
DMA_LOAD(weight, shared_mem, size) // 加载权重
CONV2D(shared_mem, reg_file, size) // 执行卷积
VRELU(reg_file, reg_file) // 执行ReLU激活
DMA_STORE(reg_file, output, size) // 存储结果
你想想看,如果每条指令都串行执行,那效率肯定不高。所以NPU的编译器会做指令调度,让DMA搬运和计算指令重叠执行。
核心要点:指令集的设计决定了算子的编程模型。理解指令集,才能写出高效的算子代码。
NPU硬件架构总览图
下面这张图展示了NPU内部的核心模块和数据流动关系。你可以看到,数据从全局内存出发,经过DMA搬运到共享内存,再进入计算单元,最后结果写回全局内存。
这张图里,数据从全局内存出发,经过DMA搬运到共享内存,再进入计算单元,最后结果写回全局内存。嗯,这里要注意,计算单元和寄存器文件之间是双向的,因为计算需要频繁读写寄存器。
总结一下
NPU的硬件架构,说白了就是围绕“计算”和“数据搬运”这两个核心来设计的。理解计算单元、存储层次、数据通路和指令集,是做好算子适配的基础。我建议你在实际项目中,多看看NPU的硬件手册,结合性能分析工具,慢慢就能摸透它的脾气。