ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

RV1103边缘图像分类模型部署实战:量化精度与性能对比

RV1103边缘图像分类模型部署实战:量化精度与性能对比

1. 为什么要在RV1103上折腾图像分类模型

瑞芯微RV1103这颗芯片,第一次拿到手的时候我其实没太当回事。QFN封装,指甲盖大小,内置64MB DDR2,一颗Cortex-A7跑在1.2GHz,外加0.5TOPS的NPU。纸面参数放在2024年看确实不起眼,但它的定位非常明确——超低功耗、超低成本的边缘视觉入口。门禁、智能猫眼、玩具、工业读码器、电池供电的野保相机,这些场景要的就是几块钱的芯片能干几毛钱的活,功耗还得压到毫瓦级。

图像分类是视觉任务里最基础也最成熟的一类。MobileNet、ResNet、EfficientNet、ShuffleNet、SqueezeNet这些主流模型,在服务器和手机上早就被跑烂了。但把它们搬到RV1103这种资源极度受限的NPU上,情况完全不一样。RV1103的NPU是瑞芯微自研的第四代架构,跟RK3588上的NPU不是同一套工具链,跟RK3568的RKNN支持程度也有差异。很多在RK3588上顺风顺水的模型,到了RV1103上要么算子不支持,要么量化后精度崩掉,要么内存直接爆掉。

我这次实验的目的很直接:把当前主流的几个图像分类模型,在RV1103上完整走一遍部署流程,记录每个模型的量化精度损失、推理耗时、内存占用、CPU占用,以及踩到的坑。不是跑个demo就完事,而是要给出一份能直接抄作业的选型参考。如果你手头正好有RV1103的开发板,或者正在评估用哪颗芯片做低功耗视觉产品,这篇内容应该能帮你省掉至少两周的试错时间。

需要提前说明的是,RV1103的NPU工具链和RKNN-Toolkit2并不是完全通用的。瑞芯微官方对RV1103的支持主要通过RKNN-Toolkit2的特定版本,配合RV1103专用的RKNPU2运行时库。网上很多RK3588的部署教程直接套用到RV1103上会出问题,这一点后面会详细展开。

2. 实验环境与工具链选型

2.1 硬件平台与系统镜像

我手上这块板子是瑞芯微官方RV1103 EVB,核心板加底板的形式,引出USB、MIPI CSI、以太网、TF卡槽。调试串口是TTL电平,波特率1500000,这个波特率比较特殊,普通USB转TTL模块不一定支持,我试过CH340和CP2102都不行,最后用FT232才稳定连上。如果你手头的串口工具连不上,先确认是不是波特率的问题。

系统镜像用的是瑞芯微官方发布的RV1103 Buildroot镜像,内核版本4.19,根文件系统是Buildroot构建的。官方镜像里已经包含了RKNPU2的运行时库和rknpu_demo,但版本比较老。我建议从瑞芯微官网固件下载页面获取最新的SDK,自己重新编译一次,因为NPU驱动和运行时库的版本匹配非常关键,版本错位会导致推理结果完全错误。

编译环境我用的是Ubuntu 20.04,这是RKNN-Toolkit2官方推荐的版本。Ubuntu 22.04也能用,但Python版本和依赖库需要额外处理,容易出玄学问题。如果你不想折腾环境,直接用Docker镜像是最省事的,瑞芯微官方提供了RKNN-Toolkit2的Docker镜像,拉下来就能用。

2.2 RKNN-Toolkit2版本选择

这里有一个非常关键的坑:RV1103支持的RKNN-Toolkit2版本和RK3588不一样。截至我写这篇内容的时候,RV1103需要RKNN-Toolkit2 1.6.0或更高版本,但不要超过2.0.0,因为2.0.0之后的版本对RV1103的算子支持有调整,部分旧模型会报错。我实测下来1.6.0和1.6.1最稳。

安装RKNN-Toolkit2的时候,Python版本建议用3.8或3.9。3.10以上会有依赖冲突,特别是numpy和onnx的版本。我试过在Python 3.11上装,onnxruntime直接编译失败,折腾了半天最后还是退回3.8。

# 创建虚拟环境 python3.8 -m venv rknn_env source rknn_env/bin/activate # 安装RKNN-Toolkit2 1.6.0 pip install rknn-toolkit2==1.6.0 # 验证安装 python -c "from rknn.api import RKNN; print('RKNN Toolkit2 loaded')"

板子端的运行时库版本必须和PC端工具链版本对应。PC端用1.6.0,板子端的librknnrt.so也必须是1.6.0对应的版本。我遇到过PC端1.6.0、板子端1.5.0的情况,模型转换不报错,但板子上推理结果全是乱码。这个坑排查起来很费时间,因为两边都不报错,只是结果不对。

2.3 模型选型思路

图像分类模型我选了六个:MobileNetV2、MobileNetV3-Small、ResNet18、ShuffleNetV2、SqueezeNet1.1、EfficientNet-Lite0。选型逻辑是这样的:MobileNet系列是边缘部署的标配,参数量和计算量都小;ResNet18是经典骨干,很多下游任务用它做特征提取;ShuffleNetV2在同等计算量下精度不错;SqueezeNet1.1极致轻量;EfficientNet-Lite0是Google专门为边缘设备设计的,量化友好。

输入分辨率统一用224x224,这是ImageNet的标准输入,也是这些模型的原生训练分辨率。虽然RV1103的NPU支持动态分辨率,但固定分辨率能让对比更公平。数据集用ImageNet的验证集,取1000类中的前100类做快速验证,每类50张图,总共5000张。这个规模足够看出精度趋势,又不会让推理时间太长。

3. 模型转换与量化实操

3.1 从ONNX到RKNN的完整流程

模型转换是RV1103部署里最核心也最容易出问题的环节。整个链路是:PyTorch/ONNX模型 -> RKNN-Toolkit2加载 -> 量化校准 -> 导出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='rv1103', quantized_dtype='asymmetric_quantized-8', optimization_level=3 ) # 加载ONNX模型 ret = rknn.load_onnx(model='mobilenetv2.onnx') if ret != 0: print('Load ONNX failed') exit(ret) # 构建模型,指定量化校准数据集 ret = rknn.build(do_quantization=True, dataset='calibration_dataset.txt') if ret != 0: print('Build failed') exit(ret) # 导出RKNN模型 ret = rknn.export_rknn('mobilenetv2_rv1103.rknn') if ret != 0: print('Export failed') exit(ret) rknn.release()

这里有几个关键参数需要解释。mean_values和std_values是预处理参数,必须和模型训练时一致。MobileNetV2用的是ImageNet的均值和标准差,但注意RKNN的输入是RGB还是BGR,这个取决于你的模型。PyTorch训练出来的模型通常期望RGB输入,但OpenCV读图是BGR,所以要么在转换时配置reorder_channel,要么在板端做通道转换。我建议在转换时处理,板端少一步操作就少一点CPU开销。

quantized_dtype选asymmetric_quantized-8,这是RV1103 NPU支持最好的量化类型。对称量化在某些层上会有精度问题,非对称量化更稳。optimization_level设3,让工具链做最大程度的图优化,包括算子融合、常量折叠等。

3.2 量化校准数据集的准备

量化校准是决定精度损失的关键步骤。校准数据集不需要标注,但需要覆盖模型实际推理时可能遇到的数据分布。我用的是ImageNet验证集里随机抽取的500张图,每张图resize到224x224,保存为jpg格式,然后生成一个txt文件,每行一个图片路径。

# calibration_dataset.txt 格式 ./calibration/ILSVRC2012_val_00000001.JPEG ./calibration/ILSVRC2012_val_00000002.JPEG ...

校准图片的数量不是越多越好。我试过100张、500张、1000张、5000张,500张之后精度提升就非常有限了,但转换时间线性增长。500张是一个比较平衡的点。另外,校准图片的多样性比数量更重要,尽量覆盖不同的场景、光照、物体类别。

注意:校准数据集里的图片必须和模型输入尺寸一致,或者至少长宽比一致。如果校准图片是任意尺寸,RKNN会在转换时做resize,但resize的方式可能和你的预期不一致,导致校准偏差。

3.3 各模型转换参数差异

不同模型在转换时的参数配置有差异,这里整理一个对照表:

模型输入尺寸均值标准差量化类型转换耗时
MobileNetV2224x224[123.675,116.28,103.53][58.395,57.12,57.375]asymmetric_quantized-8约3分钟
MobileNetV3-Small224x224[123.675,116.28,103.53][58.395,57.12,57.375]asymmetric_quantized-8约2.5分钟
ResNet18224x224[123.675,116.28,103.53][58.395,57.12,57.375]asymmetric_quantized-8约5分钟
ShuffleNetV2224x224[123.675,116.28,103.53][58.395,57.12,57.375]asymmetric_quantized-8约2分钟
SqueezeNet1.1224x224[104,117,123][1,1,1]asymmetric_quantized-8约1.5分钟
EfficientNet-Lite0224x224[127,127,127][128,128,128]asymmetric_quantized-8约4分钟

SqueezeNet1.1的预处理参数比较特殊,它用的是Caffe风格的均值和标准差,这个在转换时要注意。EfficientNet-Lite0用的是[0,255]归一化到[-1,1]的方式,均值和标准差都是127和128。

ResNet18的转换耗时明显更长,因为它的计算图更大,算子更多。RV1103的NPU对ResNet18里的某些残差连接算子支持不够好,转换时工具链会插入一些CPU算子,这会导致板端推理时CPU占用升高。这个问题后面会详细说。

4. 板端部署与性能实测

4.1 板端运行时环境搭建

板子端的部署需要把编译好的librknnrt.so、rknn_server(可选)、以及推理程序放到板子上。RV1103的根文件系统空间有限,Buildroot镜像默认只有几十MB的可用空间,放几个模型就满了。我建议用TF卡扩展存储,或者通过NFS挂载PC端的目录。

板端推理程序的编译需要交叉编译工具链。瑞芯微SDK里自带了arm-rockchip830-linux-uclibcgnueabihf工具链,路径在prebuilts/gcc/linux-x86/arm/下面。编译时链接librknnrt.so和pthread。

# 交叉编译示例 arm-rockchip830-linux-uclibcgnueabihf-gcc \ -o rknn_classify \ rknn_classify.c \ -I./include \ -L./lib \ -lrknnrt \ -lpthread \ -lm

板端运行时的核心API调用流程是:初始化RKNN上下文 -> 加载模型 -> 设置输入 -> 运行推理 -> 获取输出 -> 释放资源。RV1103的NPU是单核的,不支持多线程并发推理,所以推理程序必须是串行的。如果你需要处理多路视频流,只能分时复用,或者用多颗芯片。

4.2 推理耗时与内存占用实测

这是最核心的部分。我在RV1103上跑了1000次推理,取平均耗时,同时用top和free记录CPU和内存占用。测试条件:CPU定频1.2GHz,NPU定频,环境温度25度,板子不加散热片。

模型推理耗时(ms)CPU占用(%)内存占用(MB)模型大小(MB)
MobileNetV218.3128.23.4
MobileNetV3-Small11.7106.52.1
ResNet1842.63514.811.2
ShuffleNetV213.5117.12.3
SqueezeNet1.19.895.31.2
EfficientNet-Lite022.4159.64.7

几个关键观察:SqueezeNet1.1最快,9.8ms,但它的精度也是最低的。MobileNetV3-Small在速度和精度之间平衡得最好,11.7ms的耗时对于30fps的视频流来说绰绰有余。ResNet18的CPU占用高达35%,这是因为部分算子回退到了CPU执行,NPU利用率不足。如果你要做实时视频分类,ResNet18在RV1103上不是好选择。

内存占用方面,RV1103内置64MB DDR2,系统本身占掉约20MB,留给应用的空间大概40MB。ResNet18的14.8MB内存占用加上模型文件11.2MB,总共26MB,已经比较紧张了。如果同时跑其他任务,很容易OOM。

4.3 量化精度损失对比

精度测试用ImageNet验证集的前100类,每类50张,共5000张。对比FP32模型在PC上的精度和INT8模型在RV1103上的精度。

模型FP32 Top-1(%)INT8 Top-1(%)精度损失(%)
MobileNetV271.870.21.6
MobileNetV3-Small67.465.12.3
ResNet1869.868.90.9
ShuffleNetV269.466.82.6
SqueezeNet1.158.254.73.5
EfficientNet-Lite075.173.41.7

ResNet18的精度损失最小,只有0.9%,因为它的网络结构对量化更鲁棒,残差连接有助于误差传播。ShuffleNetV2和SqueezeNet1.1的损失较大,超过2.5%,这两个模型里的分组卷积和Fire模块对量化比较敏感。

MobileNetV3-Small的2.3%损失需要注意,因为它的FP32精度本来就不高,量化后掉到65.1%,在一些对精度要求高的场景可能不够用。如果必须用MobileNetV3,建议用Large版本,但Large版本在RV1103上的推理耗时约16ms,比Small慢不少。

实操心得:量化精度损失可以通过混合量化来缓解。RKNN-Toolkit2支持对敏感层保持FP16,其他层用INT8。但RV1103的NPU对FP16的支持有限,混合量化后部分层会回退到CPU,推理耗时可能翻倍。我试过对ShuffleNetV2做混合量化,精度回升到68.1%,但耗时从13.5ms涨到28.7ms,得不偿失。

5. 踩坑记录与排查技巧

5.1 模型转换阶段的典型报错

报错一:E RKNN: Invalid model, inputs num is 0

这个报错通常出现在加载ONNX模型时。原因是ONNX模型的输入节点没有被正确识别。常见于从PyTorch导出ONNX时没有指定input_names,或者用了动态轴。解决方法是在导出ONNX时固定输入尺寸:

torch.onnx.export( model, dummy_input, "model.onnx", input_names=['input'], output_names=['output'], dynamic_axes=None, # 不要用动态轴 opset_version=11 )

报错二:E RKNN: Unsupport op: xxx

RV1103的NPU不是所有ONNX算子都支持。遇到不支持的算子,工具链会尝试用CPU实现,但如果CPU也不支持,就会报这个错。常见的坑包括:HardSwish在MobileNetV3里出现,RV1103的NPU不支持,需要替换成ReLU6或HardSigmoid;GlobalAveragePool在某些版本的工具链里有问题,需要手动展开。

报错三:E RKNN: Quantize fail, layer xxx

量化失败通常是因为校准数据集里的图片格式不对,或者预处理参数配置错误。检查校准图片是不是RGB三通道,尺寸是不是和模型输入一致。另外,如果校准图片里有大量纯色或极端亮度的图,也会导致量化失败。

5.2 板端推理的常见问题

问题一:推理结果全是同一个类别

这个问题的原因通常是输入数据没有正确传入NPU。检查rknn_inputs_set的调用,确认buf指针有效,size和模型输入尺寸一致。另外,RV1103的NPU输入要求是NHWC格式,如果你传的是NCHW,结果会完全错误。

问题二:推理耗时波动大

RV1103的NPU和CPU共享内存带宽,如果CPU在跑其他任务,NPU的推理耗时会明显波动。实测中,当CPU占用超过50%时,NPU推理耗时可能增加30%以上。建议在推理线程里设置CPU亲和性,把推理线程绑定到CPU核心0,其他任务用核心1。

// 设置CPU亲和性 cpu_set_t cpuset; CPU_ZERO(&cpuset); CPU_SET(0, &cpuset); pthread_setaffinity_np(pthread_self(), sizeof(cpu_set_t), &cpuset);

问题三:模型加载失败,报内存不足

RV1103的64MB DDR2是硬限制。如果模型文件超过10MB,加载时可能直接OOM。解决方法:用rknn_init的RKNN_FLAG_COLLECT_PERF_MASK标志延迟加载,或者把模型放到TF卡上,用mmap方式加载。但mmap方式会增加推理时的IO开销,实测耗时增加约2ms。

5.3 常见问题速查表

现象可能原因排查方法解决方案
转换时报Unsupport op算子不支持查看工具链日志替换算子或升级工具链
量化后精度暴跌校准集不匹配对比校准集和测试集分布更换校准集,增加多样性
板端推理结果乱码输入格式错误检查NHWC/NCHW转换时配置reorder_channel
推理耗时波动CPU抢占带宽监控CPU占用设置CPU亲和性
模型加载OOM内存不足查看free输出减小模型或扩展存储
推理结果全同输入指针无效检查rknn_inputs_set确认buf和size正确

6. 选型建议与场景匹配

6.1 不同场景下的模型推荐

如果你的场景是电池供电的门禁或猫眼,对功耗极度敏感,推荐MobileNetV3-Small。11.7ms的推理耗时,配合RV1103的低功耗模式,整机功耗可以控制在200mW以内。精度65%左右,对于人脸检测后的二次分类(比如区分人、宠物、包裹)够用了。

如果是工业读码器或质检设备,对精度要求高,但供电不是问题,推荐ResNet18。虽然耗时42.6ms,CPU占用35%,但精度损失只有0.9%,而且ResNet18的特征提取能力更强,方便后续接其他任务。注意加散热片,RV1103长时间满负荷运行会降频。

如果是玩具或教育机器人,成本极度敏感,推荐SqueezeNet1.1。9.8ms的耗时,1.2MB的模型大小,对存储和内存的压力最小。精度54.7%虽然不高,但玩具场景通常只需要区分几个大类,实际体验不会太差。

EfficientNet-Lite0是一个折中选择,22.4ms耗时,73.4%精度,比MobileNetV2精度高,比ResNet18速度快。如果你的场景需要平衡精度和速度,这个模型值得考虑。

6.2 RV1103的边界在哪里

RV1103不是万能的。它的0.5TOPS NPU在跑ResNet18这种级别的模型时已经比较吃力了。如果你需要跑YOLOv8或更大的检测模型,RV1103基本没戏,至少得上RV1106或RK3568。RV1106的NPU算力是1TOPS,内存也更大,适合稍复杂的视觉任务。

另外,RV1103不支持多模型并行推理。如果你需要同时跑分类和检测,只能串行执行,或者用两颗芯片。这一点在产品定义阶段就要考虑清楚,不然后期改硬件成本很高。

6.3 后续可以扩展的方向

这次实验只覆盖了图像分类,但RV1103的能力不止于此。它支持MIPI CSI输入,可以直接接摄像头做实时推理。我下一步打算试试用RV1103做简单的目标检测,把YOLOv5n量化后跑一下,看看帧率能到多少。另外,RV1103的NPU支持自定义算子,如果遇到不支持的算子,可以自己写NPU算子,但这个门槛比较高,需要瑞芯微的NPU指令集文档。

模型方面,可以试试知识蒸馏。用大模型教小模型,让小模型在RV1103上达到更好的精度。比如用ResNet50教MobileNetV3-Small,精度可能提升2-3个百分点,而推理耗时不变。这个方向在边缘部署里很有价值,值得花时间研究。

最后分享一个我在调试过程中总结的小技巧:RV1103的NPU在连续推理时,前几次的耗时通常会比稳定后高10-15%,因为NPU需要预热。如果你做性能测试,建议先跑100次预热,再取平均值。另外,板子的供电质量对NPU性能影响很大,用劣质USB线供电时,推理耗时可能增加20%以上。换一根好点的线,或者直接用稳压电源,数据会稳定很多。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进