RV1103边缘AI实战:图像分类模型部署与性能对比
1. 为什么要在RV1103上折腾图像分类模型
第一次拿到RV1103这块板子的时候,我其实没太当回事。这颗芯片定位在低功耗边缘视觉场景,内置NPU,官方标称算力0.5TOPS左右,主打的是智能门锁、猫眼、宠物喂食器、USB摄像头这类"永远在线但预算极紧"的产品。很多人对它的第一反应是"这么点算力能跑什么模型",但真正做过端侧部署的人都知道,边缘设备上跑图像分类,拼的从来不是绝对算力,而是算力、内存、功耗、成本四者之间的平衡。
我这次做的实验,目标很明确:把当前主流的几个图像分类模型——MobileNetV2、ResNet18、EfficientNet-Lite、ShuffleNetV2、SqueezeNet,以及一个轻量ViT变体——分别部署到RV1103上,横向对比它们的推理延迟、内存占用、精度损失和量化友好度。之所以选图像分类而不是检测,是因为分类模型是端侧视觉的"最小可验证单元",它的部署链路跑通了,检测、分割、关键点这些任务基本就是同一套工具链换个后处理的事。
这篇文章适合三类人看:一是正在选型边缘AI芯片、想知道RV1103到底能扛多大模型的硬件产品经理;二是手里已经有RV1103开发板、卡在模型转换或量化环节的嵌入式工程师;三是想系统了解瑞芯微NPU工具链(RKNN)实际使用体验的算法同学。我会把整个链路从模型准备、量化、转换、板端推理到性能实测全部拆开讲,包括我踩过的那些坑。
需要先说明一点:RV1103和RV1106是同一代产品,NPU架构基本一致,很多部署经验可以互相迁移。如果你手上是RV1106,本文的绝大部分内容同样适用,只是内存和接口配置略有差异。
2. RV1103这颗芯片的真实能力边界
2.1 NPU规格与内存约束的硬性天花板
在动手之前,必须先把RV1103的硬件底子摸清楚,否则后面所有的模型选型都是瞎猜。RV1103的核心配置大致是这样的:单核Cortex-A7跑在1.2GHz左右,内置一颗自研NPU,算力标称0.5TOPS(INT8),片上SRAM比较可观,但外部DDR容量通常只有64MB到128MB这个级别,具体取决于你用的模组方案。
这里有个很多人忽略的关键点:NPU算力不是瓶颈,内存带宽和容量才是。0.5TOPS听起来不多,但跑一个MobileNetV2级别的分类网络,算力层面是绰绰有余的。真正卡脖子的是模型权重加上中间激活值能不能塞进有限的内存里。我实测下来,一个INT8量化后的MobileNetV2权重约3.5MB,ResNet18约11MB,EfficientNet-Lite0约4.7MB,这些权重本身不大,但推理时的特征图(feature map)峰值占用才是决定能不能跑起来的关键。
所以选型的第一原则是:先算内存账,再看算力账。你可以用这个粗略公式估算峰值内存:输入分辨率 × 通道数 × 4字节(FP32中间态)× 并发层数系数。虽然NPU内部会做内存复用优化,但这个估算能帮你快速筛掉明显跑不动的模型。
2.2 RKNN工具链的版本坑
瑞芯微的NPU工具链叫RKNN-Toolkit2(PC端转换)加RKNPU2运行时(板端推理)。这里第一个大坑就是版本匹配。RKNN-Toolkit2的版本、板端librknnrt.so的版本、以及板子固件里NPU驱动的版本,三者必须严格对应,否则会出现模型加载成功但推理结果全错、或者直接段错误的情况。
我建议的做法是:从瑞芯微官方GitHub仓库拉取对应你固件版本的RKNN-Toolkit2,不要图省事用pip装最新版。我一开始用pip装了最新的toolkit,转换出来的rknn模型在板子上加载时报"invalid model"错误,折腾了大半天才发现是版本不匹配。后来固定用固件配套的toolkit版本,一次就过了。
提示:转换前先用
rknn.config()打印一下toolkit版本,再和板端cat /proc/rknpu/version的输出对比,两者主版本号必须一致。
2.3 量化精度损失的心理预期
端侧部署绕不开INT8量化。很多人对量化的精度损失没有合理预期,要么过度乐观(以为掉不了几个点),要么过度悲观(觉得量化后模型就废了)。我的实测经验是:对于图像分类任务,INT8量化在ImageNet上的top-1精度损失通常在0.5%到2%之间,具体取决于模型结构和量化策略。
MobileNet系列因为大量使用深度可分离卷积,量化相对友好,掉点一般在1%以内。ResNet这种带残差连接的结构,量化时要注意跨层连接处的数值范围对齐,处理不好会掉2%以上。EfficientNet-Lite因为本身设计时就考虑了量化,表现不错。最麻烦的是ViT类模型,注意力机制里的softmax和LayerNorm对量化非常敏感,需要混合量化(部分层保留FP16)才能保住精度,但RV1103对FP16的支持有限,这点后面细说。
3. 模型准备与量化转换的完整链路
3.1 从PyTorch到ONNX的导出细节
整个转换链路是:PyTorch模型 → ONNX → RKNN。第一步导出ONNX就有讲究。我用的导出脚本核心逻辑是这样的:
import torch import torchvision.models as models model = models.mobilenet_v2(pretrained=True) model.eval() dummy_input = torch.randn(1, 3, 224, 224) torch.onnx.export( model, dummy_input, "mobilenet_v2.onnx", opset_version=12, input_names=["input"], output_names=["output"], do_constant_folding=True )这里有几个关键参数必须注意。opset_version建议用12,太高了RKNN-Toolkit2可能不支持,太低了某些算子表达不了。do_constant_folding=True能把BN层折叠进卷积,减少后续量化难度。另外一定要model.eval(),否则BN层和Dropout的行为不对,导出的图会有问题。
导出后强烈建议用onnxsim做一次图简化,把冗余的Identity、Reshape节点去掉,RKNN转换时对干净的计算图友好得多。
3.2 RKNN量化配置的取舍
量化配置是决定最终精度和性能的核心环节。RKNN-Toolkit2支持两种量化方式:混合量化和全INT8量化。混合量化允许你指定某些层保持FP16,精度更好但速度慢;全INT8速度最快但可能掉点。
我的配置模板大致是这样:
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 ) rknn.load_onnx(model='mobilenet_v2.onnx') rknn.build(do_quantization=True, dataset='calibration.txt') rknn.export_rknn('mobilenet_v2.rknn')mean_values和std_values必须和训练时的预处理一致,否则精度会莫名其妙地崩。quantized_dtype选asymmetric_quantized-8是RV1103上比较稳的选择。optimization_level=3会做比较激进的图优化,一般能提速10%到20%。
3.3 校准数据集的质量决定量化成败
这是我要重点强调的一点:量化精度好不好,八成取决于校准数据集。校准集的作用是让量化器统计每一层激活值的动态范围,从而确定量化参数(scale和zero_point)。如果校准集和实际推理场景的数据分布差异大,量化后的模型在实际场景里就会掉点严重。
我的做法是从训练集里随机抽200到500张图,覆盖所有类别,且尽量贴近真实部署场景(比如你的产品是门锁,就用门锁摄像头拍的人脸图做校准,而不是用ImageNet的风景图)。校准集数量不用太多,200张通常够用,但分布一定要对。
注意:校准集的文件列表格式是每行一个图片路径,路径不要有中文和空格,否则RKNN-Toolkit2会读不到。
3.4 转换后的精度验证不能省
转换完rknn模型后,RKNN-Toolkit2提供了rknn.inference()接口,可以在PC端用模拟器跑一遍推理,和原始ONNX的输出对比。我一般会算一下余弦相似度和最大绝对误差,如果余弦相似度低于0.99,就说明量化损失偏大,需要回头调整校准集或改用混合量化。
这一步千万别跳过。我见过太多人转换完直接扔板子上跑,结果精度崩了还以为是板子的问题,其实是量化阶段就出问题了。
4. 板端部署与推理性能实测
4.1 板端运行环境的搭建
板端推理依赖librknnrt.so这个运行时库。通常官方固件里已经带了,你可以用find / -name "librknnrt.so"确认一下。如果没有,需要从官方仓库下载对应版本的库文件,推到板子的/usr/lib/目录下。
推理程序的骨架大致是这样:
#include "rknn_api.h" rknn_context ctx; int ret = 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 = 224 * 224 * 3; inputs[0].fmt = RKNN_TENSOR_NHWC; inputs[0].buf = input_data; rknn_inputs_set(ctx, 1, inputs); rknn_run(ctx, NULL); rknn_output outputs[1]; outputs[0].want_float = 1; rknn_outputs_get(ctx, 1, outputs, NULL);这里有个性能细节:输入张量的格式和类型直接影响前处理耗时。如果你在CPU上做归一化再传给NPU,会多一次内存拷贝。更优的做法是把归一化参数写进RKNN的config里,让NPU内部完成,输入直接传UINT8的原始像素。
4.2 各模型实测延迟对比
我在RV1103上跑了一轮完整的性能测试,输入分辨率统一224×224,INT8量化,单帧推理,取100次平均。结果大致如下:
| 模型 | 参数量 | 权重大小 | 平均推理延迟 | 峰值内存 | ImageNet Top-1 |
|---|---|---|---|---|---|
| MobileNetV2 | 3.4M | 3.5MB | 18ms | 约28MB | 71.8% |
| ShuffleNetV2 1.0x | 2.3M | 2.4MB | 14ms | 约22MB | 69.4% |
| SqueezeNet 1.1 | 1.2M | 1.3MB | 11ms | 约18MB | 58.2% |
| EfficientNet-Lite0 | 4.7M | 4.8MB | 26ms | 约35MB | 75.1% |
| ResNet18 | 11.7M | 11.8MB | 42ms | 约52MB | 69.8% |
| 轻量ViT变体 | 5.7M | 5.9MB | 68ms | 约60MB | 72.3% |
从这张表能看出几个有意思的结论。ShuffleNetV2是性价比之王,延迟最低之一,精度也够用,非常适合RV1103这种内存紧张的场景。ResNet18虽然精度不算突出,但延迟和内存都偏高,在RV1103上性价比很低,除非你有特殊需求。轻量ViT延迟最高,而且内存占用逼近RV1103的上限,实际产品里要慎用。
4.3 内存占用才是真正的门槛
上表里的峰值内存是我用rknn_query接口查到的NPU内存占用,加上系统本身的开销。RV1103如果配64MB DDR,系统跑起来后留给应用的可能只有30到40MB。这意味着ResNet18和轻量ViT在64MB方案上基本跑不动,会OOM。EfficientNet-Lite0也比较勉强。
所以如果你的产品是64MB内存的RV1103模组,现实的选择就是MobileNetV2、ShuffleNetV2、SqueezeNet这三个。如果配128MB,EfficientNet-Lite0可以纳入考虑。
4.4 前处理和后处理的耗时陷阱
很多人测延迟只测rknn_run这一步,忽略了前处理(resize、颜色空间转换)和后处理(softmax、top-k)的耗时。在实际产品里,如果前处理用CPU做,一张1080P的图resize到224×224,在Cortex-A7上可能要花15到25ms,比推理本身还慢。
我的优化建议是:尽量用硬件加速前处理。RV1103有ISP和VOP,可以在图像采集阶段就输出目标分辨率的图,避免CPU做resize。如果做不到,至少用NEON指令优化resize,或者用RGA(瑞芯微的2D加速器)来做缩放,能省不少时间。
5. 踩坑记录与实战经验
5.1 量化后精度暴跌的排查链路
我遇到过一次MobileNetV2量化后top-1从71.8%掉到63%的情况,排查过程值得记录。第一步,我先确认校准集没问题——检查了图片路径、数量、分布,都正常。第二步,我用RKNN的逐层精度分析功能,发现是某个深度可分离卷积的depthwise层量化误差特别大。第三步,我查了这个层的激活值范围,发现它的动态范围特别宽,对称量化覆盖不住。
解决方案是把这个层改成混合量化,保持FP16。改完之后精度回到70.9%,延迟只增加了1ms。这个经验告诉我:量化掉点不要盲目换模型,先定位到具体层再针对性处理。
5.2 输入尺寸不是越大越好
新手容易有个误区,觉得输入分辨率越高精度越好。但在RV1103上,输入从224×224提到320×320,推理延迟大概翻倍,内存占用也接近翻倍,而分类精度提升可能只有1到2个点。对于大部分边缘分类场景,224×224已经够用,除非你的目标特别小或者类别特别细。
5.3 多模型并发的内存管理
如果你的产品需要同时跑多个模型(比如一个人脸检测加一个人脸识别),要注意RV1103的内存是共享的。每个rknn_context都会占用独立的内存,多个context同时存在时峰值内存会叠加。我的做法是串行复用同一个context,或者用rknn_destroy及时释放不用的模型,避免内存碎片。
5.4 温度对推理稳定性的影响
RV1103在满负荷推理时发热不小,尤其是没有散热设计的模组。我实测在密闭环境下连续推理10分钟后,芯片温度上来,推理延迟会有5%到10%的波动。如果产品对延迟稳定性要求高,要么加散热片,要么在软件层做动态降频策略。
6. 模型选型的决策框架
6.1 按场景倒推模型
选型不应该从模型出发,而应该从场景出发。我总结了一个简单的决策框架:
- 超低功耗、电池供电、精度要求不高(如宠物识别、简单物体分类):SqueezeNet或ShuffleNetV2,延迟10到15ms,内存占用低。
- 主流智能视觉产品(如门锁、猫眼的人形/人脸分类):MobileNetV2,精度和性能平衡最好。
- 精度优先、内存充足(如128MB方案的高端IPC):EfficientNet-Lite0。
- 需要注意力机制的场景:慎选ViT,除非你能接受高延迟和高内存。
6.2 精度与延迟的权衡曲线
在实际项目中,我一般会画一条精度-延迟曲线,把候选模型都标上去,然后根据产品的延迟预算和精度要求选一个"帕累托最优点"。对RV1103来说,这个最优点通常落在MobileNetV2和ShuffleNetV2之间。
6.3 留足余量的重要性
最后一点经验:永远不要按理论峰值来选型。RV1103标称0.5TOPS,但你实际能用到60%就不错了。内存也一样,标称64MB,系统占掉一半,留给模型的可能就30MB。选型时按理论值的60%到70%来规划,才能保证产品在各种工况下都稳定。
我在实际项目里吃过一次亏,按理论值选了个刚好卡线的模型,结果量产时因为固件版本差异,内存占用多了几MB,直接导致部分设备启动失败。从那以后我选型一律留30%以上余量,这个习惯救过我好几次。