ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于RK3588的YOLOv5边缘部署实战:量化、RKNN转换与性能优化

基于RK3588的YOLOv5边缘部署实战:量化、RKNN转换与性能优化 RK3588这块芯片这几年在边缘AI圈子里几乎成了“标配”级别的存在。原因很直接6 TOPS的NPU算力配上四核A76加四核A55的CPU组合还有Mali-G610 GPU一块板子就能跑不少轻量级视觉模型。而YOLOv5作为目标检测领域最成熟的工程化模型之一两者的结合几乎是我见过最高频的部署组合——安全帽检测、火焰识别、车流统计、工业质检基本都能在RK3588上落地。这篇文章就围绕在RK3588上部署YOLOv5的完整流程展开重点讲模型量化、RKNN转换和板端推理性能优化这几个关键环节把我在实际项目中踩过的坑和验证过的方案一次性说清楚。1. 准备环境RK3588硬件特性与工具链选型1.1 先搞清楚NPU能干什么很多刚接触RK3588的人容易把NPU想象成“无脑加速器”模型丢进去就能飞起来。实际上NPU并不是万能药。RK3588的NPU由三个NPU核心组成总算力标称6 TOPSINT8支持INT4、INT8、INT16和FP16混合量化。这个算力是什么概念呢拿YOLOv5s来说输入640x640的情况下理论帧率可以跑到30帧以上实际工程中稳定在20到25帧左右。作为对比纯用CPU跑YOLOv5s在RK3588上大概只有2到3帧GPUMali-G610配合OpenCL能到8到10帧差距非常明显。但NPU也有脾气。它只认自己的算子集和指令格式不是随便一个PyTorch模型都能直接扔进去跑。整个流程需要经历“训练框架导出ONNX - RKNN工具链转换 - 板端RKNN Runtime加载推理”这样一条链路。任何一个环节出问题结果都不是报个错那么简单而是可能模型完全跑不出正确的框或者干脆转换失败。所以第一步就是把工具链理清楚。1.2 RKNN工具链怎么选瑞芯微提供的官方工具链是RKNN-Toolkit2在PC端x86 Linux上完成模型的转换、量化和模拟验证生成.rknn格式的模型文件。板端则依赖RKNN Runtimelibrknnmrt.so来加载并执行。这里有一个必须注意的匹配问题PC端工具链的版本和板端Runtime的版本需保持一致否则会出现模型加载失败或者算子实现不一致的诡异问题。我的经验是统一使用一个经过验证的版本组合比如RKNN-Toolkit2 1.6.0配RKNN Runtime 1.6.0升级的时候两边一起升千万不要只升一边。环境搭建上PC端RKNN-Toolkit2支持两种方式一种是直接pip安装依赖Python 3.8以上推荐用conda单独建一个虚拟环境另一种是使用官方提供的Docker镜像。前者灵活调试方便后者能避免很多依赖冲突。我在实际项目里更推荐conda环境因为转换过程中经常需要反复安装不同的依赖包虚拟环境隔离住了不会把宿主机搞乱。板端的Ubuntu系统镜像建议直接从Rockchip官方或板卡厂商的发布页下载通常预装了RKNN Runtime和相关依赖。装好之后先验证Runtime是否正常# 确认NPU设备节点存在 ls -l /dev/rknpu # 查询RKNN Runtime版本不同固件路径可能不同可以用find / -name librknnmrt.so 2/dev/null定位 find / -name librknnmrt.so 2/dev/null strings /usr/lib/librknnmrt.so | grep -i version | head -5如果这两条命令能正常输出说明Runtime安装基本没问题。还有一点很重要如果后面要做摄像头实时推理需要搞定图像采集和RGA加速库。RK3588自带的RGA模块2D硬件加速器可以做图像的缩放、裁剪和格式转换把CPU从像素级操作里解放出来这一点在性能优化环节会详细展开。2. YOLOv5模型导出与ONNX转换2.1 模型选择和训练要点部署到NPU上的YOLOv5模型结构不是越复杂越好。YOLOv5s是RK3588上性价比最高的选择v5m勉强能跑v5l和v5x在NPU上的推理延迟会明显上升30帧以上的实时需求基本不用想。所以除非对精度有特别高的要求否则首选YOLOv5s。训练时的几个参数会直接影响后续部署效果输入分辨率训练时用640x640部署时也尽量保持640x640。如果板端性能吃紧想改到416x416最好重新训练并且在部署时使用相同分辨率验证精度否则mAP会掉得厉害。Batch Size训练时影响BN层的统计量如果因为显存限制只能用小batch训练BN的统计量会不稳定。RKNN转换过程中会自动做一部分BN融合但训练时用正常的batch size比如16、32仍然是最稳妥的。类别数和Anchor根据自己的数据集调整。如果只检测少数类别比如安全帽检测只有两类可以在YOLOv5s预训练权重基础上微调不要从零训练收敛快且精度更容易保证。还有一个经常被忽略的点RK3588的NPU核心并没有内置NMS算子实现。YOLOv5仓库的export.py脚本支持一键导出为ONNX但YOLOv5的检测头在导出时默认包含了一些后处理逻辑这种带NMS的ONNX在RKNN转换时很容易报“算子不支持”的错误。更稳妥的做法是导出不包含NMS的原始ONNX只有三个检测头特征图输出然后在板端后处理里自己解码候选框再用cv2.dnn.NMSBoxes或手写的NMS完成过滤。虽然多写一点代码但整个链路的可控性高很多。2.2 从PyTorch到ONNX的关键操作具体操作上建议用YOLOv5官方仓库v6.0以上保持依赖环境一致。导出ONNX的命令如下python export.py --weights yolov5s.pt --include onnx --img 640 --batch 1 --simplify这里的--simplify选项会调用onnx-simplifier对计算图做简化和常量折叠对后续RKNN转换非常有帮助。如果不加这个参数转换时可能出现一些冗余节点导致RKNN工具报“不支持的算子”。导出之后建议先用Netron打开ONNX文件确认输出节点。YOLOv5的ONNX输出通常是三个特征层分别是下采样8倍80x80、16倍40x40和32倍20x20的输出每个输出的通道数为(5类别数)*3。如果你的类别数是1那么输出通道就是18。这些参数后面写后处理代码时会用到提前确认好能少踩很多坑。2.3 ONNX模型检查与预处理方式对齐拿到ONNX后不要急着转RKNN先做两件事。第一用onnxruntime跑一遍相同输入记录浮点模型的输出结果这是后续量化精度对比的基线。第二确认输入端的layout。YOLOv5导出的是NCHW格式而RKNN在config阶段可以配置输入格式后面转成RKNN模型后板端输入可能是NHWC。这两者之间的转换由RKNN Runtime自动完成但前提是你在config阶段设置正确。预处理方式也必须对齐。YOLOv5推理时的一般做法是letterbox等比缩放加padding到640x640再除以255归一化通道顺序保持BGR。如果RKNN config里把mean_values和std_values设置成0和1那就意味着归一化全部交给用户在输入前完成此时校准数据集也应使用相同预处理方式。反过来如果让RKNN内置的归一化处理就需要在config里填入均值和方差校准数据则不用做归一化。这两种方式都可行但不要混用否则推理结果会出现系统性偏移。我习惯用提前归一化的方案因为调起来直观一旦推理结果异常少一个变量需要排查。3. 模型量化从FP32到INT8的实战操作3.1 先理解NPU为什么要量化这一节是部署性能的关键。RK3588的NPU虽然支持FP16推理但真正让它发挥出6 TOPS算力的是INT8量化。简单来说INT8量化就是把模型里常用的FP32浮点权重和激活值用8位整数来近似表示从而大幅减少内存占用和计算开销。代价是精度会有一点损失但目标检测模型通常能通过选择合适的校准算法和校准数据集把mAP损失控制在1到3个百分点以内。量化主要分训练后量化PTQ和量化感知训练QAT。在RKNN工具链上最常用的是PTQ。流程是输入一批校准图片统计每一层激活值的动态范围再映射到INT8能表达的范围。这个流程看起来简单但里面有几个关键细节直接决定最终精度校准图片的数量不能太少300到500张能覆盖典型场景的图片比较合适。太少会过拟合到统计分布导致精度崩坏太多则转换时间成倍增长收益反而不明显。校准数据分布要贴近实际推理场景。比如你的实际使用场景是夜间监控那么校准图片里就应该包含夜间图像否则量化后模型白天表现可能没变化到了晚上就开始漏检。3.2 校准数据集准备与预处理校准图片输入给RKNN工具时必须和训练时保持相同的预处理方式。我这里写了一段Python脚本用于提前把图片处理成模型输入需要的张量格式import cv2 import numpy as np import os def letterbox(img, new_shape(640, 640), color(114, 114, 114)): shape img.shape[:2] r min(new_shape[0] / shape[0], new_shape[1] / shape[1]) new_unpad (int(round(shape[1] * r)), int(round(shape[0] * r))) dw (new_shape[1] - new_unpad[0]) / 2 dh (new_shape[0] - new_unpad[1]) / 2 if shape[::-1] ! new_unpad: img cv2.resize(img, new_unpad, interpolationcv2.INTER_LINEAR) top, bottom int(round(dh - 0.1)), int(round(dh 0.1)) left, right int(round(dw - 0.1)), int(round(dw 0.1)) img cv2.copyMakeBorder(img, top, bottom, left, right, cv2.BORDER_CONSTANT, valuecolor) return img img_dir calib_imgs with open(dataset.txt, w) as f: for name in os.listdir(img_dir): path os.path.join(img_dir, name) img cv2.imread(path) # 注意cv2读进来就是BGR img letterbox(img, (640, 640)) img img.astype(np.float32) / 255.0 img np.expand_dims(img, axis0) # 变成1xHxWxC f.write(path \n)这段脚本生成的dataset.txt直接传给RKNN工具作为校准集路径列表。注意做不做归一化要跟RKNN config里的mean_values、std_values设置保持一致。一般来说如果config里设置的是[[0,0,0]]和[[1,1,1]]那么这里必须把归一化做完再给RKNN。3.3 RKNN转换配置与量化选项使用RKNN-Toolkit2转换YOLOv5s的完整代码以1.6.x版本为例from rknn.api import RKNN rknn RKNN(verboseFalse) # 配置模型输入 rknn.config( mean_values[[0, 0, 0]], std_values[[1, 1, 1]], target_platformrk3588 ) # 加载ONNX模型 ret rknn.load_onnx(modelyolov5s.onnx) if ret ! 0: print(load onnx failed) exit(-1) # 构建RKNN模型并量化 ret rknn.build( do_quantizationTrue, datasetdataset.txt ) if ret ! 0: print(build failed) exit(-1) # 导出RKNN模型 rknn.export_rknn(yolov5s.rknn) # 释放资源 rknn.release()这里有几个关键点值得展开说。第一target_platform必须明确指定为rk3588不能省略否则工具默认按较低版本的平台优化。第二config里的mean_values和std_values一旦定好就不要中途改前面我提到过这个设置必须和校准数据以及后续板端输入的预处理保持一致。第三量化算法方面RKNN一般有normal和local两种选择。normal是全局数值范围统计速度快local会对不同层做更细粒度的范围优化精度通常会好一些代价是转换时间变长。YOLOv5这种大量使用卷积和残差连接的结构如果normal模式下掉点严重建议切换到local量化再试一次。第四转换过程中的日志一定要看。RKNN在转换结束后会输出算子分配情况哪些算子跑NPU、哪些算子回退到了CPU。正常情况下YOLOv5的绝大部分算子都能映射到NPU一旦看到某个关键算子标了CPU就要注意了肯定会影响推理速度需要回头查ONNX导出过程。第五量化完成后先在PC上使用RKNN-Toolkit2的模拟器验证精度。做法是加载.rknn模型输入几张训练集之外的测试图片把输出结果和之前用onnxruntime跑出的浮点结果做对比。这一步放在板端调试之前做能把问题提前拦截在PC端。3.4 量化精度验证与调优转换完成后PC模拟器验证没过关的话先不要急着上板。一般按下面的方向调优扩充校准集数量增加边缘案例小目标、遮挡、光照变化使用local量化方法检查anchor是否与数据集分布匹配如果差得太多重新聚类anchor尝试混合量化把对精度敏感的层保留为FP16其他层用INT8混合量化是RKNN-Toolkit2里比较实用的能力。先用量化分析工具看哪些层对INT8量化最敏感然后对这些敏感层单独设置成FP16精度。我做过一个安全帽检测项目INT8整体量化后mAP掉了将近4个点后来把三个检测头附近的几个卷积层保持FP16精度损失缩窄到0.8个点推理耗时只增加了不到10%。这个思路在精度和性能需要平衡时非常值得试。还有一个容易忽略的细节如果板端推理时发现检测框位置漂移八成不是量化问题而是后处理坐标映射没写对。YOLOv5的输出是相对于输入图像坐标系的需要先乘以对应的stride再减去padding偏移最后除以letterbox缩放比例才能映射回原图。这个流程一旦出错框就会整体偏移判断起来很容易和量化掉点搞混。建议调试时分两步先在PC模拟器上确认模型输出本身是正确的再去板端排查后处理代码。4. 板端部署C API集成与性能优化4.1 交叉编译环境与工程组织RKNN Runtime提供C/C和Python两套API。如果项目需要做成嵌入式应用对性能有极致要求建议直接用C API。头文件和动态库在板端或Rockchip官方SDK里都有。编译方式我建议直接在板端本地编译因为RK3588板子本身就能跑Ubuntu系统装好gcc和cmake后省去交叉编译链配置的麻烦依赖库版本也更可控。一个最小可用的CMakeLists.txt大致长这样cmake_minimum_required(VERSION 3.10) project(yolov5_demo) find_package(OpenCV REQUIRED) include_directories(/usr/include/rknn) link_directories(/usr/lib) add_executable(yolov5_demo main.cpp) target_link_libraries(yolov5_demo rknnrt ${OpenCV_LIBS} )在板端源码目录里执行cmake make很快就能得到可执行文件。这里要提醒一句如果板端是64位Ubuntu系统动态库必须是arm64版本不要误放到x86的库文件否则运行时会报“cannot open shared object file”的错误。4.2 推理流程实现从读图到后处理一个标准的RK3588板端推理流程包含以下步骤初始化RKNN上下文并加载模型获取模型输入输出属性通道数、大小、类型读取图像并做letterbox缩放将输入数据送入NPU执行推理获取推理输出并进行解码和NMS绘制结果并显示或保存核心C代码实现如下#include rknn_api.h // 初始化并加载模型 rknn_context ctx; ret rknn_init(ctx, yolov5s.rknn, 0, 0, nullptr); if (ret 0) { printf(rknn_init failed: %d\n, ret); return -1; } // 查询输入输出数量 rknn_input_output_num io_num; ret 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); // 设置输入数据普通API方式 rknn_input inputs[1]; inputs[0].index 0; inputs[0].type RKNN_TENSOR_UINT8; inputs[0].size width * height * 3; inputs[0].fmt RKNN_TENSOR_NHWC; inputs[0].buf img_buf; // 将预处理后的图像数据填入 inputs[0].pass_through 0; // 0表示由Runtime处理归一化1表示直传原始张量 ret rknn_inputs_set(ctx, 1, inputs); // 执行推理 ret rknn_run(ctx, nullptr); // 获取输出YOLOv5s有3个输出头 rknn_output outputs[3]; outputs[0].want_float 1; // 希望得到浮点输出便于后处理 outputs[1].want_float 1; outputs[2].want_float 1; ret rknn_outputs_get(ctx, 3, outputs, nullptr); // 这里对outputs做解码和NMS处理 // ... // 释放输出 rknn_outputs_release(ctx, 3, outputs);这里的pass_through参数是个关键点。当pass_through0时RKNN Runtime会根据模型构建时的config设置自动完成数据格式转换和归一化此时我只需要把原始图像像素填入buf比如uint8的BGR数据。当pass_through1时输入数据会被视为模型直接需要的张量此时必须自己做好归一化和layout转换。简单项目建议用pass_through0少踩坑追求极致性能时再考虑pass_through1配合零拷贝。后处理部分需要根据3个输出头的shape逐一解码。以类别数为1为例三个输出的shape分别是(1, 18, 80, 80)、(1, 18, 40, 40)和(1, 18, 20, 20)其中18 3 * (1 5)。解码时遍历每个格点上的3个anchor用sigmoid激活坐标偏移量和置信度还原出相对于输入图的候选框再做一次NMS过滤。4.3 性能提升三板斧零拷贝、流水线、RGA加速推理流程跑通后你会发现实际帧率和预期往往有差距。这时一般是因为下面三件事没做到位。第一件零拷贝。常规流程需要先把图像数据拷贝到CPU内存再由NPU从内存读取。RKNN的零拷贝API可以让输入输出数据直接在NPU可访问的内存区域操作跳过部分拷贝成本。具体做法是初始化阶段用rknn_create_mem申请输入输出共享内存推理时直接往这块内存里写图像数据推理完直接读结果不再走rknn_inputs_set和rknn_outputs_get。在高分辨率输入场景下这个优化效果非常明显。第二件多线程流水线。RK3588有8核CPU完全可以实现取帧、预处理、NPU推理、后处理绘制四段并行。用三到四个线程配合双缓冲队列让NPU每帧都在工作而不是等待前端的图像处理和后面的NMS。我实测下来把预处理和后处理拆出去后帧率提升幅度在20%到30%瓶颈从“CPU忙完才轮到NPU忙”变成了“NPU持续满载”。第三件RGA硬件加速。如果从摄像头采集的原始图像存在分辨率不匹配比如1080p输入、640x640推理以及BGR/RGB格式切换需求这些操作全部交给RGA硬件模块处理CPU完全不参与像素级操作。相比OpenCV的resize和cvtColor几百微秒的耗时可以压缩到几十微秒而且不消耗CPU算力。这里分享一组不同优化阶段的性能对比数据基于YOLOv5s、640x640输入、RK3588实测优化阶段帧率/FPSCPU占用率说明初始版普通API CPU预处理18约180%后处理同步阻塞零拷贝优化21约150%输入输出减少一次拷贝添加多线程流水线26约200%编译期和推理期并行使用RGA进行预处理30约90%预处理几乎不占CPUCPU占用率180%意味着平均约等于1.8个核心满负荷运行这个数据是直观的优化效果参考。我强调一下最后达到30帧并不是模型本身的极限而是我在这个具体工程中平衡了CPU、NPU和内存带宽之后的综合结果。如果项目对CPU占用更敏感可以牺牲少量帧率来换取更低功耗这个取舍取决于产品需求。5. 实测数据与问题排查5.1 精度与性能实测为了验证整体方案的可靠性我在安全帽检测和车辆检测两个数据集上分别做了完整对比实验。安全帽数据集约8000张车辆数据集约12000张均使用YOLOv5s作为基础模型在PC显卡上训练后导出ONNX再通过RKNN-Toolkit2 1.6.0转为INT8量化模型部署在RK3588板端。精度对比结果如下模型状态安全帽mAP0.5车辆mAP0.5单帧推理耗时PyTorch FP3296.2%88.4%GPU上约3msONNX FP3296.1%88.3%-RKNN INT8PC模拟94.8%86.9%-RKNN INT8板端实测94.6%86.5%约30ms可以看到INT8量化带来的精度损失在1.5到2个百分点左右对于安全帽检测这类目标尺度相对稳定的场景完全可以接受。板端单帧推理耗时稳定在30ms以内约33FPS超过视频实时性要求。如果换成YOLOv5m板端推理耗时接近55ms精度更高但无法做到30FPS。所以在实时需求明确的前提下YOLOv5sINT8就是RK3588上的第一选择。5.2 高频问题排查清单部署过程中遇到最多的问题我整理成了一张排查表基本覆盖了初次上板时90%的情况现象可能原因排查与解决办法rknn_init返回-1模型版本与Runtime不匹配让PC端Toolkit版本与板端Runtime版本保持一致模型输入尺寸错误letterbox预处理没做或尺寸不对打印模型输入属性对齐到实际输入分辨率推理结果全是0或负值输入数据没有按照config配置归一化确认pass_through设置和mean/std配置一致输出检测框偏移严重后处理解码坐标映射有误核对stride、padding偏移、letterbox缩放比例NPU负载太高导致帧率波动多线程同步没做好NPU空等使用双缓冲或三缓冲队列保证输入数据提前就绪程序运行一段时间后崩溃内存泄漏检查是否每次推理后释放了rknn_output缓冲区转换时报不支持算子ONNX导出版本或选项问题用onnx-simplify简化尝试更新导出YOLOv5版本还有一类隐蔽问题零拷贝API对内存对齐有要求。如果使用零拷贝每个通道的stride需要满足64字节对齐。直接拿摄像头帧的buffer往往不满足这个条件要么提前分配对齐过的内存再拷贝一份数据进去要么退回普通API让Runtime自动处理对齐。这个坑排查起来很费时间我在一个项目里耗了两天才定位到其实就是多了一行memcpy的问题。5.3 关于板端系统与开发调试的补充在热搜词里我注意到大家还关心“RK3588移植Ubuntu”和“adb连接板子”这类基础操作。实际开发中RK3588跑Ubuntu系统已经相当成熟如果手头的板子是官方核心板加底板直接从官方渠道下载Ubuntu镜像烧录即可。调试时用串口登录最保险Wi-Fi和以太网配置有误时也不影响排查。adb方式适合把板子当成Android设备或者通过USB连接调试的场景前提是板端固件开启了adb服务。我个人的习惯是串口为主、SSH为辅排查底层问题的时候串口信息不可替代。如果板端需要同时部署多个模型RKNN Runtime支持在同一进程中初始化多个context只要内存允许。不过要注意NPU算力的分配是抢占式的两个模型同时跑会互相拖慢建议根据实际负载分配不同时间段或使用同一模型的多个实例后者对内存的占用会更可控。多路视频推理时也建议用同一个模型context分时处理多帧而不是重复创建多次模型这样内存开销更小NPU调度也更稳定。结尾RK3588加YOLOv5这套组合我做过的项目里已经落地了四五个从安全帽检测到农机作业统计整体稳定性不错。最后分享两点个人经验。第一量化调优不要一味追求极低的精度损失。INT8模型相对FP32掉点1到2个点属于正常范围只要实际场景的关键指标比如漏检率、误检率达标就不用在量化细节上死磕。真正需要优先关注的是板端推理的稳定性和CPU占用率因为这些直接影响整机产品的体验和功耗。第二RKNN工具链升级频率不低但不要盲目追新。每次工具链升级都意味着要重新验证一遍精度和性能。我现在的方法是固定一套经过验证的版本除非必须适配新的模型结构否则不轻易动。遇到问题时多查官方文档同时留意板卡厂商针对特定固件的适配经验往往比自己折腾版本高效得多。这套方法在RK3588边缘部署里是性价比最高的一条路可以长期沿用。
返回列表