
前阵子做边缘图像分类方案选型手头刚好有一块 RK3568 的开发板。这芯片定位很微妙四核 A55 加一颗标称 1TOPS 的 INT8 NPU跑重模型怕带不动跑太轻的模型又觉得浪费。我索性把几类主流图像分类模型逐个部署上去从 ONNX 转到 RKNN再量化、跑板、测延迟和精度把整个链路完整走了一遍。这篇文章就是我这次 RK3568 图像分类模型部署实验的记录包含工具链选择、模型改造、实测数据对比和踩坑排查思路给准备在类似边缘 SoC 上部署分类模型的读者做个参考。先说结论ResNet50 能跑但实时性一般MobileNetV2/V3 是大多数场景的稳妥选择EfficientNet 系列看具体变体DeiT-Tiny 这类小 Transformer 在这颗 NPU 上性价比不高。下面我把每个环节拆开讲所有实测数据都是同一块板子、同一种测试方式跑出来的横向对比参考价值比较大。1. 动工之前先掂量性能RK3568 这颗 NPU 到底能吃下什么样的模型1.1 硬件账面数据与实际可用算力RK3568 的 NPU 标称是 1TOPS 的 INT8 算力CPU 是四核 Cortex-A55频率大概在 1.8GHz 到 2.0GHz 之间。这个账面数据看起来不大但在边缘推理场景里算是“中间地带”的典型代表。真正上手之后你会发现决定模型能不能跑的往往不是峰值算力而是数据搬移、算子支持和 NPU 利用率这几件事。很多人会把算力直接和模型 FLOPs 做除法比如 ResNet50 一次推理约 4.1GFLOPs除以 1TOPS 得到理论下限 4.1 毫秒于是觉得跑个几十帧没问题。这个算法本身没错错在把“理论极限”当成了“实际延迟”。我实测 ResNet50 在 224x224 分辨率下单次推理中位数大概在 140ms 左右和 4ms 差了三十多倍。不是芯片虚标而是 NPU 内部要频繁搬运中间特征图卷积计算只是整个链路的一部分加上量化算子组合、内存带宽限制实际结果就是这么现实。打个比方1TOPS 的算力相当于一个大停车场里有很多车位但车辆进出只有一条窄通道。如果模型里大量操作是“停好车立刻开走”那么通道宽度内存带宽和 IO 开销就会成为瓶颈。MobileNetV2 这类轻量模型计算量小但中间张量依然要多次搬运实测能跑到 20ms 上下已经算不错了。所以选型之前别只盯着 FLOPs 看必须把“跑在真机上”这件事前置。1.2 由 FLOPs 倒推的模型选型边界动手转换之前我先把候选模型按参数量和计算量做了个粗略排序这样可以提前预判哪些模型大概率能上板、哪些需要改造后才能上板。下面这张表是我当时的初步评估模型大致参数量224x224 推理计算量预判ResNet5025.5M约 4.1GFLOPs可跑实时性偏弱MobileNetV23.4M约 0.3GFLOPs很合适优先候选MobileNetV3 Large5.4M约 0.3GFLOPs需要关注 h-swish 算子EfficientNet-Lite04.0M 左右约 0.4GFLOPs适合算子相对友好EfficientNetB05.3M约 0.4GFLOPs可跑但 Swish 可能拖慢DeiT-Tiny5.7M约 1.3GFLOPs风险大先验证算子这里要说明一点计算量只是粗略参考不同实现里 FLOPs 口径差异很大有的把乘加算成两次操作有的只算一次。我在选型时主要看数量级不纠结小数点。RK3568 这种算力级别建议优先选计算量在 1GFLOPs 以下的模型超过 2GFLOPs 的模型就要认真考虑访存和算子兼容问题了。内存也是选型边界之一。RK3568 常见内存配置有 1GB、2GB 和 4GB 几个版本如果你的设备只有 1GB 内存那 ResNet50 跑起来会非常紧张因为推理时的运行内存增量大概在 180MB 左右再加上系统本身的开销留给业务逻辑的空间就很有限了。MobileNetV2 的增量只有 30MB 上下这种模型在上线后基本不用担心内存压力。选型边界本质上就是“算力、内存和精度三者之间找平衡点”。2. 部署工具链拼装ONNX 到 RKNN 的转换链路与最容易翻车的三个环节2.1 环境版本怎么锁我这次用的转换链路是PyTorch 训练好的模型先导出 ONNX再用官方提供的 RKNN-Toolkit2 在 PC 上完成模型转换和量化最后把生成的 .rknn 文件放到板子上用 RKNNLite 运行时库做推理。板端不需要完整工具链只需要 runtime 库这对镜像体积和部署便利性都有好处。版本组合很关键。我当时在 PC 上用的是 Python 3.8、PyTorch 1.13.1、torchvision 0.14.1、onnx 1.12 和 RKNN-Toolkit2 的 2.x 版本。这个组合整体比较稳。如果你用太新的 PyTorch 导出 ONNX有时候会引入一些新算子RKNN 工具链不一定认识所以我会尽量用与工具链验证过的版本组合。版本不一致导致的报错往往很难排查因为报错信息可能只是“Unsupported op”而根子却在算子版本上。还有一个小习惯导出 ONNX 之后先用 ONNX Simplifier 做一次简化。很多 PyTorch 导出的图里会有多余的 Identity 节点、Reshape 包装等简化后不仅 RKNN 转换成功率更高生成的 .rknn 模型也更小。这一步成本很低建议所有模型都过一遍。2.2 输入预处理一致性最容易造成精度崩盘的隐形杀手图像分类模型在训练时几乎都带一套固定的预处理逻辑。以 ImageNet 上预训练的模型为例通常是 resize 到 224x224、转为 RGB、归一化到 [0,1]再按 ImageNet 的 mean 和 std 做标准化。如果导出 ONNX 时把预处理也一起导进去了转换阶段就不要重复叠加 mean/std如果导出的是“裸模型”那么预处理必须由调用方完成。我在 RKNN 转换配置里用了类似这样的方式处理归一化rknn.config( mean_values[[123.675, 116.28, 103.53]], std_values[[58.395, 57.12, 57.375]], target_platformrk3568, quantized_dtypeint8, quantized_algorithmnormal )这个 mean_values 和 std_values 对应 ImageNet 的通道级统计单位是像素值 0-255。这样配置之后板端推理时输入原始 uint8 图像即可工具链会在 NPU 前处理阶段完成归一化。我见过不少因为这里配置错导致精度崩盘的情况模型导出时已经做过一次归一化RKNN 又做了一次输入分布完全偏离训练分布Top-1 精度能掉到跟随机猜差不多。通道顺序同样要较真。OpenCV 默认读出来是 BGR而 PyTorch 系列模型通常按 RGB 训练。用 RKNNLite 推理前必须先把图像转成 RGB再确保内存布局是连续的 HWC 或 CHW。RKNN 不同版本的输入布局要求可能有变化我会在每个模型初始化后先用一张已知类别的图验证输出如果结果对不上优先检查通道顺序和 width/height 是否交换。尺寸方面RKNN 对动态 shape 的支持整体偏弱我在导出 ONNX 时就直接固定成 224x224不使用动态维度。动态 shape 除了增加转换失败概率量化校准也会变得不可控边缘部署场景没有正当理由非要用动态尺寸。2.3 训练后量化与量化感知训练的选择量化是 RK3568 NPU 能不能发挥价值的关键。RKNN 支持训练后量化PTQ和量化感知训练QAT两条路。PTQ 实现简单只需要准备一组校准图片转换工具会自动统计每层激活值的 min/max 分布并确定量化参数。QAT 需要在训练阶段模拟量化误差流程重、成本高但精度通常更好。我的建议是先用 PTQ除非 PTQ 掉点超过项目的精度红线再考虑 QAT。PTQ 里最重要的是校准集。校准图片不需要很大几十到两百张足够但必须覆盖真实应用场景的分布。举个例子如果实际部署时要识别的是工厂流水线上的工业零部件而校准集全用通用图片量化参数就会偏向错误的数据分布。我后来在这上面吃了不小的亏后面踩坑章节专门展开说。校准集文件格式很简单一个 txt 文件每行放一张图片路径即可/dataset/calib_001.jpg /dataset/calib_002.jpg ...转换脚本的大致骨架如下rknn RKNN() rknn.config(...) ret rknn.load_onnx(modelmodel.onnx) ret rknn.build(do_quantizationTrue, datasetdataset.txt) ret rknn.export_rknn(model.rknn)如果你的模型在 PTQ 之后精度不满意可以尝试更换量化算法例如从 normal 换成 KL 散度有些模型的激活分布长尾明显换算法效果立竿见影。如果还不行再考虑把特别敏感的层单独保留为 FP16 或 FP32。这里要说清楚混合精度的支持程度取决于工具链版本遇到报错或效果不符合预期时先查版本的官方说明而不是盲目相信网上老教程。3. 六个主流模型挨个过一遍转换、量化、跑板3.1 ResNet50兼容性最好的“试金石”ResNet50 是第一个上板的因为它结构经典全是 Conv、BN、ReLU 和 Add 这类基础算子RKNN 转换几乎不会遇到兼容问题。从 torchvision 导出 ONNX 后经过 ONNX Simplifier 简化再转 RKNN 一次通过。我当时还挺意外这么老的模型反而是整个实验里最省心的。量化方面ResNet50 的表现也比较稳。FP32 的 Top-1 在验证集上大概是 76% 左右INT8 量化后掉点不到 1 个百分点属于完全可接受范围。这和它的残差结构有关Add 分支把误差逐层传递单层量化噪声被整体结构抵消了一部分。所以如果你需要一个“绝对不会因为量化翻车”的模型ResNet50 是很合适的候选。当然速度就不太乐观了。我在 224x224 输入下测到单次推理约 142ms换算下来只有 7fps 左右。这个速度做离线批量分类没问题做实时视频流就力不从心了。我的判断是ResNet50 适合放在 RK3568 上作为精度基线、离线分析或者教师模型不适合直接做高并发实时分类。3.2 MobileNetV2 与 MobileNetV3 Large轻量模型的两代代表MobileNetV2 是轻量模型里转换最顺的整体算子简单量化后实测延迟在 21ms 左右约 45fps 上下。我第一次跑出来时还挺满意毕竟这个速度在 RK3568 上已经能支撑多数实时应用了。不过 MobileNetV2 对量化校准集更敏感这是因为深度可分离卷积是逐通道量化每个通道的激活统计差异大校准集覆盖不全时某些通道的量化范围会被高估导致精度雪崩。我在校准集上偷懒过一次Top-1 直接从 71% 掉到 60% 出头后来换回真实场景图片才恢复。MobileNetV3 Large 的结构比 V2 复杂一些引入了 h-swish 和 SE 模块。h-swish 在 RKNN 工具链里不一定作为单一算子支持一旦算子不支持就会回退到 CPU 执行速度会突然掉一大截。我遇到的情况是第一次转换后延迟高达 51ms比 V2 还慢两倍多明显不正常排查后发现是 h-swish 那个分支在 CPU 上跑。解决办法是在模型源码里把 h-swish 替换成等价或近似的结构最好直接用 ReLU6替换后重新微调几百个 step让模型适应新的激活函数。改完之后 MobileNetV3 Large 的延迟降到了 14ms 左右而且实测 Top-1 精度还比 MobileNetV2 高了一点。所以我的结论是在 RK3568 上MobileNetV3 Large 经过算子替换和微调之后是比 MobileNetV2 更值得优先用的实时分类模型。3.3 EfficientNet-Lite0 与 EfficientNetB0结构特殊带来的迁移成本EfficientNet 系列让我印象最深的一点是同一个家族的不同变体部署难度差别很大。EfficientNetB0 原版用的激活函数是 SwishSiLU这个算子在 RKNN 上如果支持往往也是一个由 Sigmoid 和乘法组合出来的复合算子中间会多出好几个临时张量。最终延迟比同等计算量的轻量模型高一截我实测 B0 大概在 38ms 左右而且量化后精度掉点也更明显。把它当普通轻量模型看待会低估它的部署成本。EfficientNet-Lite0 是专门为边缘设备设计的变体把 Swish 换成了 ReLU同时还调整了部分缩放系数。转换起来比 B0 顺畅很多实测延迟 24ms 左右虽然比 MobileNet 系列慢一点但精度在某些细分类任务上确实有优势。如果你看中的是 EfficientNet 家族的精度表现我建议优先考虑 Lite 版本而不是原版 B0别跟激活函数算子死磕。这里也想顺带提一句模型设计阶段如果能考虑目标硬件支持的算子集合能省掉大量部署时返工的功夫。论文里的 FLOPs 和硬件友好度是两码事这在 EfficientNet 家族上体现得非常明显。3.4 DeiT-Tiny小 Transformer 在 1TOPS NPU 上的实验Transformer 架构这两年很热小模型 DeiT-Tiny 参数量也只有 5.7M 左右看着和 MobileNetV3 差不多我就想试试它在 RK3568 上到底能不能用。结果比较残酷ONNX 导出没问题但 RKNN build 时遇到一堆不支持的算子包括 Softmax、LayerNorm、Reshape 加 Transpose 的组合等。有些算子回退到 CPU有些需要手工拆解最后生成的 .rknn 模型虽然能跑但很大比例的运算都在 CPU 上完成。实测 DeiT-Tiny 在板端推理延迟直接到了 215ms运行内存增量也到 90MB 左右比 MobileNetV2 高出一大截。精度上也没有展现出绝对优势毕竟同样规模下 CNN 依然能打。所以我的结论是在小算力 NPU 上ViT 系列目前只适合做技术验证不太适合当主力分类模型。如果你确实需要全局感受野或者关注长距离依赖可以考虑把 Deit-Tiny 的 patch size 调大、输入分辨率调小先跑通算子再优化速度但不要对实时性抱有太高期望。4. 实测数据盘点帧率、精度、内存与 CPU 占用到底差多少4.1 统一测试口径同图、同框架、同量化方式跑板之前我制定了统一的测试口径不然数据没有任何可比性。所有模型都用同一张 224x224 RGB 测试图统一转成 INT8 量化统一在 RK3568 板子上用同一套 RKNNLite 运行时做推理。测试时让板子处于 performance 供电模式尽量排除调度器和降频策略的干扰。统计方式上我先预热 20 次让 NPU 驱动把权重、缓冲区都初始化到位再连续推理 100 次取延迟的中位数而不是平均值。中位数能有效避开偶发的调度抖动和系统后台干扰表达“典型表现”更准确。代码大概是这样的import time import numpy as np from rknnlite.api import RKNNLite rknn RKNNLite() rknn.load_rknn(model.rknn) rknn.init_runtime() warmup 20 total 100 for _ in range(warmup): rknn.inference(inputs[input_img]) latencies [] for _ in range(total): start time.perf_counter() rknn.inference(inputs[input_img]) latencies.append((time.perf_counter() - start) * 1000) latencies.sort() median_latency latencies[total // 2] print(fmedian latency: {median_latency:.2f}ms)这里要注意测试时不能把预处理时间排除在外但也要区分开。实际业务里预处理同样需要占用 CPU 时间我测出的数值包含了从输入图像到输出结果的完整调用链但不包含线程队列等待时间。4.2 性能数据对比表与实测波动范围把六个模型跑完后我汇总了一张比较完整的表。必须说明的是数据只能代表我这块板子、这个固件版本、这个温度环境下的表现但它能反映模型之间的相对差距选型参考价值足够。模型224x224 延迟中位数运行内存增量INT8 相对 FP32 Top-1 损失主要问题ResNet50142ms约 180MB约 -0.8%慢但稳定MobileNetV221ms约 30MB约 -1.1%校准集敏感MobileNetV3 Large 改进版14ms约 35MB约 -0.6%需要算子替换EfficientNet-Lite024ms约 40MB约 -1.3%精度/速度均衡EfficientNetB038ms约 50MB约 -2.0%Swish 开销大DeiT-Tiny215ms约 90MB约 -2.8%算子大量回退 CPU我在记录每次延迟时还会顺带记一个范围比如 MobileNetV3 Large 是 13-16msResNet50 是 135-155ms。这个波动主要来自温度、CPU 调度和 Cache 命中。如果只看一次推理结果很容易被偶然波动误导所以上线前一定要看中位数和长尾数据。延迟长尾哪怕只占 5%也可能在真实服务里造成视频丢帧或请求超时。4.3 NPU 利用率与 CPU 互斥为什么帧率不是线性提升很多人以为只要模型计算量减半帧率就能翻倍实际完全不是这么回事。我在 MobileNetV2 上做过一次简单实验把输入从 224x224 降到 192x192计算量降低了约 27%但延迟只从 21ms 降到 17ms 左右。省下来的时间远没有理论计算量那么多因为小模型场景下NPU 计算已经不是最长的板子输入图像从 CPU 内存拷贝到 NPU 内存、结果再拷回来以及 NPU 驱动调度开销都成了不可忽略的部分。如果想要提高整体吞吐正确的方向不是减少计算量而是做流水线。一个线程专门做图像采集和预处理另一个线程专门调用 NPU 推理中间用队列连接。这样预处理时间和推理时间可以部分重叠端到端帧率会明显提升。我后来在部署服务时用了这个模式整体吞吐大约提升了 15%-20%。另一个容易忽略的问题是内存复用。RKNNLite 在inference接口内部会做输入输出 buffer 的分配和拷贝如果推理前每次都重新创建输入数组内存申请开销会很可观。我在固定输入尺寸的场景下提前分配好输入 buffer推理时只更新数据延迟又降了 2-3ms。对于 20ms 左右的模型这已经是显著优化了。5. 部署中踩过的坑与其排查思路量化崩坏、算子不支持、内存抖动和降频5.1 MobileNetV2 量化后精度断崖先查数据再查模型MobileNetV2 第一次跑 PTQ 的时候我的 Top-1 从 FP32 的 71% 直接掉到 61%第一反应是模型转换出了问题。后来我没有盲目重转而是按下面这个顺序逐步排查先检查预处理。把 RKNN 配置里的 mean_values 和 std_values 注释掉改用板端 Python 代码手动预处理跑一遍推理看输出是否正常。如果结果有明显改善说明配置重复归一化了。这一步排除了部署侧的预处理问题。再检查校准集。我当时为了省事用了五十张展示图当校准集里面干净的背景图占了大多数而真实业务图像是光照不均、遮挡很多的现场照片。校准集分布和真实分布差异太大量化 min/max 就会偏向不该投影的数据范围。换成现场拍摄的八十多张图后量化精度直接回升了好几个点。最后检查敏感层。即使校准集修好了MobileNetV2 的深层逐通道卷积仍然会有误差累积。工具链会给出每层量化的相关统计信息我看了下 SNR 最低的那几层通常是最后几个 Bottleneck 里的 depthwise conv以及分类头前面的 1x1 卷积。这些层如果支持单独设置为更高精度效果立竿见影。所以遇到量化崩盘优先怀疑输入分布和校准集不要一开始就想重新训练。5.2 遇到不支持的算子模型改写比硬凑 RKNN 配置更省事实验里最有代表性的报错来自 EfficientNetB0 的 Swish 和 DeiT 的 Softmax。RKNN build 直接提示 unsupported op或者转换成功但运行时代码回退到 CPU。第一次遇到时我试图通过修改工具链配置硬凑浪费了不少时间。后面总结出一套更实用的处理思路先接上 ONNX Simplifier把复合算子拆成基础算子很多“伪不兼容”其实是 ONNX 图里某个多余节点造成的。拆开后如果 RKNN 还是不支持就去模型源码里改结构。Swish 可以改成x * sigmoid(x)的组合形式工具链识别 sigmoid 更稳妥h-swish 可以直接换成 ReLU6LayerNorm 如果必须保留我就把它挪到 CPU 执行的 pass-through 分支上。改完结构之后利用原权重重新加载结构变化不大时不需要完全重新训练只做短暂微调即可。核心原则是硬件不支持的算子尽量在模型设计阶段就避开而不是在部署阶段硬扛。对项目周期来说改模型结构比研究工具链底层实现划算得多。5.3 内存抖动与多线程调用 NPU 的注意事项长时间运行的部署服务里内存抖动是最隐蔽的问题。我最早写了个简单的 HTTP 推理服务每个请求进来都重新 load 一次 .rknn 文件再 init_runtime。测功能的时候一切正常连续跑几小时内存就一路爬升直到触发 OOM。原因是加载 RKNN 模型和初始化 runtime 本身会分配大量资源调用结束后也没有被释放干净。正确做法是进程启动时初始化一次之后所有请求复用同一个 RKNNLite 实例。另外同一块 NPU 不建议多线程并发推理。我试过开四个线程同时调inference结果是大量报错和延迟飙升偶尔还会崩溃。NPU 的并发调度由驱动负责应用层强行加线程只会制造锁竞争和资源冲突。稳妥方案是做一个单线程推理循环外部请求通过队列进来NPU 调用始终串行CPU 侧再开线程处理其余任务。我在 RK3568 上最后就是这么做的稳定性和吞吐都让人满意。5.4 NPU 降频与温度连续跑一小时数据会骗人性能测试数据最容易忽略的因素是温度。我有一次测 MobileNetV2刚开始是 20ms连续跑了一个小时之后稳定在 27ms 左右帧率下降了差不多三成。起初以为程序有内存泄漏或者线程堆积查了一圈发现是 SoC 温度升高后触发了降频NPU 和 CPU 的频率都被压低了。排查时我看了系统 thermal 节点的温度读数发现芯片温度已经接近阈值。解决办法是加了一块小散热片和主动风扇温度降下来之后延迟又恢复了。这里给一个实操建议做性能评测的时候一定要先把板子跑热等温度稳定再记录数据否则你记录的很可能是“冷启动性能”和实际长时间运行差很远。优化上也可以控制最大推理帧率比如应用层把视频流限到 25fps给芯片留出喘息空间避免积压后排队的请求全部卡死。6. 按应用场景给选型结论不是精度最高就好是“刚好够用”最好6.1 场景与模型匹配如果做实时视频流分类比如摄像头画面识别、门禁辅助、产线抽检建议以 MobileNetV3 Large 为主力算子替换加微调后延迟 15ms 以内留给前后处理和业务逻辑的空间都很充裕。MobileNetV2 也能用但精度和速度都不如改造后的 V3。如果是离线批量分类比如夜间统一处理一批历史图片ResNet50 的 140ms 单张延迟完全可接受而且它量化稳、算子兼容好适合作为高精度基线。如果还想更高精度可以跑两个模型做投票代价也就是延迟翻倍。如果项目内存极度受限比如只有 1GB 内存的板子MobileNetV2 或 MobileNetV3 Small 会更合适。运行内存增量只有二三十 MB系统留白大后续给业务功能加缓存的心理负担也小。如果项目明确需要 Transformer 的全局建模能力DeiT-Tiny 可以跑但建议先做算子可支持性验证再评估 200ms 以上的延迟能否接受。从我这次实验看它更适合做预研不太适合直接落在实时分类产品里。6.2 我最后留下的部署组合这次实验之后我手头项目最终保留了 MobileNetV3 Large 作为主推理模型改造点就是上面说的 h-swish 替换和后续微调量化校准集用了真实业务图片。单次推理中位数稳定在 14ms 左右配合单线程推理队列加预处理流水线实际服务能扛住实时视频流的需求。ResNet50 则留作离线精校工具每隔一段时间用来复核主模型的分类结果必要时再用它的输出去微调轻量模型。这个组合不算花哨但胜在每一环都少踩坑。最后再分享一条个人体会边缘端模型选型最怕只信论文里的 FLOPs 和框架基准。同一颗芯片固件版本不同、散热不同、校准集分布不同结果可能差出一大截。最稳妥的办法是拿自己的真实数据、在目标板子上完整跑一遍你得到的那个数字才真正属于你的项目。希望这篇 RK3568 图像分类模型部署实验笔记能给你的选型省下一些时间。