1. 背景与目的
在边缘 AI 项目中,模型训练(PyTorch)与最终部署(RK35xx 系列 NPU 板卡)之间隔着一条"格式鸿沟":RKNN 工具链不能直接读取 *.pth ,必须经过 ONNX 中间表示才能进入 Rockchip 的专有格式。这条链路看似只有两个转换命令,真正决定成败的却是预处理一致性、量化标定、运行库版本配套这些细节。
本教程以一个戴帽 / 未戴帽(hut / no_hut)二分类 ResNet18 模型为完整示例,带你一比一走通:
PyTorch *.pth ──► ① torch.onnx.export ──► *.onnx ──► ② rknn-toolkit2 ──► *.rknn ──► ③ RK3588 板端推理

图1 完整链路:三步转换 + 一条一致性红线(预处理)
阅读本文你将获得:
- 一份可独立复现的示例工程(文件命名与结构见目录章节);
- 每一步的完整代码、真实运行的输出文本与实测版本号;
- 三个高频"翻车点"(top-K 越界、运行库版本不匹配、量化精度下降)的定位思路;
- 全部异常的分析与解决方案(见文末『附录:异常记录』)。
2. 目录
先建立一份通用示例工程 (我的资源里面有,板子官方自带的示例工程也有)。它不依赖任何个人环境与真实路径,你可以整体拷贝到任意目录使用,只需把示例模型换成本文示例的 hat_best.pth(或你自己的权重文件)。
text
model_zoo_example/
├── train.py # 训练脚本, 产出 *.pth(本文略, 直接提供已训练权重)
├── export_onnx.py # 第1步: pth -> onnx
├── convert_rknn.py # 第2步: onnx -> rknn(在 Linux 服务器上运行)
├── dataset.txt # 量化标定图片清单(每行一个图片路径)
├── model/
│ ├── hat_best.pth # 训练产物(二分类, 可自己训练产出)
│ ├── hat_best.onnx # 中间产物
│ ├── hat_best.rknn # 最终产物
│ ├── synset.txt # 类别标签(一行一个类)
│ └── calib/ # 标定图片集(从验证集抽出)
└── board_demo/ # 板端 C++ demo(复用官方 rknn_model_zoo/resnet 示例)
├── main.cc # topk 参数需按类别数修正
└── CMakeLists.txt

图2 目录结构:训练/中间/最终产物与脚本分离,标定图独立成目录
3. 环境与数据集准备
3.1 三个环节、三套环境
链路跨越三个执行环境,版本号是复现的第一保证。以下均为本文实测通过的版本组合:
| 环节 | 执行环境 | 实测版本 |
|---|---|---|
| ① 权重导出 | 任一台装有 PyTorch 的办公 PC(本文为 Windows + Python 3.13.9) | Python 3.13.9、PyTorch 2.13.0+cpu、torchvision 0.28.0+cpu、onnx 1.22.0、onnxruntime 1.28.0 |
| ② RKNN 转换 | Linux 服务器 + conda 虚拟环境 | Python 3.8.20、rknn-toolkit2 2.3.2、onnx 1.17.0、onnxruntime 1.19.2、numpy 1.24.4 |
| ③ 板端推理 | RK3588 板卡(aarch64) | Ubuntu 24.04 LTS、librknnrt.so(与 rknn-toolkit2 2.3.2 配套)、RKNPU 驱动 v0.9.8 |

图3 三步链路各自依赖的工具与实测版本
说明两点:
- RKNN 转换必须在 Linux 环境(rknn-toolkit2 官方仅支持 Linux / 特定 Python 版本),Windows 上无法完成第 ② 步;
- librknnrt.so 与 rknn-toolkit2 必须版本配套 ------若不一致,板端会直接报
invalid RKNN_MAGIC(详见第 4.3 节与文末附录『异常 7』)。
3.2 环境安装(通用示例)
导出端(办公 PC):
bash
conda create -n export python=3.13 -y
conda activate export
pip install torch torchvision onnx onnxruntime
转换端(Linux 服务器):
bash
conda create -n rknn python=3.8 -y # 环境名可自定义
conda activate rknn
pip install rknn-toolkit2==2.3.2 onnx onnxruntime numpy
python -c "from rknn.api import RKNN; print('ok')" # 验证安装
板端:无需安装 Python 工具链,直接使用板卡系统自带的 gcc / g++ / cmake / make 编译 C++ demo。
3.3 数据集准备
本文使用二分类帽类数据集(训练/验证两个子目录,类别文件夹名即类别名):
text
dataset/
├── train/
│ ├── hut/ # 戴帽
│ └── no_hut/ # 未戴帽
└── val/
├── hut/
└── no_hut/
- 训练脚本为通用迁移学习流程:加载预训练 ResNet18,替换全连接层为 2 类,
CrossEntropyLoss+AdamW训练; - 训练预处理:
RandomResizedCrop(224)→RandomHorizontalFlip→ToTensor→Normalize(mean=(0.485,0.456,0.406), std=(0.229,0.224,0.225)); - 验证集同时充当量化标定集 :从
val/两类中抽取若干图片放入model/calib/,并生成dataset.txt(每行一个绝对路径,共 348 张在本例中使用)。
类别标签文件 model/synset.txt(二分类,一行一个,顺序与训练类别索引一致):
text
hut
no_hut
重要 :类别文件行数 = 模型输出类别数。二分类模型不要使用官方 demo 自带的 1000 类
synset.txt(否则输出索引与标签错位,详见文末附录『异常 6:top-K 越界』)。
4. 实操(截图 + 原理解释)
4.1 第 1 步:PyTorch 权重导出 ONNX
4.1.1 代码
export_onnx.py(通用示例,可在任意目录直接运行):
python
# -*- coding: utf-8 -*-
"""第1步: *.pth -> ONNX(导出前请先安装 onnx / onnxruntime)"""
import argparse
import torch
import torchvision
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--checkpoint", default="model/hat_best.pth")
parser.add_argument("--output", default="model/hat_best.onnx")
args = parser.parse_args()
# 1) 加载权重: 示例 checkpoint 结构为 {"model": state_dict, "classes": [...]}
ckpt = torch.load(args.checkpoint, map_location="cpu", weights_only=False)
num_classes = ckpt["model"]["fc.weight"].shape[0] # 按模型的 fc 层自适应
model = torchvision.models.resnet18(num_classes=num_classes)
model.load_state_dict(ckpt["model"])
model.eval() # ★ 必须 eval, 固定 BN 统计量
print(f"已加载 {args.checkpoint}, 类别数: {num_classes}, 类别: {ckpt.get('classes')}")
# 2) 固定输入尺寸 (batch=1), 不用动态轴
dummy = torch.randn(1, 3, 224, 224)
# 3) 导出
torch.onnx.export(
model, dummy, args.output,
input_names=["data"], # ★ 与第2步 load_onnx 的 inputs 保持一致
output_names=["output"],
opset_version=11, # ★ RKNN 对高版本 opset 支持不全, 用 11 最稳
do_constant_folding=True,
dynamo=False,
)
print(f"已导出: {args.output}")
# 4) 导出后立即验证: PyTorch 与 ONNX 输出一致性
import onnxruntime as ort
sess = ort.InferenceSession(args.output, providers=["CPUExecutionProvider"])
with torch.no_grad():
pt_out = model(dummy).numpy()
onnx_out = sess.run(["output"], {"data": dummy.numpy()})[0]
diff = float(abs(pt_out - onnx_out).max())
ok = torch.allclose(torch.from_numpy(pt_out), torch.from_numpy(onnx_out), atol=1e-5)
print(f"PyTorch vs ONNX 最大输出差异: {diff:.2e} (一致: {ok})")
print(f"输出 shape: {onnx_out.shape}")
if __name__ == "__main__":
main()
运行:
bash
python export_onnx.py --checkpoint model/hat_best.pth --output model/hat_best.onnx
4.1.2 原理:tracing 是什么
torch.onnx.export 默认使用 tracing(跟踪) 机制:把 dummy 输入真实地"跑"一遍模型,沿路把每个执行过的算子、张量 shape、权重快照记录下来,拼装成一份静态 ONNX 计算图。因此 dummy 输入的 shape 决定了模型的固定输入尺寸,这也是为什么导出时禁止使用动态维度。

图4 tracing 原理:输入真实前向一次,逐算子记录并导出为 ONNX 图
4.1.3 原理:预处理一致性是精度红线
训练时的 Normalize(mean,std) 不会 写进 ONNX 模型------它由部署端在 NPU 内完成。rknn 的 config() 里填的是 0~255 整数图像 对应的缩放:mean_values=[[255*0.485, 255*0.456, 255*0.406]]、std_values=[[255*0.229, 255*0.224, 255*0.225]]。两侧只要错一位,量化后的输出就会整体漂移。

图5 同一组归一化参数,训练侧用 PyTorch 表达,部署侧用 rknn.config 表达
4.1.4 实测输出(真实运行结果)
text
已加载 model/hat_best.pth, 类别数: 2, 类别: ['hut', 'no_hut']
已导出: model/hat_best.onnx
PyTorch vs ONNX 最大输出差异: 6.56e-07 (一致: True)
输出 shape: (1, 2)
产物约 42.6 MB ,含 49 个 ONNX 节点,opset=11,单文件、权重已内嵌 (无外部 .data 文件,RKNN 加载更省心)。
4.2 第 2 步:ONNX 转 RKNN
把 model/hat_best.onnx 与 model/calib/(含 dataset.txt)上传到 Linux 服务器,进入 rknn 环境执行转换。
4.2.1 代码
convert_rknn.py(复用官方示例的调用链,增加断言便于定位失败环节):
python
# -*- coding: utf-8 -*-
"""第2步: ONNX -> RKNN(Linux + rknn conda 环境运行)"""
from rknn.api import RKNN
rknn = RKNN(verbose=True)
print("--> config")
ret = rknn.config(
mean_values=[[255 * 0.485, 255 * 0.456, 255 * 0.406]],
std_values=[[255 * 0.229, 255 * 0.224, 255 * 0.225]],
target_platform="rk3588", # 按你的板卡 NPU 型号修改
)
assert ret == 0, "config failed"
print("--> load_onnx")
ret = rknn.load_onnx(
model="model/hat_best.onnx",
inputs=["data"], # 与导出的 input_names 一致
input_size_list=[[1, 3, 224, 224]], # RKNPU2 写法; RKNPU1 用 [3,224,224]
)
assert ret == 0, "load_onnx failed"
print("--> build (i8 quant)")
ret = rknn.build(do_quantization=True, dataset="model/calib/dataset.txt")
assert ret == 0, "build failed"
print("--> export")
ret = rknn.export_rknn("model/hat_best.rknn")
assert ret == 0, "export failed"
rknn.release()
print("DONE")
运行:
bash
conda activate rknn
python convert_rknn.py
4.2.2 原理:五步调用链 & 量化标定
工具链五步是有顺序依赖的固定链路:

图6 config → load → build → export → runtime,前四步缺一不可
其中 build(do_quantization=True) 会做 INT8 量化 :用 dataset.txt 列出的一批"贴近真实分布的图片"前向统计每层激活的数值范围,据此计算每个张量的 scale / zero_point,把 32 位浮点运算替换为 8 位定点运算,换来约 4 倍的体积压缩与更快的 NPU 推理速度。标定集太小时,某些类别的数值分布没被充分采样,量化后精度会掉------这正是本例中 no_hut 类表现弱于 hut 类的原因之一。

图7 标定集统计激活分布 → 映射为 INT8(scale/zp)
4.2.3 实测输出(真实运行结果)
text
D RKNN: ... Total Weight Memory Size: 11014.3KB
D RKNN: ... (打印 49 个算子的 INT8 量化信息, 含 fc.weight INT8 (2,512,1,1))
--> export
DONE
real 0m3.765s
产物 model/hat_best.rknn 约 10.9 MB (ONNX 42.6 MB → 体积约 1/4),模型输入被量化为主流 NHWC INT8 [1,224,224,3]。
4.3 第 3 步:板端部署与运行
4.3.1 编译 C++ demo
使用官方 rknn_model_zoo/examples/resnet 作为 demo 骨架,在 RK3588 板卡上直接编译(板卡自带 gcc/cmake 即可):
bash
cd rknn_model_zoo
bash build-linux.sh -t rk3588 -a aarch64 -d resnet
编译产物安装到 install/rk3588_linux_aarch64/rknn_resnet_demo/,其中 lib/ 自带 librknnrt.so 运行时。

图8 编译 → 上传产物 → 运行,运行库与模型必须同源配套
4.3.2 上传产物并运行
bash
# 上传模型、类别标签、测试图到板卡 demo 目录
scp model/hat_best.rknn model/synset.txt test_*.jpg <board-user>@<board-ip>:<board-path>/model/
# 板卡上运行
cd <board-path>/rknn_resnet_demo
export LD_LIBRARY_PATH=./lib
./rknn_resnet_demo model/hat_best.rknn model/test_1.jpg
4.3.3 两个必须修正的"板端坑"
坑 A:top-K 必须小于等于类别数。 demo 默认 topk = 5,那是为 1000 类 ImageNet 准备的;二分类模型下,get_topk_with_indices 会越界读取 scores 数组、越界索引类别标签指针,直接段错误(Segmentation fault) 。修正 board_demo/main.cc:
cpp
// 二分类模型: topk 不能超过类别数(2), 否则越界读写 -> 段错误
int topk = 2;
resnet_result result[topk];

图9 topk=5 而类别数=2 的越界下标访问是段错误根源
坑 B:librknnrt.so 必须与 rknn-toolkit2 版本配套。 板卡本身自带了一份较旧的运行库,加载本模型时报:
text
E RKNN: parseRKNN: invalid RKNN_MAGIC!
E RKNN: rknn_init, load model failed!
这是模型文件的 RKNN 版本号与运行库支持的版本不一致 。解决:使用与本例 rknn-toolkit2 2.3.2 配套的 librknnrt.so(板卡上其他项目若已用同版本工具链生成过模型,其自带运行库即为匹配版本),替换 demo 的 lib/librknnrt.so 后恢复正常(替换后 md5sum 校验一致)。
4.3.4 修正后的真实运行输出
text
num_lines=3
model input num: 1, output num: 1
input tensors:
index=0, name=data, n_dims=4, dims=[1, 224, 224, 3], fmt=NHWC, type=INT8, qnt_type=AFFINE, zp=-14, scale=0.018658
output tensors:
index=0, name=output, n_dims=2, dims=[1, 2, 0, 0], fmt=UNDEFINED, type=INT8, qnt_type=AFFINE, zp=5, scale=0.023836
model input height=224, width=224, channel=3
rknn_run
[0] score=0.660922 class=hut
[1] score=0.339078 class=no_hut
5. 测试验证
5.1 两个层面的验证
第 1 层:PC 模拟器(转换端) 。在 rknn 环境内对同一批 build 后的模型做推理验证(注意:模拟器不支持直接 load_rknn,必须在同一进程内先 build 再 init_runtime())。真实结果:
text
hut(0): 10/10 no_hut(1): 7/10 overall: 17/20
第 2 层:板卡真机(NPU INT8)。在 RK3588 上使用 C++ demo 对 11 张验证图逐一推理,本项目真实结果如下(绿=正确,红=错误):

图10 板端 11 张测试图:10 正确 / 1 错误(红色 nohut_4 被误判为 hut)
| 图片 | 期望 | 输出 P(hut) | 判定 |
|---|---|---|---|
| test_1 | hut | 0.661 | ✅ |
| test_2 | no_hut | 0.109 | ✅ |
| dog | no_hut | 0.435 | ✅ |
| hut_1 ~ hut_4 | hut | 0.736 / 0.953 / 0.957 / 0.939 | ✅ 4/4 |
| nohut_1 ~ nohut_3 | no_hut | 0.143 / 0.264 / 0.423 | ✅ 3/3 |
| nohut_4 | no_hut | 0.634 | ❌ 误判为 hut |
真机整体 10/11 ≈ 90.9%,与训练集 val 指标(95.4%)相比略有下降,属 INT8 量化的正常代价。
5.2 精度分析
- hut 类 5/5 全对且置信度高(最高 0.957);
- no_hut 类 5/6 ,其中
nohut_4与dog两图的判定置信度都偏低(0.42~0.63 徘徊在 0.5 阈值附近),说明"未戴帽"类别的特征区分度不足; - 改善方向(按性价比排序):
- 增加 no_hut 类别的训练与标定样本(标定图从 40 张扩到 348 张后,该项实测由 6/10 提升到 7/10);
- 调高 hut 决策阈值(如 P(hut) > 0.7 才判戴帽,低于阈值判"不确定");
- 对比 fp(不量化)模型评估纯量化损失。
6. 总结
三步链路:pth → onnx(opset 11 固定尺寸)→ rknn(i8 量化免标定图 348 张)→ 板端(配套 librknnrt.so) ,全程约 10 分钟可复现。四个关键认知:一是预处理参数必须两侧同源;二是所有文件名/固定尺寸/输入名要在导出与加载两端严格对齐;三是 top-K 永远不能超过类别数;四是运行库与工具链版本配套比什么都重要 ,版本一错直接 invalid RKNN_MAGIC。量化精度损失集中在样本少的类别,用标定集扩充即可显著缓解。真机实测 11 图 10 正确,链路稳定可用。
全部异常的详细分析、复现步骤与解法见文末附录。
附录:异常记录(全链路问题分析)
本附录与正文配套。每一条异常均为真实复现过的现象(附真实报错文本),给出原因分析与可复现的解决方案。按出现顺序编号,方便排查时对号入座。
异常 1:Python 启动即报 OMP Error #15
现象:
text
OMP: Error #15: Initializing libiomp5md.dll, but found libiomp5md.dll already initialized.
OMP: Hint ... multiple copies of the OpenMP runtime have been linked into the program ...
出现环节:导入 torch / 运行任何 PyTorch 脚本。
原因分析:环境中同时存在多份 OpenMP 运行时(常见于 conda 基础环境 + 用户环境混装 torch),重复初始化冲突。
解决方案:运行前设置环境变量即可继续:
bash
export KMP_DUPLICATE_LIB_OK=TRUE # Linux / macOS
set KMP_DUPLICATE_LIB_OK=TRUE # Windows cmd
注意:该方案是"放行"而非根治;根治需要清理重复的 libiomp 动态库(保留一份)。教程环境搭建时建议在干净 conda 环境内安装 torch 以避免此问题。
异常 2:torch.onnx.export 报 "Module onnx is not installed!"
现象:
text
File "...\torch\onnx\_internal\...\onnx_proto_utils.py", line 185, in _add_onnxscript_fn
raise errors.OnnxExporterError("Module onnx is not installed!") from e
torch.onnx.OnnxExporterError: Module onnx is not installed!
出现环节:第 1 步导出 ONNX。
原因分析 :新版 PyTorch 的 ONNX 导出依赖独立的 onnx Python 包,环境里没有安装。
解决方案:
bash
pip install onnx onnxruntime
实测安装后版本:onnx 1.22.0、onnxruntime 1.28.0,随后导出正常。
异常 3:模拟器不支持直接加载 rknn 模型
现象:
text
E init_runtime: RKNN model that loaded by 'load_rknn' not support inference on the simulator, please set 'target' first!
If you really want to inference on the simulator, use 'load_xxx' & 'build' instead of 'load_rknn'!
AssertionError
出现环节 :转换端想在 PC 上先用模拟器验证已导出的 .rknn。
原因分析 :PC 模拟器只支持"同进程内 build 出来的模型对象"做推理;用 load_rknn 直接加载磁盘文件到模拟器不被支持。
解决方案 :验证脚本改为同进程完成 config → load_onnx → build → init_runtime → inference,不要拆成两个脚本(一个 build 导出、另一个 load_rknn 验证)。
异常 4:dataset.txt 生成时报 "No such file or directory"
现象:
text
bash: line 24: ../model/calib/dataset.txt: No such file or directory
出现环节:在转换端准备量化标定清单。
原因分析 :脚本/命令的当前工作目录 与相对路径不匹配。示例工程中 model/ 与脚本同级,若在 python/ 等子目录执行却写 ../model/,实际指向了不存在的上层目录。
解决方案 :标定清单一律使用绝对路径 ,或在 ls 确认目录后使用正确层级。实际验证中改为绝对路径后 348 行清单一次生成成功。
异常 5:板端编译脚本权限不足
现象:
text
bash: ./build-linux.sh: 权限不够
出现环节:板卡上运行官方编译脚本。
原因分析:仓库文件未带可执行位(常见于跨平台拷贝/解压)。
解决方案:用 bash 显式调用,或补执行权限后直接运行:
bash
bash build-linux.sh -t rk3588 -a aarch64 -d resnet
# 或
chmod +x build-linux.sh && ./build-linux.sh -t rk3588 -a aarch64 -d resnet
异常 6:demo 运行段错误(Segmentation fault)
现象:
text
段错误 (核心已转储) ./rknn_resnet_demo model/hat_best.rknn model/test_1.jpg
出现环节:板端首次运行 demo。
原因分析 :官方 demo 为 1000 类 ImageNet 设计,硬编码 topk = 5。二分类模型输出只有 2 个元素,get_topk_with_indices 循环读取 elements[2..4](越界读),随后打印阶段用越界的类别索引访问标签指针数组(越界读/写),触发段错误。根因:top-K 参数大于输出类别数。
解决方案 :修正 main.cc 中 top-K,使其不超过类别数:
cpp
// 二分类模型: topk 不能超过类别数(2), 否则越界读写 -> 段错误
int topk = 2;
resnet_result result[topk];
重新编译后恢复正常(修正前后可用 stdbuf -o0 观察崩溃位置:崩溃发生在模型推理完成之后的打印阶段,进一步佐证越界发生在后处理)。
异常 7:invalid RKNN_MAGIC! 模型加载失败
现象:
text
E RKNN: [10:04:54.493] parseRKNN: invalid RKNN_MAGIC!
E RKNN: [10:04:54.493] parseRKNN from buffer: Invalid RKNN format!
E RKNN: [10:04:54.493] rknn_init, load model failed!
free(): invalid size
已中止 (核心已转储)
出现环节 :板端加载新转换的 .rknn 模型。
原因分析 :模型文件的 RKNN 版本与板端 librknnrt.so 支持的版本不匹配 。模型由 rknn-toolkit2 2.3.2 生成,而板卡自带的运行库为旧版本(生成时间早于配套库),解析头部的 RKNN_MAGIC 失败。free(): invalid size 是 rknn_init 失败后资源释放的连锁现象,不是独立问题。
排查手段:对比模型文件头与运行库生成时间:
bash
xxd -l 32 model/hat_best.rknn # 头部 magic 与版本字节
stat -c %y <path>/librknnrt.so # 运行库生成时间
解决方案 :使用与 rknn-toolkit2 2.3.2 配套 的 librknnrt.so 替换 demo 的 lib/librknnrt.so(板卡上若已有同版本工具链产出的项目,其运行库即为匹配版本),替换后 md5sum 校验与源一致即生效。替换后模型加载与推理立即正常。
经验沉淀:转换端工具链版本与板端运行库必须记录在案;升级任一工具链后,需同步更新板端运行库。
异常 8:量化后精度下降(no_hut 类偏弱)
现象:
text
# 40 张标定图时(PC 模拟器)
hut(0): 10/10 no_hut(1): 6/10 overall: 16/20
# 348 张标定图时(PC 模拟器)
hut(0): 10/10 no_hut(1): 7/10 overall: 17/20
出现环节:转换端模拟器验证、板端真机验证(真机 11 图 10 正确,唯一错误即 no_hut 类)。
原因分析:
- 标定集过小:40 张时 no_hut 类激活分布采样不足,INT8 量化损失偏大;扩充到 348 张(含全部 val 的 no_hut 样本)后该类别提升 1/10;
- 类别不平衡:no_hut 训练/标定样本少于 hut,量化阶段该类别量化误差放大;
- 特征区分度:个别 no_hut 图(如狗、复杂背景人像)置信度徘徊在 0.5 阈值附近,误判风险天然更高。
解决方案(按性价比排序):
- 扩充标定集(至少覆盖每个类别的代表性样本,本文 348 张为有效规模);
- 训练侧增加 no_hut 样本 / 加权采样;
- 部署侧调整决策阈值(如 P(hut) > 0.7 才判 hut);
- 对比 fp 模型评估纯量化损失上限。
附:排查顺序建议
| 现象 | 优先检查 |
|---|---|
| 导出报错 | onnx/onnxruntime 是否安装 → opset 是否过高 → dummy 尺寸 |
| 转换报错 | dataset.txt 路径/图片是否有效 → 输入名与 input_size 是否匹配 |
| 板端段错误 | topk 是否 ≤ 类别数 |
| 板端加载失败 | librknnrt.so 与工具链版本是否配套 |
| 精度不达标 | 预处理参数一致性 → 标定集规模 → 类别平衡 |
