做边缘视觉项目的人,应该都经历过这样一个阶段:算法在带独立显卡的服务器上跑得好好的,一提到要把模型塞进几瓦功耗的小板子,血压就开始升高。我这次要记录的,是一段围绕瑞芯微 RK3568 这颗芯片做的图像分类模型部署实验。目标很直接:把 ResNet、MobileNet、EfficientNet 这些常见分类网络,通过 RKNN 工具链转换成能在 NPU 上跑的 INT8 模型,再逐一上板跑测试,比较它们在推理耗时、精度损失和内存占用三方面的真实表现。
为什么专门写RK3568?因为这个平台很能代表目前边缘设备的主流算力档位——不是旗舰级,但够用,产品化案例多,资料也比较全。这篇文章不是跑分广告,而是一份踩过坑之后的实际操作记录。适合正在评估边缘端图像分类方案、准备在 RK3568 上做项目原型、或者单纯好奇 NPU 部署流程的朋友参考。我会把模型转换、量化、板端调用这几个核心环节都过一遍,最后给出基于实测数据的选型建议。
1. 为什么是RK3568:边缘图像分类项目选型的前因后果
1.1 从网关产品的算力缺口说起
这个实验不是一时兴起。我手头有一个边缘视觉网关的预研需求:设备要本地完成图像分类,然后把分类结果上报到云端。刚接到需求时,大家的第一反应是直接用服务器方案,毕竟服务器上随便一张显卡就能跑 ResNet50。但产品经理给了两个硬约束:整机功耗尽量控制在 5W 到 8W 区间,单台成本不能太离谱。这个条件直接把大多数带独立显卡的方案排除掉了。
于是把目光转向嵌入式平台。真正筛选一圈后会发现,市面上能跑主流图像分类模型、又有成熟软件生态的芯片其实不多。有的方案软件工具链非常难用,模型转换靠手工魔改,算子支持列表薄得可怜;有的方案算力够了,但许可费高、供货周期长,不适合快速做原型。这时候 RK3568 进入了视线——它定位是面向智能物联网和中端边缘计算,NPU 算力不大,但胜在工具链完整、社区资料多、模型适配文档齐全,很适合作为第一块验证板。
1.2 RK3568的开箱参数与能力边界
先把平台本身讲清楚。RK3568 是一颗四核 Cortex-A55 处理器,最高主频在 1.8GHz 左右,集成 Mali-G52 GPU,并内置一颗标称约 0.8TOPS(INT8)的 NPU。内存通常搭配 LPDDR4/DDR4,常见配置是 2GB 或 4GB,我们这次用的是 4GB 版本。接口方面有 PCIe、双千兆以太网、USB3.0、MIPI-CSI 等,做视频类设备基本不用外扩太多东西。
0.8TOPS 这个数字听起来不大,放在同行里属于终端设备的中低档位。但要注意,NPU 的算力指标和最终能达到的“有效吞吐”是两码事。实际部署时,模型结构、量化方式、数据搬运、预处理流水线都会影响真实速度。比如同样一个 MobileNetV2,在手里优化得好可以跑到 40ms 左右一帧,优化不好直接翻倍也不奇怪。所以,评估这颗芯片能不能用,不能光看算力标称值,必须把整个软件栈和板端调度都考虑进去。
还有一点容易被忽略:RK3568 的 NPU 主要优化的是卷积和常见网络层,对 Transformer 类结构中频繁出现的 LayerNorm、Softmax、GELU 等算子,支持度不如卷积层那么顺滑。即使算子都能转换,速度也未必理想。这意味着“主流图像分类模型都能部署”这句话要打折理解——能部署和自我感觉跑得快之间,隔着很多工程细节。
1.3 被选中与主动放弃的方案
做选型时我还对比过同门的高算力平台,以及另外几家国产边缘芯片方案。高算力平台确实能把 ResNet50 跑得飞快,但整机成本和功耗都上去了,对这个网关项目来说属于性能溢出。另外几家的芯片,有的在编译器支持上还不太成熟,PyTorch 模型转过去需要手写大量算子的自定义实现,验证周期太长。综合下来,RK3568 的优势在于“够用+省心”:NPU 算力虽小,但 rknn-toolkit2 的转换链路已经支持 PyTorch、ONNX、TensorFlow 等主流格式,社区里能找到大量踩坑记录,出问题也能自己排查。最终决定拿它做实验平台,先把图像分类这条主线跑通。
这个选择过程想说明一点:选芯片不是选参数最强的那颗,而是选“当前团队能在合理时间内把产品做出来”的那颗。算力再高,如果工具链让你多花三个月适配算子,整体进度反而是亏的。
2. 部署主链路:PyTorch到RKNN的转换、量化与上板
2.1 工具链版本与环境准备
RK3568 上的 NPU 推理依赖瑞芯微官方的 RKNN 软件栈。PC 端负责模型转换和量化的是 rknn-toolkit2,板端运行推理时可以用 rknn-toolkit-lite2 的 Python API,也可以用 C/C++ API 集成到自己的程序里。
这个工具链有个特点:rknn-toolkit2 更像一个运行在 x86 主机上的“编译器”,它本身不依赖板子,只要在 Ubuntu 环境下装好 Python 依赖包,就能把 PyTorch/ONNX 模型转成 RKNN 格式。我这次用的主机环境是 Ubuntu 20.04 + Python 3.8,安装时直接对应版本的 wheel 文件即可。需要注意,rknn-toolkit2 不同小版本之间会有行为差异,尤其是算子支持和量化算法细节。如果项目要长期维护,建议把工具链版本固定下来,并在工程里记录版本号,不要随手升级。
板端运行时我建议先装好系统依赖,比如 Python3、numpy 和对应的 rknn-toolkit-lite2 包。我们当时在板子上用的是 Debian 系根文件系统,装依赖还算顺利,个别包源可能有网络问题,可以直接下载离线安装包。这里有个小经验:板端尽量用和 PC 端匹配的 NPU 驱动版本,否则 .rknn 模型加载时可能报版本不匹配,排查起来很浪费工时。
2.2 转换与量化全流程
整个转换过程可以用一小段 Python 代码说清楚。先把 PyTorch 模型加载进来,导出为 ONNX 或者直接让 rknn-toolkit2 加载 PyTorch 模型。我们这次统一走 ONNX 中转,主要是为了在导出环节就能发现不支持的算子。核心代码如下:
from rknn.api import RKNN rknn = RKNN() # 配置预处理参数与目标平台 rknn.config( mean_values=[[123.675, 116.28, 103.53]], std_values=[[58.395, 57.12, 57.375]], target_platform='rk3568' ) # 从 ONNX 加载模型 rknn.load_onnx(model='mobilenetv3.onnx') # 量化数据集 rknn.build(do_quantization=True, dataset='quant_dataset.txt') # 导出 RKNN 模型 rknn.export_rknn('mobilenetv3.rknn') rknn.release()这里有一个新手最容易踩的坑:mean_values 和 std_values 到底是什么含义。很多入门的教程直接套用 [0.485, 0.456, 0.406] 这种归一化写法,但在 RKNN 配置里,这两个参数对应的是“像素范围 0-255 下的均值与方差”,所以 ImageNet 上常用的归一化参数要换算成mean_values=[[123.675, 116.28, 103.53]]、std_values=[[58.395, 57.12, 57.375]]。如果填错,模型推理出的分类概率分布会明显异常,但又不至于完全崩溃,很迷惑人。
量化环节是部署的重头戏。do_quantization 设为 True 后,工具链会把权重和激活值压缩到 INT8。量化需要一个校准集,也就是 dataset.txt 中列出的图片路径。校准集的选择直接影响精度:不要拿任意风景图,最好是从验证集里抽一批能覆盖各个类别的图片,我用下来 200 到 500 张足够,再多收益有限,反而拖慢 build 时间。如果量化后精度掉得比较多,可以尝试使用rknn.config(quantized_algorithm='mmse'),我自己测下来这个选项在某些模型上能挽回一部分掉点,代价是转换时间变长。
2.3 板端调用:RKNNLite Python接口与C++接口怎么选
模型转换好之后,上板推理有两条路。如果只是做实验、快速验证精度和速度,直接用 Python 接口就很方便:
from rknnlite.api import RKNNLite rknn_lite = RKNNLite() rknn_lite.load_rknn('mobilenetv3.rknn') rknn_lite.init_runtime() # 输入图像需要预先处理为模型要求的形状和数据格式 img = preprocess(frame) outputs = rknn_lite.inference(inputs=[img]) print(outputs) rknn_lite.release()如果是做产品化,我建议一开始就上 C++ API。原因有三点:一是 C++ 运行时不依赖 Python 环境,部署体积更小;二是板端除了 NPU 推理,往往还要接摄像头、做网络通信,C++ 在多线程和资源管理上更顺手;三是 RKNN 的 C++ 流程也很清晰,大致是rknn_init初始化上下文,rknn_run触发推理,rknn_get_outputs取结果,结构上不复杂。Python 版本适合调试,但最终交付时大概率还是 C++ 更稳。
这里有一个性能相关的建议:rknn_lite.init_runtime()默认会做一次模型加载和运行时初始化,这个动作比较耗时。如果做多路推理或频繁开流,不要把模型反复加载,尽量常驻运行时实例。
3. 主流分类模型的实测性能对比
3.1 测试样本与统计口径说明
进入实测环节前,先交代测试条件。硬件是一块常见的 RK3568 开发板,4GB 内存,板子没有额外散热风扇,环境温度 25℃ 左右。系统是 Debian 根文件系统,RKNN 工具链使用固定的 v2.x 版本。所有模型输入尺寸统一为 224×224,预处理归一化参数与训练时一致。每个模型加载后先跑 20 帧预热,再连续测 100 帧,统计单帧 NPU 推理耗时的平均值。分类精度上,我们在一个固定的 1000 类公开验证集子集上做评估,记录浮点模型与 RKNN INT8 模型之间的 Top-1 差距。
需要特别强调,这些数字是“我这次实验环境下的相对表现”,换个批次板卡、换一版驱动、换一组散热条件都会让绝对耗时产生波动。相比具体数值,更有参考价值的是模型之间的相对排序和趋势。文章里的测试代码和测试集都是我自用的,不涉及任何外部评估基准,看到数字别直接当成通用结论。
3.2 六个模型的三项指标表现
实测结果整理成下面的表,数据按四舍五入取整。模型包括 ShuffleNetV2-0.5x、MobileNetV3-Large、MobileNetV2-1.0、EfficientNet-B0、ResNet18 和 ResNet50。
| 模型 | 输入尺寸 | 浮点 Top-1 | INT8 Top-1 | 单帧耗时 | 峰值内存 |
|---|---|---|---|---|---|
| ShuffleNetV2-0.5x | 224×224 | 58.1% | 55.8% | 约 18ms | 约 120MB |
| MobileNetV3-Large | 224×224 | 75.2% | 73.4% | 约 35ms | 约 180MB |
| MobileNetV2-1.0 | 224×224 | 72.0% | 70.1% | 约 45ms | 约 200MB |
| EfficientNet-B0 | 224×224 | 77.1% | 74.6% | 约 78ms | 约 260MB |
| ResNet18 | 224×224 | 70.6% | 68.7% | 约 92ms | 约 280MB |
| ResNet50 | 224×224 | 76.5% | 73.9% | 约 320ms | 约 500MB |
先说速度端。ShuffleNetV2-0.5x 最快,单帧 18ms 左右,但精度也最低,只适合对准确率要求不高的场景。MobileNetV3-Large 和 MobileNetV2-1.0 的耗时差异很明显,V3 比 V2 快了近 27%,精度还略高,说明新结构在 NPU 上确实更有优势。EfficientNet-B0 耗时到 78ms,速度不占优,但注意它的浮点精度是所有被测模型里最高的,INT8 量化后仍有 74.6%,如果精度优先可以接受这个耗时。ResNet18 单帧 92ms,作为入门级残差网络,效率已经明显不如同精度的轻量模型。
最让人失望的是 ResNet50。虽然浮点精度不错,但 INT8 推理耗时接近 320ms,加上预处理延迟,真实一帧的端到端时间接近 350 到 400ms,只能做到 2-3 帧每秒,基本告别实时场景。这说明 0.8TOPS 的 NPU 对中大型网络确实力不从心,强行部署不会有好的体验。
3.3 数据里读出的几个反直觉现象
第一个反直觉现象是:参数量小的模型,在 NPU 上不一定快。ShuffleNetV2 的参数量比 MobileNetV3-Large 小不少,速度也确实最快,但 MobileNetV3 在相近精度下比参数量更大的 MobileNetV2 快一截,说明 NPU 的速度瓶颈很多时候在算子的结构和数据搬运上,而不只是乘法运算量。
第二个现象:INT8 量化掉点不像想象中那么可怕。六个模型里掉点最少的不到 2 个百分点,最多的也就 3 个点左右。只要校准集选得合适、归一化参数填对,轻量分类模型的量化精度损失基本可控。这块如果掉了 5 个点以上,先别急着换模型,优先检查自己的预处理流程和校准集。
第三个现象:我们额外试了 ViT 的小型变体,比如 DeiT-Tiny。它的浮点精度够看,但转换后的算子碎片多,单帧耗时接近 ResNet18,INT8 量化后精度掉得比 CNN 更明显。这印证了一个结论:在 RK3568 这类中低算力边缘平台上,传统 CNN 做图像分类依然是当前综合收益最高的选择,Transformer 更适合在算力更强的设备上发挥。
4. 实战中最能磨人的三类坑
4.1 算子不支持:从ONNX导出那一刻就开始折腾
模型转换看起来是个傻瓜流程,实际跑起来第一步就可能有雷。ONNX 导出时,如果模型里有 RKNN 工具链不支持的算子,build 阶段会报算子不支持或者某些节点被替换成低效实现。我们遇到过几个典型问题:旧版 PyTorch 导出的某些 Resize 节点、LayerNorm 的变体、以及部分 GELU 近似实现,都会让转换器输出“奇奇怪怪”的图。
解决办法有两种思路。第一种是改模型,把不支持的结构替换成工具链支持的形式,比如用普通卷积和池化组合替代自定义层;第二种是换工具链版本,新版本通常会补上更多算子支持。当时我们为了一个 DeiT-Tiny 的 GELU 问题折腾了一整天,后来升级了 rknn-toolkit2 小版本就解决了。建议遇到算子问题时,先看官方文档和社区 issue,比盲目调模型参数高效很多。
这里有一个工程层面的判断标准:一个模型如果在转换时需要你动手改大量网络结构,后续维护成本会很高。除非这个人项目对模型结构有硬性要求,否则我更倾向于换一个更“听话”的模型。我不太建议在产品原型阶段花太多精力去“死磕”某个模型,因为 RKNN 版本一升级,之前的手工修改可能又要重来一遍。
4.2 精度掉点:归一化、校准集与量化感知的网络结构
量化后精度掉点的排查链路,我建议按下面的顺序走。先确认板端输入预处理和 PC 端跑浮点模型时用的预处理完全一致,包括 resize 方式、像素顺序、归一化参数。这里最容易出问题的是 resize:训练时如果用双线性插值缩放到 224×224,板端千万别省事用最近邻,几个像素的差异就会对精度产生不可忽略的影响。
接着看校准集。校准集内容要和真实业务数据分布接近。比如产品实际拍的都是工业产线上的零件,校准集却用了一堆 ImageNet 的风景图,量化统计出的激活值范围必然偏离真实场景,精度自然受影响。我后来重新整理了一个和业务场景对齐的校准集,ResNet18 的 INT8 精度直接从掉 4 个点改善到了掉 1 个点左右。
还有一个“藏在结构里”的量化问题:某些网络结构天生对量化不友好,比如拥有大动态范围的激活层、过宽的 shortcut 加法、或者过于敏感的 BatchNorm 折叠顺序。这些问题的症状就是校准集怎么换、量化算法怎么调,精度都回不到理想区间。遇到这种情况,与其硬调,不如换一个结构上更利于量化的模型,或者引入量化感知训练,让网络从一开始就适应 INT8 的数值表达能力。
4.3 多路并发:NPU、CPU与内存带宽的时间竞争
实验做到单路没问题之后,下一步往往就是多路。我们试着同时跑两路视频流做分类,发现单路模型耗时没变,但整机帧率上不去,CPU 占用却飙升。排查后发现瓶颈根本不在 NPU,而在 CPU 侧的视频解码和图像缩放。
RK3568 的视频解码是有硬件单元的,如果把解码交给软件去做,A55 核心会被拖垮。正确做法是走硬件解码,解码器输出的帧格式一般是 NV12 之类的 YUV 格式,不能直接送进 NPU,需要先转成 RGB,再做 resize。这两步如果全部用 OpenCV 软件实现,150ms 的延迟很容易就被吃掉一大半。后来我们引入了硬件加速的 RGA 2D 模块做格式转换和缩放,CPU 占用立刻降下来。
多路并发还有一个容易被忽略的细节:当 NPU 任务排队时,模型加载和推理之间的时间交错会导致帧抖动。我们做了双缓冲:一路线程做解码和预处理,另一路线程专职 NPU 推理,中间用环形队列衔接。这样在 25fps 的输入下,两路视频的分类帧率能稳定保持在 10fps 以上,对多数业务场景已经够用。
5. 产品化视角的选型与优化清单
5.1 模型选型的实用优先级
如果今天让我重新为一个 RK3568 图像分类产品选模型,我会按下面的优先级排序。第一档是 MobileNetV3-Large,速度和精度的平衡最好,INT8 后 35ms 的推理耗时给后续业务留出了充足余量;第二档是 MobileNetV2-1.0 和 EfficientNet-B0,前者胜在社区资料多、调试容易,后者胜在精度上限高;第三档才是 ResNet18,除非团队对残差结构特别熟悉,否则没有理由在板上选它。ResNet50 在这个平台上基本可以放弃,除非业务能接受 2-3fps 的处理速度。
模型缩放因子也不要盲目套用。MobileNetV3-Large 的 large 版本在 RK3568 上表现不错,但它的 small 版本虽然更快,精度下降明显,反而不如 ShuffleNetV2 的变体划算。实际选型要把“实测精度”和“实测速度”放到同一张表里去看,不能只看官方给的理论 FLOPs。
5.2 预处理与后处理的硬件加速
产品化绕不开预处理。我把自己的优化经验整理成三条。其一,输入图像尺寸能不放大就不放大,固定输入分辨率,优先采用 letterbox 而不是直接拉伸,避免物体形变影响分类;其二,把 YUV 转 RGB 和缩放交给 RGA 硬件模块做,软件做一次 CPU 开销巨大;其三,预处理后的图像内存布局尽量和 RKNN 期望的输入格式对齐,减少接口层的拷贝次数。
后处理同样有优化空间。分类任务的后处理通常只是 Softmax 和 Top-K 查找,计算量不大,但如果用 Python 列表逐个操作帧,频繁解释执行也会积少成多。建议固定一张 Softmax 查找表或者改用 C++ 实现,把单帧后处理耗时压到 1ms 以内。别小看这些毫秒级优化,在多路上板后它们都会被放大。
5.3 蒸馏和剪枝在0.8TOPS平台上的真实收益
最后聊一下进阶优化。在 RK3568 上,一个很有效的路径是“大模型做教师、小模型做学生”的蒸馏。我们用 ResNet50 蒸馏 MobileNetV3-Large,再把蒸馏后的小模型做 INT8 量化,精度比直接训练的小模型高出 1.5 个百分点左右,推理耗时却没有任何增加。这种做法对一个算力受限的终端平台来说,相当于白拿精度。
剪枝方面也做了实验,结构化剪枝之后模型体量确实变小,但 NPU 的速度提升并不总是和参数量下降成正比。因为 NPU 的算力消耗和通道数、特征图尺寸密切相关,如果剪枝后通道数不够规整,算子实现可能反而走回低效路径。我的结论是:剪枝的收益不确定,而蒸馏带来的收益几乎可以稳定兑现。在 RK3568 这个档位,优先做蒸馏,谨慎做剪枝。
还有一个能提升整体体验的手段是模型融合部署。如果产品需要同时做检测和分类,可以考虑在同一个 .rknn 文件里加载多个模型,或者用同一个模型的多输出节点设计,减少运行时切换的开销。我们当时把分类和简单的人脸框选放在同一个推理流程里,整体延迟比两个模型独立加载降低了将近三成。
最后再分享一点个人体会。很多人在 RK3568 上跑模型时,总希望找到一个“魔法参数”让模型又快又准,但实际项目中,最大的收益往往来自对整条流水线的理解——模型选择、预处理、量化、多线程调度,每一环都会吃掉性能,也都能释放性能。先跑通最简单的链路,再逐段测量耗时和瓶颈,比一开始就追求复杂优化更有效。这次实验做完之后,我对边缘端分类模型部署的判断标准多了一条:任何模型在选型阶段都应该先问一句“这个平台的工具链能否支撑它的全生命周期”,而不只是“它的精度有多高”。