4. 模型准备与格式转换基础

在 i.MX 系列芯片上部署 NPU 推理,第一步并不是写 C++ 推理代码,而是把训练框架里的模型“翻译”成 NPU 编译器能够理解的形式。i.MX 的 NPU 工具链(如 eIQ 中的 Neutron 或 Ethos-U 相关流程)通常以 ONNX 或 TensorFlow Lite 作为输入,再经过图优化、量化、算子映射,最终生成可在 NPU 上执行的二进制。因此,模型准备阶段的核心任务可以概括为三件事:正确导出、检查算子兼容性、理解中间表示(IR)。

一、三种框架的导出路径

PyTorch 模型导出 ONNX 是最常见的路径。使用 torch.onnx.export 时,必须提供一组与真实输入形状一致的 dummy input,并显式指定 opset_version。opset 版本决定了算子集合的语义,i.MX 工具链通常对 opset 11~17 支持较好,过低会缺少某些算子,过高则可能引入尚未被解析的新算子。示例:

import torch
torch.onnx.export(
    model,                      # 已 eval() 的模型
    dummy_input,                # shape=(1,3,224,224)
    "mobilenetv2.onnx",
    opset_version=13,
    input_names=["input"],
    output_names=["output"],
    dynamic_axes={"input": {0: "batch"}}
)

TensorFlow 模型则通常先保存为 SavedModel,再用 tf2onnx 转换,或直接导出 TFLite。若目标是 i.MX NPU,TFLite 路径往往更直接,因为量化信息(scale、zero_point)在 TFLite 中表达得更完整。PyTorch 若走 TFLite 路径,需要先转 ONNX,再经 onnx2tf 或直接使用工具链提供的转换器。

导出前务必调用 model.eval(),并关闭 dropout、BatchNorm 的训练态行为,否则导出的计算图会包含训练专用节点,导致后续算子检查失败。

二、算子兼容性检查

导出成功不等于能在 NPU 上跑。NPU 的算子集是有限的,常见支持的有 Conv、DepthwiseConv、Add、Mul、Relu、Relu6、MaxPool、AveragePool、Concat、Reshape、Transpose、Softmax 等;而像 GridSample、NonMaxSuppression、动态 shape 的 Resize、自定义算子,往往只能回退到 CPU。检查方法有两类:一是用 Netron 打开 ONNX/TFLite 文件,逐节点查看 op_type;二是用工具链自带的算子检查命令,例如:

# 示例:使用 Neutron 转换器做 dry-run 检查
neutron-converter --input mobilenetv2.onnx \
                  --output mobilenetv2.npu \
                  --check-only

输出中会列出 Unsupported ops 与 Fallback to CPU 的节点。若回退节点过多,推理时会出现频繁的 NPU/CPU 切换,带宽和延迟都会明显上升。此时应优先考虑用等价算子替换,例如把 HardSwish 拆成 HardSigmoid + Mul,或把不支持的 Resize 改为固定 scale 的 Upsample。

三、中间表示(IR)到底表示什么

IR 是框架与硬件之间的“合同”。以 ONNX 为例,它用 GraphProto 描述计算图,节点是 NodeProto,张量是 ValueInfoProto,权重放在 TensorProto 中。每个节点只声明算子类型、输入名、输出名和属性,不关心具体实现。NPU 编译器拿到 IR 后,会做常量折叠、算子融合、布局转换(NCHW→NHWC)、量化插入,最终映射到硬件指令。

理解 IR 的一个实用价值是:当转换报错时,你能定位到是哪个节点、哪个属性、哪个 shape 出了问题。例如报错 Unsupported attribute 'auto_pad',说明该 Conv 节点使用了 SAME_UPPER,而 NPU 只接受显式 padding。此时可以在导出时固定输入尺寸,或在前处理中手动 pad。

模型准备与格式转换:从训练框架到 NPU 可执行 IR 训练框架 PyTorch (.pt) TensorFlow SavedModel Keras / TFLite 导出与中间表示 ONNX GraphProto TFLite FlatBuffer opset / 量化参数 NPU 编译器处理 算子映射 布局转换 常量折叠 量化插入 输出:.npu / .tflite(NPU 可执行) 算子兼容性检查:哪些节点能上 NPU,哪些回退 CPU NPU 原生支持 Conv / DepthwiseConv Add / Mul / Relu / Relu6 MaxPool / AvgPool / Concat 回退 CPU GridSample / NMS 动态 shape Resize 自定义算子 / 控制流 处理建议 等价算子替换 固定输入尺寸 前处理手动 pad

四、一个可复用的检查清单

把上述流程固化成检查表,可以在每次换模型时快速过一遍:

检查项判断依据常见处理
输入 shape 是否固定ONNX 中 input 的 dim 是否为具体数值导出时去掉 dynamic_axes,或固定 batch=1
opset 版本是否在工具链支持区间(如 11~17)重新导出并指定 opset_version
不支持的算子check-only 输出中的 Unsupported ops替换为等价算子或接受 CPU 回退
量化信息TFLite 中是否有 scale/zero_point使用训练后量化或量化感知训练
布局Conv 输入是 NCHW 还是 NHWC由编译器自动转换,但需确认无额外 Transpose
不要忽略“回退 CPU”的代价。一个 224×224 的 MobileNetV2,若中间有 3~4 个节点回退,端到端延迟可能从 8 ms 上升到 25 ms 以上,因为每次切换都要做张量拷贝和同步。优先在模型层面消除回退,而不是在推理代码里硬扛。

最后要建立的一个观念是:IR 不是“中间文件”,而是可检查、可修改、可验证的工程对象。你可以用 onnxsim 做常量折叠,用 onnxruntime 做逐节点比对,用 Netron 做可视化审查。把 IR 当作调试对象,模型准备阶段的大部分问题都能在写推理代码之前解决。