1. NPU架构基础
好,咱们直接进入正题。第一章,我打算先聊聊NPU到底是个什么东西。很多同学可能听过NPU这个词,但真要你说清楚它和CPU、GPU到底差在哪,可能又有点含糊。嗯,这很正常,我刚开始接触NPU的时候也懵过一阵子。
1.1 NPU与CPU/GPU的核心区别
说白了,这三兄弟的设计哲学完全不同。
- CPU:通用性最强,什么活都能干,但一次干不了太多并行的事。它擅长的是“复杂控制”和“低延迟响应”。
- GPU:专为图形渲染设计,后来发现做并行计算也挺香。它擅长“大量简单计算”,但控制能力弱,延迟高。
- NPU:为神经网络量身定制。它不关心你玩不玩游戏,也不关心你开不开网页,它只关心一件事——矩阵乘法和卷积。
我个人习惯用一个比喻来理解:CPU像是一个全能工匠,什么工具都会用,但一次只能做一件精细活;GPU像是一个流水线工厂,几百个工人同时拧螺丝;而NPU呢?它像是一台专门为“拧特定型号螺丝”设计的全自动机器,效率极高,但换个活它就傻了。
关键点:NPU牺牲了通用性,换来了极致的能效比和吞吐量。在AI推理场景下,NPU的功耗往往只有GPU的十分之一甚至更低。
我在项目中遇到过一件事:某次做边缘端的人脸识别,用GPU跑模型,功耗直接飙到15W,散热压不住。换成NPU之后,同样精度下功耗只有2W。你想想看,这差距有多大。
1.2 NPU的存储层次结构
NPU的存储层次,和CPU的缓存体系有点像,但又有本质区别。咱们先看一张图,我专门画了个结构示意。
这张图我建议你多看两眼。NPU的存储层次和CPU最大的不同在于:CPU的缓存是硬件自动管理的,而NPU的存储层次往往需要编译器或程序员显式控制。说白了,你得告诉NPU什么时候把数据从DDR搬到全局缓存,什么时候再从全局缓存搬到本地存储。
我的经验:做算子融合时,最核心的优化思路就是“减少数据在存储层次间的搬运次数”。我曾经在一个卷积+激活+池化的融合案例中,通过合理利用本地存储,把DDR访问次数减少了70%,推理速度直接翻倍。
1.3 NPU的计算单元与数据流
NPU的计算单元,核心就是乘累加器阵列(MAC Array)。一个典型的NPU内部,可能有几十到几千个MAC单元,排成二维阵列。
为什么会排成二维?因为矩阵乘法本身就是二维操作。你想想看,一个矩阵A乘以矩阵B,每个输出元素都是A的一行和B的一列做点积。如果MAC阵列是二维的,就可以同时计算多个输出元素。
| 计算单元类型 | 功能 | 典型数量 |
|---|---|---|
| MAC(乘累加器) | 执行 a × b + c 操作 | 64 ~ 4096个 |
| ALU(算术逻辑单元) | 执行激活函数、池化等操作 | 8 ~ 64个 |
| 向量单元 | 处理向量化操作(如批量归一化) | 4 ~ 16个 |
数据流这块,我重点说三种常见的模式:
- 权重固定(Weight Stationary):把权重存在本地存储里不动,输入数据流进来。适合卷积层,因为权重复用率高。
- 输入固定(Input Stationary):输入数据不动,权重流进来。适合全连接层。
- 输出固定(Output Stationary):部分和结果不动,不断累加。适合深度较大的网络。
避坑指南:我曾经在一个项目中,默认用了权重固定的数据流,结果发现某个层的权重复用率极低,导致频繁从DDR加载权重,性能反而比GPU还差。后来改成输入固定,性能提升了3倍。所以,没有最好的数据流,只有最适合当前算子的数据流。
嗯,说到这,我想起一个细节。NPU的计算单元还有个特点:支持低精度计算。比如INT8、INT4甚至二进制。CPU和GPU虽然也支持,但NPU在硬件层面就为低精度做了优化,功耗和面积都更小。我建议你在做算子融合时,尽量把精度对齐,避免频繁的精度转换,那玩意很费时间。
最后,咱们用一段伪代码来感受一下NPU的数据流控制:
// NPU数据流控制伪代码示例
// 假设我们要执行一个3x3卷积
// 1. 从DDR加载权重到全局缓存
load_weight_from_ddr(weight_addr, global_buffer, 3*3*C_in*C_out);
// 2. 从全局缓存分发权重到各PE的本地存储
distribute_weight(global_buffer, pe_local_mem, 3*3);
// 3. 从DDR加载输入特征图到全局缓存
load_input_from_ddr(input_addr, global_buffer, H*W*C_in);
// 4. 按行分块,将输入数据搬运到PE本地存储
for each tile_row in H:
for each tile_col in W:
load_tile_to_local(global_buffer, pe_local_mem, tile_size);
// 5. 在PE阵列中执行卷积计算
for each kernel_row:
for each kernel_col:
mac_array_execute(pe_local_mem, weight_local_mem, output_local);
// 6. 将结果写回全局缓存
store_output_to_global(pe_local_mem, global_buffer, tile_size);
// 7. 最终写回DDR
store_output_to_ddr(global_buffer, output_addr, H_out*W_out*C_out);
你看,每一步都是显式控制的。这就是NPU编程和CPU编程最大的不同——你得亲自当“数据搬运工”。但好处是,一旦你掌握了这个搬运技巧,性能优化空间非常大。
好了,第一章的内容就这些。记住三个核心点:NPU是专用架构、存储层次需要显式管理、数据流模式要因算子而异。后面几章,我们会基于这些基础,深入讲算子融合的具体技巧。
本章小结:NPU通过牺牲通用性换取AI计算的极致效率;其存储层次从DDR到寄存器共四层,需要软件显式管理;计算核心是MAC阵列,数据流模式需根据算子特点选择。理解这些,是后续所有优化的前提。