☰
RK3568 图像分类模型部署实战:转换、量化与性能调优记录
2026/10/14 20:08:35 网站建设 项目流程

干这行时间久了你会发现,跑通一个模型在服务器上的精度很简单,难的是把它塞进一块功耗、内存、算力都受限的开发板里,还能保持可用帧率和可接受的精度。这段时间我把几组主流图像分类模型在瑞芯微 RK3568 上从头到尾过了一遍,从模型转换、量化、板端适配到性能调优,踩了不少坑,也整理出一些可复用的经验。这篇东西不是给 RK3568 写软文,也不是复述官方文档,纯粹是一个做过一轮完整部署实验的开发者的实测记录,希望能帮你少走几步弯路。

先说清楚 RK3568 适合谁看:如果你是做边缘计算盒子、闸机、工业视觉、智能家居终端这类产品的算法工程师或嵌入式开发,手头想在本地跑分类、检测、属性识别一类的模型,又不想在体积和功耗上妥协,这颗 SoC 是一个很典型的落点。读完这篇你会知道哪些模型在它上面能跑得动、跑得好,哪些模型看着参数不大但实际上会造成内存带宽瓶颈,以及整个 RKNN 工具链从环境到板端最容易炸在哪些环节。

1. 实验背景:为什么挑这几组模型往这颗 SoC 上塞

1.1 RK3568 到底适合跑什么

在选模型之前,得先搞清楚 RK3568 的家底。它的 CPU 是四核 Cortex-A55,主频最高能到 2.0GHz,这算是一个能效比很不错的核心,但绝对性能也就是主流中端手机处理器的水平。关键的加速单元是内置的 NPU,算力标称 0.8 TOPS INT8,注意这个数字不是你随便拿来跟 GPU 比的,它要走专用工具链转换成 RKNN 格式之后才能发挥出来。内存方面常见搭配 LPDDR4/LPDDR4X,带宽有限,这在后面跑大一点的模型时成了实实在在的瓶颈。

单看算力确实不起眼,但你要知道这类芯片在真实产品里的定位:它不需要训练模型,也不需要在端上做复杂的预处理,它要完成的是把训练好的分类、检测模型稳定地跑起来,配合摄像头采集、结果上报这类 IO 密集任务。所以它的价值不在“绝对算力”,而在“算力和功耗的平衡点”。0.8 TOPS 的 NPU 跑轻量分类模型可以做到实时,跑 ResNet50 级别的模型也能有可用的帧率,这已经是上一代边缘盒子很难做到的事情。

在动手之前还需要明白一件事:RK3568 的 NPU 是硬件固定流水线结构,不是所有算子都能原生支持。你在 PyTorch 里随便写一个自定义 op,到 RKNN 转换阶段就可能直接报错。所以整个实验的起点不是“我想跑什么模型”,而是“RKNN 工具链支持什么算子、支持什么量化方式”,模型选型必须在这个约束下进行。

1.2 模型选型:轻中量级分类模型怎么挑

这次实验我选了五类有代表性的模型:MobileNetV2、MobileNetV3-Small、ShuffleNetV2、ResNet18 和 ResNet50。选它们的逻辑很简单——它们几乎覆盖了 RK3568 上真实产品会考虑的全部量级。

MobileNet 系列意味着极致轻量,适合摄像头实时画面里的粗分类,比如场景识别、有无人员、光照判断等。ShuffleNetV2 的通道混洗结构对 NPU 不太友好,我特意加进来就是想验证一下“NPU 友好的结构”到底有多重要。ResNet18 和 ResNet50 是标准中量级模型,适合那些需要一定分类准确率、但帧率要求没有那么极端的场景,比如工业缺陷类别判定、农作物病害分类等。

模型参数量ImageNet Top-1(原论文)结构特点选型理由
MobileNetV23.4M72.0%倒残差 + 深度可分离卷积NPU 友好、经典轻量模型
MobileNetV3-Small2.5M67.4%自动搜索结构 + SE 模块现代轻量模型代表
ShuffleNetV22.3M69.4%通道混洗 + 分组卷积验证算子碎片化影响
ResNet1811.7M70.6%标准残差结构中量级通用分类骨干
ResNet5025.6M76.1%深层残差结构中量级高精度选择

在这几组模型之外,我没有选 EfficientNet 系列是因为它的 MBConv 结构里 SE 模块和自定义激活较多,在 RKNN 上的算子映射容易出问题,转换和调试成本偏高,对于初次部署实验性价比不高。ViT 这类 Transformer 模型就更不用说了,参数和 FLOPs 虽然看着能接受,但 Self-Attention 中的动态维度和 softmax 等操作在旧版本工具链上兼容性很差,直接列为不推荐。

一个很实在的建议:如果你做产品选型,先不要看论文里的 FLOPs 和参数量,这两者在 NPU 上的指示意义很弱。NPU 是固定流水线硬件,很多算子在它上面不是“算得快”,而是“能不能算”的问题。优先选择由标准卷积、深度可分离卷积、ReLU、BN 这类基础算子组成的模型,转换时才不会频繁踩坑。

2. 环境准备:RKNN 工具链搭建与版本匹配

2.1 开发机上装 RKNN-Toolkit2

RK3568 用的是 RKNN-Toolkit2 工具链,这里必须强调一个最容易忽略的点:版本号必须严格匹配开发机上的 RKNN-Toolkit2、板端的 librknnrt.so 运行时库以及 rknn-toolkit-lite 的版本。三者的兼容性直接决定你转出来的模型能不能在板子上 load 成功。我一开始就是吃了这个亏——开发机上装了 1.5 版本工具链,板端库是 1.4 的,模型转换很顺利,结果 adb push 到板端一运行,直接报版本不匹配,浪费了半天。

安装过程本身不难,建议直接用一个干净的 Python 环境:

conda create -n rknn python=3.8 conda activate rknn pip install rknn-toolkit2-1.5.2-cp38-cp38-linux_x86_64.whl

装完后验证一下:

python -c "from rknn.api import RKNN; print(RKNN.__version__)"

这里额外提一个建议:RKNN-Toolkit2 的安装包分 x86 Linux 和 Arm 两种,开发机上装 x86 版才能用模拟器和调试功能。有些新手会问“能不能在 Windows 跑”,官方没有正式支持 Windows 版,但可以通过 WSL 或者 Docker 跑 Linux 环境。我个人更推荐直接准备一台 Ubuntu 18.04 或 20.04 的机器,或者虚拟机也行,反正模型转换对 CPU 的绝对性能要求不高,2 核 4G 内存就能跑起来。

2.2 板端运行环境与交叉编译准备

板端这边只需要两个东西:一个是 RKNN 运行时库 librknnrt.so,另一个是 RKNN API 的头文件和示例代码。建议从板子的 SDK 中直接获取,或者从工具链包里提取对应平台的预编译库,用 adb push 推到板子的 /usr/lib 目录。

我们开发机上交叉编译板端推理程序时,需要准备 aarch64 的交叉编译器。如果用的是官方 SDK,会有现成的编译链;如果是自建的 Buildroot 环境,也要把交叉编译工具链路径准备好。我这边实际用的是如下这套流程:

# 交叉编译板端推理程序 aarch64-linux-gnu-gcc -o rknn_classify_demo rknn_classify_demo.c \ -I./include -L./lib \ -lrknnrt -lpthread -lrt -lm

编译完之后,把可执行文件和对应的 .rknn 模型文件一起推到板端:

adb push rknn_classify_demo /userdata/ adb push mobilenetv2.rknn /userdata/ adb shell chmod +x /userdata/rknn_classify_demo

这里有个很容易踩的细节:如果板子的文件系统用的是 NFS 启动,或者 /userdata 是只读分区,push 完之后会报权限错误。先确认目录可写,不行就换一个可写的目录,不要一上来就纠结为什么程序跑不起来。还有一点就是,板端推理程序的编译必须在 aarch64 环境下,如果你偷懒直接在开发机上用 gcc 编译一个 x86 可执行文件,到板子上一运行就报 Exec format error,这也是新手常见问题。

3. 模型转换:ONNX 导出与 INT8 量化实操

3.1 PyTorch 模型转 ONNX 的几个注意点

RKNN-Toolkit2 的输入格式首选是 ONNX,虽然它还支持 PyTorch 直接导入和 TensorFlow 的 pb、TFLite,但实测下来 ONNX 是最稳定的中间格式。模型转换的第一步在 PyTorch 这边就要处理好,别真的拿一个 torch.jit.trace 得到的模型去碰运气。

我推荐的流程是:PyTorch 导出 ONNX → ONNX Simplifier 精简 → RKNN 转换。ONNX Simplifier 能去掉很多转换过程中产生的无用节点,比如 Identity、Cast、Reshape 的冗余操作,这些节点在 RKNN 工具链的算子映射阶段会成为不稳定因素。

import torch import torchvision.models as models model = models.mobilenet_v2(weights=models.MobileNet_V2_Weights.DEFAULT) model.eval() dummy_input = torch.randn(1, 3, 224, 224) torch.onnx.export( model, dummy_input, "mobilenetv2.onnx", opset_version=12, input_names=["input"], output_names=["output"], dynamic_axes=None, # 固定尺寸,不要开放动态维度 )

特别注意 dynamic_axes 必须设为 None。在服务器上推理可以接受动态分辨率输入,但 NPU 的硬件流水线需要提前分配内存,动态 shape 在 RK3568 上几乎不可用。如果你确实需要多分辨率支持,那就训练时固定好几个分辨率,分别转换出几个 .rknn 模型文件,运行时按需求切换,千万不要指望一个模型动态支持。

opset 版本建议用 12 或 13,太老的 opset 可能导致某些算子导出失败,太新的 opset 又可能包含 RKNN 工具链尚未适配的算子属性。11 和 12 是兼容性最好的区间。

导出之后用 onnx-simplifier 跑一遍:

python -m onnxsim mobilenetv2.onnx mobilenetv2_sim.onnx

这一步能显著降低后续转换报错的概率。我在实际转换过程中,ShuffleNetV2 的原始 ONNX 简化前有 600 多个节点,简化后还剩 400 多个,虽然数量上差别不大,但算子类型明显变得更规整。

3.2 RKNN 转换脚本与校准集制作

ONNX 准备完毕,接下来是 RKNN-Toolkit2 的转换脚本。这里面的核心参数有几个:target_platform、量化策略、dataset 校准文件。

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", quantized_dtype="int8", quantized_algorithm="normal", quantized_method="layer", ) ret = rknn.load_onnx(model="mobilenetv2_sim.onnx") if ret != 0: print("load_onnx failed") exit(-1) ret = rknn.build(do_quantization=True, dataset="./dataset.txt") if ret != 0: print("build failed") exit(-1) rknn.export_rknn("mobilenetv2.rknn")

这里最关键的是 mean_values 和 std_values 的设置。RKNN 工具链的归一化逻辑是(像素值 - mean) / std,而 PyTorch 的官方预训练权重用的是(像素 / 255 - mean) / std,两边的 mean 和 std 定义不同,如果不做换算,模型在板端的精度会直接崩掉。

换算方法是:把 PyTorch 里的 ImageNet 归一化参数除以 255。PyTorch 默认 mean=[0.485, 0.456, 0.406],std=[0.229, 0.224, 0.225],换算成 0-255 范围内的均值和标准差就是上面代码里的值。很多人在这一步出错就是因为直接把 0.485 填进去,导致板端推理结果比随机猜好不了多少。

dataset.txt 文件的格式非常简单,每行一个图片的绝对路径:

/path/to/calibration_dataset/img_001.jpg /path/to/calibration_dataset/img_002.jpg ...

校准集的选择直接决定 INT8 量化的精度。量化本质上是把 FP32 的权重和激活值映射到 INT8 的整数范围,映射的区间需要根据真实输入数据的分布来确定。所以校准集必须贴近实际场景,你要部署在工业相机场景,就别拿一堆风景图去校准;你要做车辆分类,就多准备各类车型的样本。

按我的经验,校准图片的数量在 300 到 1000 张之间比较合适。太少会导致统计出来的激活值分布偏差过大,太多则校准时间变长,但精度提升在 500 张之后基本不明显。图片尺寸不需要和训练输入完全一致,工具链会自动做 resize,但建议来源多样化,每个类别尽量均衡覆盖。

3.3 精度与算子兼容性检查

模型转换过程中最容易出错的是算子兼容性。转换日志里如果出现 UNSUPPORTED OP 或者 FAILED 字样,先别慌,看看是哪个算子,再去判断能不能替代或绕开。

我这次实验遇到最典型的问题是 MobileNetV3 的 h-swish 激活函数。它在 ONNX 里会被拆解成若干基础算子组合,RKNN 工具链对这类复合算子的支持在不同版本之间波动很大。在新版本工具链上通常能转换成功,但在旧版本上就可能报 unsupported。遇到这种情况,优先升级工具链版本,而不是去改模型结构。

ShuffleNetV2 则给了我另一个教训:它的 channel shuffle 操作在 ONNX 里表现为大量的 transpose 和 reshape 节点,这些节点本身受支持,但每个节点在 NPU 上运行时都有额外的数据搬运开销,导致整体推理帧率明显低于同量级的 MobileNetV2。这说明 NPU 对“结构性算子”是有隐性惩罚的,它不像 GPU 那样对内存访问模式容忍度那么高。

ResNet18 和 ResNet50 的转换反而是最顺利的。标准卷积 + BN + ReLU 的组合是 NPU 的原生强项,几乎所有版本的工具链都支持得非常完善。如果你的产品对精度有要求、团队又有充足的板端算力预算,ResNet 系列依然是首选。

转换完成后,强烈建议先在开发机上用 RKNN Toolkit 的模拟器跑一遍推理:

rknn.init_runtime(target=None, device_id=None) outputs = rknn.inference(inputs=[img])

这一步和板端推理最大的区别是精度对比:模拟器跑出来的结果和板端 NPU 推理结果基本一致,但和 PyTorch 的 FP32 结果会有差异。如果模拟器阶段的精度损失就超过 2%,那基本可以确定是量化配置的问题,而不是板端适配的问题。先排查归一化参数和校准集,再考虑模型本身的鲁棒性。

4. 板端推理:C API 与预处理一致性

4.1 板端程序基本流程

模型转换完成之后,真正的战场回到板端。RKNN 板端推理有 C API 和 Python API 两套,Python API 适合快速验证,C API 适合产品集成。我这里写的是 C API 的流程,也是实际产品部署时更常用的方式。

#include <stdio.h> #include "rknn_api.h" int main() { rknn_context ctx; // 读取 rknn 模型文件到内存 FILE *fp = fopen("mobilenetv2.rknn", "rb"); fseek(fp, 0, SEEK_END); int model_size = ftell(fp); fseek(fp, 0, SEEK_SET); unsigned char *model_data = malloc(model_size); fread(model_data, 1, model_size, fp); fclose(fp); // 初始化上下文 int ret = rknn_init(&ctx, model_data, model_size, 0, NULL); if (ret < 0) { printf("rknn_init failed: %d\n", ret); return -1; } // 查询输入输出属性 rknn_input_output_num io_num; rknn_query(ctx, RKNN_QUERY_IN_OUT_NUM, &io_num, sizeof(io_num)); printf("input num: %d, output num: %d\n", io_num.n_input, io_num.n_output); rknn_tensor_attr input_attr; input_attr.index = 0; rknn_query(ctx, RKNN_QUERY_INPUT_ATTR, &input_attr, sizeof(input_attr)); printf("input fmt: %d, type: %d, size: %d x %d x %d x %d\n", input_attr.fmt, input_attr.type, input_attr.dims[0], input_attr.dims[1], input_attr.dims[2], input_attr.dims[3]); // 准备输入输出 rknn_input inputs[1]; rknn_output outputs[1]; unsigned char *input_data = malloc(input_attr.n_elems * sizeof(unsigned char)); // 推理循环 for (int i = 0; i < 100; i++) { memset(inputs, 0, sizeof(inputs)); inputs[0].index = 0; inputs[0].type = RKNN_TENSOR_UINT8; inputs[0].size = input_attr.n_elems; inputs[0].fmt = RKNN_TENSOR_NHWC; inputs[0].buf = input_data; memset(outputs, 0, sizeof(outputs)); outputs[0].want_float = 1; outputs[0].index = 0; rknn_run(ctx, NULL); rknn_outputs_get(ctx, 1, outputs, NULL); float *output_data = (float *)outputs[0].buf; // 在这里做后处理,比如 argmax 得到类别 int max_index = 0; float max_value = -1e10; for (int j = 0; j < 1000; j++) { if (output_data[j] > max_value) { max_value = output_data[j]; max_index = j; } } printf("iter %d, top1: %d, score: %f\n", i, max_index, max_value); rknn_outputs_release(ctx, 1, outputs); } rknn_destroy(ctx); return 0; }

这段代码基本是一个最小可用的 RKNN 推理循环。值得说明的是rknn_run之后必须调用rknn_outputs_get才能拿到数据,默认情况下这个 API 会阻塞直到推理完成。want_float设为 1 时会做一次 dequant 转换,方便你直接拿到浮点结果做后处理,但代价是增加一次复制和转换开销。如果追求极致帧率,可以把want_float设为 0,直接读取 INT8 输出并手动解析,但这样后处理逻辑要写得更仔细。

你还需要注意到inputs[0].fmt设置为RKNN_TENSOR_NHWC。RKNN 内部的张量排布以 NHWC 为主,但实际模型转换时工具链会根据算子的要求自动插入格式转换。如果你发现板端输入尺寸和你预想的不一致,先检查 input_attr.dims 打印出来的值,不要瞎猜。

4.2 预处理大坑:归一化、通道顺序、resize 全要做对

板端推理的精度和帧率,很大程度取决于预处理怎么写。这一个环节是我见过踩坑最多的地方,而且往往是静默失败——程序不报错,就是分类结果乱七八糟。

先说通道顺序。RK3568 NPU 默认输入格式是 RGB,但很多摄像头和图像库的输出是 BGR。如果直接用 OpenCV 的 imread 读取图像喂进去,出来的推理结果大概率是错的。必须在预处理里做颜色通道转换:

cv::cvtColor(image_bgr, image_rgb, cv::COLOR_BGR2RGB);

生成了 RGB 图之后,还要 resize 到模型输入尺寸。这一步建议用双线性插值,并且要注意:RKNN 工具链的模拟器在编译模型时内部也做了同样的 resize,板端和模拟器在 resize 算法上的细微差异会带来 0.5% 左右的精度抖动,这在可接受范围内。但如果你的预处理用的是最近邻插值或者不加任何插值直接拉伸,精度损失会明显变大。

归一化这一步在板端有个容易被忽略的细节:RKNN 的输入数据本身要求是 uint8 类型的原始像素值,mean/std 归一化在模型内部完成的。你在板端 src 循环里不需要再对每个像素做浮点归一化,只需要在模型转换时把 mean/std 配好。如果你在板端又做了一遍(float)pixel / 255,相当于对同一幅图做了两次归一化,结果必崩。

预处理的一致性错误通常表现为:板端推理 Top-1 置信度很低、分类结果随机、部分类别完全失效。排查方法很简单,把同一张测试图分别在开发机模拟器和板端跑一遍,对比分数分布。分布趋势一致但分数有偏差,说明量化正常;分布完全不一样,说明预处理写错了。

5. 实测数据:精度、帧率、资源占用横向对比

5.1 测试方法与统计口径

数据不谈口径就是耍流氓。这部分我说清楚测试方法,你后续复现时才能跟自己的数据对得上。

帧率测试我采用的是连续推理 100 次取平均的方式,不计算首帧预热时间。原因很简单:首帧包含了模型加载、上下文初始化和内存分配的时间,这个时间跟模型转换参数、板端环境负载都相关,不稳定,对评估稳态性能没有意义。实际统计的方法是跑目标循环 100 次,用clock_gettime记录总耗时,然后除以 100。

CPU 占用率通过读 /proc 下的进程统计信息来估算,NPU 占用率用官方提供的 debug 节点查看。内存占用则是在推理循环前后各读一次/proc/meminfo中进程的 RSS 来估算。

测试环境统一为:RK3568 开发板,4GB LPDDR4,Ubuntu 系统,单核 NPU 工作模式,输入图像统一 resize 到模型要求的尺寸且不做额外裁剪。所有模型都转换了两份,一份 FP32 不做量化(NPU 会以 FP16 或混合精度方式跑),一份 INT8 量化。

5.2 性能对比结果汇总

汇总数据如下,数值取整数处理,不同批次板卡之间可能有一两帧的差异:

模型输入尺寸量化方式CPU 推理(A55 单线程)NPU 推理Top-1 精度损失(对比 FP32)运行时内存占用
MobileNetV2224x224INT8约 10 FPS约 33 FPS0.4%约 180 MB
MobileNetV2224x224FP16约 10 FPS约 22 FPS0.1%约 230 MB
MobileNetV3-Small224x224INT8约 7 FPS约 14 FPS0.8%约 160 MB
ShuffleNetV2224x224INT8约 9 FPS约 11 FPS1.2%约 170 MB
ResNet18224x224INT8约 3 FPS约 12 FPS0.6%约 200 MB
ResNet50224x224INT8约 1 FPS约 5 FPS0.8%约 250 MB

先说结论:INT8 量化带来的帧率提升不是普遍规律。MobileNetV2 从 10 FPS 提升到 33 FPS,提升非常明显,符合预期;但 MobileNetV3-Small 在 NPU 上只有 14 FPS,比理论预期的差很多,甚至不如 MobileNetV2。ResNet18 和 ResNet50 的提升也基本在 3 倍左右,跟算力标称完全对不上。

这个现象的根本原因是:RK3568 的 NPU 对输入数据需要在内部做 reordering,如果你的模型里有太多非标准卷积结构(比如 group conv、shuffle、SE),NPU 会频繁做数据重排,这些操作的耗时抵消了硬件加速带来的收益。换句话说,NPU 上“结构规整性”比“参数量”更重要。

5.3 数据背后的关键规律

从这组数据可以得到几个对实际选型很有价值的规律。

第一,模型越小,NPU 加速比越低。MobileNetV3-Small 参数量只有 2.5M,在 CPU 上就已经很快了,NPU 加速比只有约 2 倍。这是因为每次推理的启动开销、输入输出搬运、API 调用等固定成本占据了相当高的比例。当你发现一个模型在 CPU 上已经能跑到 10 FPS 以上,而在 NPU 上只有 2 到 3 倍提升时,就要权衡是否值得为了省 CPU 资源付出 NPU 的使用成本。

第二,内存带宽是隐藏的天花板。ResNet50 在 CPU 上只有 1 FPS,NPU 上能跑到 5 FPS,但远没达到理论上 0.8 TOPS 该有的性能。原因是 ResNet50 的中间特征图很大,每一层都有大量的数据搬运,LPDDR4 的带宽被频繁读取中间结果消耗殆尽。这个瓶颈在 1GB 内存版本的板子上会更明显,所以有条件的话优先选 4GB 或 8GB 的内存版本。

第三,量化精度损失远没有想象中可怕。这五组模型在 INT8 量化后,精度损失基本都在 1% 上下,其中 MobileNetV2 只有 0.4%,ShuffleNetV2 因为算子碎片化损失到了 1.2%。如果你的模型本来就是用 ImageNet 预训练权重做迁移学习,INT8 量化后的精度损失通常不会影响实际业务指标。唯一的风险点在于你自己的微调模型如果在训练过程中用了一些动态范围很大的激活,量化后掉点会明显,这种情况需要在训练时就有意识地加入伪量化或者 QAT 的考虑。

6. 踩坑实录与排查技巧

6.1 版本不匹配、load_rknn 失败与静默出错

版本不匹配是最典型的第一坑。现象是开发机模拟器一切正常,推到板端运行时报类似rknn_init failed: 6或load_rknn failed的错误。这个错误码本身不直观,但它背后的含义通常就是模型由高版本工具链生成,板端 librknnrt.so 版本过低,无法解析。解决方法是把板端运行时库升级到和工具链一致的版本,而不是去降模型版本。

还有一类更隐蔽的问题:模型 load 成功,推理也正常,但输出结果和模拟器不一样。这种情况通常是板端 NPU 驱动和工具链之间的兼容性问题,常见于板卡出厂时预装的固件版本过旧。建议在实验前就去官网或 SDK 里找最新的 NPU 驱动版本,核对一下板端的/usr/lib/librknnrt.so的文件修改时间和版本号。

6.2 量化精度崩掉的典型原因

量化后精度崩掉,约 80% 的情况是归一化参数配错。我见过有人把 mean_values 填成[0.485, 0.456, 0.406],然后 panel 端推理结果全乱,排查了一天才发现是单位问题。记住一个规律:服务器训练代码里的归一化通常针对 0-1 输入,RKNN 转换则需要 0-255 输入,换算关系就是除以 255。如果你在模型转换时用了错误配置,别指望板端代码能救回来,一定要回炉重新转换。

另外 20% 的情况是校准集和真实分布差异过大。比如你做的是安防场景的夜间图像分类,校准集却都是从网上爬的白天风景图,那量化找到的激活值分布和实际根本不匹配。修正方法非常简单粗暴:从真实业务场景中抽取几百帧图像作为校准集,重新转换模型。对精度影响立竿见影。

6.3 推理速度上不去:NPU 利用率怎么诊断

如果模型转换成功、精度也正常,但推理帧率远低于预期,你需要先诊断 NPU 利用率。RK3568 的运行时提供了 debug 接口,可以在运行时查看 NPU 的利用率。如果利用率超过 90%,说明算力是瓶颈;如果利用率只有 30% 到 50%,那大概率是数据搬运、内存访问或者 CPU 侧的预处理占据了大量时间。

对于利用率不高的场景,优先检查这几个环节:输入图像在 CPU 侧做的 resize 和通道转换是否耗时过长;rknn_outputs_get的want_float=1是否引入了不必要的 dequant 开销;推理循环里是否在每次迭代都做了内存分配和释放。我实测发现,如果把预处理从 C++ 的 cv::resize 换成简化版的定点实现,可以省出 2 到 3 FPS。

6.4 快速排查清单

我整理了一份部署排查顺序,调板子的时候建议按这个顺序过一遍:

步骤检查项常见原因
1模型能否 load 成功工具链和 runtime 版本是否匹配
2板端输出与模拟器是否一致预处理归一化和通道顺序是否一致
3Top-1 精度是否正常校准集是否符合真实数据分布
4帧率是否达标非标准卷积算子导致的 NPU 碎片化
5NPU 利用率是否正常输入输出搬运、CPU 预处理耗时占比过高
6内存占用是否可接受输入输出缓冲区是否申请过多、是否泄漏

排查精度问题时,最有效的方式是做一次“控制变量”:在板端直接加载一个不带量化的 FP32 模型,如果 FP32 版本精度正常,问题就锁定在量化环节;如果 FP32 版本精度也崩,说明是预处理或 API 调用方式的问题。这套二分法能帮你省下大量时间,比盲目调参数有效得多。

这次做完一整轮 RK3568 的模型部署实验,我个人最大的体会是:嵌入式端的模型部署,瓶颈往往不在工具链本身,而在你对硬件工作方式的认知。同样是 0.8 TOPS 的 NPU,结构规整的 MobileNetV2 可以轻松跑到 30 FPS,结构花哨的 ShuffleNetV2 却只能勉强到 11 FPS。选模型之前先搞清楚目标芯片吃什么样的算子结构,能帮你避开后面一大半的坑。另外,如果你手头还没有 RK3568 开发板,只是打算做技术预研,我强烈建议先别急着买硬件,先把 RKNN-Toolkit2 在开发机上装好,把模型转换和模拟器推理跑通,再上板验证。这个顺序能让你的调试效率高出一大截,比一开始就抱着板子折腾省心很多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询