所属合集 RK3588 端侧 AI 部署 第 3 / 6 篇
RK3588 端侧 AI 部署(三):ONNX 转 RKNN 与推理验证
这一篇处理端侧部署中最关键、也最容易被低估的环节:把训练模型可靠地变成 RKNN,并证明转换前后做的是同一件事。
上一篇:主机与板端环境搭建
一、不要把“文件生成成功”当成转换成功
正确的验证链路应该是:
PyTorch/TensorFlow 原模型 → ONNX → ONNX Runtime 输出正确 → RKNN FP16/非量化模型输出正确 → RKNN INT8 模型精度可接受 → RK3588 真机输出正确每一步都要使用相同输入保存中间输出。这样如果最终结果错误,可以准确判断是 ONNX 导出、RKNN 图优化、量化、Runtime,还是 C++ 前后处理出了问题。
二、先定义“模型输入输出契约”
在写转换脚本前,把以下信息整理成表:
| 项目 | 示例 | 必须从哪里确认 |
|---|---|---|
| 输入名称 | images | ONNX 图、导出脚本 |
| 输入 shape | [1,3,640,640] | 训练/导出配置 |
| 内存布局 | NCHW 或 NHWC | 模型和 Runtime 查询结果 |
| 数据类型 | FP32、UINT8、INT8 | 模型输入和 RKNN 配置 |
| 通道顺序 | RGB 或 BGR | 训练预处理 |
| resize 方法 | 直接缩放或 letterbox | 训练/验证代码 |
| 像素范围 | 0~255 或 0~1 | 训练预处理 |
| mean/std | 每通道数值 | 训练预处理 |
| 输出名称/数量 | 分类一个、检测多路 | ONNX 图和后处理 |
| 输出语义 | logits、框回归、类别分数 | 模型实现 |
这张表就是 Python、RKNN 和 C++ 之间的接口文档。后续任何一层都不能凭经验猜。
三、导出和检查 ONNX
不同训练框架的导出方法不同,但导出后至少要完成三类检查。
1. ONNX 合法性和 shape 推导
pip install onnx onnxruntimeimport onnxfrom onnx import shape_inference
model_path = "model.onnx"model = onnx.load(model_path)
# 检查图结构是否合法onnx.checker.check_model(model, full_check=True)
# 补全可以静态推导的中间张量 shapeinferred = shape_inference.infer_shapes(model, strict_mode=True)onnx.save(inferred, "model_inferred.onnx")
print("ONNX check passed")官方说明可参考 onnx.checker 和 shape inference。动态 Reshape 等结构可能无法完整推导 shape,这不一定代表模型错误。
2. 用 Netron 看图
pip install netronnetron model.onnx重点检查:
- 输入输出名称、数量、shape 和 dtype;
- 是否把 NMS、解码等不适合量化的后处理留在图中;
- 是否出现大量
Transpose、Reshape; - 动态维是否符合预期;
- 检测模型多尺度输出的顺序。
Netron 是结构查看器,不是精度验证器。图能打开不代表推理结果正确。
3. 与原框架比较输出
同一输入经过完全相同预处理后,分别运行原模型和 ONNX Runtime,比较:
- 输出数量和 shape;
- 最大绝对误差、平均绝对误差;
- 余弦相似度;
- 最终任务结果,例如 Top-1、检测框或 mAP。
不要只比较最终类别。两个模型可能恰好得到同一 Top-1,但中间数值已经明显偏移。
四、理解 mean、std 和像素范围
RKNN 配置常见:
rknn.config( mean_values=[[123.675, 116.28, 103.53]], std_values=[[58.395, 58.395, 58.395]], target_platform="rk3588",)其核心语义是复现:
x' = (x - mean) / std但参数是否正确还取决于输入是 0~255 还是 0~1。例如训练代码常见:
transforms.Normalize( mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225],)如果 RKNN 输入直接使用 0~255 的 UINT8 图片,对应参数常写成:
mean = [0.485, 0.456, 0.406] × 255std = [0.229, 0.224, 0.225] × 255还要确认顺序是 RGB,而 OpenCV imread() 默认返回 BGR。
最常见的重复预处理错误
如果 mean_values/std_values 已固化进 RKNN 模型,C++ 又手动执行同一遍归一化,输入会被处理两次。相反,如果转换时没有配置,应用也没处理,模型收到的数值范围也会错误。
最稳妥的方法是把预处理拆成明确步骤并逐项记录:
读取 BGR→ BGR 转 RGB→ resize/letterbox→ 是否除以 255→ 是否减 mean/除 std→ HWC/NHWC/NCHW→ dtype五、编写可复现的 RKNN 转换脚本
下面是一个适合继续扩展的 ONNX 转 RKNN 骨架:
from pathlib import Pathfrom rknn.api import RKNN
ONNX_MODEL = Path("model.onnx")RKNN_MODEL = Path("model-rk3588-int8.rknn")DATASET = Path("dataset.txt")
rknn = RKNN(verbose=True, verbose_file="convert.log")
try: ret = rknn.config( target_platform="rk3588", mean_values=[[123.675, 116.28, 103.53]], std_values=[[58.395, 58.395, 58.395]], ) if ret != 0: raise RuntimeError(f"config failed: {ret}")
ret = rknn.load_onnx(model=str(ONNX_MODEL)) if ret != 0: raise RuntimeError(f"load_onnx failed: {ret}")
ret = rknn.build( do_quantization=True, dataset=str(DATASET), ) if ret != 0: raise RuntimeError(f"build failed: {ret}")
ret = rknn.export_rknn(str(RKNN_MODEL)) if ret != 0: raise RuntimeError(f"export_rknn failed: {ret}")
print(f"saved: {RKNN_MODEL}")finally: rknn.release()
config()的参数、支持的量化方法和 API 返回值会随版本变化。使用前应打开当前 release 的 Toolkit2 API 文档,并以本机接口为准。
量化校准集 dataset.txt
文本中每行通常写一个校准输入路径:
./calibration/0001.jpg./calibration/0002.jpg./calibration/0003.jpg校准集不是训练集随便抽几张,而应覆盖真实部署中的:
- 光照、背景、距离和目标大小;
- 不同摄像头或图像来源;
- 正常、边界和困难样本;
- 各类别和常见负样本。
样本数量要结合模型和量化算法测试。代表性通常比机械追求数量更重要。
六、命令行转换工具 rknn_convert
标准模型可使用 YAML 配置配合命令行:
python -m rknn.api.rknn_convert -hpython -m rknn.api.rknn_convert \ -t rk3588 \ -i ./model_config.yml \ -o ./output它内部仍执行 config → load → build → export。批量模型和统一流水线适合 YAML;需要自定义检查、自动对比输出时,Python 脚本更灵活。不同版本参数可能变化,所以先执行 -h,不要盲抄旧命令。
七、模拟器和真实 RK3588 推理
1. PC 模拟器
from rknn.api import RKNN
rknn = RKNN(verbose=True)rknn.load_rknn("model-rk3588-int8.rknn")rknn.init_runtime()outputs = rknn.inference(inputs=[input_tensor], data_format=["nhwc"])rknn.release()模拟器适合快速检查输入、输出和后处理,但不代表真实 NPU 的性能,也不能替代板端精度验证。
2. Toolkit2 连板调用 NPU
rknn.init_runtime( target="rk3588", # device_id="...", # 多设备时指定)outputs = rknn.inference(inputs=[input_tensor], data_format=["nhwc"])需要 ADB/rknn_server、Runtime 和驱动正常。日志中要核对:目标平台、Toolkit、Runtime 和驱动版本。
3. 板端 Lite2
from rknnlite.api import RKNNLite
runtime = RKNNLite(verbose=True)runtime.load_rknn("model-rk3588-int8.rknn")runtime.init_runtime(core_mask=RKNNLite.NPU_CORE_AUTO)outputs = runtime.inference(inputs=[input_tensor], data_format=["nhwc"])runtime.release()Lite2 适合把 Python 验证脚本直接搬到板端。最终高性能项目通常仍会进入 C/C++。
八、怎样比较不同阶段的输出
import numpy as np
def cosine_similarity(a, b): a = np.asarray(a, dtype=np.float64).reshape(-1) b = np.asarray(b, dtype=np.float64).reshape(-1) denom = np.linalg.norm(a) * np.linalg.norm(b) return float(np.dot(a, b) / denom) if denom else 0.0
for i, (golden, test) in enumerate(zip(golden_outputs, rknn_outputs)): print( i, "shape:", np.shape(test), "max_abs:", np.max(np.abs(golden - test)), "mean_abs:", np.mean(np.abs(golden - test)), "cosine:", cosine_similarity(golden, test), )余弦相似度只反映张量方向是否接近,不等同于 Accuracy/mAP。最终仍要跑完整验证集和真实业务数据。
九、转换日志应该看什么
保留 convert.log,重点关注:
- 不支持或回退 CPU 的算子;
- 输入 dtype/layout 的自动转换;
- 算子融合、常量折叠和图优化;
- 量化异常、离群值和精度警告;
- 输出节点顺序变化;
- 目标平台与模型版本。
遇到错误时按以下顺序排查:
ONNX 是否合法→ 输入输出契约是否正确→ 算子是否被当前 Toolkit 支持→ 预处理是否重复/遗漏→ 校准集是否有效→ Toolkit/Runtime/驱动是否匹配→ C++ 后处理是否假设了错误的输出顺序十、本篇实践验收
- 能写出模型输入输出契约;
- ONNX 通过 checker,并能用 Netron 和 ONNX Runtime 验证;
- 能转换 FP16/非量化和 INT8 RKNN;
- 能在模拟器和 RK3588 真机使用同一输入对比输出;
- 保存了脚本、模型哈希、转换日志、量化集清单和版本信息。
Some information may be outdated