
ONNX 与 RKNN 详解从模型格式到 RK3568 部署实战做边缘计算和嵌入式 AI 的同学应该都被同一个问题折磨过模型在电脑上跑得好好的一搬到板子上就各种报错要不算子不支持要不精度对不上要不 NPU 压根不干活纯靠 CPU 硬扛。我自己在 RK3568 上折腾部署也踩了一整年的坑从最开始的 PyTorch 模型一路导出、转换、量化、调试到最终在板子上跑出稳定帧率整个过程走下来最大的体会是部署这事七分在格式转换三分在板端调试。所以这篇我重点把 ONNX 和 RKNN 这两个格式聊透再带大家完整走一遍 RK3568 的部署流程全是实际操作中总结出来的经验不是那种照抄文档的搬运文。这篇内容适合谁如果你的工作涉及边缘计算、NPU 推理、嵌入式 Linux 开发或者你正准备把 YOLO、OCR、分类模型这类视觉模型搬到瑞芯微平台上那这篇文章基本就是为你准备的。我会把 ONNX 这个“通用交换格式”为什么存在、RKNN 这个“NPU 专属格式”是怎么设计出来的讲清楚再给出一套可以直接照着做的 RK3568 部署方案从模型转换到板端调用一站式搞定。1. 为什么部署要先搞定模型格式从训练框架到 NPU 之间的“翻译官”先说一个很多人忽略的事实PyTorch 训练出来的模型跟 NPU 能直接执行的模型中间隔着一条巨大的鸿沟。训练框架关心的是怎么高效算梯度、怎么灵活搭建网络结构而 NPU 关心的是怎么把算子在专用硬件上跑得最快、怎么让数据在有限的内存带宽里流动得最顺畅。这两者的诉求天然冲突所以必然需要一个中间层来做转换。1.1 ONNX通用交换格式是怎么诞生的ONNXOpen Neural Network Exchange最初是微软和 Facebook 联合推出来的目标很纯粹让模型能在不同框架之间自由流动。你拿 PyTorch 训好的模型导出成 ONNX再导入到 TensorRT、OpenVINO、ONNX Runtime 这些推理引擎里跑完全没问题。打个比方ONNX 就像是模型界的通用语言不管你原来是说法语的 PyTorch 还是说德语的 TensorFlow统一翻译成英语大家就都能交流了。但注意ONNX 本身并不是一个“部署格式”它更多是一个“交换格式”。它的设计目标是表达完整的计算图而不是追求极致的运行效率。所以你会看到ONNX 模型通常包含大量冗余的算子组合比如 Conv 后面的 BatchNorm 在推理时可以融合成单个 Conv但 ONNX 里它俩往往是分开的两个节点。也就是说ONNX 解决的是“能不能转换”的问题而不是“转换后快不快”的问题。1.2 RKNN瑞芯微 NPU 专属格式的设计逻辑RKNN 是瑞芯微Rockchip专门为自家 NPU 设计的模型格式全称叫 Rockchip Neural Network后缀通常是.rknn。它跟 ONNX 最大的区别在于RKNN 格式的文件里不光包含了网络结构还包含了针对特定 NPU 硬件优化过的算子调度信息、权重布局、量化参数等。RK3568 上的 NPU 算力是 0.8 TOPSINT8 精度虽然不是顶尖水平但在同价位的 SoC 里性价比相当能打。NPU 要高效运行必须把模型的算子映射到硬件单元上权重还得按硬件要求的排列方式重新排布这个过程只有 RKNN 格式能承载。所以你不能直接把 ONNX 丢给 RK3568 的 NPU 跑必须先经过 rknn-toolkit2 工具链转换。1.3 格式转换链路全景整个部署链路可以概括成一句话训练框架 → ONNX → RKNN → NPU 运行。中间每一步都有各自的坑位别以为导出了 ONNX 就万事大吉了。我见过不少新手把 PyTorch 模型导出成 ONNX 后直接拿去转 RKNN结果在转换环节爆出一堆算子不支持的报错源头其实就是导出 ONNX 的时候埋下了雷。所以正确的做法是每一步都验证清楚再往下走。导出 ONNX 后用 ONNX Runtime 跑一遍对比 PyTorch 输出是否一致转成 RKNN 后先用模拟器评估精度和性能确认没问题再烧到板子上。这个多级验证的习惯能帮你省掉至少一半的排查时间。2. ONNX 模型核心细节解析搞懂算子你就掌握了转换的主动权很多人觉得 ONNX 就是一个文件格式直接torch.onnx.export导出完事根本不关心里面是什么。等转换 RKNN 报错的时候再一个个算子去查效率极低。我建议你花半小时把 ONNX 的结构看明白后面省下的时间远超这半小时。2.1 ONNX 文件结构拆解ONNX 文件本质上是一个 protobuf 序列化的文件核心包含三部分计算图Graph描述了数据从输入到输出的完整流动过程算子节点Node图中的每个操作比如 Conv、Relu、Add、Concat、Resize 等权重与常量InitializerConv 的卷积核权重、BN 的均值和方差等参数以张量形式存储在模型内部你可以用 Netron 打开 ONNX 文件直观地看到整个网络结构。对于排查算子支持情况、检查输入输出 shape、确认模型分支结构Netron 就是最好的工具没有之一。我在 RKNN 转换报错时第一件事永远是打开 Netron 看对应算子周围连接了哪些节点再决定是改模型结构还是换算子实现。2.2 动态 shape 与静态 shape 的取舍ONNX 导出时可以设置动态维度比如 batch 维度为动态也可以全部固定。在 RK3568 部署场景下我的强烈建议是尽量用固定 shape。原因很简单NPU 擅长处理固定尺寸的输入内存预分配更容易算子融合优化空间更大。动态 shape 虽然灵活但 NPU 上往往需要额外的内存拷贝或算子重编译性能会打折扣。我曾经有个项目输入尺寸设计成动态的 (1, 3, -1, -1)转 RKNN 时反复报 shape 推导错误最后改成固定 640x640 后一次通过推理速度还提升了 15% 左右。如果实际场景确实需要动态分辨率可以考虑在预处理阶段做 letterbox 填充到固定尺寸而不是把动态交给 NPU。2.3 int8 量化基础为什么边缘设备绕不开量化RK3568 NPU 的 0.8 TOPS 算力是基于 INT8 计算的FP16 算力只有一半不到FP32 基本只能靠 CPU 硬跑。所以想在 RK3568 上获得最优性能int8 量化几乎是必选项。量化的本质是用 8 位整数来近似表示 32 位浮点数核心是确定一个缩放尺度scale和偏移zero point把浮点数值域映射到 [-128, 127] 或 [0, 255] 的整数范围。rknn-toolkit2 支持两种量化方式训练后量化PTQ和量化感知训练QAT。对于大多数场景PTQ 用几百张代表性图片做校准就能达到不错的精度实现成本低是首选方案。QAT 精度更高但需要修改训练代码成本高只有在 PTQ 精度损失无法接受时才考虑。3. rknn-toolkit2 转换全流程实操环境、脚本、量化一个都不能少工具链的版本一定要选对这是 RKNN 部署里最容易踩的坑。RK3568 对应的工具链是 rknn-toolkit2注意不是 rknn-toolkit 1.x两者针对的芯片平台完全不同。我用的是 1.5.0 版本配合板端的 librknnrt 1.5.0整体稳定推荐新区块链直接用这个组合。3.1 环境准备与安装rknn-toolkit2 提供 Python 包支持 Ubuntu 18.04/20.04 x86_64 环境。我的建议是单独创建一个虚拟环境避免跟其他深度学习库的依赖冲突。安装方式很简单# 创建虚拟环境 conda create -n rknn python3.8 conda activate rknn # 安装 rknn-toolkit2从 GitHub 仓库获取 whl 包 pip install rknn_toolkit2-1.5.0-cp38-cp38-linux_x86_64.whl # 验证安装 python -c from rknn.api import RKNN; print(rknn-toolkit2 installed)注意rknn-toolkit2 的依赖里有 numpy、opencv-python、onnx 等如果服务器本来就装了不同版本的这些库建议用 virtualenv 或者 conda 隔离我实测下来混装很容易出现奇奇怪怪的 ctypes 报错。3.2 编写转换脚本并理解关键参数下面这个脚本是我项目里一直在用的最小可运行版本它的完整链路是读入 ONNX → 设置输入预处理 → 量化 → 生成 RKNN 模型 → 用模拟器评估输出。每一步都有详细注释你可以直接复制改路径就能用from rknn.api import RKNN # 1. 创建 RKNN 对象verboseTrue 会打详细信息排查问题时建议打开 rknn RKNN(verboseTrue) # 2. 配置模型输入预处理 # mean_values 和 std_values 必须与训练时保持一致不然精度一定会崩 # target_platform 指明目标芯片这里是 rk3568 rknn.config( mean_values[[0, 0, 0]], std_values[[255, 255, 255]], target_platformrk3568 ) # 3. 加载 ONNX 模型 ret rknn.load_onnx(model./yolo11s.onnx) assert ret 0, ONNX 模型加载失败检查路径和模型格式 # 4. 构建 RKNN 模型do_quantizationTrue 表示进行 int8 量化 # dataset.txt 里每行写一张量化校准图片的路径200张左右比较合适 ret rknn.build(do_quantizationTrue, dataset./dataset.txt) assert ret 0, 模型构建失败 # 5. 导出 RKNN 模型文件 ret rknn.export_rknn(./yolo11s.rknn) assert ret 0, RKNN 模型导出失败 # 6. 用模拟器跑一遍确认精度和中间输出是否正常 rknn.release()这段脚本里最容易出问题的是rknn.config中的 mean_values 和 std_values。这两个参数定义了输入图像的归一化方式必须跟你训练时用的一致。我用 PyTorch 训练时正常做法是x / 255归一化到 0~1所以这里就设置 mean[0,0,0], std[255,255,255]。如果你训练时用了 ImageNet 的 mean[0.485, 0.456, 0.406] 和 std[0.229, 0.224, 0.225]那这里也要对应设置否则量化后的模型输出会跑偏得非常离谱看起来像“精度崩了”其实只是预处理没对齐。3.3 量化数据集的准备方法量化数据集的准备是 RKNN 转换中最容易被忽视的环节。dataset.txt里的每一行是量化校准用的图片路径这些图片用于统计激活值的分布决定量化的 scale 和 zero point。如果这张表的图片太少、内容太单一比如全是纯黑色的背景量化后的模型对真实场景的图片输出会明显偏差。我的经验是从训练集和验证集中随机抽取 200~500 张图片尽量覆盖不同亮度、不同目标大小、不同背景。图片分辨率不需要跟模型输入完全一致工具会自动做缩放处理。另外图片格式最好是 jpg 或 png不要用带 alpha 通道的图片避免通道数不一致导致报错。如果你发现量化后精度掉得厉害可以尝试多准备一些与真实场景分布一致的图片或者调整量化策略rknn-toolkit2 从 1.4.0 版本开始支持quantized_algorithm、quantized_method这些高级量化参数默认算法不够理想时可以手动调。4. RK3568 部署硬件环境与 NPU 使用要点模型转好了接下来就是把它真正放到板子上跑起来。这部分内容比较杂我挑重点讲硬件规格、跑模型的方式、还有 RK3568 调试中绕不开的设备树问题。4.1 RK3568 与 RK3588 的规格对比很多人在选型时会纠结 RK3568 还是 RK3588这里简单列个对比表方便你对照自己的项目需求项目RK3568RK3588CPU4 核 Cortex-A55主频 2.0GHz8 核4 个 Cortex-A76 4 个 Cortex-A55NPU 算力0.8 TOPS INT86 TOPS INT8内存接口LPDDR4/LPDDR4XLPDDR4X/LPDDR5视频编解码4K 60fps 解码1080P 编码8K 30fps 解码8K 30fps 编码定位性价比、中低端边缘设备旗舰、多路视频 / 复杂模型如果你的项目做的是单路视频流目标检测、OCR、简单分类RK3568 绝对够用性价比极高。如果要做多路视频分析、大模型推理那就老老实实选 RK3588。在 RK3568 上做部署时目标帧率一般定位在 10~30 FPS 左右视模型复杂度而定如果想跑更大的模型就得对模型结构做裁剪或者深度优化。4.2 板端运行时 librknnrt 与推理接口模型部署到板端后还需要安装 librknnrt.so 运行时库以及 rknn-toolkit2 对应的 Python API板端叫 rknn-toolkit-lite或者在 C 环境通过 C API 调用。我们的项目大多用 C 写业务逻辑所以这里给一个 C API 的最小示例仅供参考#include rknn_api.h // 加载模型 FILE *fp fopen(yolo11s.rknn, rb); fseek(fp, 0, SEEK_END); size_t model_size ftell(fp); fseek(fp, 0, SEEK_SET); void *model_data malloc(model_size); fread(model_data, 1, model_size, fp); fclose(fp); // 初始化 RKNN 上下文 rknn_context ctx; rknn_init(ctx, model_data, model_size, 0, NULL); // 查询输入输出信息 rknn_input_output_num io_num; rknn_query(ctx, RKNN_QUERY_IN_OUT_NUM, io_num, sizeof(io_num)); // 设置输入 rknn_input inputs[1]; inputs[0].index 0; inputs[0].type RKNN_TENSOR_UINT8; inputs[0].size 640 * 640 * 3; inputs[0].fmt RKNN_TENSOR_NHWC; inputs[0].buf img_data; rknn_inputs_set(ctx, 1, inputs); // 推理 rknn_run(ctx, NULL); // 获取输出 rknn_output outputs[1]; outputs[0].want_float 1; rknn_outputs_get(ctx, 1, outputs, NULL); // 后处理 释放 rknn_outputs_release(ctx, 1, outputs); rknn_destroy(ctx);需要注意输入数据的摆放格式NHWC 还是 NCHW必须跟转换时的一致。我默认使用 NHWC因为摄像头采集的帧基本都是 HWC 排列省去一次通道重排推理延迟能降低 1~2ms。4.3 设备树调试与系统适配RK3568 的板端调试绕不开设备树Device Tree如果用的是正点原子、野火这类开发板官方出厂系统一般已经适配好大部分外设。但如果你是自己画的板子或者需要外接摄像头比如 OV5695、OV8858设备树就得自己调。设备树调试中最典型的问题是摄像头 I2C 地址冲突、MIPI 通道配置错误、时钟频率不匹配。我调试 OV5695 时踩过一个大坑MIPI 虚拟通道没配对导致画面花屏。检查了一遍发现是设备树中>import torch from ultralytics import YOLO # 加载训练好的模型 model YOLO(yolo11s.pt) # 切换到 eval 模式固定网络权重 model.model.eval() # 构造一个固定尺寸的虚拟输入ONNX 导出时用它来追踪计算图 dummy_input torch.randn(1, 3, 640, 640) # 导出 ONNX 文件 torch.onnx.export( model.model, dummy_input, yolo11s.onnx, opset_version12, input_names[images], output_names[output0], dynamic_axesNone # 固定 shape不设动态维度 ) print(ONNX 导出完成)几个关键点opset_version 建议用 12 或 13瑞芯微工具链对这两个版本的支持最成熟dynamic_axes 这里设置为 None也就是固定输入尺寸为 (1, 3, 640, 640)原因在 2.2 节已经说过。导出完以后用 Netron 打开看一眼确认输出节点的 name 和 shape 是否符合预期这一步能提前发现网络被错误折叠或冗余节点残留的问题。5.2 ONNX 转 RKNN从转换到量化全流程导出 ONNX 后进入 RKNN 转换环节。按照 3.2 节的模板脚本我加上了量化校准和模拟器验证形成一个完整版from rknn.api import RKNN import numpy as np rknn RKNN(verboseTrue) # 配置输入预处理YOLO 模型训练时通常用 0~1 归一化 rknn.config(mean_values[[0, 0, 0]], std_values[[255, 255, 255]], target_platformrk3568) # 加载 ONNX 模型 ret rknn.load_onnx(model./yolo11s.onnx) assert ret 0 # 构建 RKNN 模型并量化 ret rknn.build(do_quantizationTrue, dataset./dataset.txt) assert ret 0 # 导出 RKNN 文件 ret rknn.export_rknn(./yolo11s.rknn) assert ret 0 # 用模拟器验证输出 # 这里构造一张真实图片作为输入检查输出 shape 是否合理 img cv2.imread(test.jpg) img cv2.resize(img, (640, 640)) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # 新建 RKNN 对象加载模型做模拟推理 rknn_sim RKNN() rknn_sim.load_rknn(./yolo11s.rknn) rknn_sim.init_runtime() outputs rknn_sim.inference(inputs[img]) print(模拟器输出:, outputs[0].shape) rknn_sim.release() rknn.release()一个常见的坑量化数据集里的图片应该和实际推理场景尽可能一致。比如你的产品是工业质检目标都是固定在流水线上的零件那校准图片就应该用流水线的实拍图而不是训练集里的网络图片。如果校准图片和实际场景差异太大量化后的模型就会对真实场景“失明”。5.3 板端推理实测与性能优化把生成的 yolo11s.rknn 拷贝到 RK3568 板子上用 C 或者 Python 调用推理。实测下来640x640 输入的 YOLO11s在 RK3568 上 int8 量化后的 NPU 推理时间大约在 30~45ms换算下来约 22~33 FPS这个性能在同级别芯片里表现算相当不错的。如果实测帧率不达标可以从三个方向优化输入尺寸降级。比如从 640x640 降到 512x512推理时间可能直接减少一半左右代价是精度小幅下降需要实验评估。后处理移到 NPU 之外。RKNN 的推理输出是原始张量NMS 等后处理如果放在 NPU 计算图上会增加耗时建议在 CPU 上做后处理只把模型的骨干检测头部署到 NPU 上。打开 rknn.config 里的性能优化选项。比如开启enable_cfg_preprocess、合理设置quantized_dtype为asymmetric_quantized-8可以提高部分算子的效率。// 板端推理时间测试示例 struct timespec start, end; clock_gettime(CLOCK_MONOTONIC, start); rknn_run(ctx, NULL); rknn_outputs_get(ctx, 1, outputs, NULL); clock_gettime(CLOCK_MONOTONIC, end); double cost_ms (end.tv_sec - start.tv_sec) * 1000.0 (end.tv_nsec - start.tv_nsec) / 1000000.0; printf(NPU inference time: %.2f ms\n, cost_ms);一般来说NPU 推理时间比较稳定不会像 CPU 那样受负载影响明显。如果你发现推理时间波动很大首先检查板子上的 CPU 频率有没有被降频、内存是否被其他进程占用这些外部因素对 NPU 的调用延迟影响很大。6. 常见问题与排查技巧实录把踩过的坑一次性讲完最后用一个专门的章节整理我在 RK3568 部署过程中遇到的高频问题。这些问题在官方文档里未必有详细描述但实际项目里几乎绕不开。6.1 算子不支持与版本兼容问题RKNN 工具链对 ONNX 算子的支持不是 100% 的尤其是 Transformer 结构里的 LayerNorm、Gelu、矩阵乘法相关的新算子老版本工具链可能直接报Unsupported operator之类的错误。我的处理顺序是升级 rknn-toolkit2 到最新版本通常能解决 80% 的算子支持问题如果还是不支持看报错信息中具体是哪个算子回到 PyTorch 侧把该算子替换为等价实现实在不行把该算子的计算留在 CPU 上通过 RKNN 的分段部署能力分割成多个模型实现实际项目中修改模型结构的优先级高于升级工具链因为工具链的迭代速度往往赶不上模型结构的发展速度。6.2 量化后精度显著下降怎么办量化精度崩塌的原因通常有三个预处理参数不匹配mean/std 设置错误校准数据集分布与真实场景差异过大模型本身对量化敏感尤其是检测小目标、分割任务排查时先做“无量化对照实验”把do_quantization设为 False转一个 fp16 或 fp32 版模型在板端对比测试精度。如果不量化的模型精度没问题量化后崩了那问题就锁定在量化环节如果未量化就崩那问题在 ONNX 导出或预处理阶段。这个对照思路能帮你快速定位问题所在。如果确认是量化精度问题优先尝试增加校准数据量或使用 QAT 重新训练。部分模型对量化敏感是因为网络中存在大数值范围的中间激活可以在导出 ONNX 前对模型添加 clip 或 relu 约束把激活值限制在合理范围内。6.3 推理性能瓶颈定位NPU 推理时间正常但整体帧率上不去那瓶颈大概率在前后处理。比如图像缩放、颜色空间转换、NMS 等操作如果在 CPU 上低效实现很容易拖后腿。我这里有一个性能开销分配的经验法则模型推理NPU约 30~45ms图像预处理CPU约 5~10ms后处理 NMSCPU约 3~8ms如果预处理超过 15ms就该检查代码是不是在逐像素循环操作了应该用 OpenCV 的矩阵操作或者 RGA 硬件加速。RK3568 内置 RGA 模块专门用来做缩放和格式转换使用 RGA 可以把 resize 从 5ms 降到不到 1ms强烈推荐。6.4 设备树、系统与周边适配的几个陷阱除了模型本身板级的问题也很常见。我在 RK3568 上调过不少外设把最容易踩的坑列成一张速查表方便你排查现象大概率原因排查思路NPU 调用失败 / 空闲板端 rknn_server 未启动或版本不匹配检查 librknnrt 版本与工具链是否一致画面花屏 / 颜色异常MIPI 虚拟通道或>