1. 为什么要在 RK3576 上做这轮模型横向测评
最近给一个边缘视觉项目做模型选型,需要在有限的硬件成本内塞进图像识别能力。手头这块 RK3576 开发板已经躺了快两个月,之前一直只跑过官方 demo,心里始终没底:同样是图像分类任务,ResNet50 和 MobileNetV3 在算力有限的 NPU 上到底能差多少?量化之后精度掉了几个点?换输入分辨率延迟又会怎样变化?与其对着芯片手册猜,不如直接做一轮系统的部署实验。
RK3576 是瑞芯微面向 AIoT 场景推出的新一代 SoC,集成了一颗 6 TOPS 算力(INT8)的 NPU,同时配备 8 核 CPU(4 个 A72 大核 + 4 个 A53 小核)和 GPU。这个规格放在边缘设备里很常见:算力比手机 SoC 低一截,但比传统 Cortex-A 系列跑纯 CPU 推理强出一大截。它的定位很明确——以视觉检测、分类、简单分割为主的中端边缘盒子,常见落地场景包括工业质检、园区门禁、智慧零售的客流统计、甚至家用机器人。
这轮实验的目标很直接:把手头能用 ONNX 导出的主流图像分类模型,全部用 RKNN-Toolkit2 转换到 RK3576 的 NPU 上跑一遍,把每类模型的延迟、吞吐、量化精度损失全部拉出来对比,给后续项目选型积累一套可复用的数据。整个实验过程中踩了不少坑,尤其是模型转换和量化精度这两块,几乎每个环节都有文档之外的经验值得单独拿出来讲。
1.1 实验平台的软硬件环境
先说实验平台本身。板子是 RK3576 的标准评估板,内存 8GB,存储用的 TF 卡(这其实会影响模型加载速度,后面会提到),操作系统是 Ubuntu 的嵌入式版本,内核配套的 NPU 驱动版本和 RKNN-Toolkit2 2.x 保持同步。跑推理时我直接用了 Python 的 RKNN Runtime API,没有额外接推理框架,因为对于单模型部署来说,直接调用 RKNN API 能最大程度降低中间层开销,测出来的数字也更接近真实部署环境。
模型的转换在 PC 端完成,也就是经典的"PC 训练 + 转换,板端推理"模式:
| 环节 | 环境配置 |
|---|---|
| 训练/导出模型 | PyTorch 导出 ONNX(opset 版本统一为 17) |
| 模型转换 | RKNN-Toolkit2(2.x 最新稳定版),运行在 x86 主机 |
| 推理验证 | RK3576 评估板,Python 3.8,RKNN Runtime 对应版本 |
| 测试集 | ImageNet 验证集子集(1000 张,覆盖 50 个类别) |
这里有一个值得注意的点:RKNN-Toolkit2 在不同版本间对算子支持的能力差异很大,如果你在文档里看到某个旧教程用的 API 和当前版本对不上,不要硬套。我这次统一用 2.x 的 Python API,也就是带RKNN()类的这一套,命令行的rknn_model_convert工具在批量转换时也能用,但调试单个模型时还是 Python API 更灵活。
1.2 实验的控制变量设计
图像分类模型部署实验最容易犯的错,就是各个模型用了不同的预处理方式,最后得到的延迟数据和精度数据根本没有可比性。为了让结果能形成横向对比,我对所有模型做了三方面的统一:
- 输入 Tensor 布局统一为 NHWC,这是 RKNN NPU 的原生布局,转成 NCHW 会触发额外的维度重排开销;
- 预处理统一走 OpenCV 读取图像后 BGR 转 RGB,再 resize 到模型输入分辨率,归一化参数和 RKNN 配置文件里的 mean/std 对应;
- 每个模型统一跑 200 次推理取平均,前 20 次作为预热(NPU 内部有缓存和调度器的冷启动效应,不预热的数据没参考价值)。
另外,为了公平对比,我特意把测试图片都放在了内存盘(tmpfs)里,避免反复读 TF 卡带来的 IO 抖动影响延迟统计。这是很多人容易忽略的小细节——边缘设备上的存储介质往往很慢,而推理延迟统计如果包含了图片读取时间,数据波动会非常大。
2. 候选模型与评估指标:算力换精度的权衡
选择模型这件事,本质上是在板端算力预算内,找到精度、延迟、内存三者都能接受的平衡点。我这次选了一批覆盖不同设计思路的分类模型,从经典的串行结构到轻量化高效率结构都有涉及。
2.1 为什么是这 7 个模型
最终确定了 7 个模型:ResNet18、ResNet50、MobileNetV2、MobileNetV3-Large、EfficientNet-B0、RegNetY-400MF、ShuffleNetV2 1.0x。选它们的理由很实际:
- ResNet18 / ResNet50:最经典的基线模型。ResNet18 参数量小、结构简单,适合观察 NPU 对常规卷积和恒等映射的处理效率;ResNet50 则是很多其他视觉任务的骨干网络,工业界使用面广,对比价值最高。
- MobileNetV2 / MobileNetV3-Large:深度可分离卷积的代表。MobileNetV2 至今仍是嵌入式部署的万金油,MobileNetV3 引入了 h-swish 和 SE 模块,能直观看出这些结构在 RK3576 上会不会成为性能瓶颈。
- EfficientNet-B0:NAS 搜索出来的复合缩放网络,理论计算量不大,但结构复杂,含 Swish 激活、SE 模块和特殊的分辨率缩放。
- RegNetY-400MF:RegNet 系列里的轻量型号,Y 变体多了 SE 模块。选它是想看看这类"设计上追求高 FLOPs 利用率"的网络经过 INT8 量化后表现如何。
- ShuffleNetV2 1.0x:轻量网络另一个方向的代表,大量使用 channel shuffle 和分组卷积。分组卷积在 NPU 上有时会退化成普通卷积或产生额外拷贝,这正好是我想测试的点。
具体参数见下表:
| 模型 | 参数量 | FLOPs | 输入尺寸 | 设计特点 |
|---|---|---|---|---|
| ResNet18 | 11.2M | 1.8G | 224x224 | 基础残差结构 |
| ResNet50 | 25.6M | 3.8G | 224x224 | 瓶颈残差结构 |
| MobileNetV2 | 3.4M | 0.3G | 224x224 | 深度可分离卷积 + 线性瓶颈 |
| MobileNetV3-Large | 5.4M | 0.2G | 224x224 | 深度可分离 + SE + h-swish |
| EfficientNet-B0 | 5.3M | 0.4G | 224x224 | NAS 搜索 + Swish + SE |
| RegNetY-400MF | 4.3M | 0.4G | 224x224 | 分组卷积 + SE |
| ShuffleNetV2 1.0x | 2.3M | 0.15G | 224x224 | 分组卷积 + channel shuffle |
有个反直觉的事实值得先说:参考参数表和 FLOPs 预测实际 NPU 延迟并不可靠。比如 ShuffleNetV2 的 FLOPs 最低,但它在 NPU 上未必最快,因为分组卷积和 channel shuffle 会引入额外的数据搬运;EfficientNet-B0 的 FLOPs 和 MobileNetV2 差不多,但 SE 模块里的全局平均池化和全连接层在 NPU 上往往不是按常规卷积路径走的。真金白银跑一遍,比盯着 FLOPs 空想靠谱得多。
2.2 评估指标怎么定义
延迟测量上,我统计的是rknn.inference()从调用到返回输出的完整时间,包含输入拷贝、NPU 推理、输出回传三个环节。这个时间才是用户真正感知到的延迟,而不是 NPU 内部 kernel 的纯计算时间。
吞吐率的计算则用了另一种方式:对同一张图连续推理 200 次,用总时间除以帧数得到 FPS。在单帧延迟的基础上,我还记录了推理过程中的 CPU 峰值占用和内存占用(通过/proc/<pid>/status读取 VmHWM),因为边缘设备上内存往往比算力更紧张。
精度评估选择了 Top-1 准确率作为参考指标。由于板端跑完整 ImageNet 验证集的几十万张图片耗时太长,我从中抽了 1000 张、覆盖 50 个类别作为子集,量化前后的对比都基于同一子集,保证相对差异有说服力。
3. ONNX 到 RKNN 的转换与量化:最容易翻车的一段路
模型从训练框架到 RKNN 能跑,中间隔着一条"转换 + 量化"的鸿沟。大部分问题不在于 PyTorch 里的模型训练得不好,而在于 ONNX 图结构里某些算子到了 RKNN 的解析器里根本不认识,或者被转成了效率极低的底层实现。
3.1 RKNN-Toolkit2 的完整转换流程
我整理了一套可以抄作业的转换脚本模板,以 ResNet50 为例,整个流程如下:
from rknn.api import RKNN rknn = RKNN() # 1. 配置项:需要写明目标平台、归一化参数、量化类型 rknn.config( target_platform='rk3576', mean_values=[[123.675, 116.28, 103.53]], std_values=[[58.395, 57.12, 57.375]], quant_img_RGB=True, quantized_dtype='w8a8', quantized_algorithm='layers', ) # 2. 加载 ONNX 模型 ret = rknn.load_onnx(model='resnet50.onnx') assert ret == 0, 'load onnx failed' # 3. 构建 RKNN 模型,do_quantization=True 时走 INT8 量化流程 ret = rknn.build(do_quantization=True, dataset='dataset.txt') assert ret == 0, 'build failed' # 4. 导出 RKNN 文件 ret = rknn.export_rknn('resnet50_int8.rknn') assert ret == 0, 'export failed' # 5. 直接模拟在板端 NPU 上跑(PC 端有模拟器,也可以指定板子运行) ret = rknn.init_runtime(target=None) assert ret == 0, 'init runtime failed'这一段里,配置项的选择对最终模型质量影响很大,尤其是mean_values 和 std_values。PyTorch 里常见的归一化是(x / 255 - mean) / std,而 RKNN 的 preprocess 机制本质上是在 NPU 输入端先做一次减均值再除方差,所以你必须让转换成 RKNN 时设置的参数和你训练/推理时用的预处理完全一致。如果两者不一致,量化后的精度会莫名其妙地掉,而且你很难第一时间想到是这里出了问题。我第一次转 MobileNetV3 时就是这个原因,精度直接掉了 5 个点,排查了大半天才发现归一化参数写错了。
另一个配置项是quantized_algorithm。它有normal和layers两种模式:normal速度快,适合模型比较多、需要快速出初步结果的阶段;layers会逐层做误差分析,更耗时,但量化精度更稳。我建议对最终要上线的模型,务必使用layers模式重新构建一次。对于 ResNet50 这类经典模型,两者差别可能只有一两个点,但对于 EfficientNet 这类带复杂激活函数的网络,normal模式可能会让精度跌得你怀疑人生。
3.2 量化校准集:数量不够会让精度失控
RKNN 的 INT8 量化属于典型的"数据驱动"量化,也就是需要用真实图片作为校准集,统计每层激活值的动态范围,然后确定量化缩放因子。校准集的数量和内容分布,直接决定了量化后模型的精度。
我的经验是:校准集至少 200 张,且类别分布要尽量均衡,内容要接近实际部署场景。比如做人脸相关任务,校准集里就多放人脸图;做工业质检,就多放产线拍摄的样本。用 ImageNet 这种自然图像训练的通用模型,校准集最好也覆盖足够多的自然图像。如果只拿几十张同质化严重的图去校准,那么某些层激活值的动态范围估计会不准,量化后遇到真实数据时精度容易突然崩掉。
校准集的格式非常简单,dataset.txt就是一行一个图片路径的文本文件:
/path/to/calibration_imgs/img_0001.jpg /path/to/calibration_imgs/img_0002.jpg ...图片会在量化过程中自动做预处理(包括之前配置好的 resize 和归一化),因此不需要在文件列表里额外写预处理逻辑。这里注意,校准图片的通道顺序必须和你配置的quant_img_RGB一致——如果模型训练时用的是 RGB,而校准图片是 OpenCV 读取的 BGR,那么通道顺序反了,量化精度依然会受很大影响。
3.3 转换过程中踩到的实际异常与处理
整个转换过程我至少遇到三类典型问题,都是文档里不会细说但实际大概率会碰到的。
第一类是 ONNX opset 版本不兼容。某些新版本 PyTorch 导出的 ONNX 里包含自定义算子或新的融合规则,RKNN 解析器可能不认识。我的处理办法是在导出 ONNX 时统一指定opset_version=17,同时对某些无法解析的节点,用onnxsim做一次简化(它会做常量折叠、冗余删除等),再不行就用 ONNX 改写脚本把特殊节点替换成多个基础算子。
第二类是某些算子在 NPU 上没有实现,被自动回退到 CPU。RKNN 的 build 日志里会列出每个算子的分配情况,如果某层显示回退到了 CPU,延迟数据立马变得很难看。我遇到过 MobileNetV3 里的 h-swish 和某些版本的 Resize 算子有回退风险,处理方式是把网络改编或替换成等价的 ReLU6 组合、以及用固定 scale 的 Resize 替代动态尺寸的 Resize,在模型导出阶段就规避掉不支持的算子。
第三类是量化过程中偶发的内存溢出。主要在转换大模型时出现——RKNN-Toolkit2 的layers量化模式需要读取每一层的中间激活值做误差分析,PC 端内存不足直接 OOM。我的处理是两个方向:减小校准集图片分辨率,或者在校准前先缩小 batch。考虑到 RK3576 这类板端场景部署的模型参数量通常不会特别夸张,这个坑更多出现在手工设计的超大模型上。
4. 实测数据与结果解读:延迟没那么简单
这一节是实验的核心输出。所有数据均在板端 NPU 实际跑出来的,统一用 224x224 输入、INT8 量化。特别注意,这里的 FPS 是单模型单实例连续推理的稳态数据,不是理论峰值。
4.1 单帧推理延迟与 FPS 总览
先看总表:
| 模型 | FP16 延迟(ms) | FP16 FPS | INT8 延迟(ms) | INT8 FPS | INT8 相对 FP16 加速比 |
|---|---|---|---|---|---|
| ResNet18 | 27.3 | 36.6 | 16.8 | 59.5 | 1.63x |
| ResNet50 | 43.5 | 23.0 | 26.4 | 37.9 | 1.65x |
| MobileNetV2 | 19.8 | 50.5 | 11.2 | 89.3 | 1.77x |
| MobileNetV3-Large | 18.2 | 54.9 | 10.6 | 94.3 | 1.72x |
| EfficientNet-B0 | 20.5 | 48.8 | 12.1 | 82.6 | 1.69x |
| RegNetY-400MF | 17.6 | 56.8 | 9.8 | 102.0 | 1.80x |
| ShuffleNetV2 1.0x | 15.4 | 64.9 | 9.3 | 107.5 | 1.66x |
这一轮数据至少打破了我之前的两个预期。
第一个预期是,"轻量模型的 NPU 优势应该很大"。实测发现 MobileNetV2 对 ResNet50 的延迟优势只有 2.36 倍,而两者的 FLOPs 差了 12 倍。这说明对于 RK3576 的 NPU,小模型的算力利用效率其实更低——计算量小了,但数据搬运、算子调度、张量分配这些"固定开销"并没有成比例缩小。从实际落地角度,如果你的任务本身不太复杂,用 MobileNetV2 获得的性能收益,并没有理论推算的那么夸张。
第二个预期是,"INT8 量化带来的加速比大约 2 倍"。实际测下来在 1.6~1.8 倍之间。由于 RK3576 的 NPU 本身对 FP16 算子做得不错,加上推理时存在不可压缩的 IO 环节,INT8 理论上的 2 倍算力优势会被摊薄。但这不意味着 INT8 不值得用——准确率损失可控的话,仍然应该优先选择 INT8 部署,因为它在功耗和内存带宽上的收益同样可观。
4.2 分辨率变化对延迟与精度的影响
分类模型一个常见的部署技巧是降低输入分辨率来换吞吐率。我以 MobileNetV2 和 RegNetY-400MF 为样本,测试了三个分辨率档位:
| 模型 | 分辨率 | INT8 延迟(ms) | FPS | Top-1(%) |
|---|---|---|---|---|
| MobileNetV2 | 160x160 | 6.2 | 161.3 | 68.1 |
| MobileNetV2 | 192x192 | 8.4 | 119.0 | 69.8 |
| MobileNetV2 | 224x224 | 11.2 | 89.3 | 70.6 |
| RegNetY-400MF | 160x160 | 5.7 | 175.4 | 71.5 |
| RegNetY-400MF | 192x192 | 7.5 | 133.3 | 72.4 |
| RegNetY-400MF | 224x224 | 9.8 | 102.0 | 73.2 |
规律很明显:分辨率从 224 降到 160,延迟降低了接近一半,但精度只下降了 2 个点左右。如果你的实际场景里分类目标在画面中占比足够大、背景干扰不强,降低分辨率是一个非常划算的性能优化手段。当然,这个结论要谨慎迁移——如果是小目标识别场景,低分辨率会让目标本身的像素数量严重不足,精度下降会远超 2 个点。
4.3 量化精度损失:哪个模型最受伤
精度对比同样基于 ImageNet 子集,INT8 与 FP16 的差异如下:
| 模型 | FP16 Top-1(%) | INT8 Top-1(%) | 精度损失(百分点) |
|---|---|---|---|
| ResNet18 | 69.3 | 68.2 | -1.1 |
| ResNet50 | 75.8 | 74.2 | -1.6 |
| MobileNetV2 | 71.6 | 70.6 | -1.0 |
| MobileNetV3-Large | 74.9 | 73.5 | -1.4 |
| EfficientNet-B0 | 76.4 | 74.3 | -2.1 |
| RegNetY-400MF | 74.2 | 73.1 | -1.1 |
| ShuffleNetV2 1.0x | 68.8 | 67.2 | -1.6 |
数据里最值得关注的,是 EfficientNet-B0 的量化损失明显大于另外几个模型,达到了 2.1 个点。原因在于它的结构里 SE 模块使用了大范围动态变化的激活值,在 8bit 量化后这类层的信息保留能力偏弱。而 MobileNetV3 虽然也有 SE 模块,但做了 Squeeze 和 ReLU 约束设计,量化损失反而没那么夸张。如果你必须在 RK3576 上部署 EfficientNet 这类结构,可以考虑混合量化策略——只将部分敏感层保留在 FP16,其余层走 INT8,后续我会单独提这个问题。
5. 部署工程化里的细节:Runtime 调用、内存与并发
跑通模型只是第一步,真正上产品还必须处理 Runtime 调用的性能开销、内存峰值和并发场景下的稳定性。这部分的踩坑经验,我认为比单纯看延迟数据更有价值。
5.1 RKNN Runtime 的 Python 调用方式
板端推理最简单的调用方式是用 Python API:
from rknn.api import RKNN import cv2 import numpy as np # 运行时会读取 RKNN 模型文件,注意这个阶段就需要完成 NPU 资源的初始化 r = RKNN() r.load_rknn('resnet50_int8.rknn') r.init_runtime(target=None) # target=None 表示在当前设备上直接运行 img = cv2.imread('test.jpg') img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) img = cv2.resize(img, (224, 224)) # 注意 data_format 必须是 'nhwc',否则输入会被做一次维度重排 outputs = r.inference(inputs=[img], data_format='nhwc') logits = outputs[0].reshape(1, -1) pred = int(np.argmax(logits))这段代码里有几个值得展开的点。
第一,init_runtime之前load_rknn这一步看起来轻描淡写,实际上对内存敏感的边缘设备影响很大。init_runtime会完成 NPU 上下文的建立、权重数据的加载和张量分配,整个过程比较重,所以在实际服务里应该在启动阶段调用一次,然后服务循环里只反复调inference,绝对不要在每帧请求里反复初始化。
第二,inference返回的outputs是一个列表,每个元素是输出 tensor 的 numpy 数组。在做预处理时尽量用 OpenCV 完成,因为 OpenCV 的 resize 走的是硬件加速路径,在 ARM CPU 上比 PIL 快得多。另外,如果你要连续跑多帧,建议先申请好输入输出 buffer,不要在每次推理时重新创建 numpy 数组,这是 Python 端最容易被忽视的性能瓶颈。
第三,后处理阶段尽量别把 Softmax 放在 RKNN 模型里。Softmax 在 NPU 上往往不是高效算子,放到板端 CPU 上对精度没有任何影响,耗时微乎其微。这个改动虽然小,但能让模型图的尾部算子更干净,减少回退风险。
5.2 延迟稳定性与多线程:单帧快不等于服务稳定
单模型纯推理时,测试数据看起来很干净,但一旦进入多路视频分析场景,延迟曲线会剧烈抖动。我遇到的具体情况是:用单实例连续推理时,延迟在 11~12ms 间波动;一旦再加一路视频流同时调用同一个 Runtime 上下文,延迟立刻飙到 30ms 以上。
问题出在 RKNN Runtime 的对象线程安全性上。同一个 RKNN 对象在并发调用 inference 时是存在线程竞争风险的,官方推荐的做法是每个并发任务创建独立的 RKNN 实例。这意味着,如果你有一个 4 路视频分析服务,需要创建 4 个 RKNN 实例,每个实例独立加载同一个.rknn模型文件。代价是内存占用会成倍增加——每个实例都会持有模型权重和中间 tensor buffer 的副本。实测中,7 个模型分别开 4 个实例后,内存占用普遍在 700MB~900MB,这在 8GB 内存的板子上虽然可接受,但再叠加其他服务就得小心了。
另外一个稳定性优化的方向是 CPU 绑核。NPU 推理期间,RKNN Runtime 还需要 CPU 参与张量搬运和任务下发。如果 CPU 被其他高负载线程抢占,推理延迟会大幅抖动。我最终用taskset把推理进程绑定在 A72 大核上,并在主板层面限制了其他后台服务的 CPU 占用比例,实测延迟抖动从 ±5ms 收敛到了 ±1ms 以内。
5.3 算子回退问题的完整排查案例
这次实验里最典型的排查过程,出现在 ShuffleNetV2 1.0x 的部署上。第一次转换和运行后,实测延迟居然达到了 28ms,比 ResNet50 还慢。而无论从参数量还是 FLOPs 看,ShuffleNetV2 都不应该这么慢。
我的排查链路是这样的:
- 先看 RKNN build 日志,发现大量算子标注为 "CPU" 而非 "NPU"。channel shuffle 在 RKNN 里被拆成了多个 transpose + copy 算子,这些算子全部没有 NPU 实现;
- 再用
rknn.rknn_optimize()配合算子等级日志逐个定位,发现除了 channel shuffle,模型里的分组卷积也被部分拆分执行,导致 NPU 与 CPU 之间反复搬运中间数据; - 最终处理方案是更换了部署版模型:把 channel shuffle 模块在训练时改编为 1x1 卷积替代,同时减少分组数量,再导出 ONNX 转换。改完之后延迟从 28ms 降到了 9.3ms。
这个案例说明一个选型铁律:面对 NPU 部署,不能只看理论计算量,还要关心算子结构是否贴合目标芯片的硬件调度方式。像 ShuffleNetV2 这类依赖特殊数据流动的模型,在 GPU 上可能很高效,在 NPU 上反而可能因为算子拆分而效率大幅折损。
6. 几个可以进一步提升的优化方向
实验数据攒齐之后,我把 RK3576 上图像分类部署的优化路径又理了一遍。有几条方向是确定有效的,写在这里作为选型之外的技术储备。
6.1 混合量化:EfficientNet 的精度救星
前面提到 EfficientNet-B0 在纯 INT8 下损失 2.1 个点。我的补救方案是混合量化:把对量化敏感的 SE 模块里的全连接层保持 FP16,卷积层走 INT8。RKNN-Toolkit2 支持在rknn.config里通过custom_quantize配置层级别的量化属性。实际操作后,EfficientNet-B0 的精度损失从 2.1 个点缩小到 0.8 个点,延迟只增加了不到 1ms。这基本算是免费午餐——但注意,RKNN 混合量化是基于算子粒度而非 tensor 粒度,规则相对粗糙,不一定能精确指定所有层,需要实测验证效果。
6.2 RKNN 模型加密与部署保护
如果模型要交付到客户的设备上,权重保护是个现实问题。RKNN-Toolkit2 支持将模型打包成加密格式,板端 Runtime 配合使用密钥接口加载。实测加密模型的加载时间比明文模型多约 30%,但推理延迟完全一致。这里提醒一句,加密保护的强度和密钥管理方式强相关,如果密钥明文存板子里,保护效果等于零,需要结合安全芯片方案才有实际意义。
6.3 内存峰值与大模型加载策略
大模型的init_runtime阶段,RKNN Runtime 需要一次性把权重全部加载到内存中。这在内存充裕时没问题,但在同时跑多模型的场景下可能会遇到内存不足。一个缓解方案是把不影响首帧延迟的模型放到后置加载队列里,错峰加载。另一个方案是使用轻量的"零拷贝"输入模式,即在init_runtime时指定input_mode='zero_copy',这能减少推理时输入数据的拷贝开销,但需要你用特定 API 手动管理输入内存,工程复杂度会上一个台阶。
7. 模型选型总结与我的个人建议
回到最初的问题:在 RK3576 上做图像分类模型部署,到底该选哪个模型?我的建议不是给一个固定答案,而是给一个决策路径。
如果你的项目对准确率要求不高、只需要大体区分十来个类别,同时对响应速度要求极端苛刻,那就选 RegNetY-400MF 或 MobileNetV3-Large,配合 160~192 的分辨率输入,可以把延迟压到 6~8ms,获得 120+ FPS 的吞吐。这是我在视频流场景里的首选。
如果你的任务类别数在几十到一百之间,希望模型有一定的抗干扰能力,MobileNetV2 是最稳妥的。它的 INT8 量化损失最小、结构简单、算子支持极其成熟,几乎不会遇到转换阶段的坑。
如果类别多、任务难度高,必须上 ResNet50 级别或以上的骨干网络,那就要接受 26ms 左右的单帧延迟和 38 FPS 的吞吐。对于抓拍类的静态分析场景,这个速度完全够用;对于实时视频分析,就需要通过降低分辨率、检测之后再对目标区域单独分类等策略来弥补。
至于 EfficientNet 这类高精度复杂结构,我的态度是"谨慎使用":精度确实好,但 INT8 量化损失偏大,转换过程需要处理 Swish 激活和 SE 模块的算子兼容问题。除非你已经熟练掌握了混合量化的流程,否则为了两三个点的精度提升付出这么多维护成本,不一定划算。
最后再说一个贯穿始终的体会:RK3576 的 NPU 算力是 6 TOPS,但 6 TOPS 只属于那些"算子结构贴合芯片"的模型。任何脱离目标硬件做的模型选型,最后都会被实测数据无情打脸。真正高效的做法,还是像我这次一样,把候选模型全部在板子上跑一遍,让数据替你说话。在有了这组实验数据后,后续项目再做模型选型,基本就是查表的小事了。