1. 为什么要在RV1103上折腾图像分类模型
瑞芯微RV1103这颗芯片在边缘视觉领域算是个异类。它不像RK3588那样堆核心拼算力,也不像纯MCU那样只能跑个阈值判断,而是把一颗轻量级NPU塞进了极低功耗的封装里,目标场景非常明确——电池供电的智能摄像头、门禁面板、工业质检终端这类需要"永远在线但功耗敏感"的设备。我第一次拿到RV1103开发板的时候,官方Demo跑的是人脸检测,帧率和功耗都挺漂亮,但当我试图把ResNet、MobileNet这些主流分类模型往上搬的时候,问题就一个接一个冒出来了。
这篇文章想聊的不是"RV1103有多强"这种宣传口径的东西,而是把几类主流图像分类模型真正部署到RV1103的NPU上时,会遇到哪些坑、哪些模型能跑、哪些模型跑不动、量化之后精度掉多少、帧率到底能不能满足业务需求。如果你手头正好有RV1103的板子,或者正在评估这颗芯片能不能撑起你的分类任务,那这些实测数据应该能帮你省掉不少试错时间。
需要先说明一点:RV1103的NPU算力标称是0.5TOPS(INT8),这个数字放在2024年的语境下不算亮眼,但它的优势在于功耗和成本的平衡。所以选模型的时候,思路不能是"哪个精度高上哪个",而是"在算力预算内怎么把精度榨到最高"。这个前提会贯穿全文。
2. RV1103的NPU到底能吃什么样的模型
2.1 算力预算与模型规模的匹配关系
先算一笔账。0.5TOPS意味着每秒能完成0.5万亿次INT8乘加运算。一个标准的MobileNetV2,输入224x224,参数量约3.4M,单次推理的MACs大约是3亿次。理论上0.5TOPS的算力跑这个模型,理想情况下每秒能跑1600多次。但实际部署中,NPU的利用率、内存带宽、数据搬运开销都会吃掉大量算力,实测下来能跑到理论值的20%到40%就算不错了。
所以选模型的第一条经验法则:单次推理的MACs控制在100M到500M之间。低于100M的模型(比如MobileNetV1的0.25倍宽度版本)精度往往不够用,高于500M的模型(比如ResNet50)在RV1103上要么跑不动,要么帧率低到没有实用价值。
这里有个容易踩的坑:很多人看模型参数量来判断能不能部署,这是不对的。参数量决定的是模型文件大小和内存占用,而决定推理速度的是MACs(乘加运算次数)。一个参数量小但特征图分辨率高的模型,MACs可能反而更大。比如某些轻量级分割模型,参数量只有1M多,但因为要在高分辨率特征图上做卷积,MACs能飙到几个G,这种模型在RV1103上就是灾难。
2.2 算子支持清单:哪些层能加速,哪些层会掉回CPU
RV1103的NPU对算子的支持是有明确边界的。根据我实际测试和官方文档的对照,以下算子能获得完整的NPU加速:
- 标准卷积(Conv2D),包括深度可分离卷积(DepthwiseConv2D)
- 全连接层(FullyConnected)
- 池化层(MaxPool、AveragePool)
- 激活函数(ReLU、ReLU6、LeakyReLU的部分参数配置)
- 逐元素加法(Add)和乘法(Mul)
- 拼接(Concat)
- 上采样(Upsample,最近邻和双线性)
而以下算子要么不支持,要么支持但性能很差:
- 全局平均池化在某些输入尺寸下会掉回CPU
- Softmax如果放在NPU上跑,精度会有细微偏差,建议放到后处理
- 自定义的激活函数(比如Swish、GELU)需要拆解成基础算子组合
- 动态shape的操作基本别想
这意味着什么?意味着你在选模型的时候,不能只看精度指标,还要看它的算子构成。比如EfficientNet系列用了大量的Swish激活和SE模块,这些在RV1103上要么需要改写,要么会拖慢整体速度。相比之下,MobileNet系列和ShuffleNet系列对NPU更友好,因为它们的核心算子就是深度可分离卷积加ReLU,几乎是为这类NPU量身定做的。
2.3 输入分辨率对帧率的影响曲线
输入分辨率是另一个关键变量。RV1103的NPU对输入尺寸没有硬性限制,但分辨率每翻一倍,MACs大约翻四倍(因为卷积的计算量和特征图面积成正比)。我实测了几组数据,用MobileNetV2在不同输入尺寸下的表现:
| 输入尺寸 | 单帧推理耗时(ms) | 理论帧率(FPS) | 实际可用帧率(含前后处理) |
|---|---|---|---|
| 96x96 | 8.2 | 122 | 75 |
| 128x128 | 13.5 | 74 | 48 |
| 160x160 | 20.1 | 50 | 33 |
| 224x224 | 38.7 | 26 | 18 |
这张表的信息量很大。如果你做的是粗略分类(比如区分"有人/没人"),96x96就够了,75FPS的可用帧率能覆盖绝大多数实时场景。但如果你要做细粒度分类(比如区分不同型号的零件),224x224可能都不够,而18FPS的帧率在工业场景下就有点捉襟见肘了。
我的建议是:先用业务需求倒推最小可用分辨率,再在这个分辨率下选模型。不要先选模型再调分辨率,那样很容易陷入"精度不够就加分辨率,加了分辨率帧率又不够"的死循环。
3. 模型转换链路上的几个关键决策点
3.1 从PyTorch到RKNN的完整转换流程
RV1103的模型部署走的是RKNN工具链。整个链路是:训练框架(PyTorch/TensorFlow)→ ONNX → RKNN。听起来简单,但每一步都有细节。
第一步,从PyTorch导出ONNX。这里最常见的坑是动态shape。RKNN对动态shape的支持很有限,所以导出ONNX的时候一定要固定输入尺寸。另外,torch.onnx.export的opset_version建议用11或12,太高了RKNN可能不认,太低了某些算子表达不了。
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 )第二步,ONNX到RKNN的转换。这一步用RKNN-Toolkit2,注意版本要和板子上的NPU驱动匹配。我用的组合是RKNN-Toolkit2 1.6.0配RV1103的1.6.0固件,跨版本经常出玄学问题。
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 ) 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是归一化参数,必须和训练时用的保持一致,否则精度会崩。quantized_dtype选asymmetric_quantized-8是INT8非对称量化,这是RV1103 NPU的原生格式,用这个能获得最好的加速比。optimization_level=3会启用算子融合等优化,但有时候会引入精度损失,如果发现量化后精度掉得厉害,可以降到2试试。
3.2 量化校准集的选择策略
量化是部署过程中最影响精度的环节。RKNN用的是训练后量化(PTQ),需要你提供一批校准数据。这批数据的选择直接决定了量化参数的质量。
我见过太多人随便拿几十张图就去做校准,结果量化后精度掉了十几个点,然后回头骂NPU不行。其实问题出在校准集上。校准集的核心要求是:覆盖真实推理时可能遇到的所有数据分布。
具体来说,如果你做的是室内场景的分类,校准集里就不能全是室外图。如果你做的是白天场景,校准集里最好也包含一些黄昏、阴天的样本。校准集的数量不需要太多,100到500张就够了,但分布一定要广。
还有一个细节:校准集的预处理必须和推理时的预处理完全一致。包括resize的方式(是直接resize还是保持长宽比padding)、归一化参数、通道顺序(RGB还是BGR)。任何一处不一致,量化参数就会偏,精度就会掉。
3.3 量化精度损失的定位方法
量化后精度掉了,怎么定位是哪个层的问题?RKNN-Toolkit2提供了一个逐层分析的工具,可以输出每一层的量化误差。但更实用的方法是分段排查:先把模型切成几段,分别量化,看哪一段的精度损失最大。
另一个经验是:深度可分离卷积的量化敏感度比标准卷积高。因为深度卷积每个通道单独做卷积,通道间的数值范围差异大,量化时容易丢失信息。如果你的模型里深度可分离卷积占比很高(比如MobileNet系列),量化后精度掉2到3个点是正常的。如果掉了5个点以上,就要检查校准集和预处理了。
如果精度实在救不回来,可以考虑混合量化:把敏感层保持FP16,其他层用INT8。但RV1103的NPU对FP16的支持有限,混合量化会导致部分层掉回CPU,速度会受影响。所以这是一个精度和速度的权衡。
4. 几类主流模型的实测表现对比
4.1 MobileNet系列:最稳妥的 baseline
MobileNetV1和V2是我在RV1103上跑得最顺的模型。V1的结构极简,就是标准卷积加深度可分离卷积堆叠,没有花哨的算子,量化后精度损失很小。在ImageNet 1000类分类任务上,V1的FP32精度是70.6%,INT8量化后掉到69.8%,只掉了0.8个点。V2因为引入了残差连接和线性瓶颈,精度更高(FP32 71.8%),量化后70.5%,掉了1.3个点。
速度方面,V1比V2快大约15%,因为V2的残差连接需要额外的内存搬运。在224x224输入下,V1的NPU推理耗时约33ms,V2约38ms。如果帧率是硬指标,V1更合适;如果精度优先,V2多出来的0.7个点可能值得那5ms。
这里有个细节:MobileNetV2的最后一个卷积层输出是1280维,接一个1000类的全连接层。这个全连接层的参数量是1.28M,占了整个模型参数量的近40%。如果你的分类类别数很少(比如只有10类),可以把全连接层砍掉,直接接一个10维的输出,这样模型文件会小很多,推理速度也会略有提升。
4.2 ShuffleNetV2:通道 shuffle 的代价
ShuffleNetV2的设计理念和MobileNet不同,它通过通道 shuffle 来促进通道间的信息流动,用更少的参数量达到相近的精度。在RV1103上,ShuffleNetV2 1.0x的FP32精度是69.4%,量化后68.1%,掉了1.3个点。速度上,224x224输入下约36ms,介于MobileNetV1和V2之间。
但ShuffleNetV2有一个问题:通道 shuffle 操作在NPU上的实现效率不高。这个操作本质上是内存重排,NPU做这种非计算密集型的操作时,算力利用率会下降。我实测发现,如果把通道 shuffle 去掉(改成普通的concat),速度能提升约10%,但精度会掉1个点左右。所以这是一个取舍。
另外,ShuffleNetV2的量化敏感度比MobileNet略高,尤其是通道 shuffle 前后的卷积层。如果量化后精度掉得厉害,可以尝试对这些层做混合量化。
4.3 ResNet18:能跑,但别指望帧率
ResNet18的参数量是11.7M,MACs约1.8G,是MobileNetV2的6倍。在RV1103上,224x224输入下的NPU推理耗时约180ms,也就是5.5FPS。这个帧率对于实时视频流来说基本不可用,但如果你的场景是"拍一张照然后分类"(比如智能门禁的抓拍识别),5.5FPS是够的。
精度方面,ResNet18的FP32精度是69.8%,量化后67.2%,掉了2.6个点。掉得比MobileNet多,原因是ResNet的残差连接在量化时容易引入误差累积。每一层的量化误差会通过残差连接传递到下一层,层数越多,累积越明显。
所以ResNet18在RV1103上的定位很明确:适合对帧率不敏感、但对精度有一定要求的离线分类场景。如果你的业务需要实时处理,还是老老实实用MobileNet或ShuffleNet。
4.4 EfficientNet-Lite:算子改写的必要性
EfficientNet-Lite是Google专门为移动端和边缘设备优化的版本,去掉了SE模块和Swish激活,换成了ReLU6。理论上应该很适合RV1103,但实际部署时还是遇到了问题。
EfficientNet-Lite0的FP32精度是75.1%,比MobileNetV2高了3个多点。但在RV1103上,224x224输入下的推理耗时约52ms,比MobileNetV2慢了36%。原因在于EfficientNet的复合缩放策略导致它的特征图通道数变化更剧烈,NPU在做通道数不匹配的卷积时,需要额外的内存对齐操作,效率会下降。
量化后精度掉到72.3%,掉了2.8个点。这个掉幅比MobileNet大,主要是因为EfficientNet的深度可分离卷积占比更高,而且通道数更多,量化时的信息损失更严重。
我的建议是:如果你愿意花时间做算子级的优化(比如手动调整通道数使其对齐到NPU友好的倍数),EfficientNet-Lite能提供比MobileNet更好的精度。但如果你想要开箱即用,MobileNetV2是更稳妥的选择。
4.5 实测数据汇总与选型建议
把上面的数据整理成一张表,方便对比:
| 模型 | 参数量 | MACs | FP32精度 | INT8精度 | 精度损失 | 224x224推理耗时 | 适用场景 |
|---|---|---|---|---|---|---|---|
| MobileNetV1 | 4.2M | 0.58G | 70.6% | 69.8% | 0.8% | 33ms | 实时分类,帧率优先 |
| MobileNetV2 | 3.4M | 0.32G | 71.8% | 70.5% | 1.3% | 38ms | 实时分类,均衡 |
| ShuffleNetV2 1.0x | 2.3M | 0.30G | 69.4% | 68.1% | 1.3% | 36ms | 内存受限场景 |
| ResNet18 | 11.7M | 1.82G | 69.8% | 67.2% | 2.6% | 180ms | 离线抓拍分类 |
| EfficientNet-Lite0 | 4.7M | 0.41G | 75.1% | 72.3% | 2.8% | 52ms | 精度优先,可接受优化 |
选型的逻辑链条是这样的:先确定帧率底线,再确定精度底线,然后在满足这两个底线的模型里选参数量最小的。参数量小意味着模型文件小、内存占用低,对于RV1103这种内存有限的芯片来说,这一点很重要。
5. 部署之后才发现的问题与调优手段
5.1 内存带宽才是真正的瓶颈
很多人以为NPU部署的瓶颈是算力,其实在RV1103这个级别上,内存带宽往往才是真正的限制。RV1103的NPU有自己的一块紧耦合内存,但容量有限,大部分中间特征图还是要放在共享内存里。当模型的特征图尺寸较大时,NPU和CPU之间的数据搬运会成为瓶颈。
我实测发现,同样MACs的两个模型,特征图分辨率高的那个往往更慢。比如一个模型在56x56的特征图上做卷积,另一个在28x28的特征图上做同样多的MACs,前者的耗时可能是后者的1.5倍。原因就是高分辨率特征图的内存占用大,搬运开销高。
所以优化的时候,除了看MACs,还要看特征图的峰值内存占用。RKNN-Toolkit2在build的时候会输出内存占用报告,这个报告值得仔细看。如果峰值内存接近RV1103的可用内存上限,推理时就会出现频繁的内存换入换出,速度会断崖式下降。
5.2 前后处理的耗时不能忽略
NPU推理耗时只是整个pipeline的一部分。实际部署中,图像采集、预处理(resize、归一化、通道转换)、后处理(softmax、top-k)都要算进去。我见过不少案例,NPU推理只花了20ms,但前后处理花了30ms,整体帧率被拖到20FPS。
预处理的优化空间很大。比如resize操作,如果用CPU做双线性插值,一张1080p的图resize到224x224大概要5到8ms。但如果用RV1103的硬件加速器(比如RGA模块)来做,耗时可降到1ms以内。通道转换(RGB到BGR)也可以用RGA来做,比CPU快很多。
后处理方面,softmax和top-k的计算量很小,但如果用Python做,开销也不容忽视。建议把后处理用C写进推理程序里,或者用NPU的CPU核直接算,避免Python解释器的开销。
5.3 多模型并行的资源竞争
如果你的业务需要同时跑多个模型(比如一个检测模型加一个分类模型),就要注意NPU的资源竞争。RV1103只有一个NPU核心,多个模型同时推理时会排队。如果两个模型的推理耗时都是30ms,并行跑的总耗时不是30ms,而是60ms。
解决办法有两个:一是把多个模型合并成一个多任务模型,共享底层特征提取,这样总耗时可能只比单个模型多一点点。二是错峰调度,让两个模型交替推理,比如检测模型每3帧跑一次,分类模型每帧都跑。具体怎么选,取决于业务对两个任务实时性的要求。
5.4 温度对持续性能的影响
RV1103在满负荷跑NPU的时候,芯片温度会上升。如果散热条件不好,温度到一定程度后,芯片会降频保护,推理速度会下降。我实测在室温25度、无散热片的情况下,连续跑MobileNetV2十分钟后,推理耗时从38ms涨到了45ms左右,降了约18%。
如果你的设备是封闭外壳,散热问题会更严重。建议在结构设计阶段就预留散热路径,比如在芯片背面贴导热垫,或者在外壳上开散热孔。如果实在没法做主动散热,就要在软件层面做限帧,比如把帧率限制在15FPS,给芯片留出冷却时间。
6. 从实验到产品化的几个经验教训
6.1 量化精度不是越高越好
刚开始做部署的时候,我总想把量化精度做到最高,校准集选了一大堆,量化参数调了又调。后来发现,量化精度和实际业务指标之间并不是线性关系。有时候量化精度掉了2个点,但业务上的准确率只掉了0.5个点,因为量化误差主要集中在那些本来就容易混淆的类别上。
所以我的建议是:用业务指标来指导量化优化,而不是用学术指标。如果你的业务只关心某几个类别的准确率,那校准集就应该重点覆盖这几个类别的数据,其他类别的精度掉了也无所谓。
6.2 模型剪枝和量化要配合使用
单独做量化,精度损失通常在1到3个点。如果先做剪枝再做量化,精度损失可能会小一些,因为剪枝去掉了冗余的通道,剩下的通道数值分布更集中,量化时信息损失更小。
但剪枝也有代价:剪枝后的模型结构变了,需要重新训练微调,这会增加开发周期。而且剪枝的比例不好控制,剪多了精度崩,剪少了没效果。我的经验是,对于RV1103这个级别的芯片,剪枝的收益不如直接选一个更轻量的模型架构。除非你的业务对某个特定模型有强依赖(比如已经围绕它做了大量工程优化),否则重新选型比剪枝更划算。
6.3 固件版本和工具链版本的匹配问题
RKNN-Toolkit2的版本和板子上的NPU固件版本必须匹配。我遇到过好几次,PC上转换出来的rknn模型,放到板子上跑不起来,报的错是"模型版本不兼容"。查了半天才发现是工具链版本比固件版本新了一个小版本。
所以建议在项目开始的时候,就把工具链版本和固件版本锁定,不要随意升级。如果必须升级,要留出足够的时间做回归测试。另外,RKNN-Toolkit2的更新日志值得仔细看,有些版本会修改量化算法,导致同样的模型和校准集,升级后精度会变化。
6.4 别忘了CPU兜底方案
NPU虽然快,但不是所有算子都能加速。如果模型里有NPU不支持的算子,RKNN会自动把它分配到CPU上跑。这时候整体速度取决于CPU和NPU的串行耗时。如果CPU部分的耗时占比很高,整体速度可能还不如纯CPU推理。
所以部署前一定要用RKNN-Toolkit2的性能分析工具,看看每个算子的执行设备。如果发现大量算子掉到了CPU,就要考虑改写模型结构,或者换一个NPU支持更好的模型。
7. 一些实测中的零碎心得
关于输入尺寸的调整,有个小技巧:RV1103的NPU对某些特定的尺寸有优化。比如128x128和160x160的推理效率比112x112和144x144高,因为前者能更好地对齐NPU的内存访问粒度。这个差异大概在5%到10%之间,选尺寸的时候可以优先考虑这些"友好尺寸"。
关于校准集的数量,我试过50张、100张、200张、500张,发现100张之后精度提升就很小了。所以没必要为了校准集去标注大量数据,100到200张覆盖主要场景就够了。关键是分布要广,而不是数量要多。
关于模型输出层的处理,如果你的分类类别数不是2的幂次,建议padding到最近的2的幂次。比如10类padding到16类,多出来的6类输出置零。这样NPU在做全连接层的时候能更好地利用向量化指令,速度会略有提升。
关于多batch推理,RV1103的NPU支持batch推理,但batch size大于1时,内存占用会成倍增加。如果你的场景是处理视频流,batch推理的收益不大,因为视频帧是串行到来的。但如果你的场景是批量处理图片(比如相册分类),batch推理能显著提升吞吐量。
最后说一个关于功耗的观察。RV1103在跑NPU的时候,整板功耗大约在0.8W到1.2W之间,具体取决于模型复杂度和帧率。如果做电池供电的设备,这个功耗水平意味着用一块2000mAh的电池,大概能连续跑6到8小时。如果加上间歇性休眠策略(比如每秒只推理5帧,其他时间休眠),续航能延长到20小时以上。所以帧率不是越高越好,够用就行,省下来的电都是续航。