1. 项目缘起与整体规划
1.1 为什么选择 RK3588 加 YOLOv5s 这套组合
手里这块 RK3588 开发板到手已经有一阵子了,一直想找个完整的项目把它从“点亮屏幕”推进到“跑通一个真实可用的视觉任务”。选来选去,最终定下了YOLOv5s 目标检测这个方向。原因很直接:YOLOv5s 是 YOLO 系列里轻量化和精度平衡得相当好的一档,模型体量小、结构规整、社区资料多,非常适合拿来吃透一颗边缘 NPU 的完整部署链路。而 RK3588 自带 6 TOPS 算力的 NPU,官方又提供了 RKNN 这套工具链,理论上完全能把这个模型跑到实时级别。
但“理论上能跑”和“实际跑起来”之间,隔着一条相当长的沟。我见过太多人卡在中间某一环——要么模型转换报错,要么量化后精度崩了,要么板子上推理速度远低于预期。所以这次我不打算只写一个“成功截图”,而是把从环境搭建、模型导出、量化转换、板端部署到性能调优的全链路完整记录下来,包括我踩过的每一个坑。
这套内容适合谁看?如果你手里有 RK3588 或类似的瑞芯微 NPU 平台,想跑通一个真实的目标检测模型,那这篇就是为你写的。如果你只是想了解边缘 AI 部署的一般流程,也能从中看到量化、算子兼容、内存布局这些通用问题的处理思路。我会尽量把每一步的“为什么”讲清楚,而不是只丢命令。
1.2 全链路拆成哪几个阶段
在动手之前,我先把整条链路在脑子里过了一遍,拆成五个阶段,后面每一篇基本对应一个阶段:
| 阶段 | 核心任务 | 关键产出 | 主要风险点 |
|---|---|---|---|
| 环境准备 | 搭建 PC 端转换环境与板端运行环境 | 可用的 RKNN Toolkit2、板端 NPU 驱动 | 版本不匹配、依赖冲突 |
| 模型导出 | PyTorch 权重转 ONNX | 结构干净的 ONNX 模型 | 动态轴、算子不支持 |
| 量化转换 | ONNX 转 RKNN,做 INT8 量化 | 可加载的 .rknn 模型 | 量化精度损失、校准集选择 |
| 板端部署 | 加载模型、预处理、推理、后处理 | 可运行的推理程序 | 输入格式、内存拷贝开销 |
| 性能调优 | 测速、定位瓶颈、优化 | 稳定的帧率与延迟数据 | 预处理成瓶颈、零拷贝未启用 |
这个拆法的逻辑是:每一阶段的输出都是下一阶段的输入,任何一环出问题都会在后面被放大。比如 ONNX 导出时如果带了动态 batch 轴,量化阶段就可能直接失败;校准集如果选得偏离真实场景,INT8 量化后的精度会掉得很难看。所以我会在每一阶段都强调“交付物是否干净”,而不是急着往下走。
提示:不要跳过环境准备直接抄转换命令。RKNN Toolkit2 对 Python 版本、ONNX 版本、torch 版本都有比较敏感的对应关系,版本错一个,后面全是玄学报错。
1.3 硬件与软件的基础盘点
先把这次用到的家底列清楚,方便你对照自己的环境。
硬件方面,核心是一块 RK3588 开发板,8 核 CPU(4 个 A76 大核加 4 个 A55 小核),Mali-G610 GPU,以及那颗 6 TOPS 的 NPU。内存我选的是 8GB 版本,因为后面做视频解码加推理,内存余量要留够。存储用的 eMMC,系统跑 Ubuntu 20.04。摄像头先用 USB 摄像头做验证,后面再考虑 MIPI 接入。
软件方面,PC 端我用的是 Ubuntu 20.04 虚拟机,Python 3.8,这是 RKNN Toolkit2 官方支持比较稳的版本。板端同样是 Ubuntu 20.04,需要确认 NPU 驱动版本和 rknn_server 是否正常。这里有个容易被忽略的点:PC 端转换工具链的版本和板端运行库的版本要能对上,否则模型加载时会报版本不兼容。
我特意没有一上来就追求最新版本,而是选了官方文档里标注为稳定组合的那一套。边缘部署这件事,稳定比新更重要,因为一旦出问题,你很难判断是模型的问题还是版本的问题。
2. 环境搭建与工具链选型
2.1 PC 端转换环境怎么搭才不翻车
PC 端的核心工具是RKNN Toolkit2,它负责把 ONNX 模型转成 RKNN 格式并完成量化。安装它之前,我建议先用 conda 建一个独立环境,别和系统 Python 混在一起。原因很简单:它依赖特定版本的 numpy、onnx、torch,一旦和系统里其他项目的依赖打架,排查成本极高。
conda create -n rknn python=3.8 conda activate rknn pip install numpy==1.21.6 onnx==1.12.0 onnxruntime==1.12.0 pip install torch==1.10.0 torchvision==0.11.0装完基础依赖,再去拿 RKNN Toolkit2 的 whl 包。这里要注意,不同版本的 toolkit 对应的板端 runtime 版本是不一样的,我用的是一套经过验证的组合,转换和运行都稳定。装完之后跑一个简单的 import 测试,确认没有报错再往下走。
from rknn.api import RKNN print("RKNN Toolkit2 loaded")如果这一步就报错,八成是 numpy 或 onnx 版本不对。我踩过一次坑,系统里预装的 numpy 版本太新,导致 toolkit 加载时直接段错误,换成 1.21.6 之后立刻正常。所以依赖版本这件事,宁可照着官方推荐表一个个对,也别图省事。
2.2 板端运行环境的关键检查项
板端这边,系统起来之后第一件事是确认 NPU 是否被正确识别。可以查一下相关的设备节点和驱动版本,确保 runtime 库和 PC 端 toolkit 是配套的。然后确认 rknn_server 是否在运行,这个服务负责在板端加载和执行模型。
# 查看 NPU 相关设备节点 ls /dev/ | grep rknpu # 查看驱动版本 cat /sys/kernel/debug/rknpu/version如果设备节点不存在,说明内核里的 NPU 驱动没起来,这时候再怎么折腾模型都没用。我遇到过一种情况:系统镜像刷的是通用版,NPU 驱动没编进去,结果所有推理程序都报“找不到设备”。解决办法是换一个带完整 NPU 支持的系统镜像,或者自己确认驱动模块已加载。
另外,板端的 Python 环境也要装好 numpy 和 opencv,因为预处理和后处理会用到。这里我建议板端尽量用系统自带的 Python 和库,别搞太复杂的虚拟环境,减少变量。
2.3 工具链版本对应关系速查
版本对应是这套流程里最容易被低估的坑。我整理了一张对照表,把关键组件的版本关系列出来,方便你排查。
| 组件 | 我的选择 | 说明 |
|---|---|---|
| Python | 3.8 | toolkit 支持最稳的版本 |
| ONNX | 1.12.0 | 导出与解析兼容性好 |
| onnxruntime | 1.12.0 | 用于量化校准推理 |
| RKNN Toolkit2 | 与板端 runtime 配套 | 转换工具 |
| 板端 runtime | 与 toolkit 配套 | 运行库 |
| NPU 驱动 | 系统镜像自带 | 决定设备能否识别 |
注意:这张表里的版本不是唯一解,但它们是互相验证过能跑通的一组。如果你换了其中一个,最好确认它和相邻组件的兼容性,别单独升级某一个。
3. YOLOv5s 模型导出与结构处理
3.1 从 PyTorch 权重到 ONNX 的正确姿势
模型导出的目标很明确:得到一个结构干净、算子常规、输入输出明确的 ONNX 文件。YOLOv5 官方仓库自带 export.py,可以直接导出 ONNX,但默认导出往往带一些对 NPU 不友好的东西,比如动态轴、额外的输出分支。
我导出时主要做了三件事。第一,固定输入尺寸为 640x640,因为 NPU 对固定 shape 的支持最好,动态 shape 会带来额外的编译和内存开销。第二,把 opset 设成 12,这个版本对常见算子的表达比较成熟,转换工具也吃得比较顺。第三,简化模型结构,去掉训练相关的分支,只保留推理需要的输出。
python export.py --weights yolov5s.pt --include onnx --img 640 --batch 1 --opset 12导出之后别急着转换,先用 onnxruntime 跑一遍,确认输出 shape 和数值正常。这一步是“体检”,能提前发现导出阶段的问题。
import onnxruntime as ort sess = ort.InferenceSession("yolov5s.onnx") print([o.shape for o in sess.get_outputs()])3.2 输出层结构为什么要改
YOLOv5 原始输出是三个不同尺度的特征图,每个特征图上带着边界框、置信度和类别信息。这种结构在 GPU 上后处理很方便,但在 NPU 上,后处理如果放在模型里,会引入大量 NPU 不擅长的算子,比如复杂的 reshape、transpose、非极大值抑制。
我的做法是把后处理从模型里剥离出来,让模型只输出三个尺度的原始特征图,后处理放到 CPU 上用 C++ 或 Python 实现。这样做的好处是模型结构变得非常规整,转换成功率高,量化也更稳定。代价是 CPU 要承担一部分计算,但实测下来这部分开销可控,而且换来了更高的部署成功率。
具体来说,导出的 ONNX 输出是三个张量,形状大致是 [1, 255, 80, 80]、[1, 255, 40, 40]、[1, 255, 20, 20]。后处理阶段再对这些张量做解码和 NMS。
3.3 导出阶段的常见报错与处理
导出阶段我遇到过几类典型问题,列出来供你对照。
第一类是opset 版本不匹配,报错信息里会出现不支持的算子。解决办法是把 opset 调到工具链支持的范围内,我用的 12 比较稳。
第二类是动态轴残留,导出的 ONNX 里 batch 维还是 dynamic。这会导致量化阶段报错。解决办法是在导出时显式指定 batch 1,并在导出后用工具检查一遍输入输出是否都是静态 shape。
第三类是权重里带了训练态节点,比如 dropout 或 BN 的训练分支。导出前要确保模型处于 eval 模式,否则会多出一些无意义的节点。
实操心得:导出后养成一个习惯,用 netron 这类工具把 ONNX 结构可视化看一遍。很多问题在图上一眼就能看出来,比对着报错猜快得多。
4. INT8 量化转换的核心细节
4.1 为什么要做 INT8 量化
这是整个项目里最值得展开讲的一环。RK3588 的 NPU 对 INT8 有专门的加速支持,算力标称的 6 TOPS 基本是按 INT8 算的。如果模型跑 FP16,速度会明显下降;如果跑 FP32,NPU 甚至可能直接不支持或者回退到 CPU,那就完全失去意义了。
所以INT8 量化不是可选项,而是发挥 NPU 性能的前提。但量化会带来精度损失,这是它的代价。量化的本质是把浮点权重和激活值映射到 8 位整数区间,用一个缩放因子和零点来表示。映射过程中信息必然有损,关键在于损失是否可控。
我一开始也担心量化后检测效果崩掉,实测下来,只要校准集选得合理,YOLOv5s 量化后的 mAP 下降通常在可接受范围内,肉眼几乎看不出差别。
4.2 校准集怎么选才靠谱
量化分两步:先统计激活值的分布范围,再据此确定缩放参数。这个统计过程依赖校准集。校准集选得好不好,直接决定量化精度。
我的经验是,校准集要满足三个条件。第一,数量适中,一般 100 到 300 张就够,太多没必要,太少统计不准。第二,覆盖真实场景,如果实际部署是检测街上的车和行人,校准集就该用类似的图,别拿一堆室内静物去校准。第三,多样性足够,光照、角度、目标大小都要有变化。
# 校准集准备:把图片路径写进一个 txt 文件 # 每行一个路径,供 toolkit 读取 rknn.config( mean_values=[[0, 0, 0]], std_values=[[255, 255, 255]], target_platform='rk3588', quantized_dtype='asymmetric_quantized-8' ) rknn.load_onnx(model='yolov5s.onnx') rknn.build(do_quantization=True, dataset='calib.txt')这里有个细节:mean 和 std 的设置要和训练时一致。YOLOv5 训练时输入是归一化到 0 到 1 的,所以我用 std 255 来做归一化。如果这里设错,量化后的数值分布会整体偏移,精度直接崩。
4.3 量化参数与精度权衡
量化配置里有几个参数值得说。quantized_dtype我选的是非对称 8 位量化,它对激活值分布不均匀的情况适应性更好。对称量化实现简单但精度略差,非对称多存一个零点,换来更细的映射。
另外,工具链支持混合量化,也就是对精度敏感的层保留 FP16,其余层用 INT8。这是个很好的折中手段。如果发现量化后某一层误差特别大,可以把它单独拎出来不量化。但混合量化会让模型里同时存在两种精度的算子,可能影响推理效率,所以要权衡。
| 量化方式 | 精度 | 速度 | 适用场景 |
|---|---|---|---|
| FP32 | 最高 | 最慢 | 精度验证基准 |
| FP16 | 高 | 中等 | 精度敏感、速度要求不高 |
| INT8 | 略降 | 最快 | 追求实时性能 |
| 混合量化 | 接近 FP16 | 接近 INT8 | 个别层精度敏感 |
我的建议是先用全 INT8 跑一遍,看精度掉多少。如果可接受就直接用;如果掉得厉害,再针对性地把问题层改成 FP16。
4.4 转换报错排查思路
转换阶段报错信息往往比较晦涩,我总结了一套排查顺序。
先看是不是算子不支持。RKNN 对 ONNX 算子的支持是有限的,遇到不支持的算子会明确报出来。这时候要么换等价算子,要么把这段逻辑挪到后处理。
再看是不是shape 问题。动态 shape、维度不匹配都会导致转换失败。用 netron 确认每一层的输入输出维度是否自洽。
最后看是不是校准集问题。如果校准集路径写错、图片读不出来,build 阶段会失败或产出异常模型。
提示:转换日志一定要完整看,别只看最后一行报错。真正的线索往往在中间几行,比如“unsupported op”或者“shape mismatch”。
5. 板端部署与推理流程
5.1 模型加载与初始化
板端部署的第一步是把 .rknn 模型加载进来,初始化 runtime。这一步的关键是确认 runtime 版本和模型版本匹配,否则加载会失败。
from rknnlite.api import RKNNLite rknn = RKNNLite() ret = rknn.load_rknn('yolov5s.rknn') ret = rknn.init_runtime(core_mask=RKNNLite.NPU_CORE_0)这里有个值得注意的点:RK3588 有三个 NPU 核心,可以通过 core_mask 指定用哪个或哪几个。单模型推理一般用一个核心就够,多模型并行时可以把它们分到不同核心上,避免互相抢占。
初始化完成后,建议先跑一次空推理,确认模型能正常执行,再接入真实数据。
5.2 预处理与后处理的实现要点
预处理这块,YOLOv5 要求输入是 640x640 的 RGB 图,归一化到 0 到 1。板端从摄像头拿到的是 BGR 格式,需要做颜色空间转换和 resize。这里有个性能陷阱:如果预处理用 Python 逐像素做,会非常慢,成为整个流程的瓶颈。
我的做法是用 OpenCV 的向量化操作完成 resize 和颜色转换,再转成模型需要的 NHWC 或 NCHW 布局。实测下来,用 OpenCV 处理一帧 640x640 的图,耗时在几毫秒级别,可以接受。
后处理就是前面说的解码加 NMS。三个尺度的特征图分别解码出边界框,再统一做非极大值抑制,过滤掉重叠的框。这部分逻辑不复杂,但要注意数值精度和阈值设置,置信度阈值和 NMS 阈值要根据实际场景调。
5.3 一次完整推理的耗时拆解
我把一次完整推理拆成四段来测:预处理、NPU 推理、后处理、结果绘制。这样能清楚看到瓶颈在哪。
| 阶段 | 耗时(毫秒) | 说明 |
|---|---|---|
| 预处理 | 约 5 | resize、颜色转换、归一化 |
| NPU 推理 | 约 15 | INT8 量化模型 |
| 后处理 | 约 8 | 解码加 NMS |
| 绘制 | 约 3 | 画框和标签 |
从数据看,NPU 推理本身很快,反而是预处理和后处理占了不小比例。这印证了一个常见结论:在边缘设备上,模型推理往往不是瓶颈,数据搬运和前后处理才是。所以优化时不能只盯着模型,要把整条流水线一起看。
5.4 零拷贝与内存优化
RK3588 的 NPU 支持零拷贝,也就是输入输出数据可以直接在 NPU 可访问的内存里操作,省去一次内存拷贝。默认情况下,数据要先从普通内存拷到 NPU 内存,这个拷贝在数据量大时开销明显。
启用零拷贝需要模型输入输出用特定的内存类型,代码上要配合。我实测下来,启用后单帧耗时能降几毫秒,对于追求高帧率的场景值得做。但零拷贝对内存对齐有要求,处理不当会报错,所以要按官方示例来。
实操心得:优化顺序建议是“先跑通,再测速,最后优化”。别一上来就纠结零拷贝,先把整条链路跑通,拿到基准数据,再针对性优化,否则很容易在细节里迷失。
6. 常见问题与排查实录
6.1 转换与加载类问题速查
| 现象 | 可能原因 | 处理方式 |
|---|---|---|
| 转换报 unsupported op | 算子不被支持 | 换等价算子或移到后处理 |
| 加载模型失败 | 版本不匹配 | 对齐 toolkit 与 runtime 版本 |
| 找不到 NPU 设备 | 驱动未加载 | 换带 NPU 支持的系统镜像 |
| 量化后精度崩 | 校准集不合理 | 换贴近真实场景的校准集 |
| 推理结果全错 | 预处理格式不对 | 检查颜色空间与归一化 |
这张表是我实际踩坑后整理的,基本覆盖了八成以上的常见问题。遇到问题先对照这张表,能省不少时间。
6.2 精度掉点的定位方法
量化后精度掉点是最让人头疼的问题,因为它不像报错那样有明确提示。我的定位方法是逐层对比:用同一张图,分别跑 FP32 模型和 INT8 模型,对比中间层输出,找出误差最大的那一层。
找到问题层之后,有两个处理方向。一是把这一层改成 FP16,用混合量化保住精度。二是检查这一层的输入分布,看是不是校准集没覆盖到这种分布。多数情况下,换一个更有代表性的校准集就能明显改善。
还有一种情况是后处理阈值没调。量化后置信度分布会整体偏移,原来 0.25 的阈值可能就不合适了,需要重新调。这个很容易被忽略,但影响很大。
6.3 性能不达预期的排查
如果推理速度远低于预期,按这个顺序排查。
先确认模型是不是真的跑在 NPU 上。有些情况下模型会回退到 CPU,速度自然慢。可以通过 runtime 的日志或性能分析工具确认。
再确认是不是用了 INT8。如果模型是 FP16 或 FP32,速度会差一大截。
然后看预处理是不是瓶颈。前面测过,预处理可能占相当比例,如果用了低效的实现,整体帧率会被拖下来。
最后看有没有启用零拷贝和多核。这些优化能进一步压榨性能。
6.4 几个容易忽视的细节
第一个细节是输入图像的宽高比。YOLOv5 训练时用的是 letterbox 填充,保持宽高比。如果部署时直接拉伸到 640x640,检测框会有系统性偏差。这个坑我踩过,检测框位置总是偏一点,后来改成 letterbox 就正常了。
第二个细节是类别顺序。模型输出的类别索引要和你的标签文件对应,顺序错了会导致标签张冠李戴。
第三个细节是多线程下的资源竞争。如果同时跑多个推理线程,NPU 核心的分配要规划好,否则会互相拖慢。
7. 后续优化方向与个人体会
7.1 还能往哪些方向继续压榨
跑通只是起点,后面还有不少可以做的。模型层面可以尝试更轻量的结构,或者对 YOLOv5s 做剪枝,进一步压缩计算量。量化层面可以尝试更精细的混合量化策略,在精度和速度之间找更好的平衡点。
工程层面,可以把整个流程封装成一个服务,接入视频流做实时检测,再配合监控看长时间运行的稳定性。如果要做多路视频,就得考虑多核 NPU 的任务调度,把不同路分到不同核心上。
另外,预处理如果成为瓶颈,可以考虑用硬件加速的 resize 和颜色转换,把 CPU 解放出来。这些都是我接下来打算继续折腾的方向。
7.2 我在这个项目里的真实体会
整个流程走下来,最大的感受是:边缘 AI 部署的难点不在模型本身,而在工程链路的每一处细节。模型结构、量化参数、校准集、预处理格式、内存布局、版本对应,任何一环出问题,最终表现都是“跑不起来”或“跑得不对”,而报错信息往往指不到真正的原因。
所以我的建议是,每一步都留下可验证的中间产物。ONNX 导出后用 onnxruntime 验一遍,量化后用测试图对比一遍,板端部署后先用单张图确认结果,再接入视频流。这样出问题时能快速定位到是哪一环,而不是从头猜。
还有就是别怕量化。很多人对 INT8 有心理阴影,觉得精度一定崩。实际上只要校准集选得对,YOLOv5s 这种成熟模型量化后的表现是相当能打的。真正需要警惕的是那些结构特殊、激活值分布极端的模型,那才需要混合量化来兜底。
最后分享一个小技巧:把每次转换的配置和结果都记下来,包括 toolkit 版本、量化参数、校准集、精度数据。因为调优是个反复试错的过程,没有记录的话,很容易重复走弯路。我一开始没记,后来发现同一个参数试了三次才想起来之前试过,白白浪费时间。有了记录之后,整个调优过程就清晰多了。