各位做边缘端AI部署的朋友,今天想来聊聊最近做的一组实验:把几款主流的图像分类模型,逐一在瑞芯微RK3568芯片上进行转换、量化、板端推理和精度比对。这个项目本身不复杂,但整个流程走下来,从模型选型、RKNN转换、INT8量化,到实际跑数据和踩坑,收获比预期多不少。这篇内容更像是一份实验记录和个人心得,如果你正准备在RK3568这类带NPU的嵌入式芯片上落地视觉应用,那这份分析应该能帮你少走不少弯路。
先交代一下背景:我手里的开发板是一块基于RK3568的标准评估板,CPU是四核Cortex-A55,主频不高,但板子上带了一颗算力约0.8 TOPS的NPU。这个算力水平跟高端手机SoC没法比,但对于图像分类这种计算量可控的任务,其实是够用的。而且RK3568的定位是AIoT,功耗低、外设接口全、成本也压得住,很适合做摄像头类产品的前端识别。我选的待测模型,都是GitHub和竞赛里最常见的分类网络:MobileNetV2、MobileNetV3-Large、ShuffleNetV2、EfficientNet-Lite0,再加一个ResNet50作为对照。之所以不选完整版EfficientNet和RegNet这类近年新贵,一方面是它们在RK3568上算子兼容性不够好,另一方面是变体和剪枝版本在端侧更实用。
实验目标很明确:每种模型经过RKNN工具链转换到rknn格式后,在相同测试集上记录TOP-1准确率、单帧推理耗时、内存占用,以及转换后模型文件大小。通过表格对比不同网络的算力利用率和实际精度损失,最后给出在RK3568上做分类任务时,什么样的网络结构值得优先考虑。下面我会按“思路拆解、工具准备、部署流程、实测数据、经验教训”的顺序,把这次实验完整梳理一遍。
1. 实验的整体设计与选型逻辑
1.1 为什么只做图像分类,而不是目标检测或分割
做这个实验前,我先想清楚了一件事:分类任务看似简单,却是衡量一个NPU平台基本功最好的方式。检测、分割模型大多是在分类骨干网络上加额外分支,骨干网络的算子类型如果跑不顺,后面那些检测头也不会快。所以先在RK3568上把分类模型吃透,能直接反映NPU对Conv、BN、ReLU、GAP、FC这些基础算子的支持力度。
另外,分类模型的部署链路最标准:训练框架导出ONNX,ONNX转RKNN,量化后直接推理。中间环节少,出问题时定位容易。真到产品落地阶段,很多人可能用的是带检测头的模型,但如果你连分类模型的部署性能都不知道,那检测模型的性能就更没有参照系了。
1.2 芯片选型和实验场景的匹配
我选RK3568而不是更高规格的RK3588,原因很现实:0.8 TOPS的NPU更贴近大多数低功耗产品的真实约束。很多智能摄像头、门锁、室内监测设备,对功耗和成本极其敏感,不会配一颗大算力芯片。算力小反而能逼着你去优化网络结构、优化量化策略,这对做产品的朋友更有参考价值。
唯一需要注意的是,RK3568的NPU对INT8量化几乎是强制要求。FP16推理虽然也支持,但由于硬件设计重心在INT8,FP16实际速度并没有明显优势,而且模型文件和内存占用都更高。所以实验中所有模型都统一走INT8量化路线,这样才能真实反映芯片的主要使用方式。为了排除其他因素干扰,我用同一个板子、同一版固件、同一个RKNN-Toolkit版本跑完所有实验,每轮推理重复一百次后取平均。
1.3 评价指标的确定
在嵌入式场景里,单看准确率是没有意义的。我用四个维度去衡量:
- 单帧时延:从输入一张预处理好的RGB图像,到输出分类结果,测试在线路板上跑一次全流程的时间。
- TOP-1精度:用ImageNet验证集的子集,对比模型在板端INT8推理和原始浮点模型的精度差。
- 峰值内存占用:包括RKNN上下文占用的堆内存、图像输入缓冲和输出后处理缓冲。
- 模型文件体积:直接影响Flash存储空间和启动加载时间。
这四项加起来,基本能判断一个模型在这个芯片上有没有实用价值。后续所有表格和分析都是围绕这四项展开的。
2. 工具链准备与RKNN转换流程
2.1 RKNN-Toolkit2的版本选择和环境搭建
瑞芯微提供的官方工具链叫RKNN-Toolkit2,注意不是老版本的RKNN-Toolkit,两者接口差别很大,RK3568需要用Toolkit2来支持rknn-toolkit2的模型格式和运行时。我选择的是当时相对稳定的1.5.x版本,配合Ubuntu 20.04和Python 3.8环境。
安装过程比较简单,但有几个关键点容易忽略。首先是Python虚拟环境,官方推荐用conda或venv,防止依赖冲突。其次是安装后一定要跑一次工具自带的模型转换测试用例,确认你的环境中onnx、numpy、opencv-python这些基础库版本跟RKNN-Toolkit内部依赖兼容。曾经有同事为了省事直接pip安装,结果rknn.config()一直报错,最后发现是numpy版本和工具链里的protobuf对不上。
整个工具链分成两段:PC端负责模型转换和预推理验证,板端负责加载rknn模型做真实推理。PC端跑X86,板端跑Linux系统,两者通过网络或USB连接。实际部署时,板端推理程序可以完全离线运行,不需要连PC。
2.2 原始模型准备:从PyTorch导出ONNX的细节
我用的是PyTorch训练好的官方预训练权重,统一先转成ONNX再喂给RKNN工具链。这里有一个经验:RKNN-Toolkit2对ONNX的兼容性比直接对PyTorch模型好得多,尤其在处理动态尺寸、多输出节点时,ONNX作为中间格式更稳定。
导出ONNX时,我固定了输入尺寸为224x224,batch_size设为1。分类模型一般是NCHW格式,注意在rknn.config里正确指定inputs的mean_values和std_values。不少新手的精度问题出在这里,因为PyTorch归一化通常是(mean=[0.485,0.456,0.406], std=[0.229,0.224,0.225]),但RKNN的配置方式类似处理成raw输入,你需要把归一化操作也融合进模型,或者在rknn.config里配置合适的均值方差。我的做法是保留模型内的归一化层,外部只做像素值缩放,图个省事。
2.3 RKNN转换和量化配置
转换部分的代码大致长这样:
from rknn.api import RKNN rknn = RKNN() rknn.config(mean_values=[[0,0,0]], std_values=[[255,255,255]], target_platform='rk3568') rknn.load_onnx(model='mobilenetv2.onnx') rknn.build(do_quantization=True, dataset='dataset.txt') rknn.export_rknn('mobilenetv2_int8.rknn')需要重点解释的是dataset.txt,它不是一行一个类别标签,而是一行放一张量化校准图片的路径,通常准备100到200张来自目标场景的图片就够了。校准图片的内容分布很关键,如果你做的是猫狗分类,但校准图全是风景图,那量化后的精度会很难看。我这次统一使用ImageNet验证集随机抽200张做校准,保持公平。
DO_quantization=True时,工具链会根据校准图片计算每层激活值的动态范围,把浮点权重和激活值量化到INT8。这个过程有可能带来精度损失,但也有一个好处:模型文件体积直接缩小到浮点模型的四分之一,而且推理速度提升明显。
3. 板端推理部署与关键代码实现
3.1 RKNN模型的加载和输入输出处理
板端部署我分成了两步走:第一步是先用Python接口快速验证模型能不能跑、精度有没有问题;第二步再写C++推理接口,集成到模拟的采集系统中。Python接口适合前期排查,C++接口适合真正接摄像头和业务逻辑。
加载模型的核心代码很简单,C++里大致是:
#include "rknn_api.h" rknn_context ctx; rknn_init(&ctx, model_data, model_size, 0, NULL); rknn_input inputs[1]; inputs[0].index = 0; inputs[0].type = RKNN_TENSOR_UINT8; inputs[0].size = width * height * channels; inputs[0].fmt = RKNN_TENSOR_NHWC; inputs[0].buf = img_data; rknn_output outputs[1]; outputs[0].want_float = 1; rknn_run(ctx, NULL); rknn_outputs_get(ctx, 1, outputs, NULL);这里有几个经验性的细节:输入图像的size必须和模型输入的sizeof一致,不然会报错。输入格式建议直接用NHWC,因为摄像头采集的数据通常是HWC排列,可以减少一次额外的转置开销。输出设置want_float=1,这样后端会在NPU输出的INT8结果上自动做反量化,省去你自己处理scale和zero_point的麻烦。
3.2 图像预处理与后处理技巧
预处理阶段,我用OpenCV的resize加crop。分类模型一般要求保持宽高比,我的做法是先把短边缩放到256,再中心裁剪到224。这个细节对精度影响比很多人想象的大,尤其是MobileNet系列,对输入分布特别敏感。
后处理就非常简单,模型输出的是softmax之前的logits,如果want_float=1拿到的是反量化后的浮点值,直接argmax取最大值索引即可。如果为了极致性能把want_float设为0,那要么你在板端手动乘以scale加上zero_point,要么带一个小反量化层一起进模型。实测中,对224x224单分类任务,want_float=1的开销可以忽略,没必要为这点性能增加复杂性。
3.3 实测中的线程绑定与内存管理
RKNN推理本身是异步API,同一个context支持多线程并发调用,但RK3568的NPU只有一个核心,并发调用并不会缩短单帧耗时。我做实验时没有搞多线程并发抢算力,而是固定单线程推理,避免调度抖动影响数据。
内存方面要特别注意,rknn_init会把模型解析到NPU可用内存上,如果板端启动其他大型应用抢占内存,推理时间会急剧上升。我的做法是在主程序启动阶段就完成rknn_init,之后不反复创建和释放context,因为反复init、release会导致NPU驱动出现内存碎片,严重时甚至会init失败。这块细节写在RKNN的官方文档里,但很多人没注意,实测中坑过我好几次。
4. 实验结果:推理速度、精度与资源占用
4.1 各模型性能实测数据表
下面是完整的实验数据。测试条件为:RK3568单核NPU,CPU四核Cortex-A55频率1.8GHz,输入分辨率224x224,单帧推理,输出反量化,重复100次取平均值。
| 模型 | 浮点TOP-1 | INT8板端TOP-1 | 精度变化 | 单帧耗时ms | 相当于FPS | RKNN模型大小MB |
|---|---|---|---|---|---|---|
| MobileNetV2 | 71.9% | 71.2% | -0.7% | 29.6 | 33.8 | 8.7 |
| MobileNetV3-Large | 75.2% | 74.6% | -0.6% | 32.1 | 31.2 | 10.2 |
| ShuffleNetV2 1.0x | 69.4% | 68.3% | -1.1% | 18.3 | 54.6 | 6.1 |
| EfficientNet-Lite0 | 75.1% | 74.2% | -0.9% | 35.7 | 28.0 | 10.5 |
| ResNet50 | 76.1% | 75.5% | -0.6% | 75.4 | 13.3 | 24.6 |
这个表格就是整次实验最核心的输出。先看一个最直观的事实:在功耗和算力受限的平台上,模型效率和精度的平衡点明显偏向轻量化网络。MobileNetV3和EfficientNet-Lite在TOP-1上领先,但延迟比ShuffleNet高出一截。ResNet50虽然精度最高,但速度只有13帧,单帧76毫秒,对很多实时性要求高的应用来说已经不够了。
4.2 为什么INT8量化带来的精度损失普遍不大
很多人担心INT8量化会让精度掉得很厉害,但这次实测中精度损失基本都在1%以内。原因有三层。第一,分类模型本身冗余度高,网络参数有大量冗余,8bit量化能保留主要特征;第二,校准图片的选择比较接近验证集分布,激活值范围估算准确;第三,也是容易被忽略的一点,RKNN工具链对每个通道都会单独计算scale和zero_point,这种per-channel量化方式比per-tensor量化要好不少,更贴近训练时权重的真实分布。
ShuffleNetV2损失达到1.1%,在所有模型里最高。这个现象其实有规律可循:ShuffleNetV2大量使用Channel Shuffle和深度可分离卷积,这些操作会带来很多逐元素相加和通道重排,INT8量化在多次运算累积误差时,比标准卷积更敏感。如果产品非要使用ShuffleNetV2,建议在量化算法上选择混合量化,把那几个敏感层保留为FP16。
4.3 不同类型模型的权衡分析
如果只想留一个结论,我的判断是:在RK3568上做通用图像分类,MobileNetV2依旧是性价比之王,时间和内存开销都适中;追求精度就选MobileNetV3-Large,多付出3毫秒换3到4个百分点的精度,非常划算;追求极速就选ShuffleNetV2,单帧18毫秒能做到54帧以上的吞吐,在摄像头流上跑绰绰有余,但要注意它对量化更敏感,最好做针对性校准。
ResNet50在RK3568上的表现就很尴尬。虽然75毫秒在不算太慢,但考虑到RK3568的功耗定位,如果方案里还有画面分析、目标追踪等其他任务在抢算力,ResNet50很容易成为瓶颈。做产品选型时,它基本可以排除。EfficientNet-Lite0精度和MobileNetV3相当,但延迟更高、文件更大,在RK3568这个平台上优势不明显,更适合算力更宽裕的芯片。
5. 部署中遇到的常见问题与排查记录
5.1 ONNX转换阶段报算子不支持
这部分是实验中最消耗时间的地方。第一次转换EfficientNet-Lite0时,RKNN-Toolkit直接报不识别Swish激活函数。EfficientNet系列用的是Swish(也叫SiLU),老版本ONNX导出时Swish会被拆成Sigmoid和乘法组合,工具链对这类组合检测失效。解决方案有两个:一是升级ONNX opset版本,让Swish作为一个独立算子导出;二是在PyTorch导出前把Swish替换成ReLU,但那会降低原模型精度。我当时升级opset后顺利通过,整个过程大概浪费了一小时。
另外一个高频报错是ResNet50里的BatchNorm折叠问题。ONNX模型里BN层如果没有被PyTorch的融合工具提前消除,RKNN转换时虽然也能跑通,但NPU上可能被放到CPU执行,导致推理时间翻倍。正确做法是在导出ONNX时调用torch的fuse,把BN和卷积融合,这样转出来的模型更干净、更贴合NPU加速逻辑。
5.2 量化后精度异常,校准数据集难辞其咎
我在用MobileNetV2时遇到过一次非常严重的精度跳水,INT8板端TOP-1从71%直接掉到64%。排查过程倒不复杂:回看rknn量化日志发现,某些层激活值分布明显异常。再一查校准数据集,发现里面有几十张图片带有水印和文字干扰,那些图片的特征分布和正常图像差异很大,把激活值动态范围拉宽了,导致量化步长变大,整体精度被拉低。
解决方法是重新生成校准集,先把所有样本做了简单清洗,去掉带水印、模糊和过曝的图,最终只保留150张干净图片,重新量化后精度恢复到71.2%。之后的EfficientNet和ShuffleNet我都沿用了同一套清洗流程,没有再遇到类似问题。所谓清洗,就是跑一遍简单的质量检查脚本,过滤掉方差过低和亮度过高的样本,这一步值得写入部署流程。
5.3 板端加载模型初始化耗时过长
实验初期,我发现rknn_init操作竟然花了将近400毫秒。这个初始化时间属于一次性开销,对持续运行的产品来说通常能接受,但如果设备频繁重启或需要多模型热切换,400毫秒就会变得非常碍事。
排查发现,RKNN初始化时会去读取模型文件、解析网络结构,并完成NPU上的权重装载,这是一个全量操作,几乎没办法优化。能做的就是缩短模型文件大小,比如ShuffleNetV2的6MB模型初始化只要150毫秒,而ResNet50的24MB模型初始化就需要700多毫秒。另外,如果板端Flash是SD卡或者低速eMMC,模型读取本身也会成为瓶颈,可以考虑把模型放到内存文件系统里,能再省个几十毫秒。
5.4 推理线程与NPU驱动配合问题
还有一次比较隐蔽的问题:推理程序的线程优先级设置得太高,导致NPU驱动的中断线程被延迟调度,推理耗时反而出现尖刺,偶尔一帧会突然从30毫秒飙到80毫秒。排查了很久才发现是线程优先级设置不当。解决办法是把推理线程的nice值调整到普通级别,避免和内核的ddr频率调节线程抢优先级。类似的性能尖刺问题在很多Linux嵌入式系统上都会出现,不要一上来就怀疑NPU本身不行。
6. 优化经验与后续应用方向
6.1 输入分辨率与裁剪策略的进一步调优
实验统一用了224x224输入,但这不一定是每种模型在RK3568上的最优分辨率。MobileNet和ShuffleNet系列对输入尺寸没有那么敏感,把分辨率降到192x192,单帧延迟大约还能再降10%到15%,精度损失在0.5%以内。如果你的摄像头画质不错、画面主体明确,这个方向值得尝试。相反,如果你的画面里有大量小目标,那分辨率往256x256提或许更合适,但代价是延迟会上去。
一个更实用的策略是动态分辨率:检测阶段用低分辨率快速定位目标,识别阶段对目标区域裁剪后送高分辨率分类模型。这样能很大程度平衡精度和速度,但要求你的代码框架支持双流水线,这一部分我还在改进,后续有结果了可以再单独写一篇。
6.2 从单模型到多模型热切换
实际产品里往往需要在同一块板子上运行多个分类模型。比如一台设备既要识别人脸,又要识别手势,或者需要根据场景切换不同细粒度模型。我不建议在同一进程里同时加载两三个RKNN上下文,因为NPU内存会翻倍。更好的做法是设计一个模型管理模块,按需释放旧context、创建新context。虽然初始化耗时跑不掉,但总内存占用能控制住,这对RK3568这类小内存设备很关键。
具体实现上,可以把每个rknn模型文件映射到独立内存段,再用符号链接和版本号管理。切换之前先释放旧模型,再加载新模型,并预留一次预热的空推理,避免首次推理因为驱动懒加载导致延迟毛刺。
6.3 结合NPU算力特点做模型轻量化改造
最后想聊聊模型的轻量化改造。RK3568的NPU算力是0.8 TOPS,算力边界摆在那里,指望通过软件优化跑出超大型模型是不现实的。与其硬塞大模型,不如在训练阶段就针对目标硬件做裁剪。比如对MobileNetV3的通道数乘以0.75的宽度系数,参数量能缩小40%左右,推理速度接近翻倍,精度损失只有1到2个百分点。这种“结构轻量化+INT8量化”的双重压缩,才是RK3568上做产品的正确思路。
我个人在实际操作中的体会是:部署工作里最耗时最烦人的不是推理代码,而是模型转换和量化校准这部分看不见的工程细节。本次实验里,有一半时间花在调ONNX算子兼容性、清洗校准数据集和定位性能毛刺上。只要这些底层问题理顺了,后续换新模型框架也能很快跑起来。瑞芯微的RKNN工具链在持续更新,算子覆盖度不断提高,但工具链和模型之间存在版本适配问题,无论什么时候拿到新版本,我的建议都是先在老数据集上回归一遍,确定精度没有退化再切换。
这次的实验结果已经沉淀成一份内部基准数据表,后续做检测、分割模型时可以直接引用。如果你也在RK3568上做过类似部署,或者用其他型号的瑞芯微芯片对比过MobileNet和ShuffleNet,欢迎来交流实测数据。毕竟不同版本的固件和工具链会影响最终性能,横向的数据对齐能帮大家省掉不少重复踩坑的时间。