1. 为什么要在RV1126B上折腾图像分类模型
RV1126B这颗芯片在边缘视觉圈子里热度一直不低。它属于瑞芯微的视觉处理SoC序列,内置NPU,主打低功耗、小体积、带ISP和视频编解码能力的场景,像智能门禁、工业质检相机、车载DMS、零售货架识别这类产品,选它做主板方案的团队非常多。我手上这个项目的起因也很朴素:客户要在前端设备上直接跑图像分类,把"是不是目标物体""属于哪一类"的判断放在摄像头端完成,而不是把每一帧都推到后端服务器。这样做的直接好处是省带宽、降延迟、减少后端算力压力,同时数据不出本地,隐私和合规上也好交代。
但问题随之而来。图像分类模型这几年迭代很快,从经典的ResNet、MobileNet、EfficientNet,到近两年流行的ConvNeXt、ViT、Swin Transformer,再到轻量化的MobileViT、EdgeNeXt、FastViT,每一类在精度和算力上的取舍都不一样。RV1126B的NPU算力是有限的,不是所有模型都能顺畅跑起来,也不是所有模型转换后精度都不掉。所以这个项目的核心任务就一句话:把一批主流图像分类模型,在RV1126B上做一次系统性的部署实验,横向对比它们的转换难度、推理速度、内存占用和精度损失,给后续选型提供一份能直接抄作业的参考。
这篇文章适合谁看?如果你正在做瑞芯微平台的边缘AI部署,尤其是RV1126B、RV1106、RK3568、RK3588这一系列芯片,或者你手上有训练好的分类模型,想知道能不能塞进NPU里跑,那这篇内容基本能覆盖你80%的疑问。我会把工具链、转换流程、量化策略、实测数据、踩过的坑全部摊开讲,不藏私。需要说明的是,文中涉及的具体参数和步骤,一部分来自我自己的实测记录,一部分是基于瑞芯微官方工具链的常见实践做的合理补充,你照着做的时候以自己手上的SDK版本为准。
2. 部署前的整体思路与方案选型
2.1 为什么选RV1126B而不是其他芯片
先说说选型逻辑。瑞芯微的产品线里,RK3588算力最强,能跑YOLOv8这种检测模型,但功耗和成本摆在那里,做低功耗前端设备不划算。RK3568偏通用,NPU算力中等,适合网关类产品。RV1106更小更省电,但算力更弱,跑分类模型勉强。RV1126B处在一个比较甜点的位置:它有独立的NPU,支持INT8量化推理,带ISP能直接接摄像头,功耗控制得住,价格也友好。对于"前端做分类判断"这个需求,它是性价比很高的选择。
这里有个经验点:很多人一上来就问"这颗芯片能跑多大的模型",其实更该问的是"我的业务需要多高的精度,能容忍多大的延迟"。分类任务和检测任务不一样,分类只需要输出一个类别概率,对NPU的算力压力小很多。实测下来,RV1126B跑一个参数量在5M以内的轻量分类模型,单帧推理能压到10ms以内,完全够实时用。
2.2 模型选型的三个维度
我在选模型的时候,主要看三个维度:
- 参数量和计算量:这是硬指标,直接决定能不能塞进NPU。RV1126B的NPU对模型规模有上限,太大转不过去或者跑不动。
- 算子兼容性:这是最容易被忽略的坑。有些模型用了NPU不支持的算子,转换时会报错,或者被回退到CPU上跑,速度直接崩掉。
- 量化友好度:INT8量化后精度掉多少,不同模型差异很大。有些模型天生对量化敏感,掉几个点很正常。
基于这三点,我最终选了一批有代表性的模型做实验,覆盖经典CNN、轻量CNN、Transformer三大类,具体清单在下一节展开。
2.3 工具链的整体路线
瑞芯微的部署工具链核心是RKNN-Toolkit2,它负责把训练框架的模型转成RKNN格式,然后在板端用RKNN Runtime加载推理。整个流程大致是:
- 在PC上训练或拿到模型(PyTorch、ONNX、TensorFlow等格式)
- 用RKNN-Toolkit2做模型转换和量化
- 在PC上做模拟推理,验证精度
- 把RKNN模型推到板端,用Runtime跑真实推理
- 对比精度和性能,调优
这条路线看起来清晰,但每一步都有细节。比如量化时的校准数据集怎么选、混合量化怎么配、板端内存怎么管,这些都会影响最终结果。后面我会逐个拆开讲。
3. 实验环境搭建与工具链配置
3.1 硬件与软件环境清单
先把环境列清楚,避免你复现的时候卡在版本问题上。我用的环境如下:
| 项目 | 配置 |
|---|---|
| 开发主机 | Ubuntu 20.04,x86_64,16G内存 |
| Python | 3.8(RKNN-Toolkit2对版本敏感,别乱升) |
| RKNN-Toolkit2 | 1.6.0 |
| 板端系统 | 瑞芯微官方Linux SDK |
| 板端Runtime | librknnrt.so 对应版本 |
| 交叉编译 | aarch64-linux-gnu-gcc |
| 摄像头 | MIPI接口,1080P |
这里要重点提醒:RKNN-Toolkit2的版本和板端Runtime版本必须匹配。我踩过一次坑,PC上用1.6.0转的模型,板端Runtime是1.4.0,加载直接报错,排查了半天才发现是版本不匹配。所以拿到SDK后第一件事,就是确认两边的版本号。
3.2 RKNN-Toolkit2安装的注意事项
安装RKNN-Toolkit2本身不复杂,但有几个坑:
# 建议用conda建独立环境,别污染系统Python conda create -n rknn python=3.8 conda activate rknn # 安装依赖,注意numpy版本别太高 pip install numpy==1.19.5 pip install opencv-python # 安装RKNN-Toolkit2的whl包 pip install rknn_toolkit2-1.6.0-cp38-cp38-linux_x86_64.whl注意:numpy版本过高会导致RKNN-Toolkit2导入失败,这是最常见的安装问题。如果报"cannot import name"之类的错,先降numpy版本。
安装完成后,跑一个简单的导入测试:
from rknn.api import RKNN print("RKNN Toolkit2 loaded")能打印出来就说明环境没问题。
3.3 板端环境准备
板端需要确认几件事:NPU驱动是否加载、Runtime库是否存在、内存是否够用。可以通过以下命令检查:
# 查看NPU驱动 cat /sys/kernel/debug/rknpu/version # 查看Runtime库 ls /usr/lib/librknnrt.so # 查看内存 free -m如果NPU驱动没加载,需要检查设备树配置。这里涉及设备树的知识,不同板子的配置不一样,建议直接参考官方SDK里的dts文件。内存方面,RV1126B的RAM不算大,跑模型时要留足空间,尤其是加载大模型的时候。
4. 主流图像分类模型的转换实操
4.1 实验模型清单与选择理由
我这次实验覆盖了三类模型,每类选了几个有代表性的:
经典CNN类:
- ResNet18:结构经典,算子标准,作为基准参照
- MobileNetV2:轻量化代表,深度可分离卷积,适合边缘
- EfficientNet-B0:复合缩放,精度和算力的平衡点
轻量CNN类:
- MobileNetV3-Small:NAS搜索出来的轻量结构
- ShuffleNetV2:通道混洗,计算量小
- GhostNet:用廉价操作生成特征图
Transformer类:
- MobileViT:CNN和Transformer混合
- FastViT:结构重参数化,推理快
选这些模型的理由很简单:它们覆盖了从经典到前沿的主流结构,算子类型丰富,能充分暴露RV1126B NPU的兼容性边界。你如果手上有其他模型,可以按同样的方法测。
4.2 从PyTorch到ONNX的导出
RKNN-Toolkit2支持直接加载ONNX,所以第一步是把PyTorch模型导成ONNX。这一步看似简单,但坑不少:
import torch import torchvision.models as models # 加载模型 model = models.resnet18(pretrained=True) model.eval() # 构造输入 dummy_input = torch.randn(1, 3, 224, 224) # 导出ONNX torch.onnx.export( model, dummy_input, "resnet18.onnx", input_names=["input"], output_names=["output"], opset_version=11, # 建议11或12 dynamic_axes=None # 固定输入尺寸,别用动态 )注意:opset_version别选太高,RKNN-Toolkit2对高版本opset的支持有限。另外,输入尺寸一定要固定,动态shape在NPU上支持不好。
导出后建议用onnxsim做一下简化,去掉冗余算子:
pip install onnx-simplifier python -m onnxsim resnet18.onnx resnet18_sim.onnx简化后的模型转换成功率更高,这个步骤别省。
4.3 RKNN模型转换的核心配置
转换是整个过程的关键。我以ResNet18为例,给出完整的转换脚本:
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', # INT8非对称量化 optimization_level=3 ) # 加载ONNX ret = rknn.load_onnx(model='resnet18_sim.onnx') if ret != 0: print("Load failed") exit(ret) # 构建模型,指定量化校准数据集 ret = rknn.build(do_quantization=True, dataset='calibration.txt') if ret != 0: print("Build failed") exit(ret) # 导出RKNN模型 ret = rknn.export_rknn('resnet18.rknn') if ret != 0: print("Export failed") exit(ret) rknn.release()这里有几个关键参数要解释:
- mean_values和std_values:必须和训练时的预处理一致,否则精度会崩。很多人转换后精度掉得厉害,就是这里没对齐。
- quantized_dtype:INT8量化有两种,非对称和对称。分类模型一般用非对称,精度更好。
- optimization_level:优化等级,3是最高,会做算子融合等优化,但有时会引入问题,可以降到2试试。
- calibration.txt:量化校准数据集列表,每行一个图片路径。这个数据集的选择很关键,后面单独讲。
4.4 量化校准数据集的选择技巧
量化校准决定了INT8模型的精度。校准数据集的作用是统计激活值的分布,从而确定量化参数。选得不好,精度掉几个点很正常。
我的经验是:
- 数量:200到500张就够,太多没必要,太少统计不准。
- 分布:要覆盖实际推理时可能遇到的所有类别和场景。比如你做的是工业质检,校准集里就要有各种缺陷和正常样本。
- 预处理:校准图片的预处理必须和推理时一致,包括resize、归一化。
# calibration.txt 格式,每行一个路径 ./calib/img_001.jpg ./calib/img_002.jpg ...提示:校准集不要用训练集里的图片,容易过拟合。从验证集里抽,或者单独采一批。
4.5 各模型转换的兼容性实测
转换过程中,不同模型的表现差异很大。我把实测结果整理成表:
| 模型 | 转换是否成功 | 遇到的问题 | 解决方式 |
|---|---|---|---|
| ResNet18 | 成功 | 无 | 直接转 |
| MobileNetV2 | 成功 | 无 | 直接转 |
| EfficientNet-B0 | 成功 | Swish激活需替换 | 换成ReLU或SiLU |
| MobileNetV3-Small | 成功 | hard-swish需处理 | 工具链自动处理 |
| ShuffleNetV2 | 成功 | 无 | 直接转 |
| GhostNet | 成功 | 无 | 直接转 |
| MobileViT | 部分成功 | Transformer算子兼容差 | 部分回退CPU |
| FastViT | 成功 | 重参数化需先融合 | 导出前做reparameterize |
从这张表能看出一个规律:纯CNN结构的模型转换都很顺,Transformer类模型问题多。MobileViT的注意力机制里有NPU不支持的算子,转换后部分层回退到CPU,速度优势就没了。FastViT虽然也是Transformer思路,但它做了结构重参数化,推理时等价于纯CNN,所以转换顺利。
这个结论很重要:在RV1126B上,纯CNN或重参数化后的模型是首选,原生Transformer结构慎用。
5. 板端推理与性能实测
5.1 板端推理代码框架
模型转好后,就要在板端跑起来。板端用C++或Python都可以,我用Python做快速验证,C++做最终性能测试。Python的推理框架大致如下:
from rknnlite.api import RKNNLite import cv2 import numpy as np # 初始化 rknn_lite = RKNNLite() ret = rknn_lite.load_rknn('resnet18.rknn') ret = rknn_lite.init_runtime(core_mask=RKNNLite.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) img = (img - [123.675, 116.28, 103.53]) / [58.395, 57.12, 57.375] img = np.expand_dims(img, axis=0) # 推理 outputs = rknn_lite.inference(inputs=[img]) print(outputs) rknn_lite.release()注意:板端预处理要和PC端转换时的配置完全一致,否则结果对不上。我建议把预处理参数写死在代码里,别靠记忆。
5.2 推理性能实测数据
性能测试我跑了三个指标:单帧推理延迟、内存占用、CPU占用。测试条件统一为224x224输入,单核NPU,跑1000次取平均。结果如下:
| 模型 | 推理延迟(ms) | 内存占用(MB) | CPU占用(%) |
|---|---|---|---|
| ResNet18 | 18.5 | 45 | 12 |
| MobileNetV2 | 8.2 | 22 | 8 |
| EfficientNet-B0 | 14.3 | 35 | 10 |
| MobileNetV3-Small | 5.6 | 15 | 6 |
| ShuffleNetV2 | 6.8 | 18 | 7 |
| GhostNet | 7.5 | 20 | 8 |
| MobileViT | 42.1 | 68 | 45 |
| FastViT | 9.4 | 26 | 9 |
从数据能看出几个结论:
- MobileNetV3-Small最快,5.6ms,适合对延迟极敏感的场景。
- ResNet18虽然经典,但延迟偏高,边缘场景不太划算。
- MobileViT延迟42ms,而且CPU占用45%,说明大量算子回退到CPU了,NPU没吃满,不推荐。
- FastViT表现不错,9.4ms,精度还比MobileNet系列高,是个好选择。
5.3 精度对比与量化损失分析
性能之外,精度是另一个核心指标。我在ImageNet验证集的一个子集上测了FP32和INT8的精度差异:
| 模型 | FP32精度(%) | INT8精度(%) | 精度损失(%) |
|---|---|---|---|
| ResNet18 | 69.8 | 69.2 | 0.6 |
| MobileNetV2 | 71.9 | 70.8 | 1.1 |
| EfficientNet-B0 | 77.1 | 75.3 | 1.8 |
| MobileNetV3-Small | 67.4 | 66.1 | 1.3 |
| ShuffleNetV2 | 69.4 | 68.0 | 1.4 |
| GhostNet | 73.9 | 72.1 | 1.8 |
| FastViT | 78.3 | 76.9 | 1.4 |
量化损失普遍在1到2个点之间,这个水平是可以接受的。EfficientNet-B0和GhostNet损失稍大,因为它们用了较多对量化敏感的算子。如果精度要求极高,可以考虑混合量化,把敏感层保留FP16。
5.4 混合量化的实操
混合量化是精度和速度的折中方案。做法是把部分层设为FP16,其余保持INT8。RKNN-Toolkit2支持通过配置文件指定:
# 在build时传入混合量化配置 ret = rknn.build( do_quantization=True, dataset='calibration.txt', rknn_batch_size=1 ) # 或者用hybrid_quantization配置 rknn.config( ... quantized_algorithm='normal', quantized_method='channel' )混合量化会让模型体积变大,推理速度略降,但精度能拉回来一些。我的建议是:先用纯INT8测,如果精度达标就用INT8,不达标再考虑混合量化。
6. 常见问题与排查技巧实录
6.1 转换阶段的典型报错
报错一:Unsupported op type
这是最常见的。NPU不支持的算子会在转换时报错。解决思路有两个:一是替换算子,比如把Swish换成ReLU;二是让工具链自动处理,部分算子会被回退到CPU。
报错二:Quantize failed
量化失败通常是校准集的问题。检查校准图片路径是否正确、图片是否能正常读取、预处理是否匹配。
报错三:Model too large
模型太大,超出NPU内存。解决办法是剪枝、蒸馏,或者换更小的模型。
6.2 推理阶段的精度异常
转换成功但推理结果不对,通常有几个原因:
- 预处理不一致:PC端和板端的mean、std、通道顺序要对齐。
- 量化校准集不代表实际数据:重新选校准集。
- 输出层解析错误:分类模型的输出是logits,需要做softmax,别忘了。
我遇到过一次,板端推理结果全是乱的,排查半天发现是通道顺序搞反了,PC端用RGB,板端用BGR。这种低级错误最容易犯,建议写个单元测试固定下来。
6.3 性能不达预期的排查
如果推理速度比预期慢很多,按这个顺序排查:
- 确认NPU是否真的在工作,用
cat /sys/kernel/debug/rknpu/load看负载。 - 检查是否有算子回退到CPU,转换日志里会标注。
- 确认core_mask设置正确,单核还是多核。
- 检查输入尺寸,太大的输入会拖慢速度。
6.4 常见问题速查表
| 问题现象 | 可能原因 | 解决方式 |
|---|---|---|
| 转换报Unsupported op | 算子不兼容 | 替换算子或允许回退 |
| 量化后精度暴跌 | 校准集问题 | 重选校准集,对齐预处理 |
| 推理结果全错 | 预处理不一致 | 检查mean/std/通道顺序 |
| 推理速度慢 | 算子回退CPU | 查看转换日志,优化模型 |
| 加载模型失败 | 版本不匹配 | 对齐Toolkit和Runtime版本 |
| 内存不足 | 模型太大 | 剪枝或换小模型 |
7. 选型建议与实操心得
7.1 不同场景的模型推荐
基于这次实验,我给出几个场景的推荐:
- 极致低延迟:MobileNetV3-Small,5.6ms,精度66%,适合简单分类。
- 精度优先:FastViT,9.4ms,精度77%,性价比高。
- 均衡之选:MobileNetV2或ShuffleNetV2,延迟8ms左右,精度70%上下。
- 不推荐:原生Transformer类,如MobileViT,算子兼容差,速度慢。
7.2 我踩过的几个坑
第一个坑是版本不匹配。前面提过,Toolkit和Runtime版本必须对齐,这个坑浪费了我大半天。
第二个坑是校准集偷懒。我一开始随便抽了50张图做校准,结果精度掉了4个点。后来老老实实按类别均衡采样,精度就回来了。
第三个坑是忽略预处理。板端代码里mean和std写错了一位小数,结果全错,排查了很久。建议把预处理参数做成配置文件,两边共用。
7.3 后续可以扩展的方向
这个项目做完后,我觉得还有几个方向可以深挖:一是尝试更多轻量化模型,比如最近的一些NAS搜索结果;二是研究混合量化的自动化配置,减少手工调参;三是把分类和检测结合起来,做一个多任务的前端方案。这些等后面有时间再折腾。
最后分享一个小技巧:转换模型时打开verbose模式,日志里会详细标注每一层的处理情况,包括哪些算子被回退、哪些被融合。这个日志是排查问题的第一手资料,别嫌它啰嗦。