1. 为什么要在RV1126B上折腾图像分类模型
瑞芯微RV1126B这颗芯片在边缘视觉圈子里热度一直不低。它本质上是一颗面向智能视觉场景的SoC,内置NPU,主打低功耗、小体积、够用的算力,常见于IPC、门禁面板、工业质检盒子和各种需要"本地看一眼就出结果"的设备。我手头拿到的是搭载RV1126B的核心板加底板方案,配套的SDK里带了RKNN工具链,目标很明确:把几个主流的图像分类模型塞进去,跑起来,看看延迟、精度、内存占用到底是个什么水平。
图像分类听起来是深度学习里最"古典"的任务,但真到了嵌入式端侧部署,事情一点都不古典。你在服务器上用PyTorch训一个ResNet50,准确率刷到76%以上轻轻松松,可要把这个模型搬到RV1126B的NPU上,中间要过模型转换、量化、算子适配、内存对齐好几道关。每一道关都可能让你的模型精度掉几个点,或者干脆转换失败。我这次实验选了四个有代表性的模型:MobileNetV2、ResNet18、EfficientNet-Lite0和ShuffleNetV2,覆盖了从轻量到中等规模的常见选择,基本能代表大多数人在RV1126B上会考虑的范围。
这篇文章适合两类人看。一类是刚拿到RV1126B开发板、想把分类模型跑起来但不知道从哪下手的嵌入式工程师;另一类是已经在用RKNN工具链、但被量化掉点或者推理速度不达预期困扰的算法部署同学。我会把整个流程拆开讲,包括环境搭建、模型转换、量化校准、板端推理和性能对比,重点放在那些文档里不会写、但实际会卡住你的细节上。
提示:RV1126B的NPU算力和RV1126有区别,工具链版本也不同,网上很多RV1126的教程直接套过来会出问题,建议先确认自己SDK里RKNN Toolkit的版本号。
2. 实验环境与工具链的整体设计思路
2.1 硬件与软件版本确认
动手之前先把版本对齐,这一步偷懒后面会加倍还回来。我这次用的环境如下:
| 项目 | 版本/型号 | 说明 |
|---|---|---|
| 主控芯片 | RV1126B | 内置NPU,支持INT8推理 |
| 开发板 | 官方核心板+底板 | 带千兆网口和USB调试口 |
| 上位机系统 | Ubuntu 20.04 | 用于模型转换和量化 |
| RKNN Toolkit | 1.7.x系列 | 与SDK配套版本 |
| 板端系统 | Buildroot Linux | SDK自带固件 |
| 推理框架 | RKNN Runtime | 板端C接口 |
| 训练框架 | PyTorch 1.13 | 导出ONNX用 |
版本这块我踩过一次坑。最开始上位机装的是比较新的RKNN Toolkit2,结果和板端Runtime对不上,模型推过去直接报版本不兼容。后来老老实实按SDK文档里标注的版本重装,问题就没了。所以第一条经验:上位机工具链版本必须和板端Runtime版本匹配,不是越新越好。
2.2 模型选型的考量逻辑
为什么选这四个模型,不是随便挑的。RV1126B的NPU算力属于入门偏上的水平,你硬塞一个ResNet50或者ViT进去,要么转换失败,要么推理慢到没法用。所以选型要卡在一个"甜区"里:
- MobileNetV2:深度可分离卷积的代表,参数量小,是端侧分类的基线模型,几乎所有人第一个跑的都是它。
- ResNet18:残差结构的经典款,比MobileNet重一些,但精度更稳,用来测试NPU对标准卷积的吞吐。
- EfficientNet-Lite0:专门为端侧设计的缩放版本,去掉了SE模块里NPU不友好的部分,理论上更适合硬件加速。
- ShuffleNetV2:通道混洗结构,计算量极低,用来探NPU对特殊算子的支持程度。
这四个模型放在一起,基本能画出RV1126B在分类任务上的能力边界。你要是只跑一个MobileNetV2,觉得"能跑",那不代表别的模型也能跑;你要是只跑ResNet18,觉得"太慢",那也不代表这颗芯片不行,可能只是模型没选对。
2.3 整体部署流程设计
整个链路我设计成五步走,每一步都有明确的输入输出,方便定位问题:
- 训练/获取模型:用PyTorch加载预训练权重,或者自己微调一个。
- 导出ONNX:固定输入尺寸,去掉训练相关的节点。
- RKNN转换:用Toolkit把ONNX转成RKNN格式,同时做量化。
- 量化校准:准备校准集,跑量化,观察精度变化。
- 板端部署:把RKNN模型推到板子上,用Runtime接口推理,测延迟和精度。
这个流程看起来线性,实际上第3步和第4步经常要来回迭代。量化掉点严重,你得回去改校准集或者调整量化策略;转换报错,你得回去改ONNX的算子实现。所以别指望一次跑通,留出调试时间。
3. 模型转换与量化环节的核心细节
3.1 从PyTorch到ONNX的导出要点
导出ONNX这一步,很多人觉得就是一句torch.onnx.export的事,其实细节不少。我以MobileNetV2为例,关键代码如下:
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", input_names=["input"], output_names=["output"], opset_version=11, do_constant_folding=True, dynamic_axes=None )这里有几个点必须注意。opset_version选11,不是随便选的。RKNN Toolkit对ONNX的opset支持有范围,太高了算子解析不了,太低了某些结构表达不出来。11是我实测下来兼容性最好的版本。dynamic_axes设为None,也就是固定输入尺寸。端侧部署不需要动态shape,固定尺寸能让NPU更好地做内存规划和算子融合。do_constant_folding打开,把能提前算的常量折叠掉,减小模型体积。
导出之后别急着转换,先用onnxsim简化一下:
python -m onnxsim mobilenet_v2.onnx mobilenet_v2_sim.onnx简化能去掉冗余节点,有时候能把一些NPU不支持的算子合并掉。我试过不简化直接转,ResNet18会报某个Add节点不支持,简化之后就过了。
3.2 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='rv1126b', quantized_dtype='asymmetric_quantized-8', optimization_level=3 ) rknn.load_onnx(model='mobilenet_v2_sim.onnx') rknn.build(do_quantization=True, dataset='calibration.txt') rknn.export_rknn('mobilenet_v2.rknn')mean_values和std_values要和训练时的预处理对齐,这个错了精度直接崩。我用的是ImageNet的标准归一化参数。target_platform必须写rv1126b,写错了生成的模型板端加载不了。quantized_dtype选asymmetric_quantized-8,这是INT8非对称量化,RV1126B的NPU对这种方式支持最好。optimization_level=3是最高优化等级,会做算子融合和内存复用,能明显降延迟。
3.3 量化校准集的准备技巧
量化是端侧部署里最容易掉精度的一环。RKNN用的是训练后量化,需要一批校准数据来统计激活值的分布。校准集怎么选,直接决定量化后的精度。
我的做法是从验证集里随机抽200张图片,覆盖所有类别,每类至少5张。校准集不是越多越好,200张对分类任务足够了,太多反而拖慢转换速度。关键是分布要均匀,不能某一类占了一大半。
校准文件calibration.txt就是一个纯文本,每行一张图片的路径:
./calib/n01440764_10026.JPEG ./calib/n01443537_10007.JPEG ...这里有个坑:图片的预处理必须和推理时一致。RKNN在量化时会按你config里设的mean/std去读图片,如果你校准集图片的尺寸、通道顺序和实际推理不一致,量化统计就偏了。我统一用224x224的RGB图片,和模型输入完全对齐。
注意:校准集图片不要用训练集里的图,用验证集或者单独准备的图,否则量化统计会过于乐观,实际部署时精度掉得更狠。
4. 板端部署与推理实测过程
4.1 板端环境准备与模型推送
板子起来之后,先确认NPU驱动和Runtime库都在。SDK固件里一般自带librknn_runtime.so,用ls /usr/lib能看到。如果没有,得从SDK里拷过去。
模型推送用scp或者adb都行,我习惯用scp:
scp mobilenet_v2.rknn root@192.168.1.100:/userdata/models/板端推理我写了一个C程序,核心是加载模型、初始化Runtime、循环推理、测时间。关键代码结构如下:
rknn_context ctx; int ret = rknn_init(&ctx, model_path, 0, 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 = img_data; rknn_inputs_set(ctx, 1, inputs); rknn_run(ctx, NULL); rknn_output outputs[1]; outputs[0].want_float = 1; rknn_outputs_get(ctx, 1, outputs, NULL);输入类型用RKNN_TENSOR_UINT8,让Runtime自己做归一化,省得在CPU上算。want_float=1表示输出要浮点结果,方便算精度。如果只关心分类结果,可以设0拿INT8输出,速度更快。
4.2 延迟与精度实测数据
四个模型跑下来,数据如下(输入224x224,单帧推理,取100次平均):
| 模型 | 转换是否成功 | 量化后Top-1精度 | 平均推理延迟 | 模型体积 |
|---|---|---|---|---|
| MobileNetV2 | 成功 | 70.8% | 12.3ms | 3.5MB |
| ResNet18 | 成功 | 68.2% | 28.7ms | 11.2MB |
| EfficientNet-Lite0 | 成功 | 72.1% | 18.5ms | 4.8MB |
| ShuffleNetV2 | 成功 | 66.5% | 9.8ms | 2.3MB |
原始浮点模型的精度分别是MobileNetV2 71.8%、ResNet18 69.8%、EfficientNet-Lite0 75.1%、ShuffleNetV2 69.4%。量化后普遍掉1到3个点,EfficientNet-Lite0掉得最多,接近3个点,ShuffleNetV2也掉了近3个点。这个掉点幅度在INT8量化里算正常范围,但如果你对精度敏感,就得考虑混合量化或者量化感知训练。
延迟方面,ShuffleNetV2最快,9.8ms,因为它的计算量确实小。ResNet18最慢,28.7ms,标准卷积对NPU的压力明显更大。EfficientNet-Lite0在精度和延迟之间平衡得最好,72.1%的精度配18.5ms延迟,实际项目里我会优先考虑它。
4.3 内存占用与功耗观察
除了延迟和精度,内存和功耗也得看。RV1126B的NPU有独立的片上内存,模型加载后会占用一部分。实测四个模型加载后的内存增量:
- MobileNetV2:约18MB
- ResNet18:约42MB
- EfficientNet-Lite0:约24MB
- ShuffleNetV2:约14MB
功耗我用功率计粗测了一下,推理时整板功耗在1.8W到2.5W之间波动,ResNet18最高,ShuffleNetV2最低。这个数据对电池供电的设备很关键,如果你要做低功耗常开方案,ShuffleNetV2或者MobileNetV2更合适。
5. 常见问题与排查技巧实录
5.1 转换阶段的高频报错
报错一:Unsupported operator
这是最常见的。RKNN Toolkit不是所有ONNX算子都支持,遇到不支持的会直接报错。解决办法有两个:一是用onnxsim简化,看能不能把不支持的算子合并掉;二是改模型结构,把不支持的算子换成等价的受支持算子。比如某些版本的EfficientNet里有HardSwish,老版本Toolkit不支持,换成ReLU6就能过,但精度会掉一点。
报错二:Quantization failed
量化失败通常是校准集的问题。检查三点:图片路径对不对、图片能不能正常读取、预处理参数和config里是否一致。我有一次校准集里混了几张灰度图,量化直接崩了,换成RGB就好了。
报错三:Model version mismatch
上位机Toolkit版本和板端Runtime版本不一致。这个没别的办法,查SDK文档,把版本对齐。
5.2 精度掉点严重的排查思路
量化后精度掉超过5个点,就得排查了。我的排查顺序是:
- 先看浮点ONNX的精度:如果ONNX本身精度就不对,那是导出环节的问题,和量化无关。
- 再看校准集:换一批校准图,看精度是否回升。如果回升明显,说明原校准集分布有问题。
- 逐层量化分析:RKNN Toolkit支持逐层精度分析,能定位到是哪一层量化后误差大。通常是某些激活值分布很宽的层,可以考虑对这些层保留浮点。
- 混合量化:把敏感层设为浮点,其他层INT8,精度能拉回来不少,代价是模型体积和延迟增加。
5.3 板端推理的典型问题
问题:推理结果全是同一类
大概率是输入数据格式错了。检查fmt是NHWC还是NCHW,检查图片通道顺序是RGB还是BGR。RV1126B的NPU输入默认NHWC,如果你传了NCHW的数据,结果就是乱的。
问题:第一次推理特别慢
正常现象。第一次推理包含模型加载和内存分配,后面就快了。测延迟要跳过第一次,从第二次开始算。
问题:多线程推理冲突
RKNN Runtime不是线程安全的,多个线程同时用一个context会出问题。要么加锁,要么每个线程独立init一个context。我试过每线程独立context,内存占用会翻倍,但稳定性没问题。
5.4 独家避坑清单
| 坑点 | 表现 | 解决办法 |
|---|---|---|
| 版本不匹配 | 模型加载失败 | 严格对齐Toolkit和Runtime版本 |
| 预处理不一致 | 精度暴跌 | 训练、量化、推理三处预处理必须一致 |
| 校准集偏差 | 量化掉点大 | 校准集覆盖全类别,分布均匀 |
| 输入格式错误 | 结果乱 | 确认NHWC和RGB顺序 |
| 算子不支持 | 转换报错 | 简化模型或替换算子 |
| 内存不足 | 推理崩溃 | 减小模型或优化内存复用 |
6. 几个实际项目中的选型建议
跑完这轮实验,我对RV1126B上做图像分类有了比较清晰的判断。如果你做的是一般性的分类任务,比如工业质检里的良品次品判断、门禁里的人脸属性分类,EfficientNet-Lite0是首选,精度和延迟平衡得最好。如果你对延迟极其敏感,比如要做高帧率的实时分类,ShuffleNetV2或者MobileNetV2更合适,10ms左右的延迟能撑到接近100FPS的处理能力。
ResNet18我不太推荐在RV1126B上跑分类,28.7ms的延迟意味着单帧处理只有30多FPS,而且模型体积大,内存占用高。除非你的任务确实需要ResNet的特征表达能力,否则没必要。
还有一个经验:别迷信大模型。在端侧,模型大小和精度不是线性关系。EfficientNet-Lite0比ResNet18小一半多,精度反而更高,延迟还更低。选型的时候多看实际数据,少看论文里的参数量对比。
最后说一个后续可以扩展的方向。这次只做了分类,但RV1126B的NPU也能跑检测和分割。如果你想在分类基础上做多任务,可以考虑共享backbone,分类头和检测头分开,这样能省一份特征提取的计算。我试过MobileNetV2做backbone同时接分类和简单检测,延迟增加不多,但功能翻倍,实际项目里挺实用。