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。
四、一个可复用的检查清单
把上述流程固化成检查表,可以在每次换模型时快速过一遍:
| 检查项 | 判断依据 | 常见处理 |
|---|---|---|
| 输入 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 |
最后要建立的一个观念是:IR 不是“中间文件”,而是可检查、可修改、可验证的工程对象。你可以用 onnxsim 做常量折叠,用 onnxruntime 做逐节点比对,用 Netron 做可视化审查。把 IR 当作调试对象,模型准备阶段的大部分问题都能在写推理代码之前解决。