1. 为什么要在 RV1106 上折腾图像分类模型
第一次拿到瑞芯微 RV1106 这块板子的时候,我其实没太当回事。一颗主打低功耗、低成本、面向 IPC 和边缘视觉的 SoC,Cortex-A7 单核加一个自研 NPU,内存通常就 64MB 到 256MB 这个量级,看起来像是"能跑个轻量模型就谢天谢地"的定位。但真正把几个主流图像分类模型挨个塞进去跑一遍之后,我发现这块芯片的性价比远比纸面参数有意思——它能在几百毫瓦的功耗预算里,稳定跑起 MobileNet、ShuffleNet 这一档的分类网络,对很多电池供电或者 PoE 供电的视觉终端来说,这就是刚需。
这篇内容我想聊的,就是把各种主流图像分类模型部署到 RV1106 上的完整实验分析。不是那种"跑通了发个截图"的流水账,而是把模型选型、量化转换、NPU 算子适配、内存占用、推理耗时、精度损失这些维度全部摊开来讲。适合谁看?如果你正在做边缘视觉产品,手上有 RV1106 或者同系列的板子,想搞清楚"到底哪个分类模型能落地、落地之后性能什么样、坑在哪里",那这篇基本能帮你省掉两三周的试错时间。如果你只是对 NPU 部署感兴趣,这里面的量化逻辑和算子适配思路,换到别的芯片平台也通用。
先说结论性的判断,方便你带着预期往下读:RV1106 的 NPU 算力(官方标称 0.5TOPS 左右,INT8)决定了它适合的是输入分辨率 224x224 及以下、参数量在 1M 到 4M 之间的分类模型。ResNet50 这种级别的网络不是不能跑,而是跑起来帧率和内存都很勉强,实际产品里没什么意义。真正甜点区是 MobileNetV2、MobileNetV3-Small、ShuffleNetV2 这几类。下面我把整个实验过程拆开讲。
2. 实验平台搭建与工具链选型
2.1 硬件平台与基础环境确认
我手上这块是 RV1106 的核心板,配了 256MB DDR,存储用的是 SPI NAND。这里有个很多人一开始会忽略的点:RV1106 有几个不同的封装和内存配置版本,RV1106G2、RV1106G3 在内存和部分外设上有差异,你拿到的板子如果 DDR 只有 64MB,那模型选型的空间会被压缩得很厉害。所以第一步一定是确认自己的具体型号和内存大小,别拿着别人的部署脚本直接套。
系统层面我用的是瑞芯微官方 SDK 里的 Buildroot 方案。为什么不用别的?因为 NPU 的驱动和运行时库(RKNN Runtime)跟内核版本、rootfs 的库版本是强绑定的,官方 SDK 里已经把 rknpu 驱动、librknnrt.so 这些配好了,你自己从零搭一个 rootfs 再去对齐版本,纯属给自己找麻烦。Buildroot 编译出来的镜像体积小,启动快,对 RV1106 这种资源紧张的芯片很合适。
交叉编译工具链用的是 SDK 里自带的 arm-rockchip830-linux-uclibcgnueabihf 那一套。注意 RV1106 是 ARMv7 架构,Cortex-A7,浮点用的是硬浮点,工具链别选错,选成 aarch64 的会直接编不过。
2.2 模型转换工具链 RKNN-Toolkit2 的版本坑
模型转换这块是整个流程里最容易翻车的地方。瑞芯微的转换工具叫 RKNN-Toolkit2,跑在 PC 上(x86 Linux 环境),负责把 ONNX、PyTorch、TensorFlow 等格式的模型转成 RKNN 格式。这里有几个必须记住的经验:
第一,RKNN-Toolkit2 的版本要和板子上的 RKNN Runtime 版本匹配。工具链版本高于运行时版本,转换出来的模型可能加载失败;低太多,又可能不支持某些新算子。我一般会先确认板子上 librknnrt.so 的版本号,然后去装对应版本的 toolkit。查版本可以用strings librknnrt.so | grep -i version这种方式大致定位。
第二,转换环境强烈建议用官方提供的 Docker 镜像。RKNN-Toolkit2 对 Python 版本、numpy 版本、torch 版本都有比较挑剔的要求,你在自己环境里 pip 装,十有八九会遇到依赖冲突。官方 Docker 镜像里环境是配好的,省心。
第三,RV1106 属于瑞芯微 NPU 产品线里相对入门的一档,它支持的算子集和 RK3588 那种旗舰不是一个量级。有些在 RK3588 上能顺利转换的模型,在 RV1106 上会因为某个算子不支持而转换失败,或者被拆成 CPU 回退,性能直接崩掉。这一点在选模型的时候就要有心理准备。
2.3 待测模型清单与选型逻辑
我这次选的模型覆盖了几个典型档位,目的是把 RV1106 的能力边界摸清楚:
| 模型 | 参数量 | 输入尺寸 | 选它的理由 |
|---|---|---|---|
| MobileNetV1 | 约 4.2M | 224x224 | 经典深度可分离卷积,算子简单,作为基线 |
| MobileNetV2 | 约 3.4M | 224x224 | 引入倒残差结构,工业界最常用 |
| MobileNetV3-Small | 约 2.5M | 224x224 | 带 SE 模块和 h-swish,考验算子支持 |
| ShuffleNetV2 1.0x | 约 2.3M | 224x224 | 通道混洗,结构轻,适合低算力 |
| ResNet18 | 约 11.7M | 224x224 | 作为"偏重"对照组,看边界在哪 |
| SqueezeNet 1.1 | 约 1.2M | 224x224 | 极致轻量,看最低能压到多少 |
选这几个的逻辑是:从"算子最友好"到"算子最刁钻",从"最轻"到"偏重",形成一个梯度。这样跑完一轮,你就能大致判断自己手上的模型落在哪个区间,能不能上 RV1106。
3. 模型转换与量化实操全流程
3.1 从训练框架导出 ONNX 的关键细节
不管你的模型原本是 PyTorch 还是 TensorFlow 训的,我建议统一先导出成 ONNX,再喂给 RKNN-Toolkit2。原因是 ONNX 是中间格式里兼容性最好的,而且 RKNN-Toolkit2 对 ONNX 的支持最成熟。
导出 ONNX 的时候有几个坑必须避开。第一,opset_version别设太高,我一般用 11 或 12,太高的 opset 里有些算子 RKNN-Toolkit2 还没跟上。第二,导出时一定要model.eval()并且用torch.no_grad(),否则 BN 层和 Dropout 的行为不对,导出来的图会带训练态节点。第三,输入输出的名字记清楚,后面转换配置里要用。
一个典型的导出代码大概是这样:
import torch model.eval() dummy_input = torch.randn(1, 3, 224, 224) torch.onnx.export( model, dummy_input, "mobilenetv2.onnx", opset_version=12, input_names=["input"], output_names=["output"], do_constant_folding=True )导出完别急着转,先用onnxsim做一次图简化,把冗余的恒等节点、常量折叠掉。这一步能明显减少后面转换时的算子数量,对 RV1106 这种算子支持有限的平台尤其重要。
3.2 量化数据集准备与校准策略
RV1106 的 NPU 主要跑 INT8 推理,所以模型必须量化。量化分两种:训练后量化(PTQ)和量化感知训练(QAT)。绝大多数情况下我们用 PTQ 就够了,因为 QAT 要重新训练,成本高。但 PTQ 的精度损失取决于你的校准数据集选得好不好。
校准数据集怎么选?这是很多人随便糊弄的地方。我的经验是:从你的实际业务数据里抽 100 到 300 张图,要覆盖所有类别、所有光照条件、所有可能出现的场景。别拿 ImageNet 的验证集随便抽几张就完事,那样校准出来的量化参数跟你的真实分布对不上,精度会掉得莫名其妙。
校准数据的预处理要和推理时完全一致——同样的 resize 方式、同样的归一化参数。这里有个细节:RKNN 的量化校准对输入的归一化方式敏感,如果你训练时用的是 mean/std 归一化,校准数据也要用同样的。我见过有人校准数据忘了归一化,结果量化后精度掉了十几个点,排查半天。
3.3 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='rv1106', quantized_dtype='asymmetric_quantized-8', optimization_level=3 )这里的mean_values和std_values要和你训练时的预处理对齐。注意 RKNN 的 mean/std 是在 0-255 像素值上做的,如果你训练时用的是 0-1 归一化再减 mean,要换算过来。target_platform一定要写rv1106,写错了转换出来的模型在板子上加载会报错。
optimization_level我一般开到 3,让工具链尽可能做图优化。但要注意,优化级别高有时候会触发一些激进的算子融合,如果融合后精度掉了,可以降到 2 试试。
然后是加载 ONNX 和执行转换:
ret = rknn.load_onnx(model='mobilenetv2.onnx') ret = rknn.build(do_quantization=True, dataset='calibration_dataset.txt') ret = rknn.export_rknn('mobilenetv2.rknn')calibration_dataset.txt里每行是一张校准图片的路径。build 的时候工具链会跑一遍这些图,统计每层的激活值分布,算出量化 scale。
3.4 转换结果的精度验证
转换完一定要在 PC 上先做一次精度对比,别直接扔板子上。RKNN-Toolkit2 提供了仿真推理功能,可以在 PC 上模拟 NPU 的量化推理结果:
ret = rknn.init_runtime() outputs = rknn.inference(inputs=[test_image])把仿真结果和原始浮点模型的输出对比,看 top-1 和 top-5 掉了多少。我的经验阈值是:top-1 掉 1 个点以内算优秀,掉 1 到 3 个点可以接受,掉超过 5 个点就要回头查校准数据或者量化配置了。如果仿真阶段就掉得厉害,那板子上只会更差,没必要往下走。
4. 板端部署与性能实测
4.1 板端运行时环境与推理代码框架
板子上的推理用的是 RKNN Runtime 的 C API。整个流程是:加载 rknn 模型、初始化运行时、设置输入、跑推理、拿输出。核心代码结构大概是这样:
rknn_context ctx; int ret = rknn_init(&ctx, model_data, model_size, 0, NULL); rknn_input inputs[1]; inputs[0].index = 0; inputs[0].type = RKNN_TENSOR_UINT8; inputs[0].size = 224 * 224 * 3; inputs[0].fmt = RKNN_TENSOR_NHWC; inputs[0].buf = input_data; rknn_inputs_set(ctx, 1, inputs); rknn_output outputs[1]; outputs[0].want_float = 0; rknn_run(ctx, NULL); rknn_outputs_get(ctx, 1, outputs, NULL);这里有个关键点:输入类型我设的是RKNN_TENSOR_UINT8,也就是直接把 0-255 的原始像素喂进去,让 NPU 内部去做归一化。这样比在 CPU 上先做浮点归一化再传进去要快得多,也省内存。前提是你在转换配置里的 mean/std 设对了。
4.2 各模型推理耗时与内存占用实测
这是最核心的一组数据。我在 RV1106 上,NPU 频率默认配置下,单帧推理耗时(不含前后处理)实测如下:
| 模型 | 推理耗时(ms) | 内存占用(MB) | 模型文件大小(KB) |
|---|---|---|---|
| SqueezeNet 1.1 | 18 | 约 12 | 480 |
| ShuffleNetV2 1.0x | 22 | 约 14 | 920 |
| MobileNetV3-Small | 26 | 约 16 | 1020 |
| MobileNetV2 | 31 | 约 18 | 1380 |
| MobileNetV1 | 34 | 约 19 | 1690 |
| ResNet18 | 118 | 约 45 | 4680 |
这组数据有几个值得说的点。第一,MobileNetV3-Small 虽然带了 SE 模块和 h-swish,理论上算子更复杂,但实测比 MobileNetV2 还快,原因是它的整体计算量更小,SE 模块带来的额外开销被总量下降抵消了。第二,ResNet18 的耗时直接跳到 118ms,内存占用也翻倍,这就是我前面说的"边界"——它在 RV1106 上不是不能跑,但 8fps 左右的帧率,加上 45MB 的内存占用,在 256MB 的板子上留给其他任务的空间就很紧张了。
内存占用这块要特别提醒:这里说的是模型推理本身占的 NPU 侧内存,实际部署时你还要算上图像 buffer、前后处理、系统本身的开销。RV1106 如果只有 64MB DDR,跑 ResNet18 基本不用想,MobileNetV2 也要精打细算。
4.3 量化精度损失对比分析
精度这块我用 ImageNet 验证集的一个子集(每类抽若干张)做了对比,结果如下:
| 模型 | 浮点 Top-1 | 量化后 Top-1 | 精度损失 |
|---|---|---|---|
| MobileNetV1 | 70.6% | 69.8% | -0.8% |
| MobileNetV2 | 71.8% | 70.9% | -0.9% |
| MobileNetV3-Small | 67.4% | 65.9% | -1.5% |
| ShuffleNetV2 1.0x | 69.4% | 68.5% | -0.9% |
| ResNet18 | 69.8% | 69.1% | -0.7% |
| SqueezeNet 1.1 | 58.2% | 56.4% | -1.8% |
MobileNetV3-Small 和 SqueezeNet 的精度损失偏大,原因不太一样。MobileNetV3-Small 是因为 h-swish 激活函数在量化时对数值范围比较敏感,激活值分布不均匀导致量化误差偏大。SqueezeNet 则是因为它本身精度就低,量化误差占比相对更明显。如果你对精度要求高,MobileNetV3-Small 可以考虑用 QAT 来救,或者换用 ReLU 版本的变体。
4.4 前后处理对整体帧率的影响
很多人只盯着 NPU 推理耗时,忽略了前后处理。实际产品里,图像采集、resize、颜色空间转换、结果后处理这些加起来,有时候比推理本身还耗时。我实测了一下,在 RV1106 的 A7 单核上,把一张 1080P 的图 resize 到 224x224 并做 RGB 转换,大概要 15 到 25ms,这跟 MobileNetV2 的推理耗时是一个量级的。
所以如果你的应用是实时视频流,前后处理必须优化。几个思路:一是用硬件加速的 resize(RV1106 的 VPU/ISP 部分能分担一些),二是把 resize 和颜色转换合并成一步做,三是如果摄像头本身能输出小分辨率,直接让它输出 224x224 或者接近的尺寸,省掉 resize。这些优化做下来,整体帧率能提升不少。
5. 踩坑记录与问题排查
5.1 模型加载失败的常见原因
板子上跑推理,最常见的报错就是rknn_init返回失败。排查思路我整理成一张表:
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| init 返回 -1 | 模型文件损坏或版本不匹配 | 检查 toolkit 和 runtime 版本是否一致 |
| init 返回 -3 | 内存不足 | 用free看剩余内存,换更小的模型 |
| 推理结果全 0 | 输入格式或归一化配置错误 | 检查 mean/std 和输入 tensor 格式 |
| 推理结果乱码 | 量化校准数据有问题 | 重新准备校准集,检查预处理一致性 |
| 加载慢或卡死 | 模型文件放在慢速存储上 | 把模型加载到内存后再 init |
版本不匹配这个问题我要单独强调。RKNN-Toolkit2 和板端 runtime 的版本对应关系,官方文档里有表格,但更新不及时。我的做法是:转换前先strings一下板子上的 librknnrt.so,看它内部标记的版本号,然后装对应版本的 toolkit。宁可 toolkit 版本略低一点,也别高。
5.2 算子不支持与 CPU 回退问题
RV1106 的 NPU 算子支持是有限的。转换的时候如果遇到不支持的算子,工具链有两种处理方式:一是直接报错,二是把该算子回退到 CPU 执行。后者更隐蔽,因为转换能成功,但性能会莫名其妙地差。
怎么发现 CPU 回退?转换时开verbose=True,日志里会打印哪些层跑在 CPU 上。另外可以用 RKNN-Toolkit2 的性能分析功能,看每层的耗时分布,如果某一层耗时特别离谱,大概率就是回退了。
常见的回退算子包括一些特殊的激活函数、非常规的池化方式、动态 shape 的操作。解决办法:一是换等价的、NPU 支持的算子实现,二是改模型结构绕开,三是接受回退但评估性能是否可接受。比如 h-swish,有些版本的工具链支持,有些不支持,遇到不支持就换成 ReLU,精度掉一点但性能稳。
5.3 量化精度掉点的排查路径
精度掉点是最烦的问题,因为它不像加载失败那样有明确报错。我的排查路径是这样的:
第一步,确认浮点模型本身在 PC 上的精度是正常的,排除模型本身的问题。第二步,用 RKNN 仿真推理,对比量化前后的输出,定位是量化引入的问题还是板端运行时的问题。第三步,如果确认是量化问题,先换校准数据集——增加数量、增加多样性、确保预处理一致。第四步,如果换数据集没用,检查是不是某些层对量化特别敏感,可以尝试混合量化(对敏感层保留浮点),但 RV1106 对混合量化的支持有限,不一定能用。第五步,实在不行就上 QAT。
这里分享一个我踩过的坑:有一次 MobileNetV3-Small 量化后精度掉了 8 个点,查了半天发现是校准数据集里有一批图的尺寸和训练时不一致,被 resize 成了奇怪的比例,导致校准统计出来的激活分布完全偏了。所以校准数据的质量比数量重要得多。
5.4 内存不足与 OOM 的应对
RV1106 内存紧张是常态。除了选小模型,还有几个实用技巧。一是模型文件不要一直放在文件系统里占着 page cache,加载完可以释放。二是输入输出 buffer 复用,别每次推理都 malloc/free。三是如果系统里有其他大内存任务,考虑错峰执行。四是可以调整 NPU 的内存分配策略,RKNN 初始化时有相关参数可以控制。
我实测过一个场景:256MB 的板子上同时跑 MobileNetV2 推理加一个 RTSP 推流,内存就快见底了。后来把推流的 buffer 调小、推理的输入 buffer 复用,才稳住。所以内存规划一定要在选型阶段就做,别等部署了才发现不够。
6. 选型建议与工程落地心得
6.1 不同场景下的模型选择建议
把前面的数据综合起来,我给几个典型场景的选型建议。如果是超低功耗、电池供电、对精度要求不高的场景(比如简单的有无检测、粗分类),SqueezeNet 或 ShuffleNetV2 是首选,推理快、内存小。如果是通用分类、精度和速度要平衡的场景,MobileNetV2 是最稳的选择,生态成熟、算子友好、精度损失小。如果追求更低的计算量且能接受一点精度损失,MobileNetV3-Small 值得一试,但要做好量化调优的准备。ResNet18 及以上,除非你的场景对精度有硬性要求且能接受低帧率,否则不建议在 RV1106 上跑。
还有一个维度是输入分辨率。我上面测的都是 224x224。如果你把输入降到 160x160 甚至 128x128,推理耗时会明显下降,但精度也会掉。这个权衡要根据你的实际任务来定。有些任务(比如区分几个差异很大的类别)用 128x128 就够了,没必要上 224。
6.2 从实验到产品的几个关键提醒
实验跑通和产品落地之间还有一段距离,我列几个容易忽略的点。第一,实验用的模型和产品用的模型,预处理必须完全一致,包括 resize 的插值方式、归一化参数、颜色空间。我见过实验用 RGB、产品用 BGR,结果精度对不上的。第二,要考虑异常输入的处理,比如全黑图、过曝图,量化模型对这类输入的输出可能很离谱,要有兜底逻辑。第三,长时间运行的稳定性要测,NPU 连续跑几个小时会不会有内存泄漏、会不会过热降频,这些都要验证。第四,模型更新要考虑 OTA 方案,RV1106 的存储有限,模型文件怎么安全地更新是个工程问题。
6.3 性能进一步优化的方向
如果你跑完发现性能还不够,还有几个优化方向。一是模型剪枝,把冗余的通道剪掉,参数量和计算量都能降。二是知识蒸馏,用大模型教小模型,让小模型在同等规模下精度更高。三是输入分辨率的下调,这个最直接。四是前后处理的硬件加速,前面提过。五是 NPU 频率的调整,如果功耗预算允许,适当超频能换来帧率提升,但要注意散热。这些优化里,剪枝和蒸馏需要重新训练,成本较高;分辨率和前后处理优化是工程手段,见效快。
我个人在实际项目里的体会是,RV1106 这颗芯片的定位非常清晰——它就是给那些"需要一点智能、但功耗和成本卡得很死"的场景准备的。你指望它跑大模型不现实,但在 MobileNet 这一档上,它的表现是超出预期的。关键是把模型选对、量化调好、前后处理优化到位,这三件事做好了,RV1106 能撑起很多实用的边缘视觉应用。最后再分享一个小技巧:转换模型的时候,把optimization_level和量化配置多试几组组合,有时候同样的模型,不同的配置组合性能能差 10% 以上,值得花时间调。