把十几款主流图像分类模型挨个折腾到瑞芯微RK3566芯片上跑一遍,绝对不是一个拿着SDK点两下就能交差的活儿。这两年边缘AI项目越来越多,RK3566这种把CPU和NPU做在一起的SoC成了很多场景的默认选项,尤其是做图像分类、物体识别这类相对标准的功能。但你能搜到的评测大多停留在“支持某某算子”的纸面参数上,真正到了量化、转换、板端实测这一步,各种意外才刚开始浮出来。这篇文章把我近期在RK3566上做的一组图像分类模型部署实验完整拆开:从模型选型、工具链版本、ONNX转换、INT8量化,到板端推理耗时、精度掉点和内存稳定性,全部用实测数据说话。适合正在评估RK3566做图像分类落地的朋友,也适合那些已经被模型转换折磨过一轮、想看看别人是怎么处理类似问题的工程师。
说句实在话,RK3566的NPU算力摆在那里,INT8下大约0.8TOPS,跑轻量级网络绰绰有余,跑大模型就得精打细算。我这次实验的目标很明确:不追求极致精度,而是找出在RK3566上“性价比”最高的图像分类模型组合,同时把部署链路走通,给后续项目留一份可以直接参考的选型清单。
1. RK3566平台的基本盘:算力有限,选择反而更重要
1.1 为什么图像分类模型成了边缘端的第一道考题
图像分类几乎是所有视觉应用的基础,也是边缘端部署最常见、最容易对照的基准任务。RK3566这颗芯片在瑞芯微的产品线里定位中低端:四核Cortex-A55 CPU,主频2.0GHz左右,NPU算力约0.8TOPS(INT8),内存带宽有限,典型场景是智能门禁、工业缺陷检测、边缘盒子这类对功耗和成本敏感的设备。跟高端的RK3588相比,它的NPU资源大约只有四分之一到五分之一,所以模型稍微大一点就会撞上算力天花板。
有意思的是,正因为算力紧张,恰恰逼着你去认真思考模型架构和部署细节,而不是无脑上大模型。我这次实验选了十来个模型,覆盖了轻量级CNN、经典残差网络、EfficientNet系列、移动端优化网络,甚至还有Vision Transformer结构的小模型,目的就是把不同结构在RK3566上的表现拉出来对照,看看哪些适合落地、哪些只能停留在理论层面。
1.2 实验想回答的三个问题
这次实验从立项开始就只围绕三个问题展开,没有贪多:
第一,RK3566上哪个图像分类模型的综合性价比最高?这里的“性价比”不是简单的帧率,而是精度、耗时、模型体积、内存占用四者的加权。第二,INT8量化对各个模型精度的影响有多大?不同模型对量化的敏感度差异极大,有些掉零点几个点,有些直接掉三四个点,必须实测。第三,RKNN工具链对不同训练框架导出的ONNX模型兼容性如何?我准备了PyTorch导出的ONNX模型和部分直接以ONNX形式发布的模型,转换过程中的算子支持情况也需要逐一记录。
这三个问题直接决定了后续项目选型时该往哪个方向走,省得每次都用最笨的“全试一遍”的办法。
2. 部署环境全记录:工具链版本与系统配置最容易翻车
2.1 RKNN工具链版本选择与运行时的匹配
如果让我只给一条部署建议,那就是先确认工具链版本和板端runtime版本严格对应。RKNN-Toolkit2在PC端负责模型转换,板端跑推理靠的是librknnrt.so运行时,两边的版本不匹配会出现各种诡异问题,比如模型加载失败、输出全零、甚至推理直接崩溃。我一开始没有太在意,用的PC端rknn-toolkit2 1.5.0,板端烧录的系统镜像里runtime却是老版本,结果ResNet18转换后上板就是拿不到正常分类结果,排查了半天才发现是版本不一致。
正确的做法是先查板端系统自带的rknpu驱动和librknnrt版本,再倒过来选择PC端工具链版本。RKNN工具链版本升级很快,新版本会修正一些算子映射问题,但也可能引入兼容性调整。我最后的组合是PC端rknn-toolkit2 1.6.0,板端系统镜像自带runtime 1.6.0,实测最稳定。
2.2 测试数据集与输入预处理约定
图像分类模型的输入预处理往往直接影响量化效果和最终精度。RKNN推理前的预处理由用户在板端代码里自行完成,一般会在rknn.config里设置mean_values和std_values。这里有个容易踩的坑:PyTorch的ImageNet预训练模型通常用mean=[0.485,0.456,0.406]和std=[0.229,0.224,0.225],但rknn.config里的mean_values和std_values接收的是反归一化之前的值,而且顺序是RGB,且需要换算成0-255尺度。
换算很简单,以PyTorch的ImageNet标准为例,mean换算为[0.485255, 0.456255, 0.406*255]=[123.675, 116.28, 103.53],std换算为[58.395, 57.12, 57.375]。不少人在这一步直接填0和1,精度掉得莫名其妙还不自知。我在所有模型测试里统一用ImageNet验证集抽出的1000张图,输入分辨率统一缩放到224x224,确保数据口径一致,不然不同模型之间的精度对比就完全失真了。
3. 模型转换:从ONNX到rknn的转换流程与算子兼容性
3.1 转换前的模型结构检查
ONNX导出之后不要急着转rknn,先做一次模型结构检查。PyTorch导出的ONNX有时会在动静态维度、Resize模式、Slice起始索引这些细节上留下隐患。我的做法是在PC上先用onnxruntime跑一遍小批量数据,确认ONNX本身输出正常,然后再进入rknn转换流程。这样可以提前排除“源模型就有问题”的干扰项。
rknn转换的流程很简单,但每一行配置都要留个心。以MobileNetV2为例:
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='rk3566' ) ret = rknn.load_onnx(model='mobilenetv2.onnx') ret = rknn.build(do_quantization=True, dataset='dataset.txt') rknn.export_rknn('mobilenetv2.rknn')这里有两个容易被忽略的点:target_platform必须写rk3566,不能写通用的rk3588或rk3399pro;dataset.txt里要放一批有代表性的真实图片路径,用于量化校准,我的经验是放200到500张比较合适,太少统计不准,太多转换时间成倍增加。
3.2 全量量化与混合量化的取舍
rknn.build里do_quantization=True代表全量量化,也会自动启用混合量化策略。对RK3566这种只能高效跑INT8的NPU来说,全量量化是必选项。实测下来,不同模型对量化的敏感度差异非常大:MobileNetV2掉点很少,Top-1精度从71.8%掉到71.3%左右;但EfficientNetB0掉点明显,从77.1%掉到75.9%,这让它在部署选型里的优势打了折扣。
如果某些层量化后精度下降特别严重,可以在rknn.config里通过customize_quantize_layer设置特定层跳过量化,换成FP16在CPU上跑。但RK3566的CPU算力又不算强,局部FP16会导致整体推理耗时增加不少。我实验下来觉得,对图像分类任务而言,只要整体掉点控制在1.5%以内,全量量化完全够用,没必要为了一点精度牺牲帧率。
3.3 转换失败的两个模型
这次实验里有两个模型在转换阶段就栽了跟头。第一个是ViT-Small,它的Transformer结构里有大量复杂的矩阵乘法和LayerNorm算子,rknn转换器对部分算子支持不完整,虽然勉强转成了rknn,但上板后推理耗时超过800毫秒,几乎不可用,NPU利用率也低得可怜。第二个是ConvNeXt-Tiny,它的核心算子GELU和若干DepthwiseConv组合在rknn工具链里吃得很吃力,转换过程反复报算子不支持。最终我只能用ONNXsimplifier对图做了优化,才勉强编译通过,但推理速度依然垫底。
这说明一个很现实的问题:RK3566的NPU是针对CNN结构优化的,Transformer类的模型在这个平台上想高效部署,还有很长一段路要走。如果项目里必须用Transformer模型,建议优先考虑第三方提供的RKNN专用导出方案,或者干脆换芯片平台。
4. 板端性能测试:实测数据与测试方法背后的坑
4.1 性能测试方案必须自己设计,别信官方的Demo
RKNN官方Demo虽然能跑通,但测试精度和速度的口径跟实际部署场景往往不一致。我在板端写了一套独立的Python推理脚本,统一使用同一个摄像头图片输入,连续推理100帧取平均耗时,同时用time.perf_counter记录单帧延迟,排除CPU频率调度和缓存冷启动的影响。这里有个关键细节:跑第一帧之前先做几次热身推理,让NPU驱动和内存缓冲区进入稳定状态,否则第一帧耗时会被明显拉高。
板端推理核心代码大致长这样:
import numpy as np from rknnlite.api import RKNNLite rknn_lite = RKNNLite() rknn_lite.load_rknn('mobilenetv2.rknn') rknn_lite.init_runtime() for i in range(10): # 热身 outputs = rknn_lite.inference(inputs=[img]) t_start = time.perf_counter() for i in range(100): # 正式测试 outputs = rknn_lite.inference(inputs=[img]) t_cost = (time.perf_counter() - t_start) / 100 * 1000搞定推理耗时之后,别忘了单独统计预处理耗时和后处理耗时。实测中,一幅224x224的RGB图像在Python里用OpenCV做resize和归一化就要花掉5到8毫秒,这个开销在MobileNetV2那种40毫秒级别的模型上占了大头,压缩到C语言实现后能减少到1毫秒以内。所以纯推理时间和端到端时间是两码事,做性能评估时一定要分开统计。
4.2 各模型实测数据汇总
以下表格是我在RK3566开发板、CPU默认调频策略、无外部散热风扇条件下跑出来的实测数据。所有模型均为INT8量化,输入224x224x3,测试集为ImageNet验证集随机抽样的1000张图片,精度数据在PC端用相同图片推理ONNX原模型得到,用于对照量化掉点。
| 模型 | 参数量(M) | 推理耗时(ms) | FPS | INT8 Top-1精度 | FP32 Top-1精度 | 量化精度损失 | NPU利用率 |
|---|---|---|---|---|---|---|---|
| MobileNetV2 | 3.4 | 42 | 23.8 | 71.3% | 71.8% | 0.5% | 85% |
| MobileNetV3-Small | 2.5 | 35 | 28.6 | 65.9% | 67.4% | 1.5% | 87% |
| MobileNetV3-Large | 5.4 | 78 | 12.8 | 73.9% | 75.2% | 1.3% | 90% |
| ShuffleNetV2 | 2.3 | 45 | 22.2 | 68.1% | 69.4% | 1.3% | 82% |
| GhostNet | 5.2 | 60 | 16.7 | 72.4% | 73.8% | 1.4% | 84% |
| ResNet18 | 11.7 | 135 | 7.4 | 68.8% | 69.7% | 0.9% | 93% |
| ResNet50 | 25.6 | 385 | 2.6 | 74.6% | 76.1% | 1.5% | 95% |
| EfficientNetB0 | 5.3 | 120 | 8.3 | 75.9% | 77.1% | 1.2% | 88% |
| ConvNeXt-Tiny | 28.6 | 520 | 1.9 | 80.3% | 82.1% | 1.8% | 91% |
| ViT-Small | 22.1 | 810 | 1.2 | 79.8% | 82.3% | 2.5% | 低 |
从表里能直接读出几件事:一,MobileNetV3-Small虽然最快,但精度崩得太厉害,只有65.9%,这对大多数分类任务来说太勉强了;二,ResNet50精度足够但速度太慢,单路实时视频流都带不动;三,EfficientNetB0在速度和精度的平衡上其实相当不错,但量化后掉点相对明显;四,ViT-Small这种结构在RK3566上就是灾难,NPU利用率低导致的功耗和耗时双双爆炸。
4.3 精度对比与量化掉点分析
量化掉点这件事,不能只看平均值,要逐模型看。MobileNetV2只掉了0.5%,主要因为它的深度可分离卷积结构对数值范围变化比较鲁棒;ResNet18掉点0.9%也可接受;MobileNetV3系列和EfficientNet掉点都在1.2%以上,原因是通道注意力机制里的全局平均池化和激活函数在INT8低比特表示下信息损失比较明显。
让我比较意外的是ViT-Small,FP32精度其实很高,但INT8量化后掉到79.8%,掉了2.5个百分点。Transformer的自注意力打分对数值精度非常敏感,这个结论也和我之前在别的NPU上测到的规律一致。所以如果遇到精度敏感场景,优先考虑CNN结构,不要因为Transformer的FP32精度高就冲动选择。
5. 数据解读:为什么算力小的板子不能只看FPS
5.1 带宽与预处理开销往往比算力更致命
RK3566的NPU算力看着只有0.8TOPS,但实际部署中更早撞墙的往往是内存带宽和CPU预处理能力。以ResNet50为例,385毫秒的推理耗时里,纯NPU计算时间大约320毫秒,剩下的65毫秒浪费在输入输出张量的内存拷贝和NPU等待CPU填充数据上。这是我在测试时打时间戳拆出来的结果,不拆不知道,一拆吓一跳。
换到轻量模型的时候,带宽瓶颈更加明显。MobileNetV2理论计算量本身不大,但逐层卷积的输出都要写回DDR,再读出来给下一层,DDR带宽很快就饱和了。这也是为什么MobileNetV2在RK3566上跑不出纸面计算量对应的帧率。想优化的话,可以把输入图片分辨率从224降到192或160,感受一下带宽压力的变化,但分类精度会跟着掉一些,需要自己权衡。
5.2 模型结构对NPU友好度的差异
不同网络结构在RK3566上的NPU利用率差距很大。ResNet18和ResNet50的NPU利用率在93%和95%,原因是标准3x3卷积在NPU里映射得特别顺畅;而ShuffleNetV2和GhostNet的利用率只有82%到84%,因为Channel Shuffle和GhostModule里的大量轻量操作会产生较多小算子,在NPU上调度开销偏高,经常出现“喂不饱”的情况。
MobileNetV2的深度可分离卷积在RK3566上的利用率能达到85%,但相比ResNet的3x3标准卷积还是有差距。实际部署选型时,一个模型的FLOPs低并不等于在NPU上跑得快,必须用实测说话。我的判断标准是先看每毫秒能蹭到多少“有效算力”,再看这个模型的精度值不值这个时间成本。
5.3 结合场景的模型选择建议
根据这批数据,我把模型分成三档推荐。第一档是MobileNetV2,推理42毫秒,精度71.3%,工程量小,转换最顺,适合对速度有要求、分类精度够用就行的项目。第二档是EfficientNetB0,推理120毫秒,精度75.9%,适合对精度有要求、又能容忍10FPS以下速度的项目,比如工业缺陷分类,虽然实时性一般,但准确率更有保障。第三档是ResNet50,推理385毫秒,精度74.6%,只在离线批量处理任务里才值得考虑。
至于MobileNetV3-Small,虽然帧率最高,但精度损失后只有65.9%,除非分类任务特别简单,不然不建议。ViT-Small和ConvNeXt-Tiny这类新结构模型,在RK3566上现阶段就是“能跑但不好用”的典型,除非你愿意为了它们放弃大概率的实时性。
6. 部署踩坑实录:推理耗时波动、内存占用和稳定性问题
6.1 推理耗时忽高忽低,元凶是CPU调频和NPU抢占
第一批测试结果出来的时候,MobileNetV2单帧耗时在42毫秒上下跳动,最差的一次跑到了60毫秒。排查了一圈,发现两个原因:一是板子的CPU调频策略对负载响应有滞后,推理脚本跑起来CPU占用起伏导致某些帧预处理变慢;二是系统日志进程时不时抢CPU。解决办法是把CPU调到性能模式,并给推理进程绑定一个专用CPU核心。
在RK3566上设置性能模式很简单,直接操作sysfs节点就行,但要注意改完之后的功耗和发热。绑定CPU核心我用的是taskset,配合cgroup限制,实测耗时波动从最高60毫秒降到了42毫秒到45毫秒之间,稳定了很多。
6.2 多模型切换时的内存碎片与耗时剧增
我的实验脚本要逐个模型加载测试,最开始每测一个模型就重新启动Python进程,内存状态很干净。后来想优化流程,改成同一个进程里连续加载和释放多个rknn模型,结果出现了一个非常典型的问题:每次切换模型后,第一次推理耗时都会异常高,后续逐步恢复。查了RKNNLite的初始化机制,推测是NPU上下文重新创建时要把模型重新加载进DDR,加上内存碎片导致缓冲区反复重新分配。
这个问题没有彻底消除的捷径,只能尽量复用同一个runtime上下文,或者接受“切换模型后第一次推理偏慢”的现实。如果要跑多模型实时切换,建议初始化时把几个模型一次性加载进来,轮询推理时根据输入切换不同句柄,而不是反复加载释放。
6.3 长时间运行的稳定性与掉电保护
连续跑了两三个小时后,居然出现了两次模型推理返回空结果的异常,检查发现是系统内存逐渐被日志缓存占用,导致NPU缓冲区分配失败。这个问题在处理小容量开发板时特别容易被忽略,我的对策是写一个简单的内存巡检脚本,当系统可用内存低于某个阈值时主动清理缓存。
另外,如果做的是无人值守的设备,建议把推理程序注册成系统服务并在启动脚本里加上掉电重启后的自恢复逻辑。RK3566这类开发板在工业环境中供电并不总是稳定,我自己就遇到过运行中突然断电导致rknn模型文件损坏的情况,重新生成模型才恢复。量产项目里一定要考虑文件系统的写保护和对rknn模型做校验,避免断电损坏后无人处理。
7. 对实验结果的深度解读与后续部署建议
7.1 为什么小模型不一定更快
一般人的直觉是参数量越少跑得越快,但在RK3566上这个直觉不总是成立。ShuffleNetV2只有2.3M参数,推理耗时45毫秒,反而比同样2.5M的MobileNetV3-Small慢了10毫秒。原因在于小模型不一定能充分利用NPU的并行度,算子间的调度开销和内存访问占比更高。所以选型还是要回归到平台实测。
7.2 一份可以直接抄的落地清单
基于这次实验,我总结了一份RK3566图像分类模型落地清单,给后续项目直接用:首推MobileNetV2,综合风险最低;追求精度就上EfficientNetB0,但要在预处理上多花功夫补偿量化损失;工业场景里如果对精度要求高且帧率不敏感,可以选EfficientNetB0配合小幅输入分辨率提升;所有模型上线前必须做至少24小时的连续跑测,确认没有内存泄漏和异常耗时波动。
如果让我重新做一次这个实验,我会在第一轮就先把系统环境、工具链版本和预处理口径固定死,把时间更多花在模型结构优化上,而不是在排查版本兼容性和量化细节上反复折腾。RK3566这个平台上限不低,但下限也很残忍:模型选对了能稳稳跑起来,选错了就是无底洞。