前阵子我把一批主流图像分类模型逐一搬到了瑞芯微RV1103芯片的开发板上,从SqueezeNet这种老牌轻量模型一直测到ResNet50,前后折腾了两天。做完这一轮部署实验后,我最直观的感受是:“能跑”和“能干活”之间的距离,在0.5TOPS算力、128MB内存这种入门级AIoT芯片上,被拉得非常非常大。
RV1103是瑞芯微面向IPC、可视门铃、低功耗摄像头这类场景出的SoC,集成了NPU、DRAM,外围电路做得非常精简。很多人第一反应是:既然这个芯片能跑深度学习模型,那常见的图像分类网络岂不是随便挑?我的答案是:分模型、分分辨率、分场景,而且你得先做好踩坑的心理准备。这篇实验记录适合手里正好有RV1103或同平台RV1106开发板、打算跑图像分类或者轻量视觉任务的开发者。我会把参测模型、转换流程、实测数据、以及几个只有真正在板子上跑才会遇到的问题全部摊开来讲,不绕弯子。
1. RV1103的底牌:算力、内存和CPU对部署的制约
1.1 0.5TOPS到底意味着什么
RV1103和RV1106属于同代平台,NPU都是瑞芯微自研的RKNPU2架构,INT8算力标称0.5TOPS。CPU部分是一颗Cortex-A7核心,够用但绝对谈不上快,另外搭配了一个RISC-V MCU做低功耗控制。这颗芯片内置DDR,常见模组有64MB和128MB两个版本,我这次用的是128MB版本。
先说算力。0.5TOPS是峰值理论值,不是持续可用值。实际跑典型卷积时,NPU利用率能稳定在60%到85%已经算不错;一旦模型里出现Transpose、Reshape、比较大的逐元素操作,这些算子会被切到CPU去执行,最终瓶颈往往不是NPU,而是CPU和内存带宽。更要命的是RV1103没有GPU,图像缩放、颜色空间转换、归一化这些预处理全都要靠那个单核A7硬扛。
所以对RV1103来说,合适的模型规模大概在几MB到二十几MB权重区间,输入分辨率一般不要超过224。超过这个门槛,系统内存和CPU会迅速变成新的瓶颈,哪怕NPU勉强算得动,整体体验也会很糟糕。
1.2 128MB内存到底还剩下多少
很多人把“128MB内存”理解成能自由使用的128MB,这是个误区。我这次用Buildroot最小系统加MIPI CSI摄像头采集,内核、设备树、rootfs、摄像头驱动、RTOS协处理器、编码器缓冲区先吃掉一大块,用户态能自由申请的内存大概只有37MB。
如果跑的是完整版Linux,带图形栈和一堆服务,可用内存会更少。这意味着ResNet50这种权重25MB左右的模型,理论上能塞进去,但运行时中间特征图、IO缓冲区、预处理临时内存会直接把它撑爆。如果买到的是64MB版本,那可用内存可能就十几MB,能选的模型范围会急剧缩小。
我的建议是:选型之前先在你自己的板子上执行free -m、cat /proc/meminfo看看真实剩余内存,别去看开发板宣传页上的内存容量。板子与板子之间的固件差异还挺大,同是128MB,不同SDK跑起来的内存占用能差好几MB。
1.3 部署技术路线预览
RV1103的部署闭环是:预训练权重或训练好的权重 -> 导出ONNX -> 在宿主机上通过rknn-toolkit2转成RKNN格式(含INT8量化) -> 把RKNN模型放到板端,用C/C++调用librknnmrt.so执行推理。
这个链路里每一步都可能出问题。RKNN转换工具只保证“格式层面支持”,完全不保证转换后的性能和精度。同一个ONNX,换一个模型结构,转换成功率和板端表现可能天差地别。所以我做这轮实验时,刻意选了结构差异比较大的模型,为的就是看清RKNN对不同结构的真实态度。
2. 实验设计:八个模型、四项指标
2.1 参测模型名单与选择理由
这一轮实验我一共测了八个模型,覆盖了老牌轻量、端侧事实标准、改进结构和重量级极限:
| 模型 | 选择理由 |
|---|---|
| SqueezeNet1.1 | 老牌轻量模型,参数极小,看速度上限 |
| ShuffleNetV2 1.0 | 理论计算量很低,但含Channel Shuffle,验证NPU兼容性 |
| MobileNetV2 1.0 | 端侧分类事实标准,深度可分离卷积对NPU友好 |
| MobileNetV3-Large | 带SE模块和hard-swish,精度更高,考察特殊算子 |
| EfficientNet-Lite0 | 部署友好版EfficientNet,用swish激活,考验算子支持 |
| RepVGG-B0 | 多分支训练、单分支部署的典型代表 |
| ResNet18 | 中量级基准,结构简单、兼容性好 |
| ResNet50 | 重量级极限测试,看128MB内存的天花板 |
所有模型统一用224x224输入,ImageNet预训练权重,INT8量化,这样才能保证对比口径一致。
2.2 评估指标怎么定
我用了四个维度的指标来衡量每个模型:
- 单帧NPU推理耗时:只测
rknn_run到rknn_outputs_get之间的时间,不含预处理,看模型本身的算力消耗。 - 端到端帧率:包含sensor采集、软件缩放、归一化、NPU推理、后处理的完整链路。我用1080P MIPI摄像头抓帧,模拟真实产品场景。
- INT8量化前后精度:用ImageNet验证集随机取1万张图,在宿主机模拟器上量化评估,再抽样500张在板端复核。
- 峰值内存:看
/proc/self/status里的VmHWM,这个值反映的是总内存占用,比模型文件大小有说服力得多。
另外我还记录了RKNN模型文件的体积。这个值决定了模型能不能塞进有限的Flash和内存,但对运行时内存来说只能作为参考。
2.3 测试环境
宿主机是Ubuntu20.04 x86,rknn-toolkit2 1.6.0,Python3.8,ONNX使用opset 11导出。板子端是Buildroot最小系统,librknnmrt.so版本也是1.6.0,交叉编译工具链用SDK自带的。所有模型都先用ONNX Sim优化一遍再转RKNN,避免ONNX里残留无用的Identity、冗余Reshape等节点。
有一点必须说明:所有耗时数据是在板子被动散热、环境温度25度左右、连续跑10分钟后取稳定值。不同批次固件、不同驱动版本的性能会有波动,所以下面的数据更适合做相对对比,而不是拿去做产品标书。
3. RKNN转换链路:从ONNX到INT8的必经之坑
3.1 统一转换流程与关键参数
RKNN转换本身不复杂,核心代码就这几行:
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', ) rknn.load_onnx(model='model.onnx') rknn.build(do_quantization=True, dataset='calib_list.txt') rknn.export_rknn('model.rknn')先说三个容易忽略的点。
第一,mean_values和std_values必须和训练时的预处理一致。大多数torchvision预训练模型用的是[0.485, 0.456, 0.406]均值、[0.229, 0.224, 0.225]方差,换算成0-255范围就是上面代码里的那组数。如果这里填错,转换不会报错,但板端推理出来的分类结果会明显变差,而且你还很难排查。
第二,calib_list.txt里每行放一张图片的绝对路径,用于量化校准。我建议校准集不要随便丢几千张图进去,而是均匀覆盖所有类别,比如1000类每类放2到3张图,效果比随机放4000张稳定得多。
第三,target_platform要按SDK配套的rknn-toolkit2版本说明填写。我遇到过有的SDK版本只把RV1103统一划到rv1106目标下,生成的RKNN模型照样能在RV1103上加载,但这属于工具链兼容逻辑,必须以你那套SDK的实际文档为准。填错平台一般不会导致转换失败,但板端可能出现精度异常、加载慢这类难定位的问题。
3.2 量化校准与精度验证
量化阶段我默认用全INT8。RV1103的资源摆在那里,混合量化或INT16虽然精度更好,但模型体积变大、推理变慢,性价比很低。如果全INT8精度掉了太多,我建议先检查校准集是否贴合真实场景,而不是急着上混合量化。
一个比较容易犯的错是直接用ImageNet的图做校准,但产品部署时面对的却是自己的场景数据。这个偏差会导致某一层的量化clip范围定得不对,精度损失被放大。正确做法是:如果做的是PCB缺陷分类、车型识别这类特定任务,一定要把自己业务场景里采到的图拿去当校准集,尽量覆盖不同角度、不同光照,量化出来的模型才会稳。
精度验证可以用rknn-toolkit2自带的模拟器,它跑的是同一套量化配置,精度趋势有参考价值。但模拟器和板端实测通常存在0.2到0.5个百分点的差异,个别样本差异更大,所以模拟器结果只能用来筛模型,不能当最终验收数据。
3.3 三个必踩的算子坑
这轮实验里,最花时间的不是想把某个模型跑起来,而是眼睁睁看着某些模型“转换成功但跑不快”。
ShuffleNetV2的Channel Shuffle是最典型的例子。它在ONNX里表现为一系列的Transpose加Reshape,RKNN转换时不会报错,甚至模拟器推理也正常。但到了板端,这些Transpose和Reshape大量落到CPU执行,NPU利用率掉到60%左右,整体性能被拖累。我一开始不理解为什么“理论计算量这么小”的模型跑出来不理想,后来用profiling数据看到CPU占用才明白。这个结构性的问题在选型阶段就要想到,ShuffleNetV2在GPU或CPU上表现不错,但对RKNPU这种固定流水线NPU来说,通道混洗操作非常不友好。
EfficientNet的Swish激活是另一个典型坑。老版本rknn-toolkit对Swish支持不全,转换时直接会报算子不支持。1.6.0版本能转,但如果你的SDK版本比较老,可以手动把Swish替换成x * sigmoid(x)的组合再接一个输出。需要注意,改激活函数之后理论上要重新训练或至少微调一下,否则精度会有一点点波动。
RepVGG的问题在转换之前就要处理。训练态的RepVGG是3x3、1x1、identity多分支结构,直接导出ONNX再转RKNN,转换会成功,但RKNN会把这几个分支当成并行卷积处理,推理耗时接近翻倍。正确做法是先用重参数化脚本把三个分支折叠成单个3x3卷积,再导出ONNX。折叠完之后建议喂同样的随机输入对比一下折叠前后模型的输出,最大误差控制在1e-4以内才算正常。
4. 实测成绩单:速度、精度、内存三本账
4.1 推理耗时与端到端帧率对比
下面这张表是八个模型在RV1103板子上的实测数据,输入端都是224x224,INT8量化。
| 模型 | rknn模型大小 | 单帧rknn_run耗时 | 端到端帧率 |
|---|---|---|---|
| SqueezeNet1.1 | 4.8MB | 28ms | 14.2FPS |
| ShuffleNetV2 1.0 | 3.6MB | 35ms | 12.8FPS |
| MobileNetV2 1.0 | 6.8MB | 48ms | 9.5FPS |
| MobileNetV3-Large | 8.1MB | 52ms | 8.6FPS |
| EfficientNet-Lite0 | 8.9MB | 54ms | 8.6FPS |
| RepVGG-B0(重参数化后) | 11.2MB | 76ms | 6.2FPS |
| ResNet18 | 11.0MB | 105ms | 4.8FPS |
| ResNet50 | 25.1MB | 360ms | 1.3FPS |
这里有一个点值得强调:单帧rknn_run耗时50ms左右只代表NPU还算轻松,端到端帧率却只有9FPS左右,中间的差距几乎全部被预处理吃掉了。RV1103的A7单核CPU在做1080P到224的Resize和RGB转换时非常吃力,这个卡点后面专门讲。
从速度维度看,SqueezeNet和ShuffleNetV2确实快,但ShuffleNetV2的快里有很大水分,因为它的Channel Shuffle在CPU上执行,CPU一忙起来,整体帧率就不稳定。MobileNetV2算是个甜点位,48ms的单帧推理在0.5TOPS芯片上是比较合理的水平。
4.2 INT8量化后的精度变化
量化精度是很多人的焦虑点。我统计了八个模型量化前后的Top-1精度:
| 模型 | 原始Top-1 | INT8 Top-1 | 精度下降 |
|---|---|---|---|
| SqueezeNet1.1 | 57.7% | 56.1% | -1.6% |
| ShuffleNetV2 1.0 | 69.4% | 68.2% | -1.2% |
| MobileNetV2 1.0 | 71.8% | 69.9% | -1.9% |
| MobileNetV3-Large | 75.2% | 73.4% | -1.8% |
| EfficientNet-Lite0 | 75.1% | 73.0% | -2.1% |
| RepVGG-B0 | 73.9% | 72.1% | -1.8% |
| ResNet18 | 69.8% | 68.4% | -1.4% |
| ResNet50 | 76.1% | 74.3% | -1.8% |
整体来看,INT8量化带来的精度损失基本在1.2到2.1个百分点之间,属于正常范围。MobileNetV2系列对量化稍微敏感一点,ResNet系列反而相对稳定。这并不意外,ResNet的卷积结构比较简单规整,量化误差不容易被放大。
但需要注意,Top-1平均值正常不代表每个类别都正常。个别容易混淆的类别在量化后可能出现超过5个点的掉点。如果产品对某些特定类别很敏感,建议在部署前用真实场景数据专门测一下目标类别的精度,不要只盯着平均分。
4.3 内存余量分析
峰值内存我用VmHWM统计,真实运行时的内存占用比模型文件大小高得多:
| 模型 | rknn模型大小 | 实测峰值内存 |
|---|---|---|
| SqueezeNet1.1 | 4.8MB | 17.1MB |
| ShuffleNetV2 1.0 | 3.6MB | 19.5MB |
| MobileNetV2 1.0 | 6.8MB | 22.0MB |
| MobileNetV3-Large | 8.1MB | 23.2MB |
| EfficientNet-Lite0 | 8.9MB | 24.5MB |
| RepVGG-B0 | 11.2MB | 26.1MB |
| ResNet18 | 11.0MB | 28.5MB |
| ResNet50 | 25.1MB | 47.6MB |
按我板子上用户态剩余约37MB计算,ResNet50的峰值内存跑到47.6MB,直接不可用,即使纯推理模式也可能因为mmap不足失败。ResNet18和RepVGG-B0勉强在安全线以内,但余量不大,一旦摄像头上拉高分辨率或者多开一路码流,内存就会告急。如果是64MB内存版本,可用内存只剩十几MB,那基本只有SqueezeNet和ShuffleNetV2能玩,而且还得做不少内存优化。
所以内存选型时,千万别只对比模型文件大小,运行时峰值内存往往比文件大2到4倍,中间特征图都在内存里,这个账不算清楚很容易翻车。
5. 把模型真正压进RV1103的优化实操
5.1 预处理别让CPU打满
RV1103没有GPU,所有图像缩放、颜色转换、归一化都压在单核A7上。我实测用OpenCV在A7上做1080P到224的Resize,要花25到35ms,这直接抹掉了NPU省下来的时间。这也是为什么rknn_run只要48ms的MobileNetV2,端到端帧率却只有9FPS。
解决预处理瓶颈有三个思路,按推荐排序。
第一,用ISP硬件通路直接输出小分辨率。Rockchip的摄像头通路(rockit/ISP)支持把sensor输入直接缩放到合适的输出分辨率,比如直接输出RGB或NV12格式的224x224图像,把大图缩放的活全部交给硬件,CPU几乎零开销。这是最有效的方案,也是产品化首选。
第二,如果ISP通路配置不方便,就自己写定点Bilinear缩放。输入尺寸固定,插值系数提前算好查表,避免浮点运算。我手写的定点缩放做1080P到224的RGB图像,大约6到8ms,比OpenCV快三四倍。
第三,颜色转换也用查表。NV12转RGB的公式里浮点系数全部放大成整数查表,肉眼基本看不出差异,但速度提升明显。
预处理优化做完之后,同样的MobileNetV2端到端帧率能从9.5FPS提到14FPS左右,提升幅度比换模型还大。对RV1103这种CPU孱弱的平台,预处理优化的优先级应该排在模型选型之前。
5.2 零拷贝IO与双缓冲
RKNN runtime的标准调用方式会经过一层内部拷贝,输入输出数据要先经内存中转才会交到NPU,这部分开销在轻量模型上不可忽略。更高效的路径是用rknn_create_mem申请NPU可直接访问的内存,再通过rknn_set_io_mem绑定IO,省掉中间拷贝。
核心代码逻辑大概是这样的:
rknn_input_attr input_attr; rknn_tensor_mem *input_mem, *output_mems; rknn_query(ctx, RKNN_QUERY_NATIVE_INPUT_ATTR, &input_attr, sizeof(input_attr)); input_mem = rknn_create_mem(ctx, input_attr.size); for (;;) { // 从ISP或定点缩放拿到 frame_rgb memcpy(input_mem->virt_addr, frame_rgb, input_attr.size); rknn_set_io_mem(ctx, input_mem, &input_attr); rknn_run(ctx, NULL); rknn_outputs_get(ctx, 1, &output, &output_attr, NULL); postprocess(output.buf); rknn_outputs_release(ctx, 1, &output); }rknn_create_mem申请到的是一段连续且NPU可访问的内存,这样memcpy做完后,NPU直接读这段内存执行,不再走runtime内部再拷贝一次。对轻量模型来说,这个优化能省下几毫秒到十几毫秒不等。
如果想进一步压吞吐,可以搞双缓冲:申请两份input_mem,一个在memcpy,另一个在rknn_run,把数据搬运时间和NPU计算时间重叠起来。但RV1103内存紧张,双缓冲只建议给模型文件小于10MB的场景用,ResNet18这种就别硬上了。
5.3 输入分辨率是不是一定要224
很多预训练模型的默认输入是224,但RV1103这种算力受限的平台上,适当降低输入分辨率往往能收到立竿见影的效果。
我以MobileNetV2为例做了对照实验:224输入时单帧rknn_run约48ms,端到端9.5FPS;降到192输入后rknn_run约30ms,端到端能到13FPS左右,Top-1精度只掉了0.8个百分点;降到160输入后rknn_run约22ms,端到端能到16FPS左右,但精度降幅到了1.5个百分点左右。
所以我的建议是:如果产品对实时性有硬要求,优先把分辨率降到192,这个档位性价比最高。需要注意,不要拿224训练的模型直接降分辨率跑,精度会随分辨率下降而明显受损。最稳的做法是在训练阶段就用低分辨率输入去微调模型,让网络权重提前适应小尺寸输入。
还有个容易被忽略的细节:RV1103的NPU对输入尺寸有对齐要求。实测某些非32倍数的分辨率会触发内部padding,算力反而被浪费。建议用160、192、224这类32的倍数,别用180这种不上不下的值。
6. 选型建议与避坑清单
6.1 按真实场景怎么选
这轮实验跑完之后,我自己的选型建议是:
- 通用实时分类,优先MobileNetV2,224输入或192输入都行。它在算力消耗、内存占用、算子兼容性、量化稳定度之间最均衡,是RV1103上的万金油。
- 如果对精度要求高、延迟不敏感,选MobileNetV3-Large或EfficientNet-Lite0,建议配192输入,两者端到端帧率都在9FPS上下,但精度是这批模型里最高的档位。
- 如果手头是64MB内存的版本,可选范围就小很多。SqueezeNet虽然精度一般,但内存占用确实最低;ShuffleNetV2也勉强能上,但你要接受它的Channel Shuffle拖累NPU利用率这个事实。
- ResNet18适合做全链路验证的基准模型。它结构简单、算子完全兼容、转换零失败,先用它把摄像头、预处理、推理、后处理整条链路跑通,再去换更复杂的模型,能少走很多弯路。
- ResNet50在RV1103上的结论可以直接写死:不要用。1.3FPS的端到端帧率,47.6MB的峰值内存,除非你只需要极低速离线分类,否则没有任何实用价值。
6.2 部署阶段的避坑清单
最后把这一轮实验里遇到的高频问题整理成清单,每条都是我实际踩过的:
- rknn-toolkit2的版本必须和板端librknnmrt.so版本对齐。版本不一致最常见的表现是rknn_init报
RKNN_ERR_PARAM_INVALID,或者加载成功后推理结果全乱。排查这类问题前先核对版本。 - target_platform要认真填,别偷懒。填错不一定报错,但可能遇到精度不对、模型加载慢等很难解释的玄学问题。
- 板端首次
rknn_init加载RKNN模型需要几百毫秒到一两秒。如果产品有开机流程,要考虑是让用户等待,还是把初始化放到后台线程,否则体验会很突兀。 - 模拟器的时间和性能数据没有参考价值,它跑在x86宿主机上,只能用来验证功能和精度趋势。真实性能必须以板端
clock_gettime的稳定值为准。 - 供电不太稳定的USB口会让NPU频率跌落,导致性能波动。测性能前用稳压电源,或者把每一个数据都多测几遍取稳定值。
- 输出层有的模型导出的ONNX带softmax,有的不带,RKNN转换后也保留了这种差异。建议C侧统一做一个后处理,先取topK再按需算softmax,避免重复softmax导致概率输出看起来很奇怪。
- 监控NPU和CPU占用时,看
/proc/rknn/rknn_process和top -H。如果发现模型明明不大但CPU占用很高,大概率是有算子落到CPU执行了,需要回到模型结构层面找原因。
6.3 我的最终建议
如果只让我推荐一个组合,我会选MobileNetV2 + 192输入 + 定点预处理 + 零拷贝IO,这是我在RV1103上试下来综合体验最稳的一套方案。理由不是MobileNetV2的Top-1精度最高,而是它在NPU算力、内存占用、算子兼容性、量化损失四个维度之间最均衡。
最后再分享一点我自己的体会。很多人拿到RV1103的第一件事是找“最快的模型”,但实际做完这一轮对比之后我越来越觉得,对这种0.5TOPS芯片来说,模型结构是否贴合NPU比理论计算量更重要。结构里藏着几个Transpose、几个动态Reshape,墙上的帧率可能就从20掉到10。你与其纠结MobileNet和EfficientNet那零点几的精度差距,不如先把手上的候选模型全部过一遍RKNN转换,拿板端的实测FPS说话。数据不会骗人。