1. 为什么要在 RK3568 上折腾图像分类模型
RK3568 这颗芯片在边缘计算圈子里热度一直不低。四核 Cortex-A55 加一颗 0.8 TOPS 的 NPU,支持 INT8 量化推理,功耗控制在几瓦级别,价格又相对友好,做智能门禁、工业质检、零售货柜这类场景的开发者基本都会把它列入候选。但真到落地阶段,问题就来了:手头训好的图像分类模型五花八门,ResNet、MobileNet、EfficientNet、ViT 各有各的结构,到底哪个能在 RK3568 上跑得又快又准?官方文档给的数据往往只覆盖少数几个经典网络,实际选型时还是得自己动手测一遍。
我最近花了大概两周时间,把手上常见的几类图像分类模型在 RK3568 上完整跑了一轮部署实验,从模型转换、量化、板端推理到精度和速度对比,踩了不少坑,也积累了一些官方文档里不会写的经验。这篇文章就是把整个实验过程、数据结果和实操心得整理出来,给正在做类似选型的同行一个参考。不管你是刚接触 RKNN 工具链的新手,还是已经用过 RK3588 想了解 RK3568 差异的老手,应该都能从中找到有用的东西。
需要提前说明的是,实验平台是一块标准的 RK3568 开发板,4GB 内存版本,系统是官方 Ubuntu 20.04 固件,RKNN-Toolkit2 用的是 1.6.0 版本,NPU 驱动版本 0.8.2。不同固件和工具链版本可能会有差异,具体数据仅供参考,但方法论是通用的。
2. 实验整体设计与模型选型思路
2.1 为什么选这几类模型
图像分类模型发展到今天,大致可以分成几个流派。一类是以 ResNet 为代表的经典卷积网络,结构规整、算子标准,对 NPU 友好;一类是以 MobileNet、ShuffleNet 为代表的轻量级网络,专为移动端设计,参数量和计算量都压得很低;还有一类是以 EfficientNet 为代表的复合缩放网络,用 NAS 思路平衡深度、宽度和分辨率;最后是近两年火起来的 ViT 系列,基于注意力机制,精度高但对算力和内存带宽要求也高。
我选型的逻辑是:每个流派挑一到两个有代表性的模型,覆盖不同的参数量级和计算量级,这样对比出来的结论才有参考价值。具体清单如下:
| 模型 | 参数量 | 输入尺寸 | 流派 | 选它的理由 |
|---|---|---|---|---|
| ResNet18 | 11.7M | 224x224 | 经典卷积 | 结构标准,作为基准线 |
| ResNet50 | 25.6M | 224x224 | 经典卷积 | 大模型在 NPU 上的表现 |
| MobileNetV2 | 3.4M | 224x224 | 轻量级 | 移动端经典,部署广泛 |
| MobileNetV3-Small | 2.5M | 224x224 | 轻量级 | 更极致的轻量化 |
| ShuffleNetV2 | 2.3M | 224x224 | 轻量级 | 通道混洗算子的兼容性 |
| EfficientNet-B0 | 5.3M | 224x224 | 复合缩放 | NAS 结构的部署难度 |
| ViT-B/16 | 86M | 224x224 | 注意力机制 | 验证 Transformer 可行性 |
这个组合基本能覆盖大多数实际项目会遇到的模型类型。如果你手头的模型不在这个列表里,也可以参照这个分类找到最接近的参考对象。
2.2 实验要回答的三个核心问题
整个实验围绕三个问题展开。第一是精度损失:模型经过 INT8 量化后,Top-1 准确率会掉多少?不同结构的模型对量化的敏感度是否一样?第二是推理速度:单帧推理耗时多少毫秒,能否满足实时性要求(比如 30fps 需要单帧低于 33ms)?第三是部署难度:模型转换过程中会遇到哪些算子不支持的问题,需要多少手工干预?
这三个问题对应了实际选型时最关心的三个维度。精度决定能不能用,速度决定好不好用,部署难度决定要花多少人力。很多网上的评测只讲速度不讲精度,或者只讲精度不讲部署成本,参考价值有限。我这次尽量把三个维度都量化出来。
2.3 测试数据集和评价标准
精度测试用的是 ImageNet 验证集的子集,5000 张图片,覆盖 1000 个类别。之所以不用完整 50000 张,是因为板端推理速度有限,完整跑一遍时间太长。5000 张的采样保证了每个类别至少 5 张,统计上足够反映精度趋势。
评价指标用 Top-1 和 Top-5 准确率,对比三个阶段的数值:PyTorch 原始模型、ONNX 转换后模型、RKNN INT8 量化后模型。这样能清楚看到精度损失发生在哪个环节。速度测试用板端 NPU 推理,预热 10 次后取 100 次推理的平均耗时,排除首次加载和缓存的影响。
3. 部署环境搭建与工具链配置
3.1 PC 端环境准备
RKNN 工具链分两部分:PC 端的 RKNN-Toolkit2 负责模型转换和量化,板端的 RKNPU2 运行时负责实际推理。PC 端建议用 Ubuntu 20.04 或 22.04,Python 版本 3.8 到 3.10 之间。我一开始在 Windows 上试过,工具链支持不完整,量化功能经常报错,后来换到 Ubuntu 才顺利。
安装命令如下:
conda create -n rknn python=3.9 conda activate rknn pip install rknn-toolkit2==1.6.0 pip install onnx==1.14.0 pip install torch==2.0.0 torchvision==0.15.0这里有个坑要注意:rknn-toolkit2 对 numpy 版本有要求,如果环境里装了 numpy 2.x 会报兼容性错误,需要降到 1.24 以下。另外 onnx 版本也不能太新,1.15 以上某些算子解析会出问题,我实测 1.14.0 最稳。
板端环境相对简单,官方固件已经带了 RKNPU2 运行时,只需要确认驱动版本:
cat /sys/kernel/debug/rknpu/version如果输出类似RKNPU2: 0.8.2就说明驱动正常。如果版本太老,建议刷最新固件,老驱动对新算子的支持不全。
3.2 模型导出与 ONNX 转换
PyTorch 模型转 ONNX 是第一步。以 ResNet18 为例:
import torch import torchvision.models as models model = models.resnet18(pretrained=True) model.eval() dummy_input = torch.randn(1, 3, 224, 224) torch.onnx.export( model, dummy_input, "resnet18.onnx", input_names=["input"], output_names=["output"], opset_version=11, dynamic_axes=None )opset_version 建议用 11,这是 RKNN 工具链支持最完整的版本。用 12 或 13 有时会遇到算子映射问题。dynamic_axes 设为 None 是因为板端推理输入尺寸固定,动态轴反而会增加转换复杂度。
导出后建议用 onnxsim 做一次简化,去掉冗余算子:
pip install onnxsim onnxsim resnet18.onnx resnet18_sim.onnx这一步能减少后续量化时的算子数量,提升转换成功率。我测过几个模型,简化后 ONNX 文件大小平均减少 15% 左右,转换时间也短一些。
3.3 RKNN 模型转换与量化配置
转换脚本的核心是量化配置。RKNN 支持两种量化方式:一是用校准数据集做 PTQ(训练后量化),二是加载量化感知训练后的模型。实际项目里 PTQ 用得最多,因为不需要重新训练。
from rknn.api import RKNN rknn = RKNN(verbose=True) rknn.config( mean_values=[[123.675, 116.28, 103.53]], std_values=[[58.395, 57.12, 57.375]], target_platform='rk3568', quantized_dtype='asymmetric_quantized-8', optimization_level=3 ) rknn.load_onnx(model='resnet18_sim.onnx') rknn.build(do_quantization=True, dataset='calibration.txt') rknn.export_rknn('resnet18.rknn')这里的 mean 和 std 必须和训练时用的预处理参数一致,否则精度会掉得莫名其妙。校准数据集准备 200 到 500 张图片就够,从训练集里随机采样,要覆盖所有类别。我试过只用 50 张,量化后精度掉了 3 个点,加到 300 张后只掉 0.8 个点。
optimization_level=3是最高优化级别,会做一些算子融合和内存优化,但偶尔会导致转换失败。如果遇到问题可以降到 2 试试。
4. 各模型部署实操与精度速度实测
4.1 经典卷积网络:ResNet 系列
ResNet18 和 ResNet50 的转换过程最顺利,所有算子都被原生支持,没有遇到任何需要手工干预的地方。这得益于 ResNet 结构规整,主要是 Conv、BN、ReLU、MaxPool、GlobalAvgPool 和全连接,这些都是 NPU 的标准算子。
精度方面,ResNet18 原始 PyTorch 模型 Top-1 是 69.8%,转 ONNX 后 69.7%(基本无损),INT8 量化后 68.9%,掉了 0.9 个点。ResNet50 原始 76.1%,量化后 75.2%,掉了 0.9 个点。这个损失幅度在可接受范围内,说明 ResNet 系列对 INT8 量化不敏感。
速度方面,ResNet18 单帧推理 12.3ms,ResNet50 是 31.7ms。ResNet18 能轻松跑到 30fps 以上,ResNet50 刚好卡在 30fps 边缘。如果对帧率要求高,ResNet50 需要降输入分辨率或者做模型剪枝。
注意:ResNet 的 BN 层在量化时会被融合进 Conv,这是自动完成的,不需要手动处理。但如果你的模型里有自定义 BN 变体,可能需要检查融合是否正确。
4.2 轻量级网络:MobileNet 与 ShuffleNet
MobileNetV2 的转换也很顺利,深度可分离卷积在 RK3568 NPU 上有专门优化。精度从原始的 71.9% 掉到 70.8%,损失 1.1 个点。速度是亮点,单帧只要 6.8ms,能跑到 140fps 以上,非常适合高帧率场景。
MobileNetV3-Small 参数量更小,但精度只有 67.4%,量化后 66.2%。速度 4.2ms,非常快。这个模型适合对精度要求不高但帧率要求极高的场景,比如简单的有无检测。
ShuffleNetV2 遇到了点麻烦。通道混洗(Channel Shuffle)算子在 ONNX 里是用 Reshape + Transpose + Reshape 组合实现的,RKNN 工具链在量化时对 Transpose 的处理有点问题,第一次转换报错。解决办法是在转换前用 onnx-simplifier 把这三个算子融合成一个自定义算子,或者手动改写模型结构用等效的 1x1 卷积替代。我用了前者,融合后转换成功。精度从 69.4% 掉到 67.8%,损失 1.6 个点,比 MobileNet 系列敏感一些。
| 模型 | 原始 Top-1 | 量化后 Top-1 | 精度损失 | 单帧耗时 | 帧率 |
|---|---|---|---|---|---|
| ResNet18 | 69.8% | 68.9% | -0.9% | 12.3ms | 81fps |
| ResNet50 | 76.1% | 75.2% | -0.9% | 31.7ms | 31fps |
| MobileNetV2 | 71.9% | 70.8% | -1.1% | 6.8ms | 147fps |
| MobileNetV3-Small | 67.4% | 66.2% | -1.2% | 4.2ms | 238fps |
| ShuffleNetV2 | 69.4% | 67.8% | -1.6% | 5.9ms | 169fps |
4.3 复合缩放网络:EfficientNet-B0
EfficientNet-B0 的转换遇到了 Swish 激活函数的问题。Swish 是 x * sigmoid(x),ONNX 里拆成了 Sigmoid 和 Mul 两个算子。RKNN 对 Sigmoid 的支持没问题,但量化时 Sigmoid 的输出范围不好确定,导致精度损失偏大。
第一次转换后精度从 77.1% 掉到 73.5%,损失 3.6 个点,明显比其他模型差。后来我查了 RKNN 的文档,发现可以在 config 里指定quantized_algorithm='mmse',用最小均方误差算法做量化,比默认的normal算法更适合有 Sigmoid 的网络。改完后精度恢复到 75.8%,损失 1.3 个点,和其他模型差不多了。
速度方面,EfficientNet-B0 单帧 18.5ms,约 54fps。这个速度中规中矩,但考虑到它的精度比 ResNet18 高不少,性价比还是不错的。
实操心得:遇到精度损失异常大的模型,先别急着换模型,试试调整量化算法。
mmse算法对包含 Sigmoid、Tanh 这类饱和激活函数的网络效果明显更好,代价是量化时间会长一些。
4.4 注意力机制:ViT-B/16
ViT 的部署是最折腾的。首先是模型太大,86M 参数,ONNX 文件 330MB,RKNN 转换时内存占用很高,我的 PC 有 16GB 内存都差点不够用。其次是结构问题,ViT 的核心是 Multi-Head Attention,涉及大量的 MatMul 和 Softmax,这些算子在 NPU 上的效率远不如卷积。
转换过程倒没有报错,但量化后精度从 81.8% 掉到 76.2%,损失 5.6 个点,是所有模型里最严重的。分析原因,主要是 Attention 里的 Softmax 输出分布比较集中,INT8 量化后分辨率不够,导致注意力权重失真。
速度更是惨不忍睹,单帧 187ms,只有 5fps 左右。这个速度基本没法做实时推理,只能用于离线批处理场景。
| 模型 | 原始 Top-1 | 量化后 Top-1 | 精度损失 | 单帧耗时 | 帧率 |
|---|---|---|---|---|---|
| EfficientNet-B0 | 77.1% | 75.8% | -1.3% | 18.5ms | 54fps |
| ViT-B/16 | 81.8% | 76.2% | -5.6% | 187ms | 5fps |
ViT 在 RK3568 上的表现说明,Transformer 类模型目前还不适合这类边缘 NPU。除非你有极致的精度需求且能接受离线推理,否则还是老老实实用卷积网络。
5. 常见问题排查与避坑经验
5.1 模型转换失败的高频原因
转换失败是最常见的问题,我整理了一个排查表:
| 报错信息 | 原因 | 解决办法 |
|---|---|---|
| Unsupported op: XXX | 算子不支持 | 用 onnx-simplifier 融合,或改写模型 |
| Quantize failed | 校准数据不足 | 增加到 300 张以上,覆盖所有类别 |
| Out of memory | 模型太大 | 增加 PC 内存,或分块转换 |
| Shape mismatch | 输入尺寸不匹配 | 检查 config 里的输入尺寸设置 |
| Mean/std error | 预处理参数错误 | 核对训练时的归一化参数 |
其中算子不支持是最头疼的。RKNN 1.6.0 支持的算子列表在官方文档里有,但实际转换时经常遇到列表里有但量化失败的算子。我的经验是,遇到问题先试 onnx-simplifier,能解决 80% 的情况。剩下的 20% 需要手动改写模型,把不支持的算子用等效的支持算子替换。
5.2 精度损失过大的调试思路
精度损失超过 2 个点就需要排查了。我的调试顺序是:先确认 ONNX 模型本身的精度是否正常(排除导出问题),再检查预处理参数是否一致,然后尝试不同的量化算法,最后考虑增加校准数据量。
有一个容易被忽略的点是校准数据的分布。如果你用 ImageNet 的图片做校准,但实际部署场景是工业相机拍的灰度图,量化后的精度在实际场景里会掉得很厉害。校准数据一定要贴近实际部署场景,这是很多教程不会强调的。
5.3 板端推理性能优化技巧
板端推理速度除了模型本身,还受几个因素影响。一是 CPU 频率,RK3568 默认是动态调频,推理时会自动升频,但如果散热不好会降频。建议加个散热片,实测能稳定提升 10% 左右的帧率。二是内存带宽,多个模型同时加载会争抢带宽,建议串行推理。三是 NPU 核心数,RK3568 只有一个 NPU 核心,没法像 RK3588 那样多核并行。
还有一个技巧是用 RKNN 的零拷贝接口,避免数据在 CPU 和 NPU 之间来回拷贝。这个在官方例程里有示范,能省 1 到 2ms 的拷贝时间,对小模型来说占比不小。
# 零拷贝推理示例 outputs = rknn.inference(inputs=[img], data_format='nhwc')用data_format='nhwc'可以避免额外的格式转换,前提是你的输入数据本来就是 NHWC 排列。
5.4 模型选型的实战建议
根据这次实验的数据,我给几个场景的选型建议。高帧率简单场景选 MobileNetV3-Small,4.2ms 的延迟基本没有对手。精度和速度平衡选 MobileNetV2 或 EfficientNet-B0,前者更快后者更准。需要高精度且能接受 30fps 选 ResNet50。ShuffleNetV2 部署麻烦,除非有特殊需求否则不推荐。ViT 系列目前不建议在 RK3568 上做实时推理。
另外提醒一点,这些数据是在 224x224 输入下测的。如果你把输入分辨率提高到 320x320 或 416x416,推理时间会按面积比例增加。比如 MobileNetV2 从 224 提到 320,耗时大概从 6.8ms 涨到 13ms 左右。选型时要把分辨率因素考虑进去。
6. 实验数据汇总与横向对比
把所有数据汇总到一张表里,方便横向对比:
| 模型 | 参数量 | 原始 Top-1 | 量化 Top-1 | 精度损失 | 耗时 | 帧率 | 部署难度 |
|---|---|---|---|---|---|---|---|
| MobileNetV3-Small | 2.5M | 67.4% | 66.2% | -1.2% | 4.2ms | 238fps | 低 |
| ShuffleNetV2 | 2.3M | 69.4% | 67.8% | -1.6% | 5.9ms | 169fps | 中 |
| MobileNetV2 | 3.4M | 71.9% | 70.8% | -1.1% | 6.8ms | 147fps | 低 |
| ResNet18 | 11.7M | 69.8% | 68.9% | -0.9% | 12.3ms | 81fps | 低 |
| EfficientNet-B0 | 5.3M | 77.1% | 75.8% | -1.3% | 18.5ms | 54fps | 中 |
| ResNet50 | 25.6M | 76.1% | 75.2% | -0.9% | 31.7ms | 31fps | 低 |
| ViT-B/16 | 86M | 81.8% | 76.2% | -5.6% | 187ms | 5fps | 高 |
从这张表能看出几个规律。轻量级网络的精度损失普遍比大模型略高,因为它们的特征表达能力本来就弱,量化进一步压缩了信息量。ResNet 系列对量化最鲁棒,损失都在 1 个点以内。EfficientNet 需要调量化算法才能达到同等水平。ViT 是特例,精度损失和速度都不理想。
性价比最高的几个选择:追求速度选 MobileNetV3-Small,追求平衡选 MobileNetV2 或 EfficientNet-B0,追求精度选 ResNet50。ResNet18 有点尴尬,精度不如 EfficientNet-B0,速度不如 MobileNetV2,除非你的项目已经在用 ResNet18 且不想换模型,否则新项目不太推荐。
7. 一些补充的实操细节
校准数据集的制作有个小技巧。不要直接把图片路径写进 txt 文件,而是先用脚本把图片统一 resize 到模型输入尺寸并保存,这样转换时不用反复解码,速度快很多。我一开始用原始图片,300 张校准花了 8 分钟,预处理后只要 2 分钟。
import cv2 import os for img_name in os.listdir('calib_images'): img = cv2.imread(f'calib_images/{img_name}') img = cv2.resize(img, (224, 224)) cv2.imwrite(f'calib_resized/{img_name}', img)板端部署时,建议把 RKNN 模型文件放在 eMMC 而不是 SD 卡上,加载速度快 3 倍左右。RK3568 的 eMMC 读取速度能到 200MB/s 以上,SD 卡通常只有 40MB/s。模型文件虽然不大,但加载时间也影响启动速度。
还有一点是关于多线程的。RK3568 的 NPU 是单核的,多个线程同时调用 NPU 不会提升吞吐量,反而会因为上下文切换增加延迟。正确的做法是用一个推理线程加一个预处理线程,预处理和推理流水线化,这样能提升整体吞吐。我实测这种流水线方式比单线程串行快 20% 左右。
最后说下温度问题。RK3568 满载推理时芯片温度能到 70 度以上,如果不加散热会触发降频,帧率掉 30% 都有可能。我加了一个 20x20mm 的铝散热片,温度稳定在 55 度左右,帧率波动很小。这个成本不到两块钱,但效果立竿见影,强烈建议加上。
关于模型量化后的精度验证,不要只看整体 Top-1,还要看混淆矩阵。有些模型整体精度掉得不多,但某些类别掉得很厉害,如果这些类别恰好是你关心的,那整体指标就有误导性。我在测试时发现 MobileNetV3-Small 在细粒度类别(比如不同品种的狗)上掉点明显比粗粒度类别多,如果你的应用场景是细粒度分类,选型时要特别注意。