1. 为什么要在RV1103上折腾图像分类模型
RV1103这颗芯片在边缘计算圈子里算是个小网红。它最大的卖点就是内置了NPU,算力标称0.5TOPS,功耗却低到可以忽略不计,价格也压到了很多创客和中小型项目能接受的区间。我最早接触它是因为一个智能门禁的项目,客户要求本地做人脸和物体分类,不能把图像传到云端,成本还要控制在百元以内。找了一圈方案,RV1103几乎是唯一解。
但问题也随之而来:这颗芯片的NPU到底能跑哪些图像分类模型?跑起来精度掉多少?帧率能不能满足实时性?官方文档写得比较粗,社区里的案例又散落在各个角落,有的只讲部署流程不讲性能数据,有的只贴了结果不说踩了什么坑。我前后花了大概三周时间,把MobileNet系列、ResNet系列、EfficientNet-Lite、ShuffleNet等主流轻量级分类模型在RV1103上挨个跑了一遍,记录了下完整的部署流程和实测数据。
这篇文章就是这轮实验的完整复盘。我会从模型选型的逻辑讲起,到RKNN工具链的转换细节,再到板端推理的实测表现,最后把过程中遇到的坑和排查方法整理出来。如果你手头正好有RV1103的开发板,或者正在评估这颗芯片能不能撑起你的分类任务,这篇内容应该能帮你省下不少试错时间。
2. 实验环境与模型选型的底层逻辑
2.1 硬件与软件环境清单
先把实验环境交代清楚,不然后面的数据没法复现。我用的硬件是RV1103原厂demo板,搭配了一块SC3336摄像头模组,内存是内置的64MB DDR2。这里要特别说明一下,RV1103的内存配置在不同封装版本里有差异,我手上这块是64MB版本,如果你的板子是128MB版本,某些大模型的部署策略会不一样。
软件环境方面,PC端用的是Ubuntu 20.04,RKNN-Toolkit2的版本是1.6.0,板端跑的是Buildroot构建的Linux系统,内核版本4.19。NPU驱动版本是0.9.6,这个版本号很重要,不同驱动版本对算子支持的程度有差异,后面讲算子兼容性的时候会具体说。
| 项目 | 配置 |
|---|---|
| 芯片型号 | RV1103 |
| 内存 | 64MB DDR2 |
| NPU算力 | 0.5TOPS |
| 摄像头 | SC3336 |
| PC端工具链 | RKNN-Toolkit2 1.6.0 |
| 板端系统 | Buildroot Linux |
| NPU驱动 | 0.9.6 |
2.2 为什么选这些模型来做实验
图像分类模型的选择不是拍脑袋决定的。RV1103的NPU算力只有0.5TOPS,内存也有限,这意味着参数量超过一定阈值的模型要么跑不起来,要么帧率低到没有实用价值。我选模型的核心逻辑是三条:参数量在5M以下、输入分辨率不超过224x224、网络结构中不包含NPU不支持的算子。
基于这个筛选标准,我最终确定了以下几类模型:
- MobileNetV1/V2:轻量级网络的经典代表,深度可分离卷积的设计天然适合NPU加速,参数量在3-4M之间,是边缘部署的基线模型。
- ResNet18/34:残差连接的开山之作,虽然参数量比MobileNet大,但结构规整,算子类型单一,NPU兼容性好。
- EfficientNet-Lite:Google专门为边缘设备设计的版本,去掉了SE模块和Swish激活函数,换成了ReLU6,对NPU更友好。
- ShuffleNetV2:旷视提出的轻量级网络,通道混洗操作在CPU上效率一般,但在NPU上表现如何值得验证。
- SqueezeNet:极致的参数压缩,参数量只有1.2M,适合内存极度受限的场景。
这里有个经验值得分享:很多人选模型只看参数量和FLOPs,但在NPU部署场景下,算子兼容性往往比理论计算量更重要。一个FLOPs更低的模型如果包含大量NPU不支持的算子,实际推理时会被切回CPU执行,帧率反而更差。所以我在选型阶段就先用RKNN-Toolkit2的算子支持列表过了一遍,把明显不兼容的模型直接排除了。
2.3 模型转换的整体流程设计
从PyTorch或TensorFlow训练好的模型到RV1103上跑起来,中间要经过一条完整的转换链路。我的流程是这样的:先在PC端用PyTorch加载预训练权重,导出ONNX模型,然后用RKNN-Toolkit2把ONNX转成RKNN格式,最后通过adb推到板端运行。
这条链路里最容易出问题的是ONNX导出和RKNN转换两个环节。ONNX导出时如果模型的某些操作不被ONNX支持,会直接报错;RKNN转换时如果遇到不支持的算子,要么报错要么静默回退到CPU。我的做法是在每个环节都加上验证步骤,确保模型结构没有在转换过程中被意外修改。
注意:RKNN-Toolkit2的版本和板端NPU驱动版本必须匹配,否则会出现模型加载失败或者推理结果异常的情况。我建议在开始转换之前,先用
rknn_toolkit2的version命令确认工具链版本,再用板端的cat /sys/kernel/debug/rknpu/version确认驱动版本。
3. 模型转换与量化实操细节
3.1 从PyTorch到ONNX的导出要点
导出ONNX这一步看起来简单,实际上有不少细节需要注意。以MobileNetV2为例,PyTorch官方提供的预训练模型在导出时如果直接调用torch.onnx.export,可能会遇到两个问题:一是动态轴设置不当导致后续RKNN转换失败,二是某些算子版本不兼容。
我的做法是固定输入尺寸为1x3x224x224,不设动态轴,这样转换出来的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=11, input_names=["input"], output_names=["output"], do_constant_folding=True )opset_version选11是有讲究的。版本太低会缺少某些算子定义,版本太高则RKNN-Toolkit2可能还没适配。我实测下来11是最稳的,12也能用但偶尔会遇到算子版本警告。
导出完成后,一定要用onnx.checker.check_model验证一下模型完整性,再用Netron打开看看结构是否符合预期。这一步花两分钟,能省掉后面半小时的排查时间。
3.2 RKNN转换的配置与量化策略
RKNN转换是整个流程的核心环节。RKNN-Toolkit2提供了两种转换模式:不带量化的浮点模型和带量化的定点模型。RV1103的NPU只支持INT8推理,所以量化是必须的。
量化的核心是校准数据集的选择。校准集的作用是统计每一层激活值的分布范围,从而确定量化参数。校准集选得不好,量化后的精度损失会非常大。我的经验是校准集至少要有100-200张图片,而且要覆盖实际应用场景中的各种情况。比如做动物分类,校准集里就不能只有猫,还要有狗、鸟、兔子等各种类别。
from rknn.api import RKNN rknn = RKNN() 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_dataset.txt') rknn.export_rknn('mobilenet_v2.rknn')这里有几个参数值得展开说。mean_values和std_values要和训练时的预处理保持一致,否则精度会掉得莫名其妙。optimization_level设为3会启用所有优化策略,包括算子融合和内存复用,对RV1103这种内存紧张的芯片特别重要。quantized_dtype选asymmetric_quantized-8是因为RV1103的NPU对非对称量化的支持更好,实测精度比对称量化高1-2个百分点。
3.3 量化精度损失的评估方法
量化后的模型精度掉了多少,不能靠感觉判断,要有量化指标。我的做法是在PC端用RKNN-Toolkit2的仿真推理功能,拿验证集跑一遍量化后的模型,和原始浮点模型的输出做对比。
具体操作是在build之后调用rknn.init_runtime,然后逐张图片推理,统计Top-1和Top-5准确率。如果精度损失超过3个百分点,就需要调整量化策略了。常见的调整手段包括:增加校准集数量、换用mmse量化算法、对敏感层单独设置量化参数。
我实测下来,MobileNetV2在ImageNet验证集上的浮点精度是71.8%,INT8量化后是70.2%,掉了1.6个百分点,完全在可接受范围内。ResNet18的浮点精度是69.8%,量化后是67.5%,掉了2.3个百分点,稍微大一点但也能用。EfficientNet-Lite0的量化损失最小,只掉了0.9个百分点,这可能和它本身就用ReLU6激活函数有关,激活值分布更集中,量化更友好。
| 模型 | 浮点Top-1 | INT8 Top-1 | 精度损失 |
|---|---|---|---|
| MobileNetV2 | 71.8% | 70.2% | -1.6% |
| ResNet18 | 69.8% | 67.5% | -2.3% |
| EfficientNet-Lite0 | 75.1% | 74.2% | -0.9% |
| ShuffleNetV2 | 69.4% | 66.8% | -2.6% |
| SqueezeNet | 58.2% | 55.1% | -3.1% |
提示:如果量化精度损失过大,可以先检查校准集是否具有代表性,再尝试调整量化算法。RKNN-Toolkit2支持
normal、mmse、kl三种量化算法,mmse通常精度最高但转换时间最长。
4. 板端部署与推理性能实测
4.1 模型部署到RV1103的完整流程
模型转换完成后,下一步是把它推到板子上跑起来。RV1103的部署流程和RK3568、RK3588略有不同,主要是内存管理策略的差异。RV1103的内存只有64MB,模型文件加上运行时内存很容易就超了,所以部署时要特别注意内存占用。
部署的具体步骤是这样的:先用adb把rknn模型文件推到板子的/userdata目录,然后在板端用C接口或者Python接口加载模型。C接口的性能更好,但开发效率低;Python接口开发快,但会占用更多内存。我建议先用Python接口验证功能,确认没问题后再用C接口做性能优化。
板端的推理代码核心就是三个步骤:初始化RKNN上下文、加载模型、循环推理。这里有个细节容易被忽略:RV1103的NPU是单核的,不支持多线程并发推理,所以不要试图用多线程来提升吞吐量,反而会增加内存开销。
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; rknn_inputs_set(ctx, 1, inputs); rknn_run(ctx, NULL); rknn_output outputs[1]; outputs[0].want_float = 0; rknn_outputs_get(ctx, 1, outputs, NULL);4.2 各模型推理帧率与内存占用对比
这是大家最关心的部分:各个模型在RV1103上到底能跑多快。我测了单帧推理耗时和连续推理帧率两个指标,输入分辨率统一为224x224,NPU频率设为默认值。
| 模型 | 单帧耗时(ms) | 连续帧率(FPS) | 内存占用(MB) | 模型大小(MB) |
|---|---|---|---|---|
| MobileNetV1 | 18.2 | 52.1 | 12.3 | 4.2 |
| MobileNetV2 | 21.5 | 44.8 | 14.1 | 3.5 |
| ResNet18 | 35.8 | 26.4 | 22.7 | 11.2 |
| ResNet34 | 62.3 | 15.2 | 38.5 | 21.3 |
| EfficientNet-Lite0 | 24.7 | 38.9 | 16.8 | 4.7 |
| ShuffleNetV2 | 19.8 | 47.2 | 13.5 | 2.3 |
| SqueezeNet | 15.6 | 60.3 | 10.2 | 1.2 |
从数据可以看出几个规律。MobileNetV1虽然精度不如V2,但速度最快,适合对实时性要求极高的场景。SqueezeNet的帧率最高,但精度只有55%左右,只能用于非常简单的分类任务。ResNet34的帧率掉到了15FPS,已经不太适合实时视频流处理了,但如果你的场景是单张图片分类,这个速度完全可以接受。
内存占用方面,ResNet34的38.5MB已经接近RV1103的64MB内存上限了,实际部署时还要留出系统运行的内存,所以ResNet34在RV1103上跑的时候要特别小心,建议把系统里不必要的服务都关掉。
4.3 输入分辨率对性能的影响
224x224是ImageNet的标准输入尺寸,但实际应用中我们往往需要根据场景调整分辨率。我测了128x128、160x160、192x192、224x224四个分辨率下MobileNetV2的推理性能,结果如下:
| 输入分辨率 | 单帧耗时(ms) | 连续帧率(FPS) | 精度(Top-1) |
|---|---|---|---|
| 128x128 | 9.8 | 89.2 | 63.5% |
| 160x160 | 13.5 | 68.4 | 67.2% |
| 192x192 | 17.2 | 54.1 | 69.8% |
| 224x224 | 21.5 | 44.8 | 70.2% |
分辨率从224降到128,帧率翻了一倍,但精度掉了将近7个百分点。这个取舍要看具体场景:如果是做粗略的物体存在性检测,128x128足够了;如果是做细粒度的分类,比如区分不同品种的狗,那还是得用224x224。
这里有个实操技巧:RV1103的NPU对输入尺寸有对齐要求,宽高最好是16的倍数。128、160、192、224都满足这个条件,但如果你设成150x150,NPU会自动padding到160x160,反而浪费了算力。所以调整分辨率时一定要选16的倍数。
5. 算子兼容性与常见问题排查
5.1 NPU不支持的算子及替代方案
RV1103的NPU算子支持列表不是无限的,有些在PC上跑得好好的算子,到了NPU上要么不支持,要么支持但效率很低。我在实验过程中整理了以下几类常见的不兼容情况:
- Swish激活函数:EfficientNet原版用的Swish,NPU不支持,必须换成ReLU6。这也是为什么我选的是EfficientNet-Lite而不是原版。
- SE模块:Squeeze-and-Excitation模块里的全局池化和全连接操作在NPU上效率很低,建议去掉或者换成其他注意力机制。
- 自定义算子:任何非标准的算子都会被切到CPU执行,严重拖慢整体速度。
- 大核卷积:7x7以上的卷积核在NPU上效率不高,建议拆成多个3x3卷积。
排查算子兼容性的方法很简单:在RKNN转换完成后,用rknn.export_rknn导出模型时加上verbose=True参数,工具链会打印每一层的执行设备。如果看到某层被标记为CPU,就说明这层没有被NPU接管,需要想办法替换或优化。
5.2 推理结果异常的排查思路
部署过程中最容易遇到的问题是推理结果异常,比如输出全是同一个类别、置信度全是0、或者结果随机跳变。这类问题的排查我总结了一个顺序:
- 检查预处理:确认板端的预处理和PC端训练时一致,包括归一化参数、通道顺序、数据类型。RV1103的NPU输入默认是NHWC格式,如果你按NCHW传数据,结果肯定不对。
- 检查量化校准集:校准集不具有代表性会导致量化参数偏差,进而影响推理结果。可以先用浮点模型跑一遍,确认浮点模型结果正常后再排查量化问题。
- 检查模型转换日志:转换时的警告信息往往藏着关键线索,比如某个算子被替换、某个层被融合失败等。
- 检查内存:RV1103内存紧张,如果模型加载时内存不足,可能会出现推理结果异常而不是直接报错。可以用
free -m命令查看内存使用情况。
注意:RV1103的NPU在推理时对输入数据的对齐有要求,如果输入buffer的地址没有按16字节对齐,可能会导致推理结果错误。在C接口中分配内存时建议用
rknn_mem_alloc而不是malloc。
5.3 常见问题速查表
| 问题现象 | 可能原因 | 解决方法 |
|---|---|---|
| 模型加载失败 | 工具链版本与驱动不匹配 | 确认RKNN-Toolkit2和NPU驱动版本一致 |
| 推理结果全为同一类 | 预处理参数错误 | 检查mean/std和通道顺序 |
| 帧率远低于预期 | 算子回退到CPU | 查看转换日志,替换不支持的算子 |
| 内存不足报错 | 模型过大或内存泄漏 | 减小模型或优化内存分配 |
| 量化后精度骤降 | 校准集不具代表性 | 增加校准集数量,覆盖更多场景 |
| 推理结果随机跳变 | 输入buffer未对齐 | 使用rknn_mem_alloc分配内存 |
6. 实际项目中的选型建议与优化技巧
6.1 不同场景下的模型选型参考
经过这轮实验,我对RV1103上的图像分类模型选型有了比较清晰的判断。如果你的场景是实时视频流分类,帧率要求25FPS以上,那MobileNetV1和ShuffleNetV2是最稳妥的选择,两者都能跑到45FPS以上,精度也够用。如果对精度要求更高,可以接受20FPS左右的帧率,EfficientNet-Lite0是更好的选择,它的精度比MobileNetV2高4个百分点左右,速度只慢了一点。
ResNet18和ResNet34更适合单张图片分类或者低帧率场景。ResNet18在26FPS左右,勉强能跑实时,ResNet34的15FPS就只适合非实时场景了。SqueezeNet虽然速度最快,但精度只有55%,除非你的分类任务特别简单,否则不太推荐。
还有一个容易被忽略的点:模型的选择还要考虑后续的维护成本。MobileNet和ResNet的社区资源最丰富,遇到问题容易找到解决方案;EfficientNet-Lite和ShuffleNetV2的案例相对少一些,排查问题的难度会大一点。
6.2 提升推理速度的实操技巧
除了换更小的模型,还有一些不换模型就能提速的技巧。第一个是调整NPU频率,RV1103的NPU默认频率不是最高档,可以通过修改设备树或者sysfs节点来超频。我实测把NPU频率从默认的594MHz提到700MHz,帧率能提升12%左右,但功耗和发热也会相应增加,需要根据散热条件权衡。
第二个技巧是复用输入输出buffer。在连续推理场景下,每次推理都重新分配内存是很浪费的,可以预先分配好buffer,每次推理时只更新数据内容。这个优化在C接口下能带来5-8%的性能提升。
第三个技巧是关闭不必要的系统服务。RV1103的系统资源很紧张,后台跑着的不必要服务会抢占CPU和内存。我在部署时会把蓝牙、WiFi、日志服务等不需要的模块都关掉,能腾出不少资源给NPU推理用。
6.3 从实验到量产需要注意的细节
实验环境跑通和量产部署之间还有一段距离。我在实际项目中踩过的坑包括:批量生产时不同板子的NPU体质有差异,个别板子跑同样的模型帧率会低10%左右;长时间运行后NPU会发热降频,需要加散热片或者降低推理频率;模型文件在生产环境中要考虑加密和完整性校验,防止被替换。
还有一个量产特有的问题:RV1103的固件烧录和模型部署是分开的,如果产线工人操作不当,可能会出现固件版本和模型版本不匹配的情况。我的做法是把模型文件打包进固件里,烧录时一次性写入,避免版本错配。
整体来看,RV1103在图像分类任务上的表现是超出我预期的。0.5TOPS的算力听起来不多,但配合轻量级模型和INT8量化,完全能撑起大多数边缘分类场景。选对模型、做好量化、注意算子兼容性,这三个环节做到位,部署过程其实没有想象中那么折腾。