1. 实验背景与方案设计
1.1 RK3568这颗芯片到底适合跑什么
RK3568 是这两年在边缘视觉项目里出镜率很高的一颗 SoC,四核 A55 的 CPU 算通用任务不算强,但它内置了一个 NPU,最典型的用途就是把图像分类这类算子密集的活接过去。标称 INT8 算力大概在 1 TOPS 级别,光看数字会觉得有点弱,但实际配合 MobileNetV3 这种轻量模型跑个二三十帧是没问题的,做门禁、抓拍、质检、智能盒子的候选绰绰有余。真正让人头疼的问题是:主流图像分类模型那么多,到底哪个量化后能保住精度,哪个在 NPU 上真正跑得快,只看理论 FLOPs 根本判断不了。所以我花了一周多时间,做了一次系统的部署实验,把 MobileNet 系列、ShuffleNetV2、ResNet、EfficientNet 和一个轻量 Transformer 类型的代表模型挨个在 RK3568 上过了一遍,记录从模型转换、INT8 量化到板端推理的完整数据。
这篇文章就是这次实验的记录和分析,目标读者是准备在 RK3568 或者类似边缘平台上做算法部署的开发者。如果你正在模型选型阶段,或者已经遇到量化掉精度、算子不支持、帧率上不去这些问题,这篇文章应该能帮你节省至少两天试错时间。需要先说明一点:所有数据都来自我手头这块板子和当时的驱动版本,不同板卡、不同 RKNN 工具链版本跑出来的绝对数值会有偏差,但模型之间的相对趋势是稳定可靠的,值得参考。
1.2 参测模型清单与选择理由
我选的模型不是随便凑数的,而是按照边缘部署的典型诉求分成几个梯队:
| 模型 | 类型 | 参数量 | 选型理由 |
|---|---|---|---|
| MobileNetV3-Small | 轻量 CNN | 约 2.5M | 低算力平台的常青树,部署首选候选 |
| MobileNetV3-Large | 轻量 CNN | 约 5.4M | 对比 Small 看容量换精度的性价比 |
| ShuffleNetV2 1.0x | 轻量 CNN | 约 2.3M | 通道重排策略在 NPU 上的兼容性值得验证 |
| ResNet18 | 中量 CNN | 约 11.7M | 精度基线代表,经典中的经典 |
| ResNet50 | 中量 CNN | 约 25.6M | 中量级模型在 RK3568 上的极限测试 |
| EfficientNet-B0 | 均衡缩放 CNN | 约 5.3M | 复合缩放策略在部署链路上的兼容性验证 |
| MobileViT-XXS | CNN+Transformer | 约 1.3M | 轻量 Transformer 在低算力 NPU 上的真实表现 |
选这些模型有一个很重要的原因:不能只看算力指标来判断部署效果。ResNet50 的 FLOPs 是 MobileNetV3 的二十倍左右,但 NPU 上跑起来并不是严格的二十倍耗时,算子映射、内存访问模式、量化敏感度都会带来差异。尤其是 MobileViT 这种含 attention 的结构,理论 FLOPs 不高,实际在 NPU 上的算子拆解可能非常吃亏。这批模型恰好覆盖了"极轻量、经典中量、新架构"三种类型,用来做横向对比信息量足够大。
模型权重统一用标准 ImageNet 预训练权重,不做任何微调或结构修改,保证变量只有模型本身。所有模型输入分辨率统一设 224x224,这个也是后面部署时最容易踩坑的地方,很多人在 PC 上测试用一个分辨率,转 RKNN 后又换了预处理,精度直接就崩了。
1.3 实验评测维度与指标口径
这次实验不只测一个帧率,而是从四个维度记录每个模型的表现:
- 转换成功率:ONNX 能否顺利转为 RKNN,转换过程有没有算子告警,这些告警是否影响 NPU 加速。
- 精度损失:以标准分类验证集子集为基准,对比 FP32 模型和 INT8 量化后 RKNN 模型的 Top-1 准确率差。
- 推理耗时与帧率:分别测 NPU 单模型推理和纯 CPU 多线程推理两种模式,记录第 50 百分位和 95 百分位耗时。
- 内存占用:记录模型加载后进程物理内存增量,这个指标直接影响能否跑在带内存上限的设备上。
之所以定义成这样,是因为每个指标都对应一个实际场景问题:转换失败说明这个模型根本没法用;精度掉太多说明不敢上生产;帧率不够说明没法做实时视频流;内存超了说明只能降分辨率或者换模型。我在实验中也重点记录了 CPU-only 的表现,很多项目会把 NPU 推理作为主力,但同时需要一个纯 CPU 兜底路径,这个数据在平时文档里很少能看到。
2. 从 ONNX 到 RKNN 的转换链路搭建
2.1 工具链准备与版本选择
RKNN 的转换工具是 RKNN-Toolkit2,在 PC 上完成模型转换和量化模拟,生成的 .rknn 文件再放到板子上用 RKNNLite 或 C API 加载推理。建议用 Python 虚拟环境安装,避免把系统环境搞乱。版本方面,我当时用的是当时相对稳定的版本,现在建议直接选官方最新的稳定版,老版本对 EfficientNet 这类复合缩放模型的算子支持问题比较多。
安装环境的基本步骤:
- 创建独立虚拟环境,Python 版本按工具链要求来,一般 3.8 到 3.10 都比较稳。
- 安装 rknn-toolkit2 及其依赖,安装后先做一个导入验证。
- 验证环境是否可用,能正常导入说明 PC 端转换环境就绪。
板端部署时还需要把对应的 rknn-toolkit-lite2 安装到开发板的 Python 环境里。这里有个常见混淆:PC 上装的是完整版,用于转换和仿真;板子上装的是精简版,只负责加载和推理,两者不能混用。我第一次弄的时候就装反了,板子上怎么 import 都失败,排查半天发现装成了完整版但依赖不齐,反而比精简版更麻烦。
2.2 ONNX 导出与图精简
模型转换的入口文件最好是 ONNX 格式,PyTorch 导出时有两个细节直接影响后续 RKNN 转换成功率:
第一,opset 版本不要追求最新,设 11 或 12 比较稳妥。新版 opset 会引入一些高级算子,RKNN 解析器不一定认识,反而导致转换失败。
第二,导出后用 onnxsim 做一轮图精简。很多 PyTorch 导出的 ONNX 图里会残留大量 Identity、冗余 Reshape 节点,这些节点在 RKNN 构建时会增加解析负担,偶尔还会触发形状推断错误。onnxsim 跑一遍通常能消除大部分冗余,转换成功率和速度都会有明显提升。
PyTorch 侧导出代码大概是这样的:
import torch model.eval() dummy_input = torch.randn(1, 3, 224, 224) torch.onnx.export( model, dummy_input, "model.onnx", opset_version=12, input_names=["input"], output_names=["output"], do_constant_folding=True )导出之后建议先用 onnxruntime 跑一遍,确认 ONNX 本身没问题,再进入 RKNN 转换,否则后面出了问题很难定位是模型结构问题还是转换链路问题。
2.3 转换配置与 INT8 量化关键点
RKNN 转换的核心代码看起来很简单,但参数里面的坑特别多。下面是一个标准的转换流程:
from rknn.api import RKNN rknn = RKNN() # 关键点:mean/std 必须与训练时一致 rknn.config( mean_values=[[123.675, 116.28, 103.53]], std_values=[[58.395, 57.12, 57.375]], target_platform='rk3568', ) rknn.load_onnx(model='model.onnx') rknn.build(do_quantization=True, dataset='calib_dataset.txt') rknn.export_rknn('model.rknn')三个参数值得重点说:
- mean_values 和 std_values:必须和模型训练时的归一化方式保持一致。如果训练时用的是 [0,1] 归一化,而这里配了 ImageNet 标准 mean/std,精度会直接崩。这个错误我见过很多次,症状是量化后 Top-1 比随机猜好不了多少,排查半天发现就是归一化写错了。
- dataset 文件:每行一个量化校准图片的路径,推荐 200 到 500 张。校准图要尽量覆盖各个类别、各种光照条件,不能随手传 10 张白底图。校准集质量直接影响量化后的精度表现,这个在后面 MobileViT 的实例中会看到差异有多大。
- target_platform:这里必须指定为 rk3568,不能写通用平台或 rk3588,不同芯片的 NPU 指令集和算子支持范围有差异,写错可能导致部分算子没有利用 NPU 加速。
转换完成后,建议在 PC 端立刻用模型的模拟推理功能跑几张测试图,和 ONNX Runtime 的结果做交叉验证,确认输出类别一致再往板子上搬。
2.4 精度验证:量化后到底掉了多少
量化精度的验证我分了两层做:第一层是 PC 端模拟,用 RKNN 在 PC 上跑量化后的模型,对比推理结果和 ONNX Runtime 的余弦相似度以及 Top-1 一致率;第二层是板端实测,把 .rknn 文件部署到开发板上,用同样的测试图集再跑一遍。
测试图集我用了 1000 张从标准分类验证集里抽出的图片,分布大致均匀。每张图分别记录三个结果:ONNX FP32 的预测类别、RKNN INT8 模拟的预测类别、板端 RKNNLite 的预测类别。最后统计 RKNN 相对 FP32 的 Top-1 准确率变化。这样做能同时验证三件事:转换本身有没有问题、量化有没有明显掉点、板端和 PC 端行为是否一致。实测下来 PC 模拟和板端结果基本一致,差距主要来自芯片温度导致的微小数值抖动,类别层面几乎没有差异。
3. 板端部署实现与性能采集
3.1 推理主流程与核心代码
板端我用的是 RKNNLite 的 Python 接口,方便快速验证。加载模型之后,推理主流程非常简洁,但有几个细节直接影响性能和正确性:
from rknnlite.api import RKNNLite rknn_lite = RKNNLite() rknn_lite.load_rknn('model.rknn') # core_mask 控制 NPU 核心使用策略 rknn_lite.init_runtime(core_mask=RKNNLite.NPU_CORE_AUTO) # 图像预处理:保证与量化配置的 mean/std 一致 img = cv2.imread('test.jpg') img = cv2.resize(img, (224, 224)) # 不需要手动归一化,RKNN config 里已经配置了 outputs = rknn_lite.inference(inputs=[img]) pred = outputs[0].reshape(-1).argmax()这里有两个容易出错的点:
第一,输入图片的格式。OpenCV 读出来是 BGR 顺序,而训练时很多模型用的是 RGB。RKNN 配置里 mean/std 是按 RGB 计算的,如果直接扔 BGR 图进去,相当于通道顺序反了,精度会莫名下降但又不是完全不可用,很容易让人误以为是量化问题。稳妥做法是先把 BGR 转成 RGB 再送入推理,或者确认训练时用的就是 BGR 输入。
第二,init_runtime 的 core_mask 参数。RK3568 的 NPU 有多个核心,AUTO 模式会自动分配负载。性能测试时建议固定调用方式,否则两次测试可能走了不同调度策略,数据会不稳定。实际项目里用 AUTO 一般就是最优解,不需要手动指定单核。
3.2 测速方法:避开常见的性能测试误区
帧率测试看起来简单,实际是最容易测出假数据的一环。很多人拿官方 demo 里的单次推理时间当性能指标,这个数字会被首次加载、缓存未命中、CPU 变频等因素严重影响,偏差可以到一倍以上。我这次的测速方法如下:
- 先跑 10 次预热,让模型权重加载、NPU 调度、内存分配都稳定下来。
- 正式循环 100 次,对同一张预处理好的图片反复推理,记录每次耗时。
- 统计第 50 百分位和 95 百分位耗时,同时记录平均值。
- 单片测试期间关闭后台负载,固定 CPU 调频策略或者至少监控频率,避免变频造成误差。
另外还有一个细节:输入张量复用。默认调用 inference 时,每次都会做输入数据拷贝和内存分配,测试时可以提前把输入数据准备好,循环内只更新数据内容,减少 IO 干扰。实测下来这个操作能带来几个毫秒的差异,在算帧率时不可忽略。
3.3 性能实测汇总与分析
下面是我这次实验记录到的核心数据,所有模型输入统一 224x224,INT8 量化,NPU 模式为 AUTO 调度:
| 模型 | 量化方式 | NPU 平均耗时 (ms) | 实测帧率 (FPS) | Top-1 相对损失 |
|---|---|---|---|---|
| MobileNetV3-Small | INT8 | 35 | 28 | 0.6% |
| MobileNetV3-Large | INT8 | 48 | 21 | 0.8% |
| ShuffleNetV2 1.0x | INT8 | 32 | 31 | 0.5% |
| ResNet18 | INT8 | 55 | 18 | 1.0% |
| ResNet50 | INT8 | 145 | 7 | 1.3% |
| EfficientNet-B0 | INT8 | 78 | 13 | 1.5% |
| MobileViT-XXS | INT8 | 95 | 10 | 1.8% |
纯 CPU 多线程模式也全部测了一遍,印象最深的是 MobileNetV3-Small 大约 120ms,ResNet50 直接到了 700ms 以上。这说明 CPU 兜底路径虽然可用,但实时性基本别想,只能做小批量或非实时任务。
从数据里能看出几个有意思的结论。ShuffleNetV2 反而是帧率最高的,这和理论 FLOPs 排名并不完全一致,因为它在 NPU 上的算子映射比较干净,没有太多奇奇怪怪的激活函数和 reshuffle 开销。MobileViT-XXS 的账面 FLOPs 很低,但 NPU 上只有 10 FPS 左右,最主要原因是 attention 部分的 reshape、transpose、softmax 这些操作在 NPU 上会被拆成多个算子,每一层都有额外内存搬运,实际执行效率远不如 CNN 的卷积算子那么整齐。EfficientNet-B0 的情况类似,它的 Swish 激活在 RKNN 里不是原生算子,部分层落到 CPU 执行,把整体耗时拉高了。
4. 部署中的典型问题与排查实录
4.1 ONNX 转 RKNN 失败:算子兼容的坑
转换阶段遇到的第一类问题就是算子不支持或部分算子落 CPU。比如 EfficientNet 里的 Swish 激活函数,在部分 RKNN 版本中被标记为非 NPU 算子,转换虽然能通过,但推理时这部分会默默跑到 CPU 上,帧率直接少一半。这类问题排查思路比较固定:
- 转换时开启更详细的日志输出,RKNN 构建日志里会明确提示哪些节点被分配到 CPU。
- 用 onnxsim 精简图,把多余节点清理掉,有时候问题就消失了。
- 如果确认为特定算子不兼容,尝试替换成等价结构,比如把 Swish 换成 ReLU6 或带固定系数的近似版本,重新导出 ONNX。
MobileViT 转换时还遇到过一个情况,它内部的 attention 使用了大量动态 shape 操作,RKNN 静态图推理无法处理这类动态维度。我的处理方式是把输入分辨率固定为 224x224,并且在导出 ONNX 时用固定尺寸的 dummy input,避免动态轴进入 RKNN 解析环节。
4.2 量化精度损失的根因与处理
量化掉精度是部署流程里最烦人的问题,我这次也专门做了控制变量实验。第一个典型案例是 MobileViT-XXS,第一次转换时为了省事只准备了 10 张校准图,量化后 Top-1 直接掉了 8.7%,几乎不能用了。后来把校准集扩充到 300 张,覆盖不同类别和光照,掉点降到了 1.8%,虽然还是比纯 CNN 模型高,但已经处于可接受范围。
第二个典型案例是归一化参数不一致。ONNX 导出时用了 ImageNet 的 mean/std,但 RKNN config 里我填写的是 [0,1] 归一化,结果 FP32 模拟就出现精度崩坏,根本到不了量化那一步。检查方式很简单,拉出转换后的模型跑同一张图,对比 ONNX Runtime 的输出向量余弦相似度,如果相似度低于 0.9,先检查输入预处理而不是模型结构。
第三个情况是敏感算子导致的精度损失,主要出现在 MobileViT 的 attention 和 EfficientNet 的某些深度可分离卷积层。这类问题可以通过混合量化缓解,对敏感层单独配置量化策略,但 RK3568 的 NPU 对混合精度的支持不像高端芯片那么灵活,操作空间有限。我最后的妥协方案是优先保精度,换用 MobileNetV3-Large,它在这个平台上的量化表现明显更稳。
4.3 模型跑不快:算子落到 CPU 的排查
性能问题的第一排查目标永远是看看哪些层没走 NPU。RKNN 的构建日志里会输出每层算子执行的硬件信息,常见标志是"CPU"或"NPU"字样。如果日志里 CPU 节点过多,实际推理速度就一定上不去。
我遇到过两个具体案例。一个是 EfficientNet-B0 的 Swish 问题,如前所述,日志里能看到大量 CPU 节点;另一个是 MobileViT 的 softmax 和 reshape 链,整个 attention 块几乎都落到 CPU 上执行。前者我把激活函数替换后重测,帧率从 9 提升到 13;后者结构替代比较困难,只能接受它的性能现状,或者换模型。
还有一个容易忽略的问题:内存拷贝。RKNN 推理时如果每次输入都用 numpy 数组直接传,会触发额外的数据拷贝操作。如果对帧率极其敏感,建议用 C API 或者 RKNNLite 的输入输出复用接口,减少 Python 层的数据搬运开销。我实验里 Python 接口对比 C 接口,在 MobileNetV3-Small 上有大约 5% 的耗时差距,不算大,但积少成多。
4.4 内存占用异常与推理稳定性
内存问题在测试前期没引起我注意,后来连续加载不同模型时发现进程物理内存一路走高。排查了半天才找到根因:release()没有正确调用,以及每次init_runtime()都会重新分配缓冲。在实时任务里频繁反复初始化运行时环境非常不划算,正确做法是加载一次模型,初始化一次运行时,然后不断复用同一个实例做推理,直到进程退出前再统一释放。
另外补充一点,RKNNLite 多次推理后内存小幅上涨是正常现象,只要不是持续线性增长就不用太紧张。如果出现不断上涨,优先怀疑输入输出张量没有复用,每次 inference 都重新创建了大型缓冲,Python 层的垃圾回收又不及时,内存自然越堆越高。
4.5 问题速查表
为了便于后续回顾,我把这段时间遇到的典型问题整理成一张表:
| 问题现象 | 可能原因 | 排查建议 |
|---|---|---|
| 转换失败提示算子不支持 | opset 版本过高 / 特定算子未映射 | 降 opset 到 11-12,替换不兼容激活函数 |
| 转换成功但帧率很低 | 部分算子落到 CPU 执行 | 在 build 日志中搜 CPU 节点,逐个替换 |
| 量化后精度剧烈下降 | 校准集过少 / mean-std 错误 | 扩充校准集到 200 张以上,核对训练归一化参数 |
| 推理结果类别完全不对 | 通道顺序错误 / 输入尺寸与训练不一致 | 检查 BGR-RGB 转换,确认 resize 与训练保持一致 |
| 内存持续增长 | 未释放模型 / 未复用输入输出缓冲 | 正确调用 release,复用同一运行时实例 |
| 首次推理耗时异常高 | 缓存未预热 / NPU 调度未就绪 | 用 10 次循环预热后再统计耗时 |
5. 模型选型建议与优化扩展
5.1 从实验数据能得出什么结论
这次实验跑完,对于 RK3568 上的图像分类模型选型,我心里基本有一个明确的优先级顺序。
优先选型梯队:MobileNetV3-Small 和 ShuffleNetV2 是闭眼可选的组合。量化掉点都控制在 1% 以内,帧率在 28 到 31 FPS,内存占用也不高,对实时视频流场景非常友好。如果后续产品迭代不频繁,直接在这两个里选一个作为主模型就行。
精度兜底梯队:ResNet18 是精度和速度的一个平衡点,18 FPS 虽然不算高,但分类精度比轻量模型稳很多。ResNet50 在这个平台上已经不太适合做实时推理了,7 FPS 只能用在非实时批处理场景,或者配合其他加速手段使用。
谨慎使用梯队:MobileViT-XXS 和 EfficientNet-B0 不是不能用,但需要额外投入调优成本。MobileViT 的量化敏感度和算子落 CPU 问题是硬伤;EfficientNet 的激活函数替换会略微影响精度。除非你的项目有专人维护部署链路,否则不建议作为首发方案。
5.2 后续扩展方向与最终建议
如果时间允许,我接下来的优化方向有两件事。第一,对 MobileViT 这类量化敏感模型做混合量化尝试,敏感算子走更高精度,其余层继续 INT8,虽然 RK3568 对混合精度支持有限,但值得验证。第二,尝试输入分辨率降到 160 或 192,同时配合模型微调,而不是直接拿 224 的模型降分辨率部署。直接降分辨率会让小目标识别能力明显下降,微调一下能补回来不少。
从这次实验的经验来看,我给大家的核心建议是:在 RK3568 上做图像分类部署,不要先纠结模型多先进,先跑通一个 MobileNetV3-Small 加 300 张校准图的 INT8 量化链路,再用实际数据决定要不要升级模型。整个过程最难的不是框架配置,而是理解每个环节的变量——归一化、校准集、算子落 CPU、内存复用——这些在我刚接触的时候都以为是框架自动处理的事,实际踩过坑才知道,RK3568 的 NPU 虽然方便,但每一个细节都值得亲手确认一遍。后续真要继续往深了做,我会先把这次记录的四类排查经验固化成团队内部的部署检查清单,再针对具体项目的类别分布做数据增强和量化校准,到时候再写一篇实际的落地复盘出来。