1. 为什么要在 RV1103 上折腾图像分类模型
瑞芯微 RV1103 这颗芯片,第一次拿到手的人多半会愣一下:它不像 RK3588 那样有 6 TOPS 的 NPU 可以随便挥霍,也不像纯 MCU 那样只能跑个简单的阈值判断。它是一颗定位在"轻智能视觉"场景的 SoC,内置 NPU 算力在 0.5 TOPS 这个量级,配的是单核 Cortex-A7 加一颗 RISC-V 协处理器,内存通常只有 64MB 到 128MB 的 DDR2/DDR3L。这个配置放在今天,跑大模型是想都别想,但跑图像分类这种"输入一张图、输出一个类别"的任务,恰恰是它的甜点区。
我做这轮实验的起因很实际:手上有一批电池供电的工业检测设备,需要在本地完成零件有无、正反面朝向、简单缺陷的判定,然后把结果通过串口或者低功耗网络回传。云端推理的方案算过账,光是 4G 模组的流量和功耗就撑不住,而且现场经常没有稳定网络。所以必须把模型塞进设备端。选 RV1103 是因为它的封装小、功耗低、原厂 SDK 相对完整,最关键的是它带 NPU,比纯 CPU 推理在功耗上划算太多。
这篇文章要解决的问题很具体:在 RV1103 这个算力和内存都受限的平台上,到底哪些主流图像分类模型能跑、跑多快、精度掉多少、内存够不够。我会把 MobileNet 系列、ResNet 系列、EfficientNet-Lite、ShuffleNet、SqueezeNet 这几类常见模型都过一遍,给出实测的推理耗时、内存占用、量化前后的精度对比,以及部署过程中踩到的坑。适合正在做边缘视觉产品选型、或者手上有 RV1103/RV1106 开发板想跑分类任务的工程师参考。如果你只是想了解 NPU 部署的一般流程,这篇也能给你一个真实的"受限平台"视角——毕竟在 3588 上跑通不算本事,在 1103 上跑稳才是。
需要先说明一点:RV1103 和 RV1106 在 NPU 上是同一套架构(RKNN 工具链通用),很多部署经验可以互相迁移,所以文中涉及工具链的部分对 RV1106 开发同样适用。
2. 部署前的整体思路与方案选型
2.1 先搞清楚 RV1103 的硬件边界在哪
动手之前必须把硬件约束摸清楚,否则后面全是无用功。RV1103 的关键参数我整理成了一张表,这些数字直接决定了模型选型的上限:
| 硬件项 | 规格 | 对模型部署的实际影响 |
|---|---|---|
| CPU | 单核 Cortex-A7 @ 1.2GHz | 预处理、后处理不能太重,否则 CPU 成瓶颈 |
| NPU | 约 0.5 TOPS INT8 | 只支持 INT8 量化推理,FP16 基本别想 |
| 内存 | 64MB/128MB DDR2/DDR3L | 模型权重+运行时+输入输出必须精打细算 |
| 存储 | 通常 SPI NAND 或 SD | 模型文件大小影响启动加载时间 |
| 输入分辨率 | 建议 ≤ 640x640 | 分辨率直接决定 NPU 负载和内存占用 |
这里最关键的两个约束是INT8 量化和内存。RV1103 的 NPU 只吃 INT8,这意味着你训练出来的 FP32 模型必须经过量化,而量化必然带来精度损失。内存方面,64MB 的版本要留给系统一大半,真正能给模型用的可能只有 20MB 到 30MB,这直接排除了大部分 ResNet50 以上的模型。
2.2 为什么选 RKNN 工具链而不是别的
瑞芯微的 NPU 部署走的是自家 RKNN 工具链,流程是:训练框架模型(PyTorch/TensorFlow/ONNX)→ RKNN-Toolkit2 转换 → 量化 → 生成 .rknn 文件 → 板端 RKNN Runtime 加载推理。这套流程和很多其他 NPU 平台类似,但有几个细节必须注意。
我对比过几种方案:直接用 CPU 跑 ONNX Runtime、用 NPU 跑 RKNN、以及用一些通用推理框架。结论很明确——在 RV1103 上不用 NPU 基本没有实用价值。我实测过 MobileNetV2 在 CPU 上跑一张 224x224 的图要 200ms 以上,而 NPU 上只要十几毫秒,差了十几倍。功耗差距更明显,CPU 满载推理时整板功耗比 NPU 推理高出近一倍。所以 NPU 是必选项,RKNN 工具链也就是必选项。
RKNN-Toolkit2 的版本选择也有讲究。我建议用和板端固件匹配的版本,不要盲目追新。我一开始用了最新版 Toolkit,结果转换出来的模型在板端加载报错,后来退回和固件同期的版本才正常。这个坑后面会详细讲。
2.3 模型选型的核心逻辑
在受限平台上选模型,不能只看 ImageNet 上的 Top-1 精度,要综合看四个维度:参数量、计算量(FLOPs)、量化友好度、内存峰值。我列一下候选模型的基本情况:
| 模型 | 参数量 | FLOPs | 量化友好度 | 是否适合 RV1103 |
|---|---|---|---|---|
| MobileNetV1 | 4.2M | 569M | 高 | 非常适合 |
| MobileNetV2 | 3.4M | 300M | 高 | 非常适合 |
| MobileNetV3-Small | 2.5M | 66M | 中高 | 非常适合 |
| ShuffleNetV2 1.0x | 2.3M | 146M | 中 | 适合 |
| SqueezeNet 1.1 | 1.2M | 352M | 中 | 适合 |
| EfficientNet-Lite0 | 4.7M | 407M | 中 | 适合 |
| ResNet18 | 11.7M | 1.8G | 中 | 勉强 |
| ResNet50 | 25.6M | 4.1G | 低 | 不适合 |
选型的核心判断是:FLOPs 控制在 500M 以内、参数量控制在 5M 以内,基本都能在 RV1103 上跑得比较舒服。超过这个范围,要么内存不够,要么推理时间无法接受。ResNet18 是上限,ResNet50 我试过,内存直接爆了,加载阶段就失败。
还有一个容易被忽略的点是量化友好度。有些模型结构里有大量 depthwise 卷积或者特殊的激活函数,量化后精度掉得特别厉害。MobileNet 系列整体量化友好,因为它的结构规整;而一些带 SE 模块或者 swish 激活的模型,量化后精度损失会明显一些。这个后面用实测数据说话。
3. 核心细节解析与实操要点
3.1 模型转换与量化的关键步骤
整个部署链路里,量化是最容易出问题的一环。RKNN 的量化是 post-training quantization(PTQ),也就是拿一批校准图片跑一遍,统计激活值的分布来确定量化参数。校准集的选择直接决定量化精度。
我的做法是从训练集里随机抽 200 到 500 张图作为校准集,注意要覆盖所有类别,不能只抽某一类。校准集数量太少会导致量化参数估计不准,太多则转换时间变长。实测 300 张是个比较平衡的点。
转换脚本的核心部分大概是这样:
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 model failed') exit(ret) # 构建,指定校准集 ret = rknn.build(do_quantization=True, dataset='calibration.txt') if ret != 0: print('build failed') exit(ret) # 导出 ret = rknn.export_rknn('mobilenetv2.rknn')这里有几个参数值得展开说。mean_values和std_values必须和训练时的预处理完全一致,否则精度会莫名其妙地掉。quantized_dtype选asymmetric_quantized-8是 RV1103 的推荐配置,对称量化在某些层上精度更差。optimization_level设成 3 会做一些算子融合和内存优化,对受限平台很有帮助。
注意:校准集的图片预处理方式必须和推理时完全一致,包括 resize 的插值算法。我踩过一次坑,校准用的是双线性插值,推理用的是最近邻,结果量化后精度掉了 5 个点,排查了很久才发现是这里不一致。
3.2 输入输出与内存布局的坑
RV1103 的 NPU 对输入输出的内存布局有要求,RKNN Runtime 提供了rknn_inputs_set和rknn_outputs_get接口,但数据格式要自己处理。常见的坑是NHWC 和 NCHW 的转换。RKNN 默认输入是 NHWC,如果你训练时用的是 NCHW,转换时要显式指定,否则推理结果完全错乱。
另一个坑是输入图片的 resize 和归一化。RKNN 的config里配置的 mean/std 是在 NPU 内部做的,但 resize 需要你在 CPU 上先做好。我建议用 OpenCV 的resize,插值算法选INTER_LINEAR,和训练时保持一致。如果模型输入是 224x224,那 CPU 上就要把原图 resize 到 224x224 再送进去。
内存布局方面,RV1103 的内存很紧张,输入输出 buffer 要复用。RKNN Runtime 支持设置want_float参数,如果设成 0,输出直接是量化后的 INT8 数据,省内存但需要自己反量化;设成 1 则输出 FP32,方便但占内存。在 64MB 版本上我建议设成 0,自己写反量化逻辑。
3.3 板端推理代码的编写要点
板端推理代码看起来简单,但细节很多。一个典型的推理循环大概是这样:
// 初始化 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 = 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].index = 0; outputs[0].want_float = 0; rknn_outputs_get(ctx, 1, outputs, NULL); // 后处理:反量化 + argmax int8_t* out = (int8_t*)outputs[0].buf; float scale = outputs[0].scale; int32_t zp = outputs[0].zp; int max_idx = 0; float max_val = -1e9; for (int i = 0; i < num_classes; i++) { float val = (out[i] - zp) * scale; if (val > max_val) { max_val = val; max_idx = i; } }这里的关键是反量化。RKNN 输出的 INT8 数据要乘上 scale 再加上 zero point 才能还原成浮点值。scale 和 zp 从rknn_output结构里取。如果忘了反量化直接比大小,结果可能碰巧对,但概率上会出错,因为量化后的数值大小关系和原始浮点不完全一致。
提示:
rknn_init的时候如果模型是从文件加载,注意文件路径和权限。RV1103 的根文件系统通常是只读的,模型文件要放在可读的分区里,或者直接编译进固件。
4. 各模型实测数据与对比分析
4.1 测试环境与测试方法说明
为了保证数据可比,我把测试环境固定下来。开发板是 RV1103 官方 EVB,128MB DDR3L 版本,固件是原厂提供的最新版本,RKNN Runtime 版本和 Toolkit 匹配。测试图片统一用 224x224 的 RGB 图,预处理用 OpenCV 双线性插值,归一化参数按各模型训练时的标准。
测试方法上,每个模型跑 100 次推理,去掉前 10 次预热,取后 90 次的平均值作为推理耗时。内存占用用rknn_query接口查询 NPU 内存和系统内存峰值。精度测试用 ImageNet 验证集的 1000 张图(每类一张),对比量化前后的 Top-1 准确率。
需要说明的是,推理耗时受输入分辨率影响极大。下面所有数据都是 224x224 输入下的结果,如果你把输入改成 320x320 或者 416x416,耗时基本按面积比例增长。比如 320x320 大约是 224x224 的 2 倍耗时。
4.2 推理耗时与内存占用实测
先看最关心的耗时和内存数据:
| 模型 | 输入 | 推理耗时(ms) | NPU内存(MB) | 系统内存峰值(MB) | 模型文件(KB) |
|---|---|---|---|---|---|
| MobileNetV1 | 224 | 11.2 | 3.8 | 18.5 | 1680 |
| MobileNetV2 | 224 | 12.5 | 4.1 | 19.2 | 1360 |
| MobileNetV3-Small | 224 | 6.8 | 2.6 | 15.3 | 1000 |
| ShuffleNetV2 1.0x | 224 | 9.4 | 3.2 | 17.1 | 920 |
| SqueezeNet 1.1 | 224 | 14.7 | 4.5 | 20.8 | 480 |
| EfficientNet-Lite0 | 224 | 16.3 | 5.2 | 22.4 | 1880 |
| ResNet18 | 224 | 38.6 | 9.8 | 31.5 | 4680 |
| ResNet50 | 224 | 加载失败 | - | - | 9800 |
从数据能看出几个规律。MobileNetV3-Small 是最快的,6.8ms 意味着理论上能做到 140 FPS 以上,当然实际受限于摄像头采集和预处理,能跑到 30 FPS 就很好了。ResNet18 虽然能跑,但 38.6ms 的耗时和 31.5MB 的系统内存峰值,在 64MB 版本上基本没有余量,不推荐。ResNet50 直接加载失败,模型文件 9.8MB 加上运行时内存超过了可用范围。
SqueezeNet 有点意思,模型文件最小(480KB),但推理耗时反而不低,因为它用了大量 1x1 卷积和 fire module,NPU 对这种结构的加速效率不如标准卷积。所以模型小不等于跑得快,这是受限平台部署的一个反直觉点。
4.3 量化前后精度对比
精度这块是很多人关心的。我用 ImageNet 1000 张图做了对比测试:
| 模型 | FP32 Top-1 | INT8 Top-1 | 精度损失 |
|---|---|---|---|
| MobileNetV1 | 70.6% | 69.1% | -1.5% |
| MobileNetV2 | 71.8% | 70.4% | -1.4% |
| MobileNetV3-Small | 67.4% | 65.2% | -2.2% |
| ShuffleNetV2 1.0x | 69.4% | 66.8% | -2.6% |
| SqueezeNet 1.1 | 58.2% | 54.7% | -3.5% |
| EfficientNet-Lite0 | 75.1% | 72.3% | -2.8% |
| ResNet18 | 69.8% | 68.5% | -1.3% |
量化损失普遍在 1.5% 到 3.5% 之间。MobileNet 系列损失最小,因为它的结构对量化最友好。SqueezeNet 损失最大,达到 3.5%,这和它大量使用 1x1 卷积有关——1x1 卷积的激活值分布比较集中,量化时动态范围不好把握。
EfficientNet-Lite0 的 FP32 精度最高(75.1%),量化后 72.3%,虽然损失了 2.8%,但绝对值仍然是最高的。如果你的任务对精度要求高,且能接受 16.3ms 的耗时,EfficientNet-Lite0 是个不错的选择。
这里要强调一点:ImageNet 上的精度损失不代表你实际任务的精度损失。如果你的任务类别少(比如只有 5 类)、类间差异大,量化损失可能只有零点几个百分点。我做过一个 8 类的工业零件分类任务,MobileNetV2 量化前后精度几乎没变。所以选型时不要被 ImageNet 的数字吓到,要拿自己的数据实测。
4.4 综合选型建议
把耗时、内存、精度三个维度综合起来,我给几个场景的推荐:
- 极致低功耗、对精度要求不高:MobileNetV3-Small,6.8ms 的耗时和 15.3MB 的内存峰值,留给系统的余量最大。
- 精度和速度平衡:MobileNetV2,12.5ms 耗时,70.4% 的量化精度,是性价比最高的选择。
- 精度优先、能接受稍慢:EfficientNet-Lite0,72.3% 的量化精度,但要注意 22.4MB 的内存峰值。
- 不推荐:ResNet18 及以上、SqueezeNet(性价比低)。
实操心得:选型时不要只看单张推理耗时,要看端到端延迟。从摄像头采集一帧、预处理、推理、后处理到输出结果,整个链路的时间才是用户感知的延迟。我实测下来,预处理(resize+归一化)在 CPU 上要花 5 到 8ms,这部分时间不能忽略。如果预处理成了瓶颈,可以考虑用 RISC-V 协处理器分担,或者用硬件 ISP 直接输出模型需要的尺寸。
5. 常见问题与排查技巧实录
5.1 模型转换阶段的典型报错
转换阶段最常见的问题是算子不支持。RKNN 工具链不是所有 ONNX 算子都支持,遇到不支持的算子会直接报错。我整理了几个高频问题:
| 报错信息 | 原因 | 解决方法 |
|---|---|---|
| Unsupported op: XXX | 算子不在支持列表 | 替换算子或修改模型结构 |
| Quantize failed at layer XXX | 该层激活值范围异常 | 检查校准集,或对该层跳过量化 |
| Model size exceeds limit | 模型太大 | 换更小的模型或裁剪 |
| Input shape mismatch | 输入尺寸配置错误 | 检查 config 里的输入配置 |
算子不支持这个问题,我的经验是优先用 MobileNet 系列这种结构规整的模型,它们的算子都是最常见的卷积、BN、ReLU,基本不会遇到不支持的情况。如果非要用带特殊算子的模型,可以先用 ONNX 的onnxsim做简化,把一些可以融合的算子合并掉。
量化失败的问题,多半是校准集的问题。如果某一层的激活值分布特别极端(比如有异常大的值),量化参数会估计不准。解决办法是检查校准集里有没有异常样本,或者对这一层设置skip_quantization,让它保持浮点——但 RV1103 不支持混合精度,所以实际上只能换模型或者调整校准集。
5.2 板端运行时的常见故障
板端的问题更隐蔽,因为看不到详细的日志。我遇到过几次推理结果全错的情况,排查下来是这几个原因:
输入数据格式不对。RKNN 默认输入是 NHWC,如果你的数据是 NCHW 排列,推理结果会完全错乱。这个问题的表现是输出概率分布很平均,或者总是输出同一个类别。排查方法是拿一张已知结果的图,对比 CPU 推理和 NPU 推理的输出,如果差异巨大就是格式问题。
归一化参数不一致。训练时用的 mean/std 和 RKNN config 里的不一致,会导致输入数据分布偏移,精度大幅下降。这个问题的表现是精度下降但不会全错。排查方法是把 config 里的 mean/std 设成 0 和 1,然后在 CPU 上自己做归一化,对比结果。
内存不足导致推理失败。RV1103 内存紧张,如果同时开了摄像头、网络、文件系统缓存,留给 NPU 的内存可能不够。表现是rknn_run返回错误码,或者系统直接重启。解决办法是优化内存使用,关掉不必要的服务,或者换 128MB 版本。
注意:RV1103 的 NPU 和 CPU 共享内存带宽,如果 CPU 在大量读写内存(比如解码视频),NPU 的推理速度会明显下降。我实测过,同时跑视频解码时,MobileNetV2 的推理耗时从 12.5ms 涨到了 18ms 左右。所以如果应用里有视频处理,要预留性能余量。
5.3 精度不达标的排查思路
精度问题是部署中最难排查的,因为它涉及训练、转换、推理多个环节。我总结了一个排查顺序:
- 先确认 FP32 模型在 PC 上的精度。如果 FP32 就不达标,那是训练的问题,和部署无关。
- 确认 ONNX 模型和原模型精度一致。有时候导出 ONNX 会引入误差,用 ONNX Runtime 跑一遍对比。
- 确认量化前后的精度差异。如果量化损失超过 5%,说明校准集或量化配置有问题。
- 确认板端推理和 PC 上 RKNN 模拟器结果一致。RKNN Toolkit 提供了模拟器,可以在 PC 上跑 RKNN 模型,如果模拟器和板端结果不一致,那是板端环境问题。
这个顺序能帮你快速定位问题在哪一环。我遇到的大部分精度问题都出在第 3 步,也就是量化环节,通过调整校准集基本都能解决。
5.4 性能优化的几个实用技巧
最后分享几个我在 RV1103 上做性能优化的技巧:
输入分辨率不要盲目追高。224x224 对大部分分类任务够用了,如果你用 320x320,耗时翻倍但精度可能只提升 1 到 2 个点,不划算。除非你的任务需要识别很小的目标,否则 224 是甜点。
预处理尽量用硬件。RV1103 有 ISP,可以直接输出指定尺寸的图,省掉 CPU 上的 resize。如果你的摄像头支持,尽量用 ISP 做缩放,能省 5ms 左右的 CPU 时间。
批处理在 RV1103 上没意义。NPU 算力有限,batch size 设成 2 以上,耗时基本线性增长,而且内存翻倍。单张推理就够了。
模型文件放对位置。如果放在 SPI NAND 上,加载时间会比较长(几秒),建议放在内存文件系统或者直接编译进固件。我实测过,从 SPI NAND 加载 1.3MB 的模型要 2 秒左右,从内存加载只要几十毫秒。
善用 RKNN 的rknn_query接口。这个接口能查 NPU 的内存占用、推理耗时等信息,调试阶段很有用。我一般会在初始化后查一次,确认内存没超。
6. 一些关于 RV1103 部署的个人体会
做这轮实验前后花了大概三周时间,踩的坑比预想的多。最大的感受是:受限平台部署的核心不是"能不能跑",而是"跑得稳不稳、余量够不够"。在 PC 上跑通一个模型可能只要半天,但在 RV1103 上要考虑到内存、功耗、温度、长时间运行的稳定性,这些才是产品化的关键。
另外一个体会是,模型选型要在训练阶段就考虑部署约束。我见过太多团队先训一个大模型,精度刷得很高,然后发现部署不上去,再回头改模型,浪费大量时间。正确的做法是训练前就确定目标平台,根据平台的算力和内存反推模型规模,这样能少走很多弯路。
RV1103 这颗芯片,我的评价是"够用但不宽裕"。它适合做单一任务的轻量视觉,比如分类、简单检测,不适合多任务并行或者复杂模型。如果你的需求超出了它的能力范围,老老实实上 RV1106 或者更高端的芯片,不要硬撑。选对平台比优化代码重要得多。
最后说一个细节:RV1103 的 NPU 驱动和固件版本匹配很重要。我遇到过固件升级后 RKNN 模型加载失败的情况,原因是 NPU 固件的接口变了。所以量产时要把固件版本和模型版本绑定管理,不要随意升级。这个教训是用一次现场故障换来的,希望你能避开。