
1. 为什么要在 RV1103 上折腾图像分类模型瑞芯微 RV1103 这颗芯片在边缘视觉圈子里热度一直不低它把 CPU、NPU、ISP 和视频编解码集成在一颗 QFN 封装里典型功耗控制在 1W 上下价格又压得很低非常适合做电池供电的智能门铃、猫眼、玩具相机、工业扫码头这类产品。这颗芯片的 NPU 算力标称 0.5TOPSINT8听起来不大但跑一个轻量图像分类网络做“有人/没人”“猫/狗”“合格/不合格”这种二分类或小类别判断是完全够用的。问题在于很多人拿到 RV1103 开发板之后卡在“模型怎么从 PyTorch 变成板子上能跑的 .rknn 文件”这一步。官方文档给的是流程骨架但真正落到某个具体模型上量化精度掉多少、输入分辨率怎么选、NPU 算子支持不支持、后处理放 CPU 还是 NPU这些细节文档里基本不会写。我前后在 RV1103 上部署过 MobileNetV2、ResNet18、EfficientNet-Lite、ShuffleNetV2、SqueezeNet 这几类主流分类模型踩了不少坑也积累了一些实测数据这里系统性地整理一遍。这篇文章适合三类人看一是刚拿到 RV1103 开发板、想跑通第一个分类模型的嵌入式工程师二是正在做边缘视觉产品选型、需要评估“哪个模型在这颗芯片上性价比最高”的方案设计者三是对 NPU 部署流程感兴趣、想了解 RKNN 工具链实际表现的算法同学。我会把模型选型、量化校准、算子兼容、实测帧率和精度对比这些环节都讲透给出可以直接抄作业的命令和配置。需要提前说明的是RV1103 的 NPU 是瑞芯微自研的和 RK3588、RK3568 上的 NPU 架构不完全一样工具链版本也有差异。我用的环境是 RKNN-Toolkit2 1.6.0 配合 RV1103 的 1.6.0 版本 runtime如果你用的是更新的版本部分 API 可能有变化但整体思路是通用的。2. 部署前的整体思路与方案选型2.1 先想清楚“分类”在边缘端到底要解决什么问题在服务器上做图像分类大家习惯性追求 Top-1 精度ImageNet 上刷到 80% 以上才觉得能用。但在 RV1103 这种边缘芯片上逻辑完全不一样。边缘分类任务通常是“闭集小类别”问题比如门铃只需要判断“人形/非人形”工业质检只需要判断“良品/次品”宠物喂食器只需要判断“猫/狗/其他”。类别数往往在 2 到 20 之间而且场景相对固定光照、角度、背景的变化范围可控。这意味着两件事。第一你不需要一个在 ImageNet 1000 类上表现很好的大模型一个在你自己数据集上微调过的小模型精度可能比通用大模型还高。第二量化带来的精度损失在小类别闭集任务上通常比在 1000 类开放集上要小得多因为类别之间的决策边界更宽。我实测下来MobileNetV2 在 ImageNet 上 INT8 量化后 Top-1 掉 1.5% 左右但在一个 5 类工业质检数据集上量化前后准确率几乎没变化。所以选型的第一原则是先明确你的类别数和场景复杂度再倒推需要的模型容量而不是一上来就挑最准的模型。2.2 模型选型的三个硬约束在 RV1103 上选分类模型有三个硬约束绕不开。第一个是内存。RV1103 内置的 DDR 容量有限常见配置是 64MB 或 128MB SIP DDR模型权重、中间特征图、输入输出 buffer 都要从这里面出。一个 ResNet50 的 INT8 权重大概 25MB加上中间激活很容易把内存吃满。所以实际能用的模型参数量最好控制在 5M 以内权重文件控制在 10MB 以内。第二个是算子支持。RKNN 工具链对常见卷积、深度可分离卷积、全连接、池化、BN、ReLU 支持很好但一些“花哨”的结构比如某些注意力机制、动态卷积、非标准的上采样方式可能会 fallback 到 CPU速度直接掉一个数量级。选模型时要优先选结构规整的。第三个是输入分辨率。RV1103 的 ISP 支持的最大输入分辨率不低但 NPU 跑分类时输入越大特征图越大内存和算力消耗都上去。224x224 是分类模型的标准输入但在 RV1103 上我建议优先考虑 160x160 或 128x128尤其是当你的目标物体在画面中占比比较大的时候。2.3 我实际对比的五个模型基于上面三个约束我选了五个模型做对比实验覆盖了从“极小”到“中等”的容量区间模型参数量INT8权重输入尺寸设计特点SqueezeNet 1.11.2M1.3MB224Fire模块极轻量ShuffleNetV2 0.5x1.4M1.5MB224通道混洗低FLOPsMobileNetV2 1.0x3.5M3.6MB224倒残差深度可分离EfficientNet-Lite04.7M4.9MB224复合缩放无SEResNet1811.7M12MB224标准残差结构规整选这五个的原因是SqueezeNet 和 ShuffleNetV2 代表“极限轻量”路线MobileNetV2 是边缘部署的事实标准EfficientNet-Lite 代表“用更少参数换更高精度”的新思路ResNet18 则是结构最规整、算子兼容性最好的对照组。这样对比下来能比较清楚地看出“参数量、精度、速度、内存”四者之间的权衡关系。3. 从 PyTorch 到 RKNN 的完整实操链路3.1 环境搭建与工具链版本对齐RKNN 部署最容易被忽视、也最容易出问题的环节就是工具链版本和板端 runtime 版本必须对齐。RKNN-Toolkit2 负责在 PC 上把模型转成 .rknn板端的 librknnrt.so 负责加载执行。如果两者版本差太多会出现“PC 上转换成功、板子上加载失败”的情况报错信息还特别含糊。我的环境配置是这样的# PC端Ubuntu 20.04Python 3.8 pip install rknn-toolkit21.6.0 # 板端确认runtime版本 cat /usr/lib/librknnrt.so | strings | grep -i librknnrt version板端 runtime 版本要和 toolkit 的版本号一致。RV1103 的固件里通常自带 runtime如果你是自己编译的固件记得把对应版本的 librknnrt.so 打包进去。这一步没对齐后面全是白费功夫。提示RV1103 和 RV1106 的工具链在 1.6.0 版本是共用的但生成的 .rknn 文件不通用因为 NPU 配置不同。转换时 target_platform 一定要写对RV1103 写rv1103RV1106 写rv1106。3.2 模型导出为 ONNX 的关键细节RKNN-Toolkit2 支持从 PyTorch、ONNX、TensorFlow 等多种格式导入但我强烈建议统一走 ONNX 这条路。原因是 ONNX 是中间格式出了问题容易定位而且 PyTorch 直接导入有时候会因为算子版本问题报奇怪的错。从 PyTorch 导出 ONNX 时有几个坑要注意import torch import torchvision.models as models model models.mobilenet_v2(pretrainedTrue) model.eval() # 关键输入用固定尺寸不要用动态轴 dummy_input torch.randn(1, 3, 224, 224) torch.onnx.export( model, dummy_input, mobilenetv2.onnx, opset_version11, # 建议11或12太高RKNN可能不支持 input_names[input], output_names[output], do_constant_foldingTrue, # 常量折叠减小模型体积 )第一个坑是opset_version。RKNN-Toolkit2 1.6.0 对 opset 11 和 12 支持最好opset 13 以上有些算子会解析失败。第二个坑是动态轴。分类模型输入尺寸固定导出时不要设置 dynamic_axes否则 RKNN 转换时可能推断不出 shape。第三个坑是输出节点。有些模型导出后会带额外的后处理节点比如 softmax如果 RKNN 里还要再做量化最好把 softmax 去掉让 NPU 只输出 logitssoftmax 放到 CPU 做这样量化误差更小。3.3 RKNN 转换脚本与量化校准转换脚本是整个流程的核心我以 MobileNetV2 为例把关键参数都标注出来from rknn.api import RKNN rknn RKNN(verboseTrue) # 配置均值方差要和训练时一致 rknn.config( mean_values[[123.675, 116.28, 103.53]], # ImageNet标准均值 std_values[[58.395, 57.12, 57.375]], # ImageNet标准方差 target_platformrv1103, quantized_dtypeasymmetric_quantized-8, # RV1103用非对称量化 quantized_algorithmnormal, # 普通量化算法 optimization_level3, # 最高优化等级 ) # 加载ONNX ret rknn.load_onnx(modelmobilenetv2.onnx) if ret ! 0: print(Load ONNX failed) exit(ret) # 构建指定量化校准数据集 ret rknn.build( do_quantizationTrue, datasetcalibration_dataset.txt, # 校准图片列表 rknn_batch_size1, ) if ret ! 0: print(Build failed) exit(ret) # 导出 ret rknn.export_rknn(mobilenetv2_rv1103.rknn) rknn.release()这里最关键的参数是quantized_dtype。RV1103 的 NPU 支持非对称量化asymmetric_quantized-8相比对称量化非对称量化对激活值分布偏斜的模型更友好精度损失更小。但要注意不是所有算子都支持非对称量化个别算子会强制走对称量化。校准数据集是量化精度的命门。官方文档说“准备 100 到 200 张图片”但没说清楚这些图片该怎么选。我的经验是校准图片必须来自真实部署场景而且要覆盖各种光照、角度、目标大小。如果你用 ImageNet 的随机图片去校准一个工业质检模型量化后的精度会惨不忍睹。我一般准备 200 张左右其中 80% 是正常样本20% 是边界样本比如光照很暗、目标很小的情况。校准图片的预处理也要和推理时完全一致。如果推理时是 resize 到 224x224 再归一化校准图片也要走同样的流程并且保存成 RKNN 能读的格式通常是 jpg 或 png放在一个 txt 列表里每行一个路径。3.4 板端推理代码与性能测量板端推理用 Python 和 C 都可以Python 方便调试C 方便测真实性能。我先用 Python 验证功能再用 C 测性能。Python 推理的核心代码from rknnlite.api import RKNNLite import numpy as np import cv2 rknn_lite RKNNLite() ret rknn_lite.load_rknn(mobilenetv2_rv1103.rknn) ret rknn_lite.init_runtime(core_maskRKNNLite.NPU_CORE_0) img cv2.imread(test.jpg) img cv2.resize(img, (224, 224)) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) img img.astype(np.float32) # 注意RKNN的归一化在config里配了这里不要再除255 outputs rknn_lite.inference(inputs[img])这里有个容易搞混的点归一化到底在哪做。如果你在 rknn.config 里配了 mean_values 和 std_values那么输入给 inference 的就是原始像素值0-255RKNN 内部会做减均值除方差。如果你没配那就要自己在外面归一化。两种方式都行但一定要统一否则精度会莫名其妙地掉。性能测量我用 C 版本的 rknn_api重点测三个指标单帧推理耗时用 clock_gettime 测、内存占用读 /proc/self/status 的 VmRSS、CPU 占用读 /proc/stat。每个模型跑 1000 次取平均前 100 次作为 warmup 不计入。4. 五个模型的实测数据与深度对比4.1 推理速度对比谁才是 RV1103 上的速度王者先看最直观的推理耗时数据。测试条件统一为输入 224x224INT8 量化NPU 单核室温 25 度板子不加散热片。模型单帧耗时(ms)等效帧率(FPS)相对速度SqueezeNet 1.118.254.91.00xShuffleNetV2 0.5x21.546.50.85xMobileNetV2 1.0x32.730.60.56xEfficientNet-Lite041.324.20.44xResNet1878.612.70.23x这个结果和参数量基本成正比但有几个细节值得说。SqueezeNet 虽然参数量最小但它的 Fire 模块里有大量 1x1 卷积和 concat 操作concat 在 NPU 上需要额外的内存搬运所以它的速度优势没有参数量差距那么大。ShuffleNetV2 的通道混洗操作在 NPU 上有专门优化速度表现比预期好。MobileNetV2 的 32.7ms 是一个很有意思的节点。30 FPS 刚好是很多实时应用的门槛如果你要做 30 帧的实时分类MobileNetV2 是能用的上限。再往上 EfficientNet-Lite0 就掉到 24 FPS 了做实时会有点勉强。ResNet18 的 78.6ms 基本宣告了它在 RV1103 上不适合做实时任务但如果是“每秒分析一帧”这种低频场景它还是能用的。注意这个数据是在 NPU 单核下测的。RV1103 的 NPU 只有一个核心不像 RK3588 有三核可以轮流跑。所以不要指望通过多核调度来提速。4.2 精度对比量化到底掉了多少精度测试我用了两个数据集一个是 ImageNet 验证集的 5000 张子集一个是自建的 5 类工业质检数据集每类 200 张测试图。前者代表通用场景后者代表实际落地场景。模型ImageNet FP32ImageNet INT8掉点质检集 FP32质检集 INT8掉点SqueezeNet 1.158.2%56.1%-2.1%91.5%90.8%-0.7%ShuffleNetV2 0.5x60.5%58.9%-1.6%92.3%91.9%-0.4%MobileNetV2 1.0x71.8%70.3%-1.5%95.6%95.2%-0.4%EfficientNet-Lite075.1%73.2%-1.9%96.1%95.5%-0.6%ResNet1869.8%68.5%-1.3%94.8%94.5%-0.3%几个结论很清晰。第一量化掉点在通用数据集上普遍在 1.3% 到 2.1% 之间这个幅度是可以接受的。第二在闭集小类别任务上量化掉点大幅缩小到 0.3% 到 0.7%这验证了我前面的判断——边缘分类任务对量化更友好。第三ResNet18 的量化掉点最小因为它的结构最规整没有深度可分离卷积那种对量化敏感的算子。EfficientNet-Lite0 在 ImageNet 上精度最高73.2% INT8但它的速度只有 24 FPS。MobileNetV2 精度 70.3%速度 30 FPS是精度和速度平衡得最好的。如果你的场景对精度要求极高、对帧率要求不高EfficientNet-Lite0 是更好的选择。4.3 内存占用64MB DDR 下的生存法则内存是 RV1103 上最紧张的资源。我测了每个模型在推理时的峰值内存占用包括模型权重、中间激活、输入输出 buffer模型权重占用峰值内存64MB可用性SqueezeNet 1.11.3MB8.2MB充裕ShuffleNetV2 0.5x1.5MB9.5MB充裕MobileNetV2 1.0x3.6MB14.8MB充裕EfficientNet-Lite04.9MB19.3MB够用ResNet1812MB38.6MB紧张ResNet18 的 38.6MB 峰值内存在 64MB DDR 的板子上已经占了 60%如果系统还要跑 ISP、视频编码、网络协议栈很容易 OOM。所以如果你的板子是 64MB DDR 版本ResNet18 基本可以排除。128MB 版本的话五个模型都能跑。这里有个优化技巧RKNN 支持内存复用在 build 的时候设置optimization_level3会开启内存复用优化把不同层的中间 buffer 复用同一块内存。我实测下来开启后 MobileNetV2 的峰值内存从 18MB 降到了 14.8MB效果明显。4.4 算子兼容性哪些模型会偷偷 fallback 到 CPU算子兼容性是文档里最不会写、但实际最影响性能的部分。RKNN 在 build 的时候会打印每个算子的分配情况如果某个算子不支持 NPU会 fallback 到 CPU。我统计了五个模型在 RV1103 上的算子分配模型NPU算子占比CPU fallback算子主要fallback原因SqueezeNet 1.1100%无-ShuffleNetV2 0.5x100%无-MobileNetV2 1.0x100%无-EfficientNet-Lite098.7%少量Resize非标准上采样ResNet18100%无-EfficientNet-Lite0 有少量 Resize 算子 fallback 到 CPU这是它速度偏慢的一个原因。虽然占比只有 1.3%但 Resize 操作的数据量不小CPU 处理起来耗时明显。实操心得build 的时候一定要开 verboseTrue仔细看算子分配日志。如果发现大量算子 fallback 到 CPU要么换模型要么改模型结构。我曾经遇到一个带 SE 模块的模型SE 里的全局池化和乘法全部 fallback速度直接掉了 5 倍。5. 常见问题排查与避坑经验5.1 转换阶段的高频报错报错一E build: The input shape is not supported这个通常是因为 ONNX 的输入 shape 带了动态维度。解决方法是在导出 ONNX 时固定输入尺寸或者用rknn.load_onnx(inputs[input], input_size_list[[1,3,224,224]])显式指定。报错二E quantize: Unsupport quantized dtypeRV1103 只支持asymmetric_quantized-8如果你写了dynamic_fixed_point-8或dynamic_fixed_point-16会报这个错。改过来就行。报错三E build: Meet unsupported op: xxx某个算子 RKNN 完全不认识。这种情况要么换模型要么用 ONNX 的算子替换功能把不支持的算子拆成支持的算子组合。比如某些自定义的激活函数可以拆成 ReLU 加线性变换。5.2 精度异常的排查思路量化后精度掉得离谱比如掉了 20%排查顺序是这样的第一步检查校准数据集。是不是图片太少是不是和推理场景差异太大是不是预处理不一致我遇到过最离谱的一次校准图片是 RGB 顺序推理时是 BGR精度直接崩了。第二步检查 mean/std 配置。RKNN 的 mean_values 和 std_values 是作用在 0-255 的像素值上的不是 0-1。如果你训练时用的是 0-1 归一化那 RKNN 里应该配 mean0, std255而不是 mean0.485, std0.229。第三步逐层对比输出。RKNN 支持导出中间层结果可以拿 FP32 和 INT8 的中间层输出做对比看是哪一层开始误差放大的。通常是第一个卷积层或者最后一个全连接层最容易出问题。第四步尝试混合量化。RKNN 支持对特定层保持 FP16其他层 INT8。如果某一层对量化特别敏感可以把它单独设为 FP16。代价是模型体积和内存会增大。5.3 板端运行的稳定性问题问题一推理几次后程序崩溃大概率是内存泄漏。RKNN 的 inference 接口每次调用会分配输出 buffer如果没释放跑几百次就 OOM 了。Python 版本一般不用担心C 版本要手动管理。建议用rknn_outputs_release释放输出。问题二帧率忽高忽低RV1103 的 NPU 和 CPU 共享 DDR 带宽如果同时跑视频编码或者网络传输NPU 的带宽会被挤占帧率就波动。解决方法是给 NPU 任务更高的优先级或者错峰执行。问题三板子发热后降频RV1103 在持续满载下会发热温度到 85 度左右会触发降频帧率掉 20% 到 30%。如果产品是密闭外壳一定要考虑散热。我一般会在 NPU 上方贴一块小铝片成本几毛钱效果很明显。5.4 常见问题速查表现象可能原因解决方法转换成功但板端加载失败工具链版本不匹配对齐 toolkit 和 runtime 版本精度掉超过 5%校准集或 mean/std 配置错误检查预处理一致性速度比预期慢很多算子 fallback 到 CPU看 build 日志换模型或改结构推理崩溃内存泄漏或 OOM释放输出 buffer减小模型帧率波动大DDR 带宽竞争错峰执行提高 NPU 优先级发热降频散热不足加散热片降低环境温度6. 我的选型建议与实战体会如果你现在就要在 RV1103 上做一个图像分类项目我的建议是这样的。追求极致速度、精度要求不高选 SqueezeNet 1.1 或 ShuffleNetV2 0.5x。前者 55 FPS后者 46 FPS都能轻松跑满 30 帧。精度在闭集任务上 90% 左右做二分类绰绰有余。精度和速度要平衡MobileNetV2 1.0x 是首选。30 FPS 刚好卡在实时门槛上70% 的 ImageNet 精度和 95% 的闭集精度覆盖绝大多数场景。它的算子兼容性也是满分不会给你添麻烦。精度优先、帧率可以妥协EfficientNet-Lite0。24 FPS 做实时有点勉强但如果你做的是“每 100ms 分析一帧”这种准实时任务它的精度优势值得。注意它有少量算子 fallback实际速度可能比标称的再低一点。需要结构规整、方便二次开发ResNet18。虽然慢12.7 FPS且吃内存38.6MB但它的结构最标准改起来最方便。如果你的板子是 128MB DDR且任务对帧率要求不高它是个稳妥的选择。最后分享一个我在实际项目中总结的小技巧不要一开始就追求最优模型先用 MobileNetV2 把整条链路跑通包括数据采集、标注、训练、量化、部署、后处理。链路通了之后再根据实测的精度和速度瓶颈决定是换更快的模型还是更准的模型。我见过太多人一上来就纠结选哪个模型结果卡在量化校准这一步好几天链路根本没跑通。先把能跑的跑起来再优化这是边缘部署最务实的路径。