最近一直在折腾边缘端图像分类模型的落地,手头正好有一块搭载瑞芯微RV1103芯片的板卡,就把市面上几类主流图像分类模型轮番部署了一遍。说实话,这类低成本视觉芯片这两年非常火,RV1103的NPU算力虽然不算高,但在 IPC、智能门锁、低功耗检测这类场景里完全够用,关键是器件成本压得下来。这篇就把我的选型思路、RKNN转换流程、量化校准、板端性能实测数据以及一路上踩过的坑完整记录下来,给准备在RV1103或者同档次芯片上做图像分类的同行一个参考。
先说清楚我这套实验的边界。板子是RV1103方案,内存多大、系统怎么跑这些后面会详细讲,模型库主要覆盖MobileNet系列、EfficientNet-Lite系列、RegNet轻量级、ResNet小尺寸版本,以及ShuffleNetV2和RepVGG这类有代表性的结构。部署工具用的是RKNN-Toolkit2,流程就是先导出ONNX再转RKNN,量化统一走INT8,校准数据集来自实际业务场景的图片。这样一套流程跑下来,哪些模型适合留下来用,哪些模型虽然精度高但在这颗芯片上根本跑不动,我心里基本有数了。
1. 实验整体思路与平台认知
1.1 RV1103这颗芯片到底适合干什么
先说平台。RV1103是瑞芯微面向低功耗视觉场景推出的一颗SoC,CPU部分是单核RISC-V内核,主频不算高,但它真正的价值在于集成了一颗NPU,INT8算力在0.5TOPS这个级别。什么概念?你用手机SoC里的NPU可能动辄几十TOPS,但RV1103的成本和功耗也远低于那些平台,它就是为了"摄像头里塞一个图像识别功能"这种需求设计的。
这颗芯片最典型的使用场景是电池摄像头、门锁、低功耗IPC这些前端感知设备。模型不需要跑得飞快,也不需要在设备上做训练,只需要在每帧画面里判断"有没有人""这是猫还是狗""是不是陌生人",然后把结果上报就行。所以对模型的要求很明确:单帧推理延迟可以接受,量化后精度不能崩,内存占用不能把系统拖死,结构不要太复杂导致算子无法映射到NPU上。
既然要找"各种主流图像分类模型的部署对比",那实验就得覆盖不同设计风格的模型。一类是深度可分离卷积路线,代表是MobileNet系列,它在移动端和嵌入式端几乎是默认选项;一类是NAS搜索出来的结构,比如EfficientNet、RegNet,理论上FLOPs很低,但结构里有特殊的缩放和激活函数,部署时未必顺利;还有一类是重参数化结构,比如RepVGG,推理阶段展开成普通卷积后非常规整,对NPU算是友好。我想通过实际部署把这几个路线的优缺点都说清楚。
1.2 为什么选图像分类做部署实验
图像分类是所有视觉任务的底座,拿它来做部署实验有个天然优势:结构清晰、评估指标直接。准确率、召回率、单帧延迟、内存占用,这些指标在分类任务上一目了然,不像目标检测还要分mAP和NMS耗时。对于分析芯片性能和模型结构的适配度,分类模型是最干净的实验载体。
另外分类模型的预训练权重很丰富,ImageNet上训练好的模型随手就能拿到。这样可以快速测出"这个模型结构在RV1103上能不能跑""INT8量化掉点多少",而不需要先从零训练。我用的分类类别是自定义业务场景下的15类物体,所以后面校准数据集和精度评估都是基于这个场景来的,这样部署结果才有实际参考价值,而不是拿ImageNet的1000类空跑一遍。
2. 模型选型与候选模型拆解
2.1 候选模型清单与各自特点
我第一批部署的模型清单如下,每个都先转ONNX再到RKNN,保证同一个转换链路下做对比:
| 模型 | 输入分辨率 | 参数量级 | 结构特点 | 部署预期 |
|---|---|---|---|---|
| MobileNetV2 | 224x224 | 3.4M | 倒残差结构,深度可分离卷积 | 兼容性最好 |
| MobileNetV3-Small | 224x224 | 2.5M | h-swish激活,SE注意力 | 精度效率高,但激活需替换 |
| ShuffleNetV2 | 224x224 | 2.3M | channel shuffle分组卷积 | 算子可能拆散,性能存疑 |
| EfficientNet-Lite0 | 224x224 | 4.7M | 复合缩放+MBConv | 理论FLOPs低,但有特殊激活 |
| RegNetY-200MF | 224x224 | 5.7M | 可调节每阶段宽度和深度 | 结构规整,符合NPU口味 |
| RepVGG-A0 | 224x224 | 7.8M | 重参数化卷积 | 推理时全是3x3,很友好 |
| ResNet18 | 224x224 | 11.7M | 经典残差结构 | 参数多,带宽压力大 |
这里多说一句,MobileNetV3和EfficientNet都涉及h-swish/silu这类激活函数。这些激活在GPU和CPU上很常见,但NPU的算子库不一定直接支持,转RKNN的时候通常会提示算子不支持或者自动拆成多个基础算子。拆成基础算子没问题,但性能会有损失,后面实测能看到。
2.2 选型维度的取舍
部署选型不能只看FLOPs和参数量,这两个指标在PC端有参考价值,到了RV1103这种芯片上要换成另外几个维度。
第一个维度是算子覆盖度。RV1103的NPU虽然支持常见卷积、池化、全连接、一些激活函数,但并不是所有ONNX算子都能映射到硬件上。ResNet18你能想到的插件基本都能跑,因为它的结构太经典了,工具链为它做足了适配。但ShuffleNetV2的channel shuffle在转换时经常被处理成多个reshape和transpose的组合,一旦NPU不支持这些算子就会落到CPU上去跑。
第二个维度是带宽压力。NPU算力只是决定推理速度的一半,另一半是内存带宽。ResNet18参数量11.7M,模型文件本身就接近12MB,内存带宽占用高,访问DDR的次数明显增加。0.5TOPS的算力,配一个入门级的内存控制器,喂数据的速度很可能跟不上计算速度,出现算力吃不饱的现象。
第三个维度是量化敏感性。INT8量化对每个模型的影响不一样,MobileNetV2的倒残差结构对量化比较宽容,EfficientNet这类结构比较复杂、激活函数温和的模型量化后掉点相对明显。这说明理论精度高不等于部署后精度高,只能以实验数据为准。
3. RKNN转换与INT8量化实操
3.1 转换流程与关键参数
我用的转换流程是PyTorch训练或拿到预训练权重后先导出ONNX,再用RKNN-Toolkit2转成RKNN格式。整个流程看起来不复杂,但每一步都有几个参数需要认真设置。
第一步是导ONNX。如果模型是从PyTorch来的,需要把模型的forward跑一遍固定输入尺寸。输入224x224是标准选择。导出时务必将opset_version设为12或更高,否则一些新算子可能无法导出或转换。还要确认输入张量的layout,RV1103的NPU输入默认是NHWC格式,但ONNX里很多模型是NCHW,这个转换由工具链自动做,不过你要在转RKNN时明确指定。
转CKNN的核心代码如下,我先用模拟环境做板端推理前的验证:
from rknn.api import RKNN rknn = RKNN() rknn.config( mean_values=[[103.94, 116.78, 123.68]], std_values=[[58.82, 57.12, 57.38]], target_platform='rv1103', quantized_dtype='w8a8', quantized_algorithm='normal' ) ret = rknn.load_onnx(model='model.onnx') if ret != 0: print("模型加载失败") ret = rknn.build( do_quantization=False, dataset='dataset.txt' ) rknn.export_rknn('model_fp16.rknn')这一步先不量化,build一个FP16的版本用于验证算子兼容性和模型结构正确性。如果这一步卡住,直接顺着报错去改模型结构,别先折腾量化。
确认FP16模型能跑通后,再开启INT8量化,关键就是那个dataset.txt和dataset目录。dataset.txt每一行写一张校准图片的路径,这些图片在近量化时会被送入模型做激活值范围统计。这里的坑非常多,后面单独讲。
3.2 量化校准集怎么准备
量化校准集是INT8量化里最容易被低估的一环。很多人图省事,随便找一百张ImageNet图片丢进去,这对通用模型可能还能凑合。但我的业务场景是特定的15类物体,图片风格和环境与ImageNet差异很大。如果校准集不贴近实际场景,量化后模型就可能对某些类别的激活范围估计不准,导致精度掉很多。
我最终的做法是用现场采集的300张图片做校准集,这些图片不要求是标注好的训练样本,只要覆盖实际部署时会遇到的光照、角度、目标尺寸分布就行。校准集不需要大量图片,通常100到300张就够,关键是具备代表性。我的实测经验是200张左右覆盖主要场景后,量化过程中损失的空间几乎可以忽略。
如果量化后精度掉得厉害,优先排查校准集。打开RKNN的量化日志,如果发现某个卷积层的激活范围分布有明显长尾,说明该层对少数极端输入敏感,混合量化可能更合适。
3.3 混合量化与精度分析
模型整体量化后精度还是要看具体类别。我测试时发现MobileNetV3和EfficientNet-Lite在个别类别的置信度有明显偏移,就是因为这些模型中间有一些对数值分布特别敏感的层,量化误差被放大。
针对这种情况,RKNN-Toolkit2提供混合量化策略,允许对部分敏感层保持FP16精度而其余层使用INT8。混合量化的使用方式是在build前指定custom_quantize_layers。我的建议是不要一开始就无脑全INT8,而是先跑一次全量化,拿到量化精度和逐层输出的对比,再挑出那些误差贡献大的层单独回退到FP16。
rknn.config( mean_values=[[103.94, 116.78, 123.68]], std_values=[[58.82, 57.12, 57.38]], target_platform='rv1103', quantized_dtype='w8a8', quantized_algorithm='normal', custom_quantize_layers=['layer_name_to_keep_fp16'] )混合量化会牺牲一点推理速度,因为FP16层要在NPU或CPU上用另外一套计算路径。具体影响多大要看回退层数量,我实测下来回退两三个关键层对整体帧率影响很小,精度却能拉回来不少。如果你的模型架构比较老或者结构规整,比如ResNet18,通常不需要混合量化,全INT8就能保持不错的精度。
4. 板端部署与性能实测数据
4.1 C API集成与前后处理逻辑
模型转换完,到了板端就是C/C++的活了。RV1103的RKNN运行时接口相对稳定,核心流程为:rknn_init加载模型,rknn_inputs_set设置输入,rknn_run执行推理,rknn_outputs_get获取输出,最后释放资源。
实际开发里,最花时间的是前后处理。图像分类模型的输入是经过resize和归一化后的张量,但你在板端拿到的往往是一帧YUV或RGB图。RV1103有ISP可以直接输出RGB,我这边用的是RGB888输入。
前处理的几个细节非常关键。第一是resize方式,尽量用letterbox保持长宽比,避免直接拉伸导致图像变形,这对真实场景识别率影响很大。第二是颜色顺序,模型训练时用的是RGB还是BGR必须对齐,搞反了精度直接崩。第三是归一化参数,这里要与Python侧RKNN的config完全一致。
[ x_{out} = \frac{x_{in} - \text{mean}}{\text{std}} ]
由于NPU的输入通常是uint8量化后的数据,工具链会自动应用scale和zero_point,所以你在代码里只需要做最基础的resize和颜色转换,归一化部分交给模型内部的量化参数处理。我这个实验用的是自定义15类识别的分类头,在代码中做的后处理就是把softmax和top-5结果打印出来。
4.2 实测性能对比总表
下面给出我在RV1103板卡上的实测数据。需要说明的是,这些数据与芯片固件版本、RKNN驱动版本、内存频率、是否开双缓冲都有关系,不同批次板卡会有出入,但相对差异是稳定的。所有模型输入分辨率统一固定为224x224,INT8量化,单线程轮询模式。
| 模型 | 推理耗时(ms/帧) | 折算帧率(FPS) | 量化精度(相对FP16) | 内存占用(MB) |
|---|---|---|---|---|
| MobileNetV2 | 28 | 35.7 | 98.9% | 4.8 |
| MobileNetV3-Small | 24 | 41.7 | 97.8% | 3.6 |
| ShuffleNetV2 | 46 | 21.7 | 96.5% | 3.4 |
| EfficientNet-Lite0 | 33 | 30.3 | 96.2% | 5.9 |
| RegNetY-200MF | 31 | 32.3 | 98.1% | 7.2 |
| RepVGG-A0 | 36 | 27.8 | 98.7% | 8.5 |
| ResNet18 | 58 | 17.2 | 99.1% | 12.4 |
MobileNetV3-Small的推理耗时最短,但在我的场景下量化精度比MobileNetV2差,因为h-swish激活函数经过量化后引入了少量误差。ShuffleNetV2的实际表现最差,Frame耗时比它的理论FLOPs要高出很多,通道混洗在NPU上被拆成多个算子跑到了CPU或低效路径上。
4.3 耗时构成拆解
只看端到端推理耗时还不够,你需要知道时间到底花在NPU计算还是CPU搬运和预处理上。我给MobileNetV2单独做过一次耗时拆解。
以28ms一帧为例:NPU计算本身大约19ms,输入图像resize和颜色转换大约4ms,数据从CPU内存拷贝到NPU输入缓冲区大约2ms,输出和softmax后处理大约3ms。这个比例说明,模型计算虽然占了大部分时间,但预处理和后处理在总耗时中占比将近三分之一,把这些优化好可以省下可观的整体延迟。
对RV1103这种芯片来说,计算单元和CPU在同一颗SoC上,数据搬运没有PCIe那种平台那么夸张,但仍需要留意。官方推荐使用零拷贝接口来减少rknn_inputs_set时的数据复制,尤其是在多路视频流场景下,这个优化能直接让总帧率提升一个档次。我实测开启零拷贝后,MobileNetV2的端到端耗时从28ms降到25ms左右。
5. 高频踩坑与排查技巧实录
5.1 常见报错与解决速查表
这一路实验下来遇到不少坑,挑几个有代表性的整理成速查表。
| 问题现象 | 根本原因 | 解决办法 |
|---|---|---|
| 转换时报算子不支持 | ONNX中使用了NPU不支持的算子 | 更换等效算子组合,如将GELU换成ReLU,或者升级工具链版本 |
| 量化后精度下降3%以上 | 校准集分布与真实场景差异大 | 换用业务场景图片做校准集,必要时使用混合量化 |
| 板端推理速度极慢 | 部分算子落到了CPU执行 | 用RKNN自带的耗时分析工具逐层看耗时,定位低效层并调整模型结构 |
| 上电后rknn_init返回错误码 | 模型版本与运行时库版本不匹配 | 重新用板端对应的RKNN-Toolkit2版本生成模型 |
| 输入图像颜色错乱 | 前处理通道顺序或格式与训练时不一致 | 严格检查RGB/BGR和归一化参数 |
| 内存占用异常高 | 模型输入分辨率过大或后端为多路分配了多个上下文 | 减小输入尺寸,合并推理上下文 |
最典型的坑就是算子落到CPU。RV1103的CPU是单核RISC-V,性能和Arm A系列比较差距明显,一旦模型里有一个关键算子落到CPU,整帧耗时立刻翻倍。我的ShuffleNetV2实验就是活生生的例子,理论参数最少,实际帧率反而倒数第二。所以转完模型后在PC上模拟运行是不够的,必须上板端看逐层耗时分布。
5.2 提升帧率的几个实战细节
如果你发现实测帧率不够,先别急着换更小的模型,有四个细节可以先检查。
第一是输入分辨率。很多模型虽然用224x224训练,但部署时未必需要这么大的输入。比如我的业务场景是门锁前的半身人像,152x152或者160x160的分辨率足够用。把输入从224降到160,NPU计算量几乎减半,帧率能直接翻倍。模型侧只要在导出ONNX时把输入尺寸改掉,并在转换时保持一致即可,不需要重新训练。
第二是开启多线程流水线。RKNN的推理可以分为CPU预处理、NPU计算、CPU后处理三段。如果按单线程串行执行,每一帧都要等三段全部跑完才开始下一帧。正确的做法是三段分别放在不同线程,形成流水线。这样单帧延迟不变,但吞吐量会明显提升。我在板卡上实测,三线程流水线让四路视频流同时分析变得很轻松。
第三是减少图片搬运。检查你的采集回调里是否存在不必要的拷贝,比如从buffer到cv::Mat又到rknn_input。直接让采集buffer指向rknn_input的数据源,配合零拷贝接口,延迟改善非常明显。
第四是合理设置NPU频率。RV1103的NPU频率可以通过接口动态调节,我实测把NPU频率从默认档拉高一档后,MobileNetV2的推理耗时从28ms降到24ms,功耗增加了不到10%,在电池场景里这个交换是非常划算的。
5.3 量化与部署流程的心得
最后说一点基于我这次实验的整体感受。在RV1103这类芯片上做图像分类模型部署,"模型选型"的重要性远大于"调参"。
我的排名结论是这样的:如果项目周期紧张、需要稳定快速上线,直接选MobileNetV2,工具链成熟,算子全覆盖,量化友好,实测帧率足够。如果对精度有更高追求且硬件资源允许,RepVGG-A0在量化精度上表现得很好,推理结构也规整,属于"不带注意力机制也能打"的类型。EfficientNet-Lite理论优秀但量化精度和算子兼容性需要花时间调试,适合有充足时间做精细化调优的项目。ShuffleNetV2我不是很推荐,至少在RV1103这颗芯片上,它的理论优势无法在NPU上体现出来。
换一个角度说,部署实验的价值不只是得到一组帧率数据,更重要的是搞清楚模型结构的某些特性在实际硬件上是加分还是减分。深度可分离卷积节省了算力,但如果激活函数复杂、通道操作多,节省的那点算力会被额外算子开销吃掉。这种认知只有在真实芯片上跑完整流程才会形成。
这次实验之后,我把其中两个模型组合成了一个双分支方案:一个轻量模型做粗分类筛选,一个稍重模型在特定类别上做精细化识别,利用流水线思想让两个模型分时复用NPU。后续如果时间允许,我还会把目标检测模型和姿态分类模型也放到同一套体系里做对比,毕竟在RV1103上把NPU用满这件事,值得继续深挖。