1. 为什么要在RV1106上折腾图像分类模型
瑞芯微RV1106这颗芯片在边缘视觉圈子里热度一直不低。它本质上是一颗面向低功耗智能视觉场景的SoC,内置NPU,主打的就是在极低功耗预算下跑轻量级神经网络推理。我最早接触它是因为一个电池供电的工业巡检项目,需要在本地做简单的图像分类来判断设备状态,而不是把图片传到云端。当时选型的核心诉求就三条:功耗要低、成本要低、能跑得动分类模型。RV1106基本满足这三点,但真正上手之后才发现,从“能跑”到“跑得好”之间有一大段路要走。
这篇文章想聊的是我在这颗芯片上部署各种主流图像分类模型的完整实验分析。所谓“主流”,我选了MobileNetV2、ResNet18、EfficientNet-Lite这几个在边缘端最常见的 backbone,也顺带试了ShuffleNetV2和SqueezeNet。选它们的原因很直接:这些模型在开源社区里权重好找、结构成熟、参数量和计算量跨度大,能比较全面地反映RV1106 NPU在不同模型规模下的表现。如果你手头正好有RV1106的开发板,或者正在评估这颗芯片能不能撑起你的分类任务,这篇内容应该能帮你少走一些弯路。
需要提前说明的是,RV1106的NPU工具链和RK3588那套不完全一样。RK3588用的是RKNN-Toolkit2,而RV1106目前主要走的是RKNN-Toolkit1的路线,或者用瑞芯微提供的RV1106专用SDK里的转换工具。这个差异会直接影响你模型转换的流程和算子支持范围,后面会详细展开。
2. RV1106的NPU到底能吃什么样的模型
2.1 NPU算力与内存约束的真实边界
RV1106的NPU标称算力是0.5TOPS,这个数字放在今天看不算亮眼,但它的定位本来就不是跑大模型。关键在于,这个0.5TOPS是INT8精度下的理论峰值,实际能跑出多少取决于模型结构、算子映射效率和内存带宽。我实测下来,一个标准的MobileNetV2分类模型(输入224x224),在RV1106上单帧推理大概在30-40ms左右,换算下来差不多25-30FPS。这个成绩对于大多数边缘分类任务已经够用了。
但内存才是真正的瓶颈。RV1106的片上内存非常有限,具体型号不同会有差异,但总的来说你不能指望它像RK3588那样跑ResNet50还留有余量。我试过直接把一个完整的ResNet18转过去,结果模型加载阶段就报了内存不足。后来把输入分辨率从224降到160,同时把模型量化做得更激进一些,才勉强跑起来。所以你在选模型之前,第一件事是确认你的开发板具体型号和可用内存,这比看算力参数重要得多。
2.2 算子支持清单里那些容易踩的坑
RKNN工具链对算子的支持是有白名单的。有些算子虽然在列表里,但实际转换后会退化到CPU执行,导致推理速度断崖式下跌。我遇到最典型的是GlobalAveragePooling在某些版本的工具链里会被拆成多个操作,效率很低。还有HardSwish激活函数,在EfficientNet-Lite里大量使用,但早期版本的RKNN对它支持不好,转换后精度掉得厉害。
提示:转换完成后一定要用RKNN-Toolkit自带的性能分析工具看每一层的耗时分布。如果发现某一层耗时异常高,大概率是算子退化到了CPU。
另外,RV1106的NPU对输入张量的布局有要求。默认是NHWC,但有些PyTorch模型导出ONNX后是NCHW,转换时需要显式指定。这个细节如果不注意,模型能转换成功但推理结果完全不对,排查起来很费时间。
2.3 量化策略对精度的影响实测
RV1106只支持INT8推理,所以量化是绕不过去的。我对比了两种量化方式:一种是直接用RKNN工具链的默认量化(基于KL散度校准),另一种是自己准备校准数据集做精细量化。实测结果差异很明显。
以MobileNetV2为例,默认量化在ImageNet验证集上的Top-1精度大概掉1.5到2个百分点,而用我自己从业务场景里采的500张校准图做量化,精度只掉了0.8个百分点左右。这说明校准数据的分布非常关键。如果你做的是特定场景的分类(比如工业缺陷分类),千万不要用通用的ImageNet图片做校准,一定要用你自己场景的数据。
3. 从PyTorch到RV1106的完整转换链路
3.1 模型导出ONNX时的那些讲究
第一步是把PyTorch模型导出成ONNX。这一步看起来简单,但有几个细节直接决定后续能不能顺利转换。首先是opset版本,我建议用opset 11或12,太新的版本RKNN工具链可能不认,太老的又可能缺少某些算子定义。其次是动态轴的问题,导出时一定要把batch size固定为1,RV1106不支持动态batch。
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 )导出之后,我强烈建议用onnxsim做一次简化。很多模型导出后会带一堆冗余的Identity节点和常量折叠没做干净的操作,简化之后不仅文件更小,转换成功率也更高。
3.2 RKNN模型转换的参数配置逻辑
RV1106的转换工具和RK3588不太一样,你需要用瑞芯微RV1106 SDK里提供的rknn_toolkit。核心配置项包括mean_values、std_values、quantized_dtype和quantized_algorithm。这里重点说两个:
mean_values和std_values必须和你训练时的预处理完全一致。我见过有人训练时用的是ImageNet的均值和方差,转换时忘了改,结果精度直接崩了。quantized_algorithm建议选mmse而不是默认的kl_divergence,尤其是在校准数据量不大的情况下,MMSE对异常值更鲁棒。
转换命令大概长这样:
python3 rknn_convert.py \ --model mobilenet_v2_sim.onnx \ --mean_values "123.675 116.28 103.53" \ --std_values "58.395 57.12 57.375" \ --quantized_dtype asymmetric_quantized-8 \ --quantized_algorithm mmse \ --dataset calibration_images/ \ --output mobilenet_v2.rknn转换完成后,工具会输出每一层的量化误差信息。如果某一层的余弦相似度低于0.95,就要警惕了,这一层可能是精度损失的主要来源。
3.3 板端推理代码的编写要点
RV1106的板端推理用的是librknn_api,C接口。初始化流程大概是:加载模型、查询输入输出属性、设置输入数据、运行推理、获取输出。这里有个容易忽略的点是输入数据的排布格式。RV1106的NPU期望的输入是NHWC,而且数据类型是UINT8。如果你从摄像头拿到的是YUV数据,还需要先做颜色空间转换。
rknn_context ctx; rknn_init(&ctx, model_data, model_size, 0, NULL); rknn_input_output_num io_num; rknn_query(ctx, RKNN_QUERY_IN_OUT_NUM, &io_num, sizeof(io_num)); rknn_input inputs[1]; inputs[0].index = 0; inputs[0].type = RKNN_TENSOR_UINT8; inputs[0].fmt = RKNN_TENSOR_NHWC; inputs[0].size = 224 * 224 * 3; 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);实测下来,want_float设为1会让输出自动反量化成float,省去手动处理的麻烦,但会稍微增加一点后处理时间。如果你的分类后处理很简单(就是取argmax),这点开销可以忽略。
4. 五个主流分类模型的实测数据对比
4.1 推理速度与精度的权衡表
我把五个模型都在同一块RV1106开发板上跑了一遍,输入分辨率统一用224x224(SqueezeNet用227x227),量化方式都用MMSE加自备校准集。结果如下:
| 模型 | 参数量 | INT8精度(Top-1) | 单帧耗时 | 内存占用 |
|---|---|---|---|---|
| MobileNetV2 | 3.4M | 70.2% | 35ms | 较低 |
| ResNet18 | 11.7M | 68.5% | 82ms | 较高 |
| EfficientNet-Lite0 | 4.7M | 72.8% | 48ms | 中等 |
| ShuffleNetV2 | 2.3M | 66.1% | 28ms | 低 |
| SqueezeNet | 1.2M | 58.3% | 22ms | 很低 |
这张表里最值得说的是ResNet18。它的精度并没有比MobileNetV2高多少,但耗时是后者的两倍多,内存占用也明显更大。在RV1106这种资源受限的平台上,ResNet18的性价比很低。反而是EfficientNet-Lite0表现最好,精度最高,耗时也在可接受范围内。
4.2 不同输入分辨率下的性能变化
输入分辨率对推理耗时的影响几乎是线性的。我把MobileNetV2分别用128、160、192、224四个分辨率跑了一遍,耗时分别是12ms、18ms、26ms、35ms。精度方面,224降到160大概掉2个百分点,降到128掉5个百分点左右。
这里有个经验:如果你的分类任务类别数不多(比如少于20类),而且类间差异明显,完全可以用160甚至128的分辨率,省下来的时间可以用于更复杂的后处理或者多帧投票。我在一个只有8类的工业场景里用128分辨率,精度只掉了1.2个百分点,但帧率翻了一倍多。
4.3 量化前后精度损失的逐模型分析
量化损失这件事,不同模型的表现差异很大。MobileNetV2的深度可分离卷积对量化比较敏感,尤其是逐通道卷积那部分,权重分布不均匀导致量化误差偏大。EfficientNet-Lite因为本身设计时就考虑了量化友好性,损失相对小。ShuffleNetV2的通道混洗操作在量化后信息损失比较明显,这也是它精度偏低的原因之一。
注意:如果你发现某个模型量化后精度掉得特别厉害,可以试试混合量化,把敏感层保持FP16,其他层用INT8。RKNN工具链支持这个功能,但RV1106上FP16层的执行效率会低一些,需要权衡。
5. 部署过程中那些让我熬夜的坑
5.1 模型转换成功但推理结果乱码
这个问题我遇到过两次,两次原因不一样。第一次是输入数据的归一化参数搞错了,训练时用的是0-1归一化,转换时配的是ImageNet的mean/std,导致输入分布完全不对。第二次是ONNX导出时没有把模型设为eval模式,BatchNorm层还在用训练时的统计量,转换后行为不一致。
排查这类问题的思路是:先在PC上用ONNX Runtime跑一遍,确认ONNX模型本身的输出是对的。然后用RKNN-Toolkit的仿真模式跑一遍,对比仿真结果和ONNX结果的差异。如果仿真就不对,说明是转换配置的问题;如果仿真对但板端不对,那就是板端代码的问题。
5.2 内存不足导致的模型加载失败
RV1106的内存是真的紧张。我一开始试图加载一个输入为320x320的ResNet18,直接报rknn_init失败。后来用rknn_query查了一下模型需要的内存,发现超过了可用上限。解决办法有三个:降低输入分辨率、换更小的模型、或者把模型拆成两段分别加载(但这样会增加推理延迟)。
实际项目中我建议在模型选型阶段就把内存预算算清楚。一个粗略的估算方法是:模型文件大小加上输入输出张量的大小,再乘以1.5的安全系数。如果超过可用内存的70%,就要考虑换模型了。
5.3 多线程推理时的资源竞争
RV1106的NPU是单核的,不支持多线程并行推理。我一开始想用多线程来 overlap 预处理和推理,结果发现两个线程同时调用rknn_run会直接崩溃。正确的做法是用一个推理线程加一个预处理线程,通过队列传递数据,推理线程串行执行。这样虽然不能并行推理,但预处理的时间可以被隐藏掉,整体吞吐量还是有提升的。
6. 针对不同场景的模型选型建议
6.1 高帧率优先场景怎么选
如果你的场景要求帧率优先,比如高速流水线上的缺陷检测,那ShuffleNetV2或者SqueezeNet是首选。虽然精度低一些,但28ms和22ms的耗时意味着你能跑到35-45FPS,这对于捕捉快速运动物体很关键。配合多帧投票策略,精度损失可以被部分弥补。
6.2 精度优先场景的取舍
如果精度是第一位,比如医疗辅助诊断或者精细农业分类,那EfficientNet-Lite0是最优解。72.8%的Top-1精度在RV1106上已经很难得了,48ms的耗时也不算离谱。如果还不够,可以考虑用EfficientNet-Lite1,但内存和耗时都会明显上升,需要实测确认能不能跑得动。
6.3 超低功耗场景的极限压缩
有些场景是电池供电,比如野外监测设备,功耗比帧率更重要。这时候可以把输入分辨率降到96x96,用MobileNetV2的窄版本(width multiplier 0.5),推理耗时能压到8ms以内,功耗也大幅下降。代价是精度会掉到60%左右,只适合类别极少且类间差异极大的任务。
7. 一些实测出来的调优技巧
7.1 校准数据集的质量比数量重要
我试过用1000张随机图片做校准和用200张场景相关图片做校准,后者的量化精度明显更好。校准数据的分布要尽量覆盖你实际推理时会遇到的各种情况,包括不同光照、不同角度、不同背景。如果某些类别的样本特别少,量化后这些类别的精度会明显偏低。
7.2 输入数据的预处理尽量放在NPU外
RV1106的NPU做归一化和颜色空间转换的效率不高。我建议把mean/std的减除和除法操作放在CPU上做,NPU只负责纯推理。虽然这样会增加一点CPU负载,但整体延迟反而更低。实测下来,把预处理从NPU移到CPU后,端到端延迟降低了大概15%。
7.3 模型剪枝和量化要配合使用
单纯量化能带来的压缩有限,如果配合通道剪枝,效果会好很多。我用一个简单的L1-norm剪枝把MobileNetV2剪掉了30%的通道,精度只掉了0.5个百分点,但推理耗时从35ms降到了26ms。剪枝后的模型再量化,精度损失也比直接量化小,因为剪枝本身起到了一定的正则化作用。
7.4 板端后处理的优化空间
分类模型的后处理通常就是softmax加argmax,看起来没什么优化空间。但在RV1106上,如果类别数很多(比如1000类),softmax的计算量也不可忽视。我试过用查表法替代指数运算,在类别数大于500时能省下2-3ms。另外,如果只需要Top-5结果,没必要对整个输出向量做完整排序,用部分选择算法就够了。
8. 从实验数据回头看RV1106的定位
折腾完这一轮,我对RV1106的定位有了更清晰的认识。它不适合跑大模型,也不适合做需要高精度的复杂任务。但如果你需要一个成本极低、功耗极低、能跑轻量级分类模型的边缘推理方案,它确实是个不错的选择。关键是要选对模型、做好量化、把预处理和后处理的负担从NPU上卸下来。
我目前在一个农业虫情监测项目里用RV1106跑MobileNetV2的160分辨率版本,配合多帧投票,实际分类准确率能到85%以上,整机功耗控制在1W以内,用太阳能板加小电池就能常年运行。这个案例让我觉得,边缘AI的价值不在于跑多大的模型,而在于用合适的算力解决具体的问题。
最后分享一个我在调试过程中养成的习惯:每次转换完模型,先别急着上板子,用RKNN-Toolkit的仿真模式跑一遍完整的验证集,把每一类的精度都看一下。如果某一类的精度异常低,大概率是校准数据里这类样本太少,补一些进去重新量化就行。这个步骤花不了十分钟,但能帮你省下大量在板子上反复烧录调试的时间。