1. 为什么要在RV1126B上折腾图像分类模型
边缘端跑图像分类,这两年从"能不能跑"变成了"怎么跑得划算"。瑞芯微RV1126B这颗芯片在安防、工业质检、智能零售这些场景里出镜率很高,它自带NPU,标称算力对付ResNet、MobileNet这一档的分类网络绰绰有余。但"标称算力"和"实际能落地的吞吐"之间隔着一条河,河里全是量化精度、算子支持、内存带宽这些暗礁。
我这次做的事情很直接:把几类主流图像分类模型——从轻量的MobileNet系列、ShuffleNet,到中等体量的ResNet、EfficientNet,再到稍重的ConvNeXt-Tiny——统一走一遍RV1126B的部署流程,记录每一步的转换损耗、量化前后的精度变化、以及实机推理的帧率和内存占用。目标不是刷某个榜单,而是给正在选型的人一张"什么模型配什么场景"的对照表。
适合读这篇的人有三类:一是手里已经有RV1126B开发板、准备把训练好的分类模型塞进去的算法工程师;二是正在做方案选型、纠结用哪颗芯片配哪个网络的架构师;三是刚接触瑞芯微工具链、被RKNN转换报错折磨过的朋友。我会把踩过的坑、参数怎么调、为什么这么调都摊开讲,尽量让你少走我走过的弯路。
需要提前说明的是,下面所有实验数据都来自我自己的测试环境,不同批次的板子、不同的散热条件、不同的系统镜像版本都可能让数字有出入,所以重点看趋势和方法,别死磕绝对值。
2. 实验环境与工具链的搭建细节
2.1 硬件与软件版本锁定
部署实验最怕的就是版本漂移。我见过太多人拿着A版本的转换工具去转B版本固件的模型,然后对着报错怀疑人生。所以第一步是把版本钉死。
我的测试环境如下:
| 项目 | 配置 |
|---|---|
| 开发板 | RV1126B核心板 + 底板 |
| NPU驱动版本 | 与固件配套的版本,通过系统内命令查询确认 |
| 转换工具 | RKNN-Toolkit2,运行在x86主机上 |
| 目标运行时 | RKNPU2运行时库,部署在板端 |
| 训练框架 | PyTorch,导出ONNX作为中间格式 |
| 主机系统 | Ubuntu 20.04 |
这里有个关键点:RKNN-Toolkit2的版本必须和板端RKNPU2运行时版本匹配。工具链更新很快,新版本工具转出来的模型在老版本运行时上可能直接加载失败。我的做法是先确认板子固件里NPU驱动的版本号,然后去选对应区间的工具版本,而不是无脑装最新。
提示:板端查询NPU相关版本信息时,可以查看系统里与NPU运行时相关的库文件版本,或者用官方提供的查询工具。版本对不上是新手最常见的翻车点。
2.2 模型导出ONNX时的几个隐形陷阱
从PyTorch导出ONNX看起来是一行代码的事,但分类模型有几个地方特别容易出问题。
第一个是动态维度。训练时很多人习惯用动态batch,导出ONNX时如果没固定输入shape,后面RKNN转换会报维度不匹配。我的做法是导出时明确指定input_names和dynamic_axes,把batch固定为1,宽高固定为模型预期的输入尺寸。
第二个是算子版本。ONNX的opset版本选太高,RKNN工具可能不认识;选太低,某些算子又表达不出来。实测下来opset 11到12是比较稳的区间,大部分分类网络都能顺利过。
第三个是后处理。很多分类模型在PyTorch里forward的最后会带一个softmax,导出ONNX时如果保留,RKNN转换后板端输出就是概率;如果去掉,输出就是logits。我建议导出时把softmax去掉,让板端拿到logits自己处理,这样量化时数值范围更可控,也方便你后续做阈值调整。
# 导出ONNX的典型写法,注意固定shape和opset torch.onnx.export( model, dummy_input, "model.onnx", input_names=["input"], output_names=["output"], opset_version=12, do_constant_folding=True, dynamic_axes=None # 固定维度,避免转换报错 )导出后别急着转RKNN,先用ONNX Runtime跑一遍,确认输出和PyTorch一致。这一步能帮你把模型本身的问题和工具链的问题分开,省下大量排查时间。
2.3 RKNN转换配置文件的参数取舍
RKNN转换的核心是一个配置对象,里面几个参数直接决定成败:
mean_values和std_values:必须和训练时的预处理对齐。很多人训练用ImageNet的均值和方差,转换时忘了填,结果板端精度暴跌还找不到原因。target_platform:填rv1126b,填错平台会导致生成的模型无法在板端加载。quantized_dtype:分类模型一般用asymmetric_quantized-8,也就是int8非对称量化。quantized_algorithm:有normal和mmse等选项,后面会专门讲怎么选。
配置文件写好后,转换过程本身很快,但转换日志一定要逐行看。日志里会告诉你哪些算子被量化了、哪些回退到了CPU、哪些被融合了。这些信息直接关系到板端的实际性能。
3. 量化策略对分类精度的影响实测
3.1 为什么int8量化会让精度掉得莫名其妙
int8量化本质是把浮点权重和激活值映射到256个整数格子里。分类模型对这件事的敏感度差异很大:MobileNet这种深度可分离卷积堆出来的网络,激活值分布比较集中,量化后精度掉得少;ResNet的残差结构里有些层的激活值动态范围很大,量化后容易掉点。
我实测下来,同一套量化参数下,不同模型的精度损失能差出好几倍。这不是模型"好不好"的问题,而是网络结构对量化的友好度不同。理解这一点,你选型时就不会只盯着浮点精度看。
3.2 逐层量化与混合量化的选择逻辑
RKNN工具支持逐层量化分析,转换后会给你一份每层的量化误差报告。我的习惯是先把所有层都量化,跑一遍精度,如果掉点超过可接受范围,再去看报告里误差最大的那几层。
对于误差大的层,有两个处理方向:一是把这些层设为不量化,回退到浮点计算,代价是速度变慢;二是调整量化算法,比如从normal换成mmse,后者会在量化时做误差最小化搜索,通常能挽回一部分精度,但转换时间会变长。
实测数据大致是这样的趋势:MobileNetV2全int8量化后,Top-1精度损失在1%以内;ResNet18损失在1.5%到2.5%之间;EfficientNet-B0因为包含swish激活和SE模块,损失可能到3%以上,需要混合量化才能压下来。
| 模型 | 全int8精度损失 | 混合量化后损失 | 转换耗时 |
|---|---|---|---|
| MobileNetV2 | <1% | 可忽略 | 短 |
| ShuffleNetV2 | <1% | 可忽略 | 短 |
| ResNet18 | 1.5%~2.5% | <1% | 中 |
| EfficientNet-B0 | 3%+ | 1%~1.5% | 长 |
| ConvNeXt-Tiny | 2%~3% | 1%左右 | 长 |
注意:精度损失还和你的校准集质量强相关。校准集要从真实业务数据里采样,覆盖各种光照、角度、类别,别拿训练集随便抽几张糊弄。校准集选得好,量化损失能少掉一半。
3.3 校准集制作的经验细节
校准集一般准备200到500张图就够了,但选图有讲究。我的做法是按类别分层采样,每个类别至少保证有代表性的样本,同时刻意加入一些边缘样本——比如过曝的、模糊的、遮挡的。这些样本能让量化工具看到激活值的真实分布范围,避免量化区间被"平均样本"压缩得太窄。
另外一个细节是校准集的预处理必须和推理时完全一致。如果你推理时做了归一化,校准集也要做同样的归一化。这个一致性如果破坏,量化出来的模型在板端会表现得莫名其妙。
4. 各模型在板端的实测性能对比
4.1 推理帧率与内存占用的横向对比
板端测试我用的是同一段C++推理代码,输入统一resize到各模型的原生输入尺寸,batch为1,循环跑1000次取平均。散热用风扇压住,避免降频干扰。
| 模型 | 输入尺寸 | 平均帧率 | 内存占用 | 模型文件大小 |
|---|---|---|---|---|
| MobileNetV2 | 224x224 | 高 | 低 | 小 |
| ShuffleNetV2 | 224x224 | 高 | 低 | 小 |
| ResNet18 | 224x224 | 中 | 中 | 中 |
| EfficientNet-B0 | 224x224 | 中低 | 中 | 中 |
| ConvNeXt-Tiny | 224x224 | 低 | 高 | 大 |
具体数字我这里不写死,因为不同板子的散热和内存配置差异很大。但趋势是明确的:轻量模型能跑到实时甚至超实时,中等模型勉强实时,重模型就只能做低频分析了。
这里有个容易被忽略的点:帧率不是线性的。MobileNetV2和ResNet18的帧率差距,往往比它们浮点FLOPs的差距更大。原因是RV1126B的NPU对深度可分离卷积有专门优化,而对标准卷积的利用率没那么高。所以选型时不能只看FLOPs,要看NPU对这类算子的实际加速比。
4.2 首帧延迟与持续推理的差异
很多人只测持续帧率,忽略了首帧延迟。在实际产品里,首帧延迟决定了用户按下快门到出结果的时间,体验影响很大。
首帧延迟主要花在模型加载和内存分配上。模型文件越大,加载越慢。ConvNeXt-Tiny这种模型,首帧延迟可能是MobileNetV2的好几倍。如果你的场景是"触发式"的——比如检测到物体才推理一次——那首帧延迟比持续帧率更重要。
我的优化手段是把模型加载放在初始化阶段,推理阶段只做前向计算。另外,如果内存允许,可以预分配输入输出buffer,避免每次推理都重新申请内存。
4.3 多模型切换场景下的资源竞争
有些产品需要同时跑多个模型,比如一个做检测、一个做分类。RV1126B的NPU是共享资源,多模型并发时会出现排队。实测下来,两个中等模型并发,总吞吐不是简单相加,而是会打折扣。
如果确实要多模型,我的建议是错峰调度:检测模型跑低频,分类模型在检测到目标后才触发。这样既能保证响应,又不会让NPU过载。另外,多个模型可以共享同一个RKNN上下文,减少重复初始化开销。
5. 部署过程中踩过的坑与排查链路
5.1 转换成功但板端加载失败
这个坑我踩过不止一次。转换日志一切正常,模型文件也生成了,但板端加载时报错。排查下来通常是三个原因:
一是工具版本和运行时版本不匹配。前面强调过,这里再强调一遍。解决办法是查清板端运行时版本,回退工具版本重新转换。
二是模型里包含了板端不支持的算子。转换时工具可能把不支持的算子静默回退到CPU,但某些情况下会直接失败。解决办法是看转换日志里的算子支持列表,把不支持的算子替换掉。
三是模型文件传输过程中损坏。这个最隐蔽,校验一下文件哈希就能排除。
5.2 精度对不上:从预处理开始查
板端推理结果和PC端对不上,是第二常见的坑。排查顺序应该是:预处理、量化、后处理。
预处理最容易出问题。训练时的归一化参数、通道顺序(RGB还是BGR)、resize的插值方式,任何一项不一致都会导致精度偏差。我的做法是写一个最小测试:拿同一张图,分别在PC和板端跑,打印预处理后的张量值,逐位对比。
如果预处理一致,再查量化。把板端模型换成不量化的浮点版本跑一遍,如果浮点版本精度正常,那就是量化的问题,回去调校准集或量化算法。
后处理相对简单,主要是softmax和argmax的顺序,以及输出的维度排列。RKNN的输出维度有时候和ONNX不一样,需要打印出来确认。
5.3 内存不足导致的随机崩溃
RV1126B的内存不算宽裕,跑大模型时如果同时开了其他服务,容易出现内存不足。表现是推理跑着跑着突然崩溃,或者帧率断崖式下跌。
排查方法是监控推理过程中的内存曲线。如果内存持续增长,说明有泄漏,检查是不是每次推理都新建了RKNN上下文或者没释放buffer。如果内存一开始就接近上限,那就得换小模型或者裁剪输入尺寸。
提示:板端部署时尽量用静态内存分配,避免频繁malloc/free。NPU相关的buffer最好在初始化时一次性分配好。
6. 模型选型的决策框架
6.1 按场景倒推模型规格
选型不该从模型出发,而该从场景出发。我一般先问四个问题:输入分辨率是多少?要求的帧率是多少?可接受的精度下限是多少?内存预算多少?
比如智能门锁的人脸分类,输入可能就112x112,帧率要求不高,那MobileNetV2甚至更小的模型就够。工业质检的分类,输入可能是512x512,要求高精度,那就得上ResNet18或EfficientNet,同时接受帧率下降。
把这四个问题的答案列出来,再对照前面的性能表,基本就能圈定候选模型。
6.2 精度与速度的平衡点怎么找
平衡点不是拍脑袋定的,而是算出来的。我的方法是:先确定业务能接受的最低精度,然后在这个精度约束下选帧率最高的模型。
具体操作是画一条曲线:横轴是帧率,纵轴是精度,把各个模型的点标上去,然后画一条精度下限的水平线,线以上的点里选最靠右的。这个方法比"我觉得ResNet比较准"靠谱得多。
另外要注意,精度指标要用业务相关的指标,不是Top-1。如果你的业务只关心某几个类别,那就看这几个类别的召回率,别被整体Top-1误导。
6.3 留出升级余量的思路
选型时别把NPU吃满。我一般留30%左右的算力余量,原因是:业务后期可能加模型、加分辨率、加帧率,如果一开始就吃满,后面没有腾挪空间。
同样,内存也要留余量。RV1126B上跑分类模型,内存占用最好控制在总可用内存的60%以内,剩下的留给系统和其他服务。
7. 一些让部署更顺手的实操技巧
7.1 用脚本固化整个转换流程
手动敲命令转换模型,迟早会出错。我的做法是写一个转换脚本,把ONNX导出、RKNN转换、精度验证串起来,每次改模型只跑脚本,不碰手动步骤。脚本里把版本号、预处理参数、量化配置都写成变量,方便追溯。
这样做还有个好处:当你需要转十几个模型做对比时,脚本能批量跑,省下大量重复劳动。
7.2 板端推理代码的复用结构
板端代码我习惯分成三层:模型加载层、预处理层、推理层。模型加载层负责读RKNN文件、初始化上下文;预处理层负责resize、归一化、转张量;推理层只负责调用NPU和解析输出。
这样分层的好处是换模型时只改加载层和预处理层的参数,推理层不动。多模型对比实验时,这套结构能让你快速切换。
7.3 精度验证的自动化
每次转换完模型,手动跑几张图看结果太慢。我写了一个小工具,自动从验证集里抽100张图,分别在PC和板端跑,输出精度对比和混淆矩阵。这样每次转换后跑一遍,几秒钟就知道这次量化有没有问题。
这个工具的核心是对齐预处理。只要预处理对齐了,后面的对比就是纯数值比较,很可靠。
8. 关于这批实验我个人的几点体会
折腾完这一轮,最大的感受是:边缘端部署没有"最好"的模型,只有"最合适"的模型。MobileNetV2在RV1126B上跑得飞起,但它的精度上限摆在那里;ConvNeXt-Tiny精度好看,但帧率和内存都让人肉疼。选型的本质是在约束条件下找最优解,而约束条件来自你的业务,不是来自论文。
第二个体会是量化这件事值得花时间。很多人转换完模型精度掉了就换模型,其实调一调校准集、换一换量化算法,往往能把精度找回来。量化不是黑盒,它是有规律可循的工程问题。
第三个体会是版本管理要严格。工具链、固件、运行时,任何一个版本漂移都可能让你前功尽弃。我现在养成了习惯:每做一个实验,先把所有版本号记下来,后面复现时直接照抄。
最后分享一个小技巧:如果你不确定某个模型能不能在RV1126B上跑,先别急着完整转换,用工具跑一遍算子支持检查,几分钟就能知道结果。这个检查能帮你省下大量无效转换的时间。