1. 为什么要在 RK3566/RK3568/RK3588 上折腾 RKNN NPU
手里攥着一块 RK3566 或者 RK3588 的板子,看着规格书里那个 0.8 TOPS 或者 6 TOPS 的 NPU 算力,第一反应肯定是想跑个模型试试。但真到动手的时候,很多人会卡在第一步:环境怎么搭?工具链从哪来?模型怎么转?板子上跑起来为什么报错?
这一套流程我前前后后在三块板子上走过好几遍,从 RK3566 的轻量场景到 RK3588 的多核 NPU 调度,踩过的坑足够写一本小册子。这篇内容就是把这套流程完整地摊开,从宿主机环境准备、RKNN-Toolkit2 安装、模型转换、板端运行时部署,一直到第一个模型推理验证跑通。不管你是刚拿到板子的新手,还是已经在调设备树但还没碰过 NPU 的老手,都能从这里找到能直接抄的步骤。
先明确几个概念,避免后面混淆。RKNN是瑞芯微这套 NPU 工具链的名字,包含两大部分:跑在 PC 上的RKNN-Toolkit2(负责模型转换和仿真)和跑在板子上的RKNN Runtime(负责实际推理)。NPU就是板子上的神经网络加速单元,RK3566/RK3568 是单核 0.8 TOPS,RK3588 是三核 6 TOPS 且支持多核协同。这三个芯片的 NPU 架构同源,工具链基本通用,但 RK3588 多了多核调度和更大的内存带宽,实际部署时有些参数需要区别对待。
我见过太多人一上来就去搞模型量化、算子优化,结果连rknn_server都没跑起来。所以这篇的路线是:先把最小闭环跑通,再谈优化。一个最简单的 MobileNet 或者 YOLOv5n,能在板子上出结果,比什么都重要。
2. 宿主机环境搭建:别在版本上栽跟头
2.1 系统选择与 Python 版本陷阱
RKNN-Toolkit2 对系统环境有明确要求,官方推荐 Ubuntu 20.04 或 22.04。我实测下来,Ubuntu 22.04 + Python 3.8 到 3.10是最稳的组合。为什么强调这个?因为 Toolkit2 的 wheel 包是按 Python 版本编译的,你用 Python 3.11 或者 3.12,大概率找不到对应的包,或者装上了但 import 就报错。
有个细节很多人忽略:conda 环境和系统 Python 的冲突。如果你用 conda 建了虚拟环境,装完 Toolkit2 后跑模型转换,可能会遇到libstdc++.so.6版本不匹配的问题。原因是 conda 自带的 libstdc++ 版本比系统的高,而 Toolkit2 依赖系统库。解决办法要么用系统 Python 加 venv,要么在 conda 环境里手动指定库路径。我个人的习惯是直接用系统 Python 建 venv,省去这层麻烦。
# 确认系统版本 lsb_release -a # 安装基础依赖 sudo apt update sudo apt install -y python3-pip python3-venv python3-dev sudo apt install -y libgl1 libglib2.0-0 libsm6 libxext6 libxrender-dev # 创建虚拟环境 python3 -m venv rknn_env source rknn_env/bin/activate pip install --upgrade pip注意:如果你用的是 WSL2,USB 设备透传会有问题,板子连接可能不稳定。建议要么用原生 Ubuntu,要么在 WSL2 里只做模型转换,板端调试换到 Windows 或者另一台 Linux 机器。
2.2 RKNN-Toolkit2 安装与验证
Toolkit2 的包从瑞芯微的 GitHub 仓库或者官方下载站获取。截至我写这篇的时候,稳定版本在 1.6 到 2.0 之间。版本选择有个原则:板端 Runtime 的版本要和 Toolkit2 版本匹配,否则模型转换出来的 rknn 文件在板子上加载会报版本不兼容。
# 下载对应版本的 wheel 包,以 2.0.0b0 为例 pip install rknn_toolkit2-2.0.0b0+9bab5682-cp38-cp38-linux_x86_64.whl # 验证安装 python3 -c "from rknn.api import RKNN; print('RKNN Toolkit2 loaded')"如果这行命令没报错,说明基础环境 OK。接下来验证一下 Toolkit2 能不能正常连接板子。这里需要板子已经烧录好固件,并且通过 USB 或者网络和宿主机连通。
from rknn.api import RKNN rknn = RKNN() # 通过 adb 连接,板子需要开启 adb ret = rknn.init_runtime(target='rk3588') print('Runtime init:', ret) rknn.release()init_runtime返回 0 表示连接成功。如果返回非零,先检查adb devices能不能看到板子。RK3588 的板子有些默认 adb 是关闭的,需要在串口终端里执行setprop persist.adb.tcp.port 5555然后adb connect。
2.3 板端 Runtime 环境确认
板子这边,固件里通常已经带了librknnrt.so和rknn_server。你可以通过以下命令确认:
# 在板子上执行 ls /usr/lib/librknnrt.so ls /usr/bin/rknn_server # 查看版本 strings /usr/lib/librknnrt.so | grep -i version如果固件里没有,需要从 SDK 里编译或者从官方仓库下载对应架构的库。RK3566/RK3568 是 32 位还是 64 位取决于你的 rootfs,RK3588 基本都是 64 位。库的架构必须和 rootfs 匹配,否则加载会报wrong ELF class。
3. 模型转换:从 ONNX 到 RKNN 的关键步骤
3.1 模型准备与算子兼容性检查
拿一个训练好的模型来转,第一步不是直接跑转换脚本,而是先检查算子支持情况。RKNN 对算子的支持是有限制的,尤其是自定义算子或者一些新出的激活函数。我一般会先用 Toolkit2 的load_onnx加载模型,然后看日志里有没有unsupported op的警告。
以 YOLOv5n 为例,从 PyTorch 导出 ONNX 的时候,opset_version建议用 11 或者 12。太高了 RKNN 可能不认,太低了某些算子表达不了。导出命令:
import torch model = torch.hub.load('ultralytics/yolov5', 'yolov5n') dummy = torch.randn(1, 3, 640, 640) torch.onnx.export(model, dummy, 'yolov5n.onnx', opset_version=12, input_names=['images'], output_names=['output'])导出后,用 Netron 看一眼结构,确认没有奇怪的算子。然后进 RKNN 转换:
from rknn.api import RKNN rknn = RKNN(verbose=True) # 配置 rknn.config( mean_values=[[0, 0, 0]], std_values=[[255, 255, 255]], target_platform='rk3588', quantized_dtype='asymmetric_quantized-8', optimization_level=3 ) # 加载 ONNX ret = rknn.load_onnx(model='yolov5n.onnx') if ret != 0: print('Load ONNX failed') exit(ret) # 构建,需要量化数据集 ret = rknn.build(do_quantization=True, dataset='./dataset.txt') if ret != 0: print('Build failed') exit(ret) # 导出 ret = rknn.export_rknn('yolov5n.rknn') rknn.release()这里有几个关键参数需要解释。mean_values和std_values是预处理参数,必须和训练时的归一化方式一致,否则精度会崩。quantized_dtype选asymmetric_quantized-8是 INT8 量化,RK3588 的 NPU 对 INT8 支持最好。optimization_level=3会做一些算子融合,但有时候融合后精度下降,可以降到 2 试试。
3.2 量化数据集制作与精度调优
量化数据集是 INT8 转换的核心。数据集不需要标注,只需要一批有代表性的输入图片,通常 100 到 300 张就够。图片要覆盖实际场景的各种情况,比如光照变化、目标大小变化。如果只用几张图,量化参数估计不准,精度会掉得很厉害。
dataset.txt的格式很简单,每行一个图片路径:
./images/001.jpg ./images/002.jpg ...我一般会从训练集里随机抽 200 张,resize 到模型输入尺寸,放到一个文件夹里。注意不要用训练时增强过的图,用原图就行。
量化完之后,Toolkit2 会输出每层的量化误差。如果某层误差特别大,可以考虑把这层设为不量化,或者调整量化算法。不过对于大多数常规模型,默认配置就能到可用的精度。
3.3 RK3588 多核配置与 RK3566 的差异
RK3588 的 NPU 有三个核,转换的时候可以通过target_platform指定,但多核调度是在板端运行时决定的。Toolkit2 里有个npu_core参数,可以设成0、1、2或者auto。auto会让 Runtime 自动分配,适合多模型并行或者单模型多核加速。
RK3566 和 RK3568 只有单核,所以这个参数设不设都一样。但 RK3588 上如果你跑的是大模型,比如 ResNet50 或者 YOLOv8m,开多核能明显提升帧率。实测 YOLOv8n 在 RK3588 上单核大概 30 FPS,三核能到 70 FPS 左右,当然这也取决于模型结构和输入尺寸。
提示:多核不是万能的。如果模型本身很小,多核调度的开销可能反而拖慢速度。建议先用单核跑个基准,再试多核对比。
4. 板端部署与首个推理验证
4.1 交叉编译与板端程序编写
板端推理程序可以用 C++ 或者 Python。Python 版依赖rknn_toolkit_lite2,C++ 版直接链接librknnrt.so。我一般先用 Python 快速验证,确认模型没问题再上 C++ 做性能优化。
Python 版在板子上的安装:
# 板子上执行,注意架构匹配 pip install rknn_toolkit_lite2-2.0.0b0-cp38-cp38-linux_aarch64.whl然后写一个最小的推理脚本:
from rknnlite.api import RKNNLite import numpy as np import cv2 rknn_lite = RKNNLite() ret = rknn_lite.load_rknn('yolov5n.rknn') ret = rknn_lite.init_runtime(core_mask=RKNNLite.NPU_CORE_0) img = cv2.imread('test.jpg') img = cv2.resize(img, (640, 640)) img = img[:, :, ::-1] # BGR to RGB img = np.expand_dims(img, axis=0) outputs = rknn_lite.inference(inputs=[img]) print('Output shape:', outputs[0].shape) rknn_lite.release()core_mask参数在 RK3588 上可以指定用哪个核,NPU_CORE_0、NPU_CORE_1、NPU_CORE_2或者NPU_CORE_AUTO。RK3566/RK3568 上这个参数无效,但写上也不报错。
4.2 推理结果验证与常见报错
第一次跑推理,最常见的报错是输入尺寸不匹配和数据类型不对。RKNN 模型对输入的要求很严格,NHWC 格式,UINT8 或者 FP32 取决于量化配置。如果你传进去的是 FP32 但模型是 INT8 量化的,Runtime 会自动转换,但速度会慢。
另一个高频问题是内存不足。RK3588 跑大模型的时候,如果 rootfs 的 CMA 内存不够,会报malloc failed。解决办法是在设备树里调大 CMA 预留,或者用rknn_lite.init_runtime的时候指定perf_mode为RKNNLite.NPU_FULL_PERF来优化内存分配。
我整理了一个常见报错速查表:
| 报错信息 | 可能原因 | 解决办法 |
|---|---|---|
load_rknn failed | 模型文件损坏或版本不匹配 | 重新转换,确认 Toolkit2 和 Runtime 版本一致 |
init_runtime failed | NPU 驱动未加载或权限不足 | 检查/dev/rknpu是否存在,用 root 运行 |
inference failed | 输入尺寸或格式不对 | 打印输入 shape,确认 NHWC 和量化类型 |
malloc failed | CMA 内存不足 | 调大设备树 CMA,或减小模型输入尺寸 |
unsupported op | 模型含不支持算子 | 替换算子或改用 CPU 推理该层 |
4.3 性能基准测试与优化方向
跑通之后,下一步是测性能。RKNN Runtime 提供了eval_perf接口,可以输出每层的耗时。C++ 版里用rknn_query查RKNN_QUERY_PERF_DETAIL。
# Python 版性能测试 rknn_lite.init_runtime(core_mask=RKNNLite.NPU_CORE_AUTO) # 跑 100 次取平均 import time start = time.time() for _ in range(100): rknn_lite.inference(inputs=[img]) end = time.time() print('Average latency:', (end - start) / 100 * 1000, 'ms')优化方向主要有几个:输入尺寸(320 比 640 快一倍以上)、量化精度(INT8 比 FP16 快)、多核调度(RK3588 专属)、算子融合(转换时开 optimization_level)。但要注意,优化不能牺牲精度,我一般会先跑一个精度基准,优化后再对比 mAP 或者 Top-1 准确率。
5. 实操心得与避坑指南
5.1 版本匹配是最大的坑
我遇到过最折腾的问题就是版本不匹配。Toolkit2 是 1.6,板子上的 Runtime 是 1.4,转换出来的模型加载直接报version mismatch。解决办法只有一个:统一版本。要么升级板端 Runtime,要么降级 Toolkit2。升级 Runtime 需要重新编译或者替换librknnrt.so,降级 Toolkit2 相对简单,直接 pip 装旧版本。
还有一个隐藏的坑:RKNN 模型文件不向下兼容。用 2.0 转换的模型,1.6 的 Runtime 加载不了。所以团队协作的时候,一定要把 Toolkit2 版本写进文档,别让同事用不同版本转模型。
5.2 量化数据集的代表性决定精度上限
很多人量化完发现精度掉得厉害,第一反应是量化算法不行。其实大部分时候是数据集没选好。我试过用 50 张同一场景的图做量化,结果模型在别的场景下完全不能用。后来换成 200 张覆盖各种光照和角度的图,精度就回来了。
量化数据集的选取原则:覆盖实际部署场景的所有变化。如果是安防场景,白天黑夜、晴天雨天都要有;如果是工业质检,不同批次的产品图都要有。数量上 100 到 300 张足够,但质量比数量重要。
5.3 RK3588 多核调度的实际表现
RK3588 的三核 NPU 听起来很美好,但实际用起来有几个限制。首先,多核只对单模型推理有效,如果你同时跑多个模型,Runtime 会自动分配核,但每个模型只能用一个核。其次,多核加速比不是线性的,三核大概能到 2 到 2.5 倍,因为内存带宽和调度有开销。
我实测过一个 YOLOv8s 模型,输入 640x640,单核 25 FPS,三核 55 FPS。但换成 YOLOv8n,单核 60 FPS,三核 80 FPS,提升就没那么明显。所以小模型没必要开多核,大模型才值得。
5.4 板端调试的实用技巧
板子上调试没有宿主机方便,我一般会做几件事:把日志级别调高,RKNN Runtime 支持RKNN_LOG_LEVEL环境变量,设成debug能看到详细的加载和推理信息;用strace跟踪系统调用,排查库加载问题;保留一个最小可复现的测试程序,出问题的时候先跑这个,排除业务代码的干扰。
还有一个细节:板子的散热。RK3588 跑 NPU 满载的时候发热很大,如果散热不好会降频,性能直接掉一半。我一般会加个风扇,或者至少贴个散热片。RK3566 发热小一些,但长时间跑也要注意。
6. 从首个模型到实际项目
跑通第一个模型只是起点。实际项目里还会遇到多模型串联、视频流推理、结果后处理等问题。比如做目标检测,NPU 输出的是原始张量,还需要在 CPU 上做 NMS 和后处理。这部分如果优化不好,可能比推理本身还耗时。
我的建议是:先用 Python 把整个 pipeline 跑通,再逐步把耗时模块移到 C++ 或者用 RGA 加速。RK3588 有 RGA 硬件加速器,可以做图像缩放和格式转换,比 CPU 快很多。MPP 则负责视频编解码,和 NPU 配合能实现完整的视频分析 pipeline。
另外,模型转换这块,现在社区里有一些自动化工具,比如把 ONNX 直接转 RKNN 的脚本,但我建议还是手动走一遍 Toolkit2 的流程,因为自动化工具隐藏了太多细节,出问题的时候不好排查。手动走一遍,你知道每一步在做什么,调优的时候才有方向。
最后分享一个我常用的调试方法:在宿主机上用 Toolkit2 的仿真模式先跑一遍。init_runtime的时候不指定target,Toolkit2 会用 CPU 仿真 NPU 的行为。虽然速度慢,但能快速验证模型转换是否正确,不用每次都烧到板子上。仿真通过了,再上板子,效率高很多。