最近瑞芯微以及地瓜RDK上做NPU/BPU的神经网络部署,所以在这里总结一下
瑞芯微模型转化链接:https://github.com/airockchip/rknn-toolkit2
名词解释
模型转换(整体流程)
PyTorch / ONNX / TensorFlow 转成 硬件适配的格式
.pt / .onnx转化为.rknn(瑞芯微)/.bin(地瓜)
.rknn(瑞芯微)/.bin(地瓜)均为二进制文件
解析计算图
映射支持的算子
保存权重和网络结构
适配目标芯片的运行方式
计算图(Computational Graph)
节点(Nodes):代表运算操作(如卷积、加法、矩阵乘法)。
边(Edges):代表数据张量(Tensor)的流动方向。
图优化
对模型的计算图做重写和精简,让推理更高效。
常见优化包括:
- 删除无用节点
- 融合多个算子(比如:×3×2 - > ×6)
- 折叠常量
- 调整数据流顺序
- 减少内存拷贝
- transpose 消除
比如:Conv + BatchNorm + ReLU 优化成一个更高效的组合执行方式。
算子适配/算子映射
把模型里的每一种运算,映射到目标硬件或推理引擎支持的实现上。
深度学习算子 NPU 适配总结表
| 算子名称 | 所属类别 | 核心功能 | NPU 适配友好度 | 主要适配问题 / 原因 | 常见优化/处理方法 |
|---|---|---|---|---|---|
| Conv | 计算类 | 图像/特征提取的核心操作,进行滑窗加权求和。 | 🟢 原生友好 | 无。所有NPU都为卷积设计了专门的加速单元。 | 作为融合优化的核心,常与BN、激活函数融合。 |
| BatchNorm (BN) | 计算类 | 推理时对特征图进行标准化(线性变换)。 | 🟢 原生友好 | 无。但在部署时为了性能,需要被处理掉。 | 算子融合:在图优化阶段,将其参数“折叠”进相邻的Conv层权重中,从而被消除。 |
| ReLU | 激活函数 | 引入非线性:f(x) = max(0, x)。 | 🟢 原生友好 | 无。硬件指令集几乎都原生支持。 | 算子融合:被硬件直接吸收进Conv等算子的激活选项中,零开销。 |
| SiLU (Swish) | 激活函数 | YOLOv5/v8主力激活:f(x) = x * sigmoid(x)。 | 🟢 良好 | 较老的NPU可能无原生指令支持。 | 融合/替换:新NPU支持融合;老NPU可能拆解为sigmoid + mul,也可考虑替换为ReLU。 |
| MaxPool | 池化/采样类 | 下采样,取池化窗口内的最大值。 | 🟢 原生友好 | 无。NPU通常有专门指令支持。 | 关注kernel_size、stride、padding等参数是否符合硬件限制。 |
| AveragePool | 池化/采样类 | 下采样,取池化窗口内的平均值。 | 🟢 原生友好 | 无。NPU通常有专门指令支持。 | 关注kernel_size、stride、padding等参数是否符合硬件限制。 |
| GlobalAveragePool | 池化/采样类 | 对整个特征图取全局平均值,常用于分类头。 | 🟢 原生友好 | 无。通常可高效实现。 | 常作为YOLO分类头或辅助头的输出层,可直接被NPU支持。 |
| Add | 元素级运算类 | 逐元素相加,常用于残差连接(Residual Connection)。 | 🟢 原生友好 | 无。这是最基础的逐元素操作。 | 常与Conv融合,形成Conv + Add的优化模式。 |
| Mul | 元素级运算类 | 逐元素相乘,常用于注意力机制中的加权。 | 🟢 原生友好 | 无。NPU支持良好。 | 注意Mul的第二个输入是常量还是动态张量,广播规则是否支持。 |
| Concat | 张量操作类 | 沿指定维度拼接多个张量,常用于特征融合(如FPN)。 | 🟢 良好 | 拼接的维度信息必须在编译时确定(静态)。 | 图优化:在可能的情况下,与前后算子融合消除。 |
| Reshape | 张量操作类 | 改变张量形状,但总元素数不变。 | 🟢 良好 | 本身开销极小。问题在于其输入形状可能来自动态算子。 | 图优化消除:编译器常可优化掉,不产生实际指令。 |
| Squeeze | 张量操作类 | 移除张量中所有尺寸为1的维度。 | 🟢 良好 | 无。编译时可确定。 | 常被编译器自动优化或与Reshape合并。 |
| Unsqueeze | 张量操作类 | 在指定位置插入一个尺寸为1的维度。 | 🟢 良好 | 无。编译时可确定。 | 常被编译器自动优化或与Reshape合并。 |
| Cast | 张量操作类 | 改变张量的数据类型(如float32 → int64)。 | 🟢 良好 | 需确认目标硬件是否支持该类型转换。 | 通常在图优化阶段保留,注意类型转换是否有精度损失。 |
| Flatten | 张量操作类 | 将张量展平为一维。 | 🟢 良好 | 无。编译时可确定。 | 常被编译器优化,或与Gemm等算子融合。 |
| Split | 张量操作类 | 沿指定维度将一个张量分割成多个子张量。 | 🟢 良好 | 分割的维度和数量必须静态确定。 | 常与Concat配对出现,有时可被优化消除。 |
| ConvWithBnAct | 融合优化类 | 部署工具生成的融合算子,并非原始模型中的独立算子。 | 🟢 优化产物 | 无。这是所有NPU都追求的最终目标形态。 | 由图优化器自动生成,代表了最佳的适配结果。 |
| Resize | 池化/采样类 | 上采样或下采样特征图,改变空间尺寸(如双线性插值、最近邻插值)。 | 🟢 良好 | 主要适配问题在于mode和scales是否被目标NPU支持。scales须为常量。 | 首选mode='nearest'(硬件友好)。若必须用linear,确认NPU是否支持;不支持时可用TransposedConv替代或写Custom Op。 |
| Softmax | 激活/归一化类 | 将输入向量映射为概率分布,所有元素值在(0,1)之间且和为1。 | 🟡 中等 | 需确认NPU对axis(沿哪个维度做Softmax)参数的支持范围。某些NPU要求axis必须是最后一维。 | 尽量确保axis=-1(最后一维)或硬件支持的其他静态轴;性能敏感时可考虑用LogSoftmax替代(如果后续接NLLLoss)。 |
| Transpose | 张量操作类 | 交换张量维度(如 NCHW <-> NHWC)。 | 🟡 中等 | 导致内存重排,增加耗时;且会阻断相邻算子的融合。 | 消除/吸收:通过修改相邻算子的数据布局参数来吸收,或在图分析中被消除。 |
| Slice | 张量操作类 | 从张量中切取一部分子集(如starts和ends指定范围)。 | 🟡 中等 | starts、ends、steps等参数必须为常量(静态),动态切片极难处理。 | 尽量确保切片参数在编译时固定。若不可避免,考虑用StridedSlice替代或写Custom Op。 |
| Gather | 选择/过滤类 | 根据提供的索引,从张量中取出对应数据。 | 🟡 中等 | 本身功能简单。问题在于索引(indices)的来源。 | 若索引来自固定常量,则无问题;若来自TopK等动态算子,则需整体替换为Custom Op。 |
| Upsample | 池化/采样类 | (注:与Resize功能重叠,ONNX中已统一为Resize)上采样特征图。 | 🟡 中等 | 与Resize相同,关注mode和scales。 | 同Resize。 |
| MatMul | 矩阵运算类 | 执行矩阵乘法,在注意力机制、全连接层中常见。 | 🟡 中等 | 权重形状需对齐;需确认NPU对批次矩阵乘(BMM)的支持。 | 确保输入维度是静态的;考虑是否有Conv 1x1替代的可能。 |
| ReduceSum | 规约运算类 | 沿指定维度求和,降低张量维度。 | 🟡 中等 | 需确认axes(沿哪个轴)和keepdims(是否保留维度)的支持情况。 | 确保axes参数是常量。 |
| ReduceMean | 规约运算类 | 沿指定维度求平均,降低张量维度。 | 🟡 中等 | 与ReduceSum类似,关注axes和keepdims。 | 确保axes参数是常量。 |
| Mish | 激活函数 | YOLOv4/v7使用的激活函数:x * tanh(ln(1+exp(x)))。 | 🔴 困难 | 表达式复杂,老NPU无原生支持。拆解为多个基础算子后,计算量大且精度可能下降。 | 算子替换:建议直接替换为SiLU并重新微调模型,是性价比最高的方案。 |
| LayerNorm | 归一化类 | 对特征图沿特征维度进行标准化,在Transformer结构中常见。 | 🔴 困难 | 无法像BN那样被融合消除,需要NPU原生支持;对精度敏感。 | 确认NPU是否支持;若性能不佳,考虑混合精度或写Custom Op。 |
| InstanceNorm | 归一化类 | 对每个样本的每个通道独立做标准化,在风格迁移等任务中常见。 | 🔴 困难 | 与LayerNorm类似,无法简单融合,需要硬件原生支持。 | 确认NPU是否支持;若支持不好,考虑修改网络结构替代。 |
| Shape | 张量操作类 | 获取张量各维度的大小(运行时动态值)。 | 🔴 困难 | 输出为动态值,会引入数据依赖,导致NPU编译器无法进行静态内存分配。 | 消除:尽量在模型导出时固定输入尺寸,避免使用依赖Shape的动态逻辑。 |
| TopK | 选择/过滤类 | 在指定维度上选取数值最大(或最小)的前K个值及索引。 | 🔴 困难 | 涉及排序或部分排序逻辑,NPU的并行矩阵单元不擅长处理串行逻辑。 | 替换/插件:常与Gather配合,整体替换为Custom Op(如NMS插件)。 |
| Exp | 元素级运算类 | 逐元素计算指数(e的x次方)。 | 🔴 困难 | 指数运算对NPU硬件支持要求较高,可能精度不足或速度慢。 | 确认NPU是否支持;若精度不达标,考虑替换或使用查找表(LUT)近似。 |
| Log | 元素级运算类 | 逐元素计算自然对数。 | 🔴 困难 | 与Exp类似,对硬件支持要求高。 | 确认NPU是否支持;若精度不达标,考虑替换或使用查找表(LUT)近似。 |
| Pow | 元素级运算类 | 逐元素计算幂运算(x的y次方)。 | 🔴 困难 | 指数部分须为常量,否则NPU难以处理。 | 确保指数是常量(initializer);若指数是动态张量,基本无法适配。 |
| NonZero | 选择/过滤类 | 找出张量中所有非零元素的索引。 | 💀 极难 | 非零元素数量完全动态,输出形状不确定(变长),NPU基本无法处理。 | 绝对避免:这是NPU的“杀手算子”,任何部署场景下都应极力避免。 |
| Unique | 选择/过滤类 | 对张量进行去重,返回唯一值。 | 💀 极难 | 去重后元素数量动态,输出形状不确定,与NonZero问题性质相同。 | 绝对避免:几乎无法在NPU上直接部署,需在模型设计阶段规避。 |
适配时可能会遇到:
硬件支持某个算子,但参数有限制
某些算子需要拆分成多个更基础的操作
某些算子只能放到 CPU 上跑,不能放到 NPU 上
需要针对芯片特性做特殊实现
custom op 自定义算子处理
见官方文档:
https://github.com/airockchip/rknn-toolkit2/tree/master/rknn-toolkit2/examples/functions/custom_op/gen_custom_onnx_from_pytorch/register_pytorch_op
INT8 量化
把原本用 FP32(float point,浮点数) 计算的模型参数和激活值,压缩成 INT8(8 位整数) 表示。
目的:
更省内存
推理更快
更适合 NPU / 边缘设备
减少模型大小,加速深度学习推理的速度
需要:
一批代表性数据做校准
计算浮点到整数的映射关系
尽量减少精度损失
scale(缩放因子)和 zero_point(零点)估计方法
- Direct Min-Max(直接最大最小值法)
- Percentile / Histogram(百分位 / 直方图法)
- KL 散度(相对熵)
模型转换整体流程(以瑞芯微为例)
https://github.com/airockchip/rknn-toolkit2
训练好的模型(网络结构、权重参数、输入输出定义)+ 校准数据
↓
模型解析
- 模型文件 → 计算图
- 权重文件 → 参数张量
↓
图优化
↓
算子映射
↓
量化
↓
生成 .rknn 文件
模型部署整体流程
设备端加载
↓
输入预处理
↓
rknn runtime 执行
↓
输出张量
↓
后处理—工作重点
↓
最终结果
瑞芯微官方演示流程
https://github.com/airockchip/rknn-toolkit2/tree/master/rknpu2/examples
地瓜BPU模型部署
Model Zoo
https://github.com/D-Robotics/rdk_model_zoo/blob/rdk_x5/README_cn.md
一些训练好的模型示例
原始模型(PyTorch/ONNX)→ 定点量化转换 → 推理运行 → 结果解析 →
c/c++规范
samples/vision/yolov5/runtime/cpp
配置结构体
每个模型必须定义一个独立的配置结构体,用于存放模型初始化和运行时参数;模型类
构造函数 - 模型对象创建
职责:
- 创建模型对象本身;
- 不执行任何重资源操作、不加载模型、不分配 Tensor 内存;
- 为后续显式调用 init() 做准备;
init - 模型资源初始化
职责
- 加载模型(如通过 Horizon DNN API);
- 查询模型元信息;
- 分配输入 / 输出 tensor 所需的内存;
返回值
返回错误码,0 表示成功,非 0 表示失败;
推理相关函数
pre_process – 预处理
职责:
将用户输入数据转换为 runtime 所需格式;
对不支持的 image_format 应及时报错,而非静默失败;
模型的input tensor必须以引用方式传递并写入,便于后续多线程/并行扩展;
infer – 前向推理执行
职责:
创建推理任务;
提交任务到 BPU 调度器;
等待任务完成;
释放任务句柄;
可通过参数配置底层硬件的调度参数,如sched_param,参数设置缺省值;
输入输出tensor需以形参的形式传入,便于后续多线程/并行扩展;
返回值:
当推理出现底层错误时返回错误码,0为无错误,非0为异常;
post_process – 后处理
职责:
将 raw 输出 tensor 转换为业务语义结果;
模型输出tensor需以形参的形式传入,便于后续多线程/并行扩展;
算法工具链
https://developer.d-robotics.cc/api/v1/fileData/x5_doc-v126cn/index.html
地平线BPU架构的计算平台使用的是int8的计算精度
训练后量化(PTQ)Post-Training Quantization 量化感知训练(QAT)Quantization-Aware Training
训练后量化PTQ:使用一批校准数据对训练好的模型进行校准,将训练过的FP32模型直接转换为定点计算的模型,过程中无需对原始模型进行任何训练,只需要对几个超参数进行调整就可以完成量化过程, 且过程简单快速,无需训练。
量化感知训练QAT:是将训练过的模型量化后又再进行重训练。由于定点数值无法用于反向梯度计算,实际操作过程是在某些OP前插入伪量化节点(fake quantization nodes), 用于在训练时获取流经该OP的数据的截断值,便于在部署量化模型时对节点进行量化时进行使用。我们需要在训练中通过不断优化精度来获取最佳的量化参数。
为什么定点数值无法用于反向梯度计算?