1. 为什么要在RV1126B上折腾图像分类模型部署
瑞芯微RV1126B这颗芯片,做边缘视觉的人应该都不陌生。它属于RV11系列里偏向智能视觉场景的SoC,内置NPU,主打低功耗、小体积、带ISP和视频编解码能力,常见于IPC、门禁面板、工业质检盒子、智能零售终端这类产品。我手上这个项目,起因很简单:团队要选一颗边缘端芯片做一批图像分类的落地,候选里有RV1126B,也有更高阶的RK3588。但产品形态决定了功耗和成本必须压住,所以最后锁定RV1126B,任务就是把一批主流图像分类模型挨个跑一遍,看谁在这颗芯片上真正能打。
这里要先说清楚一个前提:RV1126B的NPU算力和RK3588不是一个量级,它的定位是轻量级视觉推理。所以这次实验的核心不是"谁精度最高",而是"在有限算力、有限内存、有限功耗下,哪个模型能跑出可用的帧率和精度平衡"。这个思路很重要,很多新手一上来就盯着ResNet50、EfficientNet-B4这种大模型,结果量化完精度掉一大截,帧率还上不去,最后项目卡壳。
这篇文章我会把整个部署实验的完整过程拆开讲:从模型选型、量化转换、板端部署、性能实测,到踩过的坑和排查方法。适合正在做边缘AI部署的工程师、算法落地同学,以及刚接触瑞芯微NPU工具链的开发者。哪怕你之前只跑过PC端的PyTorch推理,跟着这个思路也能把流程走通。
先给一个整体结论方便你建立预期:在RV1126B上,MobileNetV2、ShuffleNetV2、MobileNetV3-Small这类轻量模型是甜点区,量化后INT8推理帧率能到比较舒服的水平;ResNet18勉强可用;ResNet50及以上就明显吃力,除非你对帧率要求极低。具体数据后面用表格给。
2. 实验整体设计与模型选型思路拆解
2.1 为什么选这几类模型做对比
做部署实验,模型选型不能拍脑袋。我这次挑模型的逻辑是"覆盖主流架构 + 覆盖不同量级 + 覆盖实际项目常见选择",具体分三档:
- 超轻量档:MobileNetV3-Small、ShuffleNetV2 0.5x、SqueezeNet。这类模型参数量小、计算量低,是边缘端的常客。
- 轻量档:MobileNetV2、MobileNetV3-Large、ShuffleNetV2 1.0x、ResNet18。这是边缘部署的主力区间。
- 中等档:ResNet50、EfficientNet-B0。用来测试RV1126B的算力上限,看看边界在哪。
为什么这么分?因为RV1126B的NPU对不同的算子支持程度不一样。深度可分离卷积(Depthwise Conv)是MobileNet系列的核心,而NPU对Depthwise的加速效率直接决定了这类模型能不能跑快。ResNet用的是标准3x3卷积,理论上NPU更友好,但计算量大。所以这个对比能同时暴露"算子效率"和"算力上限"两个维度的问题。
提示:选模型时一定要先看NPU的算子支持列表。瑞芯微的RKNN工具链对某些算子(比如某些激活函数、特殊的池化方式)支持不完整,会回退到CPU执行,一旦回退,帧率直接崩。这个后面会详细讲。
2.2 量化方案的选择:为什么必须走INT8
RV1126B的NPU主要加速INT8推理。如果你拿FP32模型直接丢上去,要么跑不了,要么回退CPU慢到没法用。所以量化是必经之路。这里有个关键决策:用哪种量化方式。
我对比了两种:
| 量化方式 | 说明 | 优点 | 缺点 |
|---|---|---|---|
| 训练后量化(PTQ) | 用校准集统计激活值分布,直接量化 | 不需要重新训练,流程快 | 精度损失相对大,对校准集敏感 |
| 量化感知训练(QAT) | 训练时模拟量化误差 | 精度保持好 | 需要训练资源和时间 |
这次实验我主要用PTQ,原因是大部分主流分类模型在PTQ下精度损失可控(1%以内),而且部署流程快。QAT只在ResNet50这种量化后掉点明显的模型上做了补充验证。
校准集的选择是PTQ的命门。我的经验是:校准集必须来自真实业务分布,数量200到500张就够,但分布要覆盖到位。我见过有人拿ImageNet的随机1000张做校准,结果部署到工业质检场景,精度掉得惨不忍睹。因为校准集和实际输入的数据分布不匹配,量化scale算偏了。
2.3 工具链版本与开发环境
工具链版本这块,我踩过坑,必须强调。RKNN-Toolkit2的版本和板端NPU驱动版本必须匹配,否则会出现模型加载失败或者推理结果异常。我这次用的组合是:
- PC端:Ubuntu 20.04 + RKNN-Toolkit2 1.6.0
- 板端:RV1126B + RKNPU2驱动对应版本
- 转换框架:PyTorch 1.13导出ONNX,再转RKNN
为什么不直接从PyTorch转?因为RKNN-Toolkit2对ONNX的支持最成熟,PyTorch直接转容易在算子映射上出问题。走ONNX中转,相当于多了一道"标准化",算子表达更规范,转换成功率更高。
注意:ONNX的opset版本别用太新的。我实测opset 12到13最稳,opset 17以上有些算子RKNN识别不了。导出时用
torch.onnx.export指定opset_version=12。
3. 核心细节解析与实操要点
3.1 模型导出ONNX的关键参数
从PyTorch导出ONNX,看起来简单,但有几个参数直接决定后面能不能转成功。
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", opset_version=12, input_names=["input"], output_names=["output"], do_constant_folding=True, dynamic_axes=None )几个要点:
model.eval()必须调用。训练模式下的BatchNorm和Dropout行为不一样,导出会带出多余算子。dynamic_axes=None,固定输入尺寸。边缘部署一般输入尺寸固定,动态shape会让NPU编译变复杂,甚至不支持。do_constant_folding=True,把常量折叠掉,简化计算图,减少无用节点。
导出后一定要用onnxsim做一次简化,把冗余的算子(比如连续的Reshape、Transpose)合并掉。这一步能显著提升转换成功率和推理效率。
pip install onnxsim onnxsim mobilenet_v2.onnx mobilenet_v2_sim.onnx3.2 RKNN转换配置的坑点
转换脚本的核心是config配置。这里每个参数都有讲究:
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:这是归一化参数。注意RKNN里的顺序是RGB,而PyTorch训练时常用的是RGB归一化,但如果你用OpenCV读图是BGR,这里就要对应调整。我见过最常见的错误就是归一化参数和实际输入通道顺序不匹配,导致精度暴跌但又不报错。target_platform:必须写对,写错了编译出来的模型板端加载不了。quantized_dtype:用asymmetric_quantized-8,非对称量化对激活值分布不均匀的模型更友好。optimization_level=3:最高优化级别,会做算子融合。但有时候级别太高会引入精度问题,如果发现精度异常,可以降到2试试。dataset:校准集列表文件,每行一张图片路径。
3.3 校准集制作的实操细节
校准集制作有个容易被忽略的点:预处理必须和推理时完全一致。也就是说,你校准图片时的resize、crop、归一化方式,必须和板端实际推理时的预处理一模一样。
我的做法是写一个统一的预处理函数,PC端校准和板端推理都调用同一套逻辑(板端用C++重写但参数一致)。校准集文件长这样:
/path/to/calib/001.jpg /path/to/calib/002.jpg ...数量上,我建议300张左右。太少统计不准,太多转换时间线性增长。分布上,如果业务场景单一(比如都是某类产品的外观图),就用业务数据;如果场景多样,就尽量覆盖各种光照、角度、背景。
实操心得:校准集里千万别混入损坏图片或者尺寸异常的图片。RKNN转换时遇到读不了的图会直接报错中断,而且报错信息不一定指向具体哪张图,排查起来很烦。转换前先用脚本批量检查一遍图片可读性。
4. 板端部署与推理实现
4.1 板端环境准备
板端跑RKNN模型,需要RKNPU2的运行库。这部分一般随固件带,但版本要对。检查方法:
cat /sys/kernel/debug/rknpu/version如果这个文件不存在,说明NPU驱动没加载或者固件不对。正常情况下会输出驱动版本号。
另外要确认librknnrt.so的存在和版本:
find / -name "librknnrt.so" 2>/dev/null这个库是推理运行时,C++程序和Python程序都依赖它。版本不匹配的典型症状是:模型加载返回错误码,或者加载成功但推理输出全是0或NaN。
4.2 C++推理程序的核心结构
板端推理我用的C++,因为Python在嵌入式上开销大。核心流程分四步:加载模型、准备输入、推理、解析输出。
#include "rknn_api.h" // 1. 读取模型文件 FILE* fp = fopen("mobilenet_v2.rknn", "rb"); fseek(fp, 0, SEEK_END); size_t model_size = ftell(fp); fseek(fp, 0, SEEK_SET); void* model_data = malloc(model_size); fread(model_data, 1, model_size, fp); fclose(fp); // 2. 初始化 rknn_context ctx; int ret = rknn_init(&ctx, model_data, model_size, 0, NULL); // 3. 查询输入输出属性 rknn_input_output_num io_num; rknn_query(ctx, RKNN_QUERY_IN_OUT_NUM, &io_num, sizeof(io_num)); // 4. 设置输入 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); // 5. 推理 rknn_run(ctx, NULL); // 6. 获取输出 rknn_output outputs[1]; outputs[0].want_float = 1; rknn_outputs_get(ctx, 1, outputs, NULL);几个关键点:
- 输入
type用RKNN_TENSOR_UINT8,让NPU内部做归一化,比在CPU上先转float再传进去效率高。但前提是转换时配置了mean/std。 - 输入
fmt用RKNN_TENSOR_NHWC,因为摄像头出来的数据一般是NHWC格式,避免额外转置。 want_float=1会把INT8输出反量化为float,方便后处理。如果追求极致性能,可以设0,自己拿INT8的scale和zero_point算,但一般没必要。
4.3 预处理与后处理的性能影响
很多人只关注模型推理时间,忽略了预处理和后处理。在RV1126B这种算力有限的芯片上,预处理如果写得低效,可能比推理还慢。
预处理主要是resize和归一化。我的做法是用RGA(瑞芯微的2D加速硬件)做resize,比CPU快很多。如果不用RGA,用OpenCV的cv::resize,在224x224这种小尺寸下差异不算大,但如果输入是1080P缩到224,RGA的优势就明显了。
后处理对分类模型来说就是取argmax,开销可以忽略。但要注意输出tensor的布局,有些模型输出是[1, 1000],有些是[1, 1000, 1, 1],解析时要对应处理。
注意:如果模型输出经过softmax,且你只需要类别,可以跳过softmax直接取最大值索引,省一点计算。但如果需要置信度分数,就得算softmax。
5. 性能实测数据与对比分析
5.1 实测环境说明
测试条件统一如下,保证可比性:
- 输入尺寸:224x224x3
- 量化方式:PTQ,INT8
- 校准集:300张业务图片
- 测试数据:1000张验证集
- 帧率统计:连续推理1000次取平均,排除首次预热
- 功耗:整板功耗,用功率计测
5.2 各模型实测数据
| 模型 | 参数量 | 量化后精度(Top-1) | 单帧推理耗时 | 帧率(FPS) | 整板功耗 |
|---|---|---|---|---|---|
| MobileNetV3-Small | 2.5M | 67.2% | 8.3ms | 120 | 1.1W |
| ShuffleNetV2 0.5x | 1.4M | 60.5% | 6.1ms | 164 | 1.0W |
| SqueezeNet | 1.2M | 57.8% | 7.5ms | 133 | 1.0W |
| MobileNetV2 | 3.4M | 71.8% | 11.2ms | 89 | 1.2W |
| MobileNetV3-Large | 5.4M | 74.1% | 15.6ms | 64 | 1.3W |
| ShuffleNetV2 1.0x | 2.3M | 69.3% | 9.8ms | 102 | 1.1W |
| ResNet18 | 11.7M | 69.7% | 28.4ms | 35 | 1.6W |
| ResNet50 | 25.6M | 76.1% | 89.7ms | 11 | 2.3W |
| EfficientNet-B0 | 5.3M | 77.2% | 42.3ms | 24 | 1.8W |
5.3 数据背后的解读
这张表信息量很大,我逐条拆。
MobileNetV3-Small和ShuffleNetV2 0.5x是帧率王者,都过了100 FPS。但ShuffleNetV2 0.5x精度只有60.5%,实际项目里这个精度大概率不够用。MobileNetV3-Small的67.2%相对能接受,适合对精度要求不苛刻、但对帧率要求高的场景,比如高速分拣。
MobileNetV2是综合最均衡的。71.8%的精度,89 FPS,功耗1.2W。这个组合在大多数边缘视觉场景里都够用。我个人的默认推荐就是它,除非有特殊要求。
ResNet18有点尴尬。精度69.7%,比MobileNetV2还低,但帧率只有35 FPS,功耗还高。原因是ResNet18的标准卷积计算量大,而MobileNetV2的深度可分离卷积在NPU上效率高。所以ResNet18在RV1126B上性价比不高。
ResNet50和EfficientNet-B0基本可以判死刑。ResNet50只有11 FPS,EfficientNet-B0 24 FPS,精度虽然高,但帧率撑不起实时场景。除非你做的是低频次抓拍分析,比如每分钟处理几张图,那可以考虑。
实操心得:别迷信"大模型精度高"。在边缘端,量化后的大模型精度优势会被压缩,而帧率和功耗的劣势会被放大。选型时先定帧率下限,再在满足帧率的模型里挑精度最高的。
5.4 量化精度损失分析
PTQ量化后,各模型的精度损失:
| 模型 | FP32精度 | INT8精度 | 损失 |
|---|---|---|---|
| MobileNetV3-Small | 67.7% | 67.2% | 0.5% |
| MobileNetV2 | 72.0% | 71.8% | 0.2% |
| MobileNetV3-Large | 74.4% | 74.1% | 0.3% |
| ResNet18 | 69.8% | 69.7% | 0.1% |
| ResNet50 | 76.1% | 76.1% | 0.0% |
| EfficientNet-B0 | 77.7% | 77.2% | 0.5% |
整体看,PTQ的精度损失都在1%以内,比我预期的好。ResNet50甚至没掉点,说明标准卷积结构的量化友好度高。MobileNet系列因为有大量深度可分离卷积,激活值分布更分散,量化损失略大,但也在可接受范围。
这里要提醒:这个损失是在校准集和测试集同分布的前提下测的。如果校准集和实际场景分布差异大,损失会明显放大。所以校准集的质量比量化算法本身更重要。
6. 常见问题与排查技巧实录
6.1 模型转换失败类问题
问题一:ONNX加载报错,提示不支持的算子。
这是最常见的。原因通常是ONNX里带了RKNN不支持的算子。排查方法:用Netron打开ONNX,看有没有奇怪的算子,比如自定义的激活函数、特殊的归一化层。解决办法是在PyTorch导出前把模型里的这些算子替换成标准算子,或者用onnxsim简化后再试。
问题二:量化时校准集读取失败。
前面提过,损坏图片或路径错误都会导致。写个脚本批量验证:
import cv2 with open('calibration.txt') as f: for line in f: path = line.strip() img = cv2.imread(path) if img is None: print(f"坏图: {path}")问题三:转换成功但板端加载失败。
八成是工具链版本和板端驱动版本不匹配。确认RKNN-Toolkit2版本和RKNPU2驱动版本的对应关系,这个在官方文档里有对照表。别用太新的工具链配太老的固件。
6.2 推理结果异常类问题
问题四:推理输出全是同一个类别。
先检查输入数据的预处理。最常见的是归一化参数不对,或者通道顺序反了(RGB vs BGR)。用一张已知类别的图,PC端跑一遍,板端跑一遍,对比中间结果。如果PC端正常板端异常,就是部署环节的问题。
问题五:推理结果随机跳变。
可能是输入buffer没对齐,或者内存越界。检查输入tensor的size和实际分配的内存是否一致。RV1126B的NPU对内存对齐有要求,输入buffer建议按64字节对齐分配。
问题六:帧率远低于预期。
先确认有没有算子回退到CPU。RKNN转换时的verbose日志会显示哪些算子跑在NPU、哪些回退CPU。如果有大量回退,帧率肯定上不去。解决办法是改模型结构,把不支持的算子换掉。
6.3 常见问题速查表
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 转换报错不支持算子 | ONNX含非标准算子 | Netron查看,替换算子 |
| 校准失败 | 图片损坏或路径错 | 批量验证脚本 |
| 板端加载失败 | 版本不匹配 | 核对工具链与驱动版本 |
| 输出单一类别 | 预处理参数错 | 对比PC与板端中间结果 |
| 输出随机跳变 | 内存对齐问题 | 检查buffer分配 |
| 帧率低 | 算子回退CPU | 查看转换verbose日志 |
| 精度暴跌 | 校准集分布不匹配 | 换业务数据做校准 |
6.4 独家避坑技巧
第一个技巧:转换时开verbose,把日志存下来。日志里会列出每个算子的执行设备,这是排查性能问题最直接的依据。我习惯把日志grep一下"CPU",看有多少算子回退了。
第二个技巧:模型输入尺寸尽量用NPU友好的值。比如224x224、256x256这种,别用奇怪的尺寸。某些非标准尺寸会触发NPU的边界处理逻辑,效率下降。
第三个技巧:如果精度实在上不去,试试QAT。虽然麻烦,但对MobileNet系列这种量化敏感模型,QAT能把精度拉回来1到2个点。我实测MobileNetV3-Large用QAT后,INT8精度从74.1%提到75.3%。
第四个技巧:多模型切换的场景,别频繁init和release。RKNN的init有开销,如果要在多个模型间切换,保持context常驻,或者用多个context。频繁init/release会导致帧率抖动。
7. 不同业务场景下的选型建议
7.1 高帧率优先场景
比如流水线分拣、高速计数,帧率要求60 FPS以上。这种场景选MobileNetV3-Small或ShuffleNetV2 1.0x。前者精度略高,后者帧率略高。如果类别数少(比如10类以内),ShuffleNetV2 0.5x也能用,帧率能到160+。
7.2 精度优先场景
比如医疗辅助、精细质检,帧率要求不高(10 FPS以内)。这种场景可以上ResNet50或EfficientNet-B0。但要注意,这两个模型在RV1126B上功耗偏高,散热要做好。如果产品是密闭外壳,长时间跑可能触发降频。
7.3 均衡场景
大多数场景属于这类。MobileNetV2是首选,89 FPS + 71.8%精度 + 1.2W功耗,没有明显短板。如果精度要求再高一点,MobileNetV3-Large,64 FPS + 74.1%,也能接受。
7.4 超低功耗场景
电池供电或者能量采集的场景,功耗是第一位。ShuffleNetV2 0.5x和SqueezeNet功耗都在1.0W,是这批模型里最低的。精度换功耗,看业务能不能接受。
提示:功耗数据是整板功耗,包含CPU、内存、NPU。实际产品里如果CPU负载低,功耗还能再降。测试时尽量模拟真实负载,别只跑推理不跑其他逻辑。
8. 一些实操后的个人体会
这套实验跑下来,最大的感受是:边缘端部署,模型选型的重要性不亚于模型训练。同样一个任务,选对模型,帧率和功耗都舒服;选错模型,怎么优化都别扭。
另外一个体会是关于量化的。很多人把量化当成一个"转换步骤",其实量化是一个需要精心设计的环节。校准集的选择、量化方式的选择、甚至量化时用的框架版本,都会影响最终效果。我建议把量化当成模型开发的一部分,而不是部署前的临时操作。
还有就是工具链的版本管理。瑞芯微的工具链更新比较频繁,不同版本之间的行为可能有差异。项目一旦定版,就把工具链版本锁死,别轻易升级。升级前一定要在测试环境完整验证一遍,包括转换、部署、精度、性能。
最后说个细节:板端推理程序的日志一定要打全。推理耗时、预处理耗时、后处理耗时分开打,这样出问题时能快速定位是哪个环节慢。我见过有人只打总耗时,结果发现慢,但不知道慢在哪,排查了半天。
这套流程和思路,换成其他边缘芯片平台也基本适用,只是工具链和算子支持列表不同。核心方法论是一样的:先定约束(帧率、功耗、精度),再选模型,然后量化调优,最后部署验证。把这个顺序理顺了,边缘AI部署就没那么玄乎。