ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

RV1103边缘NPU实战:图像分类模型部署与性能对比

RV1103边缘NPU实战:图像分类模型部署与性能对比

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

第一次拿到瑞芯微 RV1103 这块板子的时候,我其实没抱太大期望。这颗芯片定位是低功耗 IPC(网络摄像头)和边缘视觉场景,CPU 是单核 Cortex-A7 加一个 RISC-V 小核,主频不高,内存通常配 64MB 或 128MB DDR2/DDR3L,跟那些动辄几 GB 内存的开发板完全不是一个量级。但它内置了一颗 0.5 TOPS 算力的 NPU,这就很有意思了——意味着你可以在一个功耗只有几百毫瓦的芯片上跑神经网络推理,这在电池供电或者 PoE 供电的摄像头场景里非常实用。

我这次做的实验,核心目标很明确:把当前主流的几个图像分类模型,包括 MobileNet 系列、ResNet 系列、EfficientNet-Lite 系列,以及近两年比较火的轻量级 ViT 变体,分别部署到 RV1103 上,跑一遍完整的推理流程,记录精度、帧率、内存占用、NPU 利用率这些关键指标,最后给出一个选型参考。说白了,就是回答一个问题:在 RV1103 这个级别的芯片上,到底哪个图像分类模型最值得用?

这个问题看起来简单,但实际操作下来坑不少。RV1103 的 NPU 工具链跟 RK3588、RK3568 那套 RKNN 工具链虽然同源,但版本和算子支持有差异,很多在 RK3588 上跑得好好的模型,转到 RV1103 上就会遇到算子不支持、量化精度掉点、内存溢出等问题。而且 RV1103 的内存非常紧张,模型文件稍微大一点,加载都会失败。所以这篇文章不只是跑个 benchmark 那么简单,我会把整个部署流程、踩过的坑、参数怎么调、模型怎么选,都掰开揉碎讲清楚。

适合谁看?如果你正在做边缘视觉产品选型,或者手头有 RV1103/RV1106 的开发板想跑点 AI 功能,又或者你只是好奇低功耗 NPU 到底能跑多复杂的模型,这篇文章应该都能给你一些直接能用的参考。我尽量不堆术语,把每个操作背后的逻辑讲明白,让你看完能自己复现一遍。

2. 实验平台搭建与工具链选型

2.1 硬件平台与系统环境确认

我手上这块板子是 RV1103 的官方 EVB,配置是 128MB DDR3L,SPI NAND 128MB 存储,系统跑的是瑞芯微官方的 Buildroot Linux。如果你用的是 RV1106,两者 NPU 架构基本一致,本文的结论大部分可以通用,但内存配置不同会导致模型大小上限有差异,这个后面会具体说。

先确认几个关键信息。串口连上去之后,用cat /proc/cpuinfo看 CPU 信息,用cat /proc/meminfo看内存,最重要的是确认 NPU 驱动版本:

cat /sys/kernel/debug/rknpu/version

我这边输出的是RKNPU driver version: 0.8.2,对应的 RKNN Toolkit 版本是 1.6.0。这里有个坑:RV1103 的 NPU 驱动版本和 RKNN Toolkit 版本必须匹配,否则模型加载会直接报错。我一开始用了 1.5.0 的 Toolkit 转出来的模型,在板子上加载时报RKNN model version mismatch,折腾了半天才发现是版本问题。

另外确认一下 NPU 的算力配置。RV1103 的 NPU 是 0.5 TOPS(INT8),支持 INT8 和 INT16 量化,不支持 FP16。这一点很关键,意味着你所有模型都必须做量化,不能直接跑浮点模型。量化带来的精度损失是这次实验要重点关注的指标之一。

2.2 RKNN 工具链安装与版本匹配

工具链这边,我用的是 Ubuntu 20.04 的 x86 主机来做模型转换。RKNN Toolkit2 的安装方式有两种:pip 安装和 Docker 安装。我推荐用 Docker,因为依赖关系比较复杂,pip 装容易出问题。瑞芯微官方提供了 Docker 镜像,直接拉下来就能用:

docker pull rknn-toolkit2:1.6.0-cp38

如果你非要 pip 装,注意 Python 版本必须是 3.6/3.8,我试过 3.10 会报错。安装命令大概是:

pip install rknn_toolkit2-1.6.0-cp38-cp38-linux_x86_64.whl

装完之后验证一下:

from rknn.api import RKNN rknn = RKNN() print(rknn.version())

能打印出版本号就说明装好了。这里提醒一句,RKNN Toolkit2 和 RKNN Toolkit(一代)不兼容,RV1103 必须用 Toolkit2,别搞混了。

2.3 模型选型思路与候选清单

这次实验我选了以下几类模型,覆盖了从超轻量到中等规模的典型代表:

模型参数量输入尺寸原始 Top-1 精度选它的理由
MobileNetV23.4M224x22472.0%经典轻量模型,部署最广泛
MobileNetV3-Small2.5M224x22467.4%更轻量,适合内存紧张场景
ResNet1811.7M224x22469.8%结构简单,算子兼容性好
EfficientNet-Lite04.7M224x22475.1%精度高,但算子较复杂
ShuffleNetV2 1.0x2.3M224x22469.4%计算量小,适合低算力
MobileViT-XXS1.3M256x25669.0%ViT 轻量变体,测试 Transformer 支持

选这些模型的逻辑是:MobileNet 系列是边缘部署的基准线,必须测;ResNet18 用来验证 NPU 对标准卷积网络的支持程度;EfficientNet-Lite 测试复杂算子的兼容性;ShuffleNetV2 看分组卷积和通道混洗的支持;MobileViT 则是探一探 Transformer 类模型在 RV1103 上的可行性。

数据集方面,我用 ImageNet 的验证集抽了 1000 张图做精度对比,同时用一张 224x224 的测试图反复跑 1000 次测帧率。这样既能看精度,也能看速度。

3. 模型转换与量化实操全流程

3.1 从 PyTorch 到 ONNX 的导出要点

RKNN Toolkit2 支持从 PyTorch、ONNX、TensorFlow 等多种格式转换,但实际用下来,ONNX 是最稳的中间格式。我建议统一走 PyTorch -> ONNX -> RKNN 这条路。

导出 ONNX 的时候有几个关键点。第一,opset_version建议用 12,太高或太低都可能遇到算子不支持的问题。第二,输入输出的名字要固定,方便后面转换时指定。第三,动态轴要处理好,RV1103 只支持静态 shape,所以导出时要把 batch size 固定为 1。

以 MobileNetV2 为例,导出代码大概是这样:

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, "mobilenetv2.onnx", opset_version=12, input_names=["input"], output_names=["output"], do_constant_folding=True )

导出之后,强烈建议用onnxsim做一次简化,把冗余算子去掉,能减少转换时的报错概率:

pip install onnxsim onnxsim mobilenetv2.onnx mobilenetv2_sim.onnx

这一步我踩过的坑是:有些模型导出后带Identity算子或者多余的Reshape,RKNN 转换时会报错,用 onnxsim 简化后基本都能解决。

3.2 RKNN 量化配置与校准集制作

量化是 RV1103 部署的核心环节,因为 NPU 只支持 INT8/INT16。RKNN Toolkit2 提供两种量化方式:do_quantization=True的默认量化和混合量化。我这次全部用默认的 INT8 量化,因为 INT16 虽然精度高,但速度会慢不少,而且 RV1103 的算力本来就有限。

量化需要一个校准集,通常准备 100-200 张代表性图片就够了。校准集的质量直接影响量化精度,我建议从训练集或验证集里随机抽,覆盖不同类别和场景。校准集的图片要做和推理时一样的预处理,包括 resize、归一化等。

转换脚本的核心部分:

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_sim.onnx') if ret != 0: print('Load ONNX failed') exit(ret) # 构建,指定校准集 ret = rknn.build(do_quantization=True, dataset='calibration.txt') if ret != 0: print('Build failed') exit(ret) # 导出 RKNN 模型 ret = rknn.export_rknn('mobilenetv2.rknn')

这里有几个参数需要解释。mean_values和std_values是预处理参数,RKNN 会在推理时自动做归一化,这样你板子端就不用再处理了。quantized_dtype选asymmetric_quantized-8是 RV1103 推荐的量化方式。optimization_level=3是最高优化级别,会做一些算子融合,能提升速度。

校准集文件calibration.txt就是一个文本文件,每行是一张图片的路径。注意图片路径要用绝对路径,相对路径容易出错。

3.3 转换过程中的常见报错与解决

转换过程中我遇到最多的报错是算子不支持。比如 EfficientNet-Lite0 里的Sigmoid和Mul组合,RKNN 1.6.0 对某些激活函数的支持不完整。解决办法有两个:一是用optimization_level调整,二是手动修改模型结构,把不支持的算子替换掉。

还有一个常见问题是量化后精度掉得厉害。我测下来,MobileNetV2 量化后 Top-1 精度从 72.0% 掉到 70.5%,掉了 1.5 个百分点,这个可以接受。但 MobileViT-XXS 量化后精度掉了将近 8 个百分点,从 69.0% 掉到 61.2%,这就很难用了。原因是 Transformer 里的 LayerNorm 和 Softmax 对量化非常敏感,INT8 精度不够。

如果遇到精度掉太多,可以尝试这几个方法:增加校准集数量到 300-500 张;用hybrid_quantization把敏感层保持 FP16;或者干脆换模型。我最后的结论是,在 RV1103 上,CNN 类模型的量化友好度远高于 Transformer 类模型。

4. 板端部署与推理性能实测

4.1 板端运行环境准备

模型转好之后,就要放到板子上跑了。RV1103 的 Buildroot 系统里已经预装了 RKNPU 的运行库,但版本要和 Toolkit 匹配。确认一下:

ls /usr/lib/librknn*

应该能看到librknn_runtime.so和librknn_api.so。如果没有,需要从 SDK 里拷贝对应的库文件过去。

板端推理我写了一个简单的 C++ 程序,用 RKNN 的 C API 加载模型并推理。核心流程是:初始化 RKNN 上下文、加载模型、设置输入、运行推理、获取输出。代码结构大概是这样:

rknn_context ctx; int ret = rknn_init(&ctx, model_path, 0, 0, NULL); if (ret < 0) { printf("rknn_init failed: %d\n", ret); return -1; } 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; ret = rknn_inputs_set(ctx, 1, inputs); ret = rknn_run(ctx, NULL); rknn_output outputs[1]; outputs[0].want_float = 1; ret = rknn_outputs_get(ctx, 1, outputs, NULL);

编译的时候要链接 RKNN 的库:

arm-linux-gnueabihf-g++ main.cpp -o test -lrknn_api -lrknn_runtime

这里有个细节:RV1103 的 CPU 是 ARMv7,编译工具链要用对应的交叉编译器,别用 aarch64 的。

4.2 各模型推理帧率与内存占用对比

跑完一轮测试,数据整理如下。测试条件是:单张 224x224 图片,连续推理 1000 次取平均,CPU 定频,NPU 满负荷。

模型推理耗时(ms)帧率(FPS)模型文件大小运行时内存占用量化后Top-1
MobileNetV218.554.13.5MB12MB70.5%
MobileNetV3-Small12.381.32.6MB9MB65.8%
ResNet1842.723.411.8MB28MB68.9%
EfficientNet-Lite028.635.04.9MB16MB73.2%
ShuffleNetV2 1.0x14.867.62.4MB10MB67.1%
MobileViT-XXS56.217.81.4MB22MB61.2%

从数据能看出几个明显的规律。MobileNetV3-Small 和 ShuffleNetV2 是最快的,都能跑到 60 FPS 以上,适合对实时性要求高的场景。MobileNetV2 居中,54 FPS 也够用。EfficientNet-Lite0 精度最高,量化后还有 73.2%,速度 35 FPS,算是精度和速度平衡得比较好的选择。ResNet18 虽然精度一般,但速度慢、内存占用大,在 RV1103 上性价比不高。MobileViT-XXS 则是全面落后,速度慢、精度掉得多,不建议在 RV1103 上用。

内存占用这块要特别说一下。RV1103 只有 128MB 内存,系统本身要占掉 40-50MB,留给应用的空间大概 70-80MB。ResNet18 运行时占用 28MB,加上输入输出缓冲和系统开销,实际峰值能到 50MB 以上,稍微再跑点其他任务就容易 OOM。所以如果你内存紧张,优先选 MobileNetV3-Small 或 ShuffleNetV2。

4.3 NPU 利用率与功耗表现

除了帧率和内存,我还测了 NPU 的利用率和整板功耗。NPU 利用率可以通过 debugfs 查看:

cat /sys/kernel/debug/rknpu/load

跑 MobileNetV2 的时候,NPU 利用率大概在 75%-85% 之间波动,说明 NPU 基本跑满了,瓶颈在 NPU 算力本身。跑 MobileNetV3-Small 时利用率只有 60% 左右,说明瓶颈转移到了内存带宽或者 CPU 预处理上。

功耗方面,我用功率计测了整板功耗。待机时大概 0.4W,跑 MobileNetV2 推理时升到 0.9W,跑 ResNet18 时到 1.2W。这个功耗水平对于电池供电的摄像头来说是可以接受的,但如果用太阳能供电或者能量采集,就得选更轻量的模型。

这里有个经验:NPU 利用率不是越高越好。如果利用率长期 100%,说明 NPU 是瓶颈,换更小的模型能提升帧率。如果利用率只有 50%-60%,说明瓶颈在别处,换模型可能提升不明显,得优化数据搬运或预处理。

5. 部署避坑指南与常见问题排查

5.1 模型加载失败的几种典型情况

模型加载失败是最常见的问题,我遇到过的报错和原因整理如下:

报错信息原因解决办法
RKNN model version mismatchToolkit 和 Runtime 版本不一致统一用 1.6.0 版本
Invalid RKNN model模型文件损坏或转换中断重新转换,检查磁盘空间
rknn_init failed: -1内存不足换更小的模型或减少输入尺寸
Unsupported operation算子不支持用 onnxsim 简化或替换算子
Input shape mismatch输入尺寸和模型不匹配检查预处理代码的 resize 参数

其中内存不足这个问题最隐蔽,因为报错信息不直观。我一开始跑 ResNet18 的时候,rknn_init返回 -1,查了半天才发现是内存不够。后来用free -m看内存,发现加载模型后剩余内存不到 20MB,推理时直接 OOM。解决办法是把输入尺寸从 224 降到 192,内存占用能减少 30% 左右。

5.2 量化精度掉点的排查思路

量化精度掉点也是高频问题。排查思路是这样的:先看掉点幅度,如果掉 1-2 个百分点,属于正常范围,可以接受;如果掉 5 个百分点以上,就要找原因了。

第一步,检查校准集。校准集数量太少或者分布不均衡,会导致量化参数估计不准。我建议至少 200 张,覆盖所有类别。第二步,检查预处理。RKNN 的mean_values和std_values要和训练时一致,不一致会导致输入分布偏移,量化误差放大。第三步,看哪些层掉点严重。RKNN Toolkit 提供了逐层精度分析工具,可以定位到具体是哪一层的问题。

如果确实是某些层对量化敏感,可以用混合量化,把这些层保持 FP16。配置方式是在config里指定quantized_algorithm和quantized_method,或者在 build 时传入hybrid_quantization参数。不过混合量化会增加模型大小和推理时间,要权衡。

5.3 实际项目中的选型建议

基于这次实验,我给几个实际场景的选型建议:

如果是智能门铃或猫眼,对帧率要求不高(5-10 FPS 够用),但对精度有要求,推荐 EfficientNet-Lite0,73.2% 的精度在同类模型里最高,35 FPS 也完全够用。

如果是工业质检或分拣,需要高帧率实时处理,推荐 MobileNetV3-Small 或 ShuffleNetV2,80 FPS 左右能跟上产线速度,精度 65%-67% 对于二分类或少量类别分类够用。

如果是电池供电的野外监测,功耗优先,推荐 MobileNetV3-Small,功耗最低,内存占用最小,能延长续航。

如果是多模型串联(比如先检测再分类),内存是瓶颈,推荐 ShuffleNetV2,模型文件只有 2.4MB,运行时内存 10MB,给其他模型留足空间。

ResNet18 和 MobileViT-XXS 在 RV1103 上我不推荐,前者性价比低,后者量化不友好。当然,如果你的场景对精度要求极高,可以考虑用 RV1106 或者 RK3568,算力和内存都更充裕。

5.4 提升推理速度的几个实用技巧

最后分享几个实测有效的提速技巧。第一,输入尺寸不要盲目用 224,如果你的分类类别少、目标大,用 160 或 192 能显著提速,精度掉得也不多。我测过 MobileNetV2 在 160 输入下能跑到 90 FPS,精度只掉 2 个百分点。

第二,预处理尽量用硬件加速。RV1103 有 ISP,可以在摄像头采集阶段就做好 resize 和格式转换,减轻 CPU 负担。如果走 CPU 预处理,用 NEON 指令优化过的 OpenCV 版本,比默认版本快不少。

第三,模型转换时开启optimization_level=3,RKNN 会做算子融合和内存复用,实测能提升 10%-15% 的速度。

第四,如果帧率要求不高,可以用多线程流水线,一个线程做预处理,一个线程做推理,一个线程做后处理,能把整体吞吐量提升 20% 左右。

我在实际项目里还发现一个细节:RV1103 的 NPU 在连续推理时会有轻微的热降频,跑久了帧率会掉 5% 左右。如果对稳定性要求高,建议加个小散热片,或者把推理间隔拉长一点,让 NPU 有时间散热。这个在规格书里不会写,但实际用下来确实存在。

RELATED READING

延伸阅读

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