3、算子映射基础:映射的定义与目标
说实话,我第一次接触算子映射这个概念时,也觉得挺玄乎的。说白了,就是把深度学习模型里的各种算子——卷积、池化、全连接这些——给安排到NPU的硬件单元上跑。听起来简单?嗯,这里面的门道可不少。
3.1 映射到底在干什么?
我习惯把映射理解成一种「翻译+调度」的过程。你想想看,模型里的算子是用框架(比如PyTorch、TensorFlow)定义的,它们不知道NPU长什么样。而NPU只知道怎么执行自己的指令集。映射就是中间那座桥。
具体来说,映射要干三件事:
- 算子拆分:一个大算子可能拆成多个小操作。比如一个3x3卷积,在硬件上可能要拆成行扫描、乘加、累加几个步骤。
- 资源分配:决定每个子操作放在哪个PE(处理单元)上跑,用哪块Buffer存数据。
- 时序编排:安排好谁先谁后,别让数据等计算,也别让计算等数据。
映射的核心目标:在满足精度要求的前提下,最大化硬件利用率,最小化延迟和功耗。
我见过不少团队,映射做得好不好,性能能差出3-5倍。真的不夸张。
3.2 硬件资源抽象:PE、ALU、Buffer
做映射之前,你得先搞清楚NPU里有什么。但注意,我们做映射时看的不是晶体管级别的电路,而是抽象后的资源视图。我个人习惯把NPU抽象成三层:
PE(处理单元)
PE是NPU的「打工人」。每个PE里通常包含一个乘加器(MAC)和一些本地寄存器。我遇到过的一个项目里,NPU有256个PE,排成16x16的阵列。每个PE负责一小块数据的计算。
映射时你要想:这个算子的计算量,能不能均匀分摊到所有PE上?有没有PE闲着没事干?
ALU(算术逻辑单元)
ALU是PE里的「特种兵」。除了乘加,它还负责一些特殊操作,比如ReLU、量化、池化中的比较操作。嗯,这里要注意:不是所有NPU的ALU都一样。有的NPU把ALU集成在PE内部,有的单独拎出来。
我曾经踩过一个坑:某个算子里的LeakyReLU,我以为PE能直接处理,结果发现那个NPU的PE只支持标准ReLU,LeakyReLU必须走ALU。映射方案得重做。
Buffer(缓冲区)
Buffer是NPU的「仓库」。一般分三级:
| 层级 | 容量 | 速度 | 用途 |
|---|---|---|---|
| L1 Buffer(PE本地) | 几KB | 最快 | 存当前计算块的输入/输出 |
| L2 Buffer(共享) | 几十到几百KB | 中等 | 存中间结果、权重 |
| L3 Buffer(全局) | 几MB | 较慢 | 存特征图、大块数据 |
映射时最头疼的就是Buffer规划。数据放哪层、什么时候搬、搬多少,直接决定了计算效率。
我的经验:做映射时,先把数据流画出来。哪块数据从哪来、到哪去、在哪个Buffer里待多久。画清楚了,问题就解决了一半。
3.3 映射的输入与输出
映射不是凭空产生的。它需要明确的输入,也会产生具体的输出。
输入:三样东西
- 计算图:来自深度学习框架的中间表示。比如ONNX模型、TVM的Relay IR。图里包含了算子类型、数据依赖关系、张量形状等信息。
- 硬件描述:NPU的架构参数。PE数量、Buffer大小、带宽、支持的算子类型等。这些通常以JSON或YAML配置文件的形式给出。
- 约束条件:比如延迟要求(必须在10ms内完成)、功耗预算(不超过5W)、精度要求(INT8量化)。
输出:映射结果
映射完成后,你会得到:
- 算子到硬件的分配表:哪个算子跑在哪个PE上,用哪块Buffer。
- 执行时序图:每个操作在什么时间开始、什么时间结束。
- 数据搬运指令:DMA(直接内存访问)的搬运序列,告诉硬件什么时候搬数据、搬多少、搬到哪。
注意:映射输出不是一次性的。我经常要迭代好几轮——先出一个粗糙的方案,跑仿真看性能,发现瓶颈再调整。有时候调整一个算子的映射位置,整个时序都得重排。
3.4 一个简单的映射示例
光说不练假把式。我们来看一个具体的例子。
假设我们有一个简单的模型片段:
输入: 1x3x224x224 的图像
算子1: Conv2d(3→64, kernel=3, stride=1, padding=1)
算子2: ReLU
算子3: MaxPool(kernel=2, stride=2)
NPU配置:16个PE,每个PE有1个MAC,L1 Buffer 4KB,L2 Buffer 64KB。
映射过程大致如下:
- 分析计算量:Conv2d的乘加次数 = 64个输出通道 × 224×224个位置 × 3×3×3个输入 = 约86M次。16个PE,每个PE要处理约5.4M次。
- 拆分数据:把224×224的特征图切成16块,每块56×56,每个PE负责一块。
- 分配Buffer:每个PE的L1 Buffer存当前块的输入和权重。L2 Buffer存相邻块的重叠数据(padding部分)。
- 编排时序:先让DMA把权重从DDR搬到L2,再分发给各PE的L1。然后PE开始计算,算完的结果直接写回L2。ReLU和MaxPool跟在Conv后面,流水线执行。
你看,就这么一个简单的映射,已经涉及了数据拆分、资源分配、时序编排三个核心步骤。
关键点:映射不是「把算子塞进硬件」那么简单。它是在有限资源下做最优决策的过程。每个决策——数据怎么切、放哪、什么时候算——都影响最终性能。
我刚开始做映射时,总觉得手动调优太麻烦。后来发现,理解这些基础概念后,再去用自动映射工具(比如TVM的AutoScheduler),才能看懂它到底在干什么,出了问题也知道怎么修。
嗯,这一节的内容就到这。映射的基础打牢了,后面讲数据流优化、内存层级管理这些高级话题时,你才不会觉得飘。
小技巧:刚开始学映射时,别急着上复杂模型。拿一个Conv+ReLU+Pool的小网络练手,手动画出数据流和资源分配图。画个三五遍,感觉就来了。