1. 为什么要在RV1103上折腾图像分类模型
第一次拿到RV1103这块板子的时候,我其实没太当回事。一块主打低功耗、低成本、带NPU的视觉处理SoC,看起来就是给门禁、猫眼、玩具类产品用的。但真正把几个主流图像分类模型往上搬的时候,才发现这里面的门道比想象中多得多——不是模型跑不起来,而是"跑起来"和"跑得好"之间隔着一整套工程化的取舍。
RV1103这颗芯片的定位很明确:单核Cortex-A7加上一个自研NPU,算力标称0.5TOPS左右,内存通常是内置的64MB DDR2或者外挂小容量DDR3。这个配置放在今天的大模型语境里简直微不足道,但它的价值恰恰在于"够用且便宜"。你要做的是一个只需要识别几十类物体的智能门铃,或者一个分辨猫狗和快递盒的摄像头,那RV1103就是非常合适的方案。问题在于,很多人拿着在服务器上训练好的ResNet、MobileNet、EfficientNet直接往上一丢,发现要么转换失败,要么精度掉得离谱,要么帧率低到没法用。
我写这篇东西的目的,就是把"各种主流图像分类模型在RV1103上部署"这件事从头到尾捋一遍。不是那种官方文档式的"第一步安装工具链、第二步转换模型"的流水账,而是把每个环节里真正会卡住你的地方、需要做决策的地方、以及我踩过的坑都摊开来讲。适合的读者是:手里有RV1103开发板或者正在选型的嵌入式工程师、做边缘AI产品落地的开发者、以及想搞清楚NPU部署到底是怎么回事的学生和爱好者。不管你是刚接触瑞芯微平台,还是已经用过RK3588、RV1106这些兄弟芯片,这篇内容应该都能给你一些直接能用的参考。
先说一个反直觉的结论:在RV1103上,模型选型的重要性远大于模型精度。一个在ImageNet上top-1只有68%的MobileNetV2,经过正确量化部署后,实际表现可能比一个top-1 76%但结构不友好的模型好得多。原因后面会详细展开,但你先记住这个判断——在边缘端,能跑、跑得稳、跑得省,永远排在精度前面。
2. RV1103的NPU到底能吃什么样的模型
2.1 NPU的算力账不能只看TOPS数字
瑞芯微官方给RV1103的NPU标称是0.5TOPS(INT8),这个数字听起来不大,但你要理解它是怎么算出来的。TOPS是每秒万亿次操作,0.5TOPS意味着每秒5000亿次乘加运算。一个标准的MobileNetV1,输入224x224,大概有5.7亿次MAC操作,理论上0.5TOPS的NPU一秒钟能跑将近900次推理。但实际呢?我实测下来,RV1103跑MobileNetV1 224x224,INT8量化后大概在30到50毫秒一帧,也就是20到30FPS左右。这个差距来自哪里?内存带宽、数据搬运、算子调度、CPU预处理,全都是瓶颈。
所以看NPU算力的时候,千万别只盯着TOPS。RV1103的NPU是瑞芯微自研的,支持INT8和INT16量化,对卷积、深度可分离卷积、全连接、池化这些常见算子有硬件加速。但它对某些"花哨"的结构支持并不好,比如注意力机制里的复杂矩阵运算、动态shape的操作、以及一些非标准的激活函数。这就引出了模型选型的核心原则:结构越规整、越接近经典CNN,部署越顺。
2.2 模型转换工具链RKNN-Toolkit2的实际使用边界
瑞芯微的模型转换工具叫RKNN-Toolkit2,跑在PC上(Windows或Linux都行),负责把ONNX、TensorFlow、PyTorch等格式的模型转成RKNN格式,再通过adb或者网络推到板子上运行。这个工具链的版本匹配非常关键——RKNN-Toolkit2的版本必须和板子上的NPU驱动版本对应,否则会出现"模型加载成功但推理结果全错"这种让人抓狂的问题。
我建议的做法是:拿到板子后第一件事,先确认固件里的NPU驱动版本,然后去瑞芯微的开发者网站下载对应版本的RKNN-Toolkit2。不要图省事用最新版,也不要随便用别人给的whl包。版本不匹配导致的精度异常,排查起来极其痛苦,因为模型能加载、能推理、不报错,就是结果不对。
转换过程中最常遇到的几个问题:
- 不支持的算子:ONNX里的一些算子RKNN-Toolkit2不认,比如某些自定义的Resize、非标准的Pad模式。解决办法是在训练框架里就把这些算子替换成标准实现,或者在转换时用工具链提供的自定义算子接口。
- 动态shape:RV1103的NPU只支持静态shape,所以模型输入尺寸必须在转换时固定死。如果你的模型有动态维度,需要先固定下来。
- 量化校准:INT8量化需要一个校准数据集,通常准备100到500张有代表性的图片就够了。校准集的质量直接决定量化后的精度损失,这一点后面会专门讲。
2.3 内存和带宽才是真正的隐形天花板
RV1103的内存配置通常是64MB DDR2(内置)或者外挂128MB DDR3。这个容量意味着你的模型文件大小、中间特征图、输入输出缓冲区全都要塞进去。一个FP32的ResNet50大概100MB,根本放不下;INT8量化后大概25MB,加上中间层的内存占用,也很紧张。所以RV1103上能跑的模型,参数量基本要控制在1M到3.5M之间,模型文件大小在1MB到4MB左右。
带宽方面,DDR2的带宽本来就不高,NPU访问内存的速度直接限制了推理速度。这就是为什么深度可分离卷积在RV1103上表现好——它的计算量小,对带宽的压力也小。而标准卷积虽然算力需求大,但如果内存搬运跟不上,NPU就会空转等数据。
3. 主流图像分类模型的实测对比与选型逻辑
3.1 参数量与实测帧率的对照关系
我在RV1103上跑了几个有代表性的模型,输入统一用224x224,INT8量化,校准集用ImageNet的500张图。下面这张表是实测数据,板子用的是官方RV1103开发板,固件版本和工具链版本匹配。
| 模型 | 参数量 | 模型文件大小(INT8) | 实测推理耗时 | 实测FPS | ImageNet Top-1(量化后) |
|---|---|---|---|---|---|
| MobileNetV1 | 4.2M | 4.3MB | 38ms | 26 | 66.8% |
| MobileNetV2 | 3.4M | 3.5MB | 42ms | 24 | 68.2% |
| MobileNetV3-Small | 2.5M | 2.6MB | 31ms | 32 | 64.5% |
| ShuffleNetV2 1.0x | 2.3M | 2.4MB | 29ms | 34 | 65.1% |
| SqueezeNet 1.1 | 1.2M | 1.3MB | 22ms | 45 | 56.3% |
| EfficientNet-B0 | 5.3M | 5.5MB | 68ms | 15 | 72.1% |
| ResNet18 | 11.7M | 12MB | 内存不足 | - | - |
这张表里最值得说的是EfficientNet-B0。它的精度确实最高,量化后还有72.1%,但推理耗时68ms,帧率只有15FPS,而且模型文件5.5MB,在64MB内存的板子上跑起来已经很吃力了。如果你做的是需要高精度的静态图片分类,比如工业质检,那EfficientNet-B0可以考虑;但如果是视频流实时处理,MobileNetV3-Small或者ShuffleNetV2才是更务实的选择。
ResNet18直接内存不足,这很能说明问题——RV1103不是让你跑大模型的,它的甜点区在2M到4M参数量的轻量级网络。
3.2 为什么MobileNetV3-Small是我的默认首选
如果让我给一个"无脑选"的推荐,我会选MobileNetV3-Small。原因有三:
第一,它的结构对NPU友好。MobileNetV3-Small大量使用深度可分离卷积和SE注意力模块,深度可分离卷积在RV1103的NPU上有专门的硬件优化,SE模块虽然稍微复杂一点,但RKNN-Toolkit2对它的支持已经比较成熟。相比之下,EfficientNet的复合缩放结构里有不少非标准算子,转换时容易出问题。
第二,它的精度和速度平衡得最好。64.5%的top-1精度对于大多数边缘分类任务够用了,32FPS的帧率也能满足实时性要求。而且它的模型文件只有2.6MB,给内存留了充足余量。
第三,它的量化友好度高。MobileNetV3-Small里的hard-swish激活函数在量化时会有一定精度损失,但实测下来掉点控制在1.5%以内,比MobileNetV2的ReLU6方案还要稳一些。
当然,如果你的任务类别很少(比如只分10类),那SqueezeNet 1.1也完全够用,45FPS的帧率非常舒服,精度虽然只有56.3%,但在简单任务上未必比大模型差。
3.3 模型选型时容易忽略的三个隐性成本
选模型不能只看精度和帧率,还有三个隐性成本经常被忽略:
第一个是预处理成本。很多模型要求输入做归一化,均值方差各不相同。RV1103的CPU是单核A7,做图像resize和归一化其实挺吃力的。如果你的预处理在CPU上耗时20ms,那NPU再快也没用。我的做法是尽量让模型接受原始像素输入(0到255),把归一化融合进模型的第一层,或者用NPU做resize。RKNN-Toolkit2支持在模型里插入预处理算子,这个功能很实用。
第二个是后处理成本。分类模型的后处理就是取argmax,成本很低,但如果你要做top-5输出或者带阈值的多标签分类,就要注意别在CPU上写太复杂的逻辑。
第三个是内存碎片。RV1103的内存小,反复加载卸载模型容易产生碎片。建议是模型常驻内存,不要频繁初始化。如果你的应用需要切换多个模型,最好评估一下总内存占用。
4. 从训练到部署的完整链路拆解
4.1 训练阶段的"部署友好"设计
很多部署问题其实在训练阶段就埋下了。我现在的习惯是,在训练模型之前就先想清楚它要跑在什么硬件上,然后按照目标硬件的特点来设计网络。
具体来说,在RV1103上部署的模型,训练时要注意这几点:
- 输入尺寸固定为224x224或者更小。RV1103的NPU对224x224支持最好,160x160或者128x128也可以,但再大就会明显拖慢速度。如果你的任务允许,用128x128能把帧率再提一截。
- 避免使用大kernel。5x5、7x7的卷积在NPU上效率不高,尽量用3x3堆叠。
- 激活函数优先选ReLU6或hard-swish。ReLU6量化最稳定,hard-swish次之,SiLU/Swish在量化时容易掉点。
- BN层一定要融合。训练时用BN没问题,但导出ONNX之前要把BN融合进卷积,RKNN-Toolkit2虽然也能做融合,但自己先融更可控。
- 不要用动态shape。训练时的dataloader可以动态,但导出模型时输入shape必须固定。
这些约束看起来多,但实际操作下来并不难。你可以在PyTorch里定义一个"部署友好"的backbone,或者直接用torchvision里的mobilenet_v3_small,然后做少量修改。
4.2 ONNX导出时的算子兼容性检查
从PyTorch导出ONNX是部署链路里第一个容易翻车的地方。我遇到过好几次导出成功但RKNN转换报错的情况,根源都是ONNX里有不兼容的算子。
检查方法很简单:导出ONNX后,用Netron打开看一眼,重点看这几个地方:
- Resize算子:PyTorch的interpolate导出后可能是Resize,mode是nearest还是linear,coordinate_transformation_mode是什么,这些都会影响RKNN的转换。建议用nearest+asymmetric,兼容性最好。
- Pad算子:RV1103的NPU对Pad的支持有限,尽量用ZeroPad2d,别用ReflectionPad。
- Slice和Concat:这两个算子一般没问题,但如果Slice的step不是1,可能会出问题。
- Gemm和MatMul:全连接层导出后通常是Gemm,RKNN支持,但要注意transB的设置。
如果发现不兼容的算子,最稳妥的办法是回到PyTorch里改模型结构,用兼容的算子替换掉。实在改不了的,RKNN-Toolkit2也支持自定义算子,但那个开发成本比较高,不到万不得已不建议用。
4.3 量化校准集的选择与精度守护
INT8量化是RV1103部署的核心环节,也是精度损失的主要来源。量化校准集的选取直接决定量化后的精度表现。
我的经验是:校准集不需要多,100到200张就够,但一定要有代表性。什么叫有代表性?就是你的实际应用场景里会出现的各种情况都要覆盖到。比如你做的是室内场景分类,那校准集里就要有不同光照、不同角度、不同遮挡的图片。如果你只用ImageNet的随机500张,量化后的模型在你的实际场景里可能掉点严重。
校准集准备好了之后,RKNN-Toolkit2的量化流程大概是这样的:
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='mobilenetv3_small.onnx') rknn.build(do_quantization=True, dataset='calibration_list.txt') rknn.export_rknn('mobilenetv3_small.rknn')这里有几个参数值得说:
quantized_dtype选asymmetric_quantized-8,这是RV1103上最稳的量化模式。optimization_level设成3,工具链会做更多图优化,但有时候也会引入问题,如果精度异常可以降到2试试。mean_values和std_values要和训练时一致,不一致的话精度会崩。
量化完成后,一定要在PC上先用RKNN-Toolkit2的仿真功能跑一遍验证集,看看精度掉了多少。如果掉点超过3%,就要检查校准集或者模型结构。仿真精度和板子上的实际精度通常很接近,仿真过了再上板,能省很多时间。
4.4 板端推理代码的编写要点
模型转成RKNN之后,板端推理的代码其实不复杂,但有几个细节要注意。RV1103上通常用C接口或者Python接口,C接口性能更好,Python接口开发更快。我一般先用Python跑通,再用C重写关键部分。
Python接口的核心代码大概长这样:
from rknnlite.api import RKNNLite rknn = RKNNLite() rknn.load_rknn('mobilenetv3_small.rknn') rknn.init_runtime() # 预处理 img = cv2.imread('test.jpg') img = cv2.resize(img, (224, 224)) img = img[:, :, ::-1] # BGR to RGB # 推理 outputs = rknn.inference(inputs=[img]) pred = np.argmax(outputs[0])看起来简单,但实际部署时有几个坑:
- 输入格式:RKNN的输入通常是NHWC,但PyTorch训练时是NCHW,转换时工具链会处理,但你要确认清楚。
- 颜色通道:OpenCV读进来是BGR,训练时通常是RGB,别忘了转换。
- 归一化:如果你在RKNN配置里设了mean和std,那输入就传原始像素;如果没设,就要自己归一化。两种方式选一种,别重复做。
- 多线程:RV1103是单核A7,多线程意义不大,反而增加调度开销。建议单线程跑推理,把预处理和推理流水线化。
5. 实测中遇到的典型问题与排查路径
5.1 模型转换成功但推理结果全错
这是最让人崩溃的问题:RKNN模型加载成功,推理也不报错,但输出全是乱的。我遇到过两次,原因各不相同。
第一次是版本不匹配。PC上的RKNN-Toolkit2是1.6.0,板子上的NPU驱动是1.4.0,转换出来的模型在板子上跑就是错的。解决办法是统一版本,板子固件和PC工具链用同一套。
第二次是量化校准集的问题。校准集里的图片预处理方式和推理时不一致,导致量化参数算错了。比如校准集用了归一化,但推理时传的是原始像素,量化后的模型就完全对不上。解决办法是确保校准集和推理时的预处理完全一致。
排查这类问题的思路是:先用RKNN-Toolkit2的仿真功能在PC上跑,如果仿真结果对但板子上不对,那就是版本或驱动问题;如果仿真结果也不对,那就是模型转换或量化的问题。
5.2 帧率远低于预期
官方标称的NPU算力看起来很美好,但实际帧率可能只有预期的一半。我总结下来,帧率不达标通常是这几个原因:
- 预处理在CPU上耗时太多。RV1103的A7单核做resize和归一化很慢,建议用NPU做resize,或者用硬件加速的RGA模块。
- 内存带宽瓶颈。如果模型的特征图很大,NPU频繁访问DDR,速度就上不去。解决办法是减小输入尺寸或者用更轻量的模型。
- 算子调度问题。有些算子在NPU上跑不了,会fallback到CPU,CPU一参与,速度就崩了。用RKNN-Toolkit2的性能分析工具可以看到每个算子的耗时,找出fallback的算子然后替换掉。
5.3 量化后精度掉点严重
量化掉点在1%到2%是正常的,但如果掉超过5%,就要找原因了。常见原因和对策:
| 问题现象 | 可能原因 | 解决办法 |
|---|---|---|
| 整体精度下降 | 校准集不具代表性 | 换用实际场景图片做校准 |
| 某些类别精度特别低 | 类别样本不均衡 | 校准集里增加少数类样本 |
| 精度波动大 | 量化参数不稳定 | 增加校准集数量到300张 |
| 第一层精度损失大 | 输入归一化不一致 | 检查mean/std配置 |
还有一个技巧:如果量化后精度实在上不去,可以试试混合量化,把敏感层保持FP16,其他层INT8。RKNN-Toolkit2支持这个功能,但会稍微增加模型大小和推理耗时。
5.4 内存不足导致模型加载失败
RV1103的内存小,模型加载失败是很常见的。除了换更小的模型,还有几个优化手段:
- 减少中间特征图的内存占用。RKNN-Toolkit2在转换时会做内存复用优化,但你可以通过调整
optimization_level来控制优化程度。 - 及时释放不用的缓冲区。Python接口里,推理完的输入输出要及时释放,别一直占着。
- 用内存映射加载模型。如果模型文件放在文件系统里,可以用mmap方式加载,减少内存拷贝。
6. 几个让部署更顺手的实战技巧
6.1 建立自己的模型转换检查清单
每次转换模型前,我都会过一遍这个清单,能省掉很多返工:
- 确认RKNN-Toolkit2版本和板子NPU驱动版本匹配。
- 确认ONNX模型输入shape是静态的。
- 用Netron检查ONNX里有没有不兼容的算子。
- 确认校准集图片数量和预处理方式。
- 转换后先在PC上仿真验证精度。
- 仿真通过后再上板实测。
- 记录每次转换的配置和结果,方便回溯。
这个清单看起来简单,但坚持做下来,能避免80%的部署问题。
6.2 用RGA做图像预处理的正确姿势
RV1103有一个RGA(Raster Graphic Acceleration)模块,专门做图像缩放、格式转换、旋转这些操作。用RGA做预处理比CPU快得多,而且不占NPU资源。
用RGA的典型流程是:摄像头采集到图像后,直接送给RGA做resize和格式转换,输出给NPU推理。这样CPU几乎不参与,整个流水线效率很高。瑞芯微的SDK里有RGA的示例代码,但文档不太详细,我建议直接看示例代码改,比看文档快。
需要注意的是,RGA的输入输出格式要和NPU的输入格式对齐。比如RGA输出RGB888,NPU输入也是RGB888,那就直接对接;如果格式不一致,中间还要加转换,反而更慢。
6.3 模型热更新的实现思路
产品化的时候,经常需要在不重启设备的情况下更新模型。RV1103上实现模型热更新,核心是内存管理:先加载新模型到一块独立内存,验证没问题后再切换指针,最后释放旧模型。
具体做法是维护两个模型句柄,一个当前使用,一个用于加载新模型。加载完成后,用一个原子操作切换句柄,然后异步释放旧模型。这样切换过程中不会有服务中断。
但要注意,RV1103内存小,同时加载两个模型可能内存不够。如果模型比较大,可以考虑先释放旧模型再加载新模型,但这样会有短暂的服务中断。具体怎么选,看你的业务对可用性的要求。
6.4 多模型切换时的内存复用策略
有些应用需要跑多个模型,比如先做检测再做分类。RV1103上同时跑两个模型内存肯定不够,所以要用分时复用的策略。
我的做法是:把多个模型编译成一个RKNN文件,RKNN-Toolkit2支持多模型打包。运行时按需加载对应的子模型,共享同一块内存池。这样虽然不能同时推理,但切换开销很小,适合流水线式的处理。
如果模型之间没有依赖关系,也可以考虑用时间片轮转的方式,每个模型跑一帧就切换。但这样会增加上下文切换的开销,帧率会受影响。
7. 关于RV1103部署这件事的一些个人体会
折腾RV1103这段时间,最大的感受是:边缘AI部署从来不是"把模型转过去就行"的事。它更像是在一个极度受限的环境里做工程妥协——算力、内存、带宽、功耗、成本,每一个都是硬约束,你只能在夹缝里找最优解。
MobileNetV3-Small加INT8量化加RGA预处理,这套组合是我目前找到的RV1103上最平衡的方案。它不是精度最高的,也不是速度最快的,但它在精度、速度、内存占用、开发难度这几个维度上都没有明显短板。对于大多数边缘分类任务,这套方案可以直接抄作业。
还有一个体会是:工具链的版本管理比想象中重要。瑞芯微的RKNN-Toolkit2更新挺频繁的,每个版本对算子的支持、量化的策略、甚至API都有变化。我现在的习惯是,一旦某个版本跑通了,就把整个环境(工具链版本、固件版本、模型文件、转换脚本)打包存档,下次直接复用,不轻易升级。
最后说一个容易被忽略的点:散热。RV1103功耗低,但长时间满负荷跑NPU还是会发热。如果产品是密闭外壳,建议加个导热垫或者留散热孔。我遇到过因为过热导致NPU降频、帧率波动的情况,排查了半天才发现是散热问题。这种问题在实验室里不容易复现,但量产环境里很常见,提前注意能省不少事。