ARTICLE DETAIL

资讯详情

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

RK3588 NPU部署YOLOv8/YOLOv8-seg实战:性能对比与避坑指南

RK3588 NPU部署YOLOv8/YOLOv8-seg实战:性能对比与避坑指南 RK3588 这颗芯片在国产边缘设备里的存在感确实很强8 核 CPU 加上 6 TOPS 的 NPU跑 YOLO 系列检测模型算是它的“本职工作”。但我发现很多人把 YOLOv8、YOLOv8-seg 搬上这块 NPU 时总是卡在模型转换、算子支持或者后处理上要么速度跑不满标称算力要么分割结果直接没法看。这篇文章就把我实际在 RK3588 NPU 上做过的 YOLOv8 与 YOLOv5 速度对比以及 YOLOv8-seg 分割模型部署时踩过的坑完整整理出来包括工具链版本怎么配、量化怎么做、算子回退怎么查、分割头的输出怎么解析希望能帮你少走几个月弯路。这篇文章适合正在做边缘端视觉落地的同学尤其是手里已经拿到 RK3588 开发板、但又不知道从哪开始烧模型的人。读完你会对 NPU 部署的整体流程有一个清晰闭环也能直接照着里面的代码和排错思路动手跑起来。1. 部署方案与工具链选择1.1 认识 RK3588 的 NPU 与 RKNN 工具链RK3588 集成的 NPU 官方标称 6 TOPS 算力指的是 INT8 精度下的算力实际使用中还要看算子利用率和内存带宽。这颗 NPU 不是像 GPU 那样通用可编程的架构它更接近一堆固定功能计算单元的组合对卷积、ReLU、池化这类常见算子支持得很好但对动态 shape、某些上采样方式、sort 类操作就非常不友好。理解这一点是后面所有排错思路的基础。官方配套的部署工具链叫 rknn-toolkit2它负责把 PyTorch、ONNX、TensorFlow 甚至是 Caffe 训练出来的模型转换成 RK3588 NPU 能跑的 .rknn 格式文件。转换过程包含算子映射、权重重排、量化等环节最终生成的 rknn 文件由板端的 runtime 库rknn-toolkit-lite2 或 C API加载执行。整个部署链路是这样的训练好的权重 - 导出为 ONNX - 用 rknn-toolkit2 在 PC 端转换为 .rknn - 把 .rknn 拷贝到板子 - 用 lite2 或 C API 加载推理。这里最关键的一点是PC 端转换用的 rknn-toolkit2 版本和板端 runtime 版本必须匹配否则会出现模型加载失败或者算子解析错乱的问题。官方虽然在文档里强调过这一点但实际项目里很多人就是栽在版本不一致上。1.2 环境安装与版本匹配实操PC 端环境建议直接用 Ubuntu 20.04 或 22.04Python 版本 3.8 到 3.10 之间都可以我实测 3.8 最稳。安装 rknn-toolkit2 直接用 pip 装就行但要注意先创建虚拟环境因为它对 onnx、onnxruntime、numpy 的版本比较敏感装乱了会波及你其他项目。依赖安装顺序我一般是这样的# 创建虚拟环境 python -m venv rknn_env source rknn_env/bin/activate # 安装基础依赖 pip install numpy1.24.4 onnx1.14.0 onnxruntime1.16.0 opencv-python # 从官方 GitHub 下载 rknn-toolkit2 的 wheel 包并安装 pip install rknn_toolkit2-2.0.0b0-cp38-cp38-linux_x86_64.whl # 验证安装 python -c from rknn.api import RKNN; print(ok)这里有个隐藏比较深的坑onnxruntime 的版本会影响模型解析高版本 onnxruntime 在某些情况下会做额外的算子融合导致导出的模型和你预期不一致。我遇到过明明在 PyTorch 里模型输出正常转成 ONNX 后测试输出也对但 RKNN 转换时报算子不支持的诡异情况最后发现是 onnxruntime 版本太新导致的。所以如果你在转换阶段卡住优先把 onnxruntime 降下来试试。版本匹配这一块我现在的做法是PC 端 rknn-toolkit2 用 2.0.0 及以上版本板端 lite2 也用同版本在板子上跑pip show rknn-toolkit-lite2确认版本号。目标平台参数在转换时要显式写成target_platformrk3588不要偷懒不写否则默认可能跑到 rk3566 或 rk3568 的配置上速度会有明显差异。1.3 用官方示例工程起步rnkk_model_zoo 是官方维护的模型仓库里面已经有 YOLOv5、YOLOv8、YOLOv8-seg 全套转换和部署的示例代码。我的建议是永远不要自己从头写转换脚本直接在这个仓库基础上改至少能保证基础环节是对的。你把rknn_model_zoo/examples/yolov8目录里的 python 脚本跑通一遍基本就理解了数据集准备、量化、推理的标准姿势。跑通示例后再换成自己的模型这时你只需要改权重路径、类别数和预处理方式出问题的概率会小很多。这个路径看起来绕了一圈实际上是最快的一种方式。2. YOLOv8 与 YOLOv5 在 RK3588 NPU 上的速度实测2.1 模型结构差异与理论预估做速度对比前先得搞清楚 YOLOv8 和 YOLOv5 到底差在哪。YOLOv5 的 backbone 用的是 C3 模块加早期的 Focus 结构检测头比较简单输出层直接给出 box 和 class。YOLOv8 把 C3 换成了 C2f 模块去掉了 Focus检测头改用 DFLDistribution Focal Loss结构同时把 anchor 机制改成了 anchor-free。C2f 模块在结构上比 C3 多了更多分支和拼接操作计算量会略高一些。DFL 结构在推理时需要对 16 个通道做 softmax 再加权求和这个操作在 NPU 上不是完全“免费”的有一部分可能会回退到 CPU。而 YOLOv5 的检测头就是纯粹的卷积加 sigmoid对 NPU 来说属于最友好的算子组合。所以在没测之前我心里大概预期 YOLOv5s 会比 YOLOv8s 快 10% 到 20%但精度上 YOLOv8 确实通常要更好一些。具体怎么选完全取决于你的项目是更看重延迟还是更看重精度。2.2 实测环境与推理方法我实测用的板子是 RK3588 标准开发板8G 内存版本系统是 Ubuntu 22.04NPU 驱动用的官方 rknpu 固件。模型都是 ultralytics 官方仓库导出的输入尺寸固定为 640x640量化精度 INT8batch size 固定为 1这也是边缘部署最常用的配置。推理代码用的是 rknn-toolkit-lite2 的 Python 接口核心逻辑如下from rknnlite.api import RKNNLite rknn RKNNLite() rknn.load_rknn(yolov8s.rknn) rknn.init_runtime(core_maskRKNNLite.NPU_CORE_0_1_2) # 预热 img np.random.randint(0, 255, (640, 640, 3), dtypenp.uint8) for _ in range(5): rknn.inference(inputs[img]) # 正式计时 import time start time.perf_counter() for _ in range(50): outputs rknn.inference(inputs[img]) end time.perf_counter() print((end - start) / 50 * 1000, ms)这里要提醒两点一是 NPU 第一次推理会做初始化速度很慢必须预热几次再计时。二是core_mask这个参数一定要用NPU_CORE_0_1_2表示同时用三个 NPU 核心我见过有人默认只跑一个核速度几乎掉了三倍。另外我在转换时打开了 verbose log目的是看有没有算子回退 CPU。命令是rknn.config(verboseTrue)日志里如果出现WARNING: fallback或者某些层显示在 CPU 上计算那速度绝对会有影响。2.3 测试结果与对比分析直接放我实测的数据。注意这里数值受编译版本和板子散热影响会有浮动但相对关系是稳定的模型输入尺寸量化精度NPU推理耗时整体帧率(含后处理)YOLOv5s640x640INT831.5 ms22 FPSYOLOv8s640x640INT838.2 ms19 FPSYOLOv8s640x640FP1677.6 ms10 FPSYOLOv8s-seg640x640INT855.4 ms12 FPS只看 NPU 推理时间YOLOv8s 比 YOLOv5s 慢了大概 20%这和理论预估基本吻合。速度差距主要来自两个方面一是 C2f 模块分支多NPU 在做 concat 和残差相加时数据搬运开销更大二是 DFL 头包含一些 NPU 不友好的操作部分算子被放到了 CPU 上跑。FP16 相比 INT8 差了将近一半性能这个结果也很正常因为 RK3588 的 NPU 对 FP16 的处理效率远不如 INT8。所以除非你的模型精度对 INT8 量化非常敏感否则推荐直接用 INT8 部署。如果你在意的是整体帧率一定要把后处理时间算进去。我上面的整体帧率已经包含了 NMS 和坐标解码的耗时YOLOv8 的 DFL 解码本身就比 YOLOv5 要复杂多一次 softmax 和加权求和CPU 端后处理大约多花 3-4ms。这个差距在低帧率场景下不明显但如果你要做实时视频流分析每帧多 3ms 就意味着丢帧概率更高。2.4 后处理耗时对“整体帧率”的影响很多人在测速时只盯着 NPU 推理时间实际落地时发现帧率远低于预期就是因为后处理跑在 CPU 上。尤其是 YOLOv8它的后处理流程包括从输出张量中分离 box 和 cls、对 box 做 DFL 解码、计算每个框的坐标和置信度、过滤低置信度框、跑 NMS。这些操作对 8400 个候选框来说纯 Python 循环非常慢。我建议无论用哪种模型后处理都尽量用 numpy 向量化实现避免逐框循环。另外RK3588 是 8 核 CPU可以开一个线程专门做 NPU 推理另一个线程做后处理和显示用双缓冲交替工作整体吞吐能提高不少。3. YOLOv8-seg 分割模型部署难点3.1 分割模型的结构与输出解读YOLOv8-seg 和检测版最大的区别是多了分割头。整个模型结构包含 backbone、neck、检测头和一个额外的分割头。分割头会输出一个 proto 分支这个分支生成一组低分辨率的分割原型图同时检测头的输出里会多出一段掩码系数最终的 mask 是由 proto 和掩码系数矩阵乘得到的。具体到输出张量假设你的类别数是 80输入 640x640模型的原始输出是一个 1x116x8400 的张量其中 4 是 box 坐标80 是类别概率32 是 mask 系数。还有一个输出是 1x32x160x160 的 proto 张量。注意这里的 160x160 是原型图的分辨率最后需要上采样回原图尺寸。很多人拿到这个输出是懵的不知道该拿哪些维度去算。我当时也折腾了好一阵才搞清楚对应关系所以这里先明确mask 系数和 box、cls 在同一个张量里分开切片后用矩阵乘把 mask 系数和 proto 算出来上采样再做 sigmoid阈值化才能得到最终的二值 mask。原型图本身不是最终 mask必须先和系数组合。3.2 第一个大坑上采样算子不支持YOLOv8-seg 转 NPU 时最容易踩的坑就是上采样算子。分割头里有一个从低分辨率特征图做双线性插值上采样的操作这个操作在 ONNX 里通常对应Resize算子或者Upsample算子。RNK 工具链对Resize的支持非常有限即使支持也经常会被判定为效率太低而回退到 CPU导致整体推理速度大幅下降。我在转换 yolov8s-seg 时第一次就遇到了这个算子回退日志显示Resize跑在 CPU 上。解决思路有两种一种是在导出 ONNX 时把上采样方式改成最近邻插值NPU 对最近邻的支持好很多但精度会略微下降另一种是把上采样从模型里拿掉让模型只输出低分辨率的 proto上采样放到 CPU 后处理用 OpenCV 的cv2.resize做。我实际采用的是第二种方案效果最稳定且不损失精度。具体做法是在导出 ONNX 时手动把模型最后的 proto 输出的上采样节点删掉让 proto 直接输出 160x160 的结果。然后在后处理代码里用cv2.resize将 mask 上采样到 640x640 或原图分辨率。NPU 只计算卷积部分上采样这些“脏活”交给 CPU 做实测速度提升非常明显。3.3 第二个大坑导出与维度匹配问题分割模型的 ONNX 导出也要格外小心。用 ultralytics 官方接口导出时默认可能会把一些后处理逻辑也带进去比如 sigmoid、argmax 这类操作。这些操作有的 NPU 支持得不好有的甚至不支持。我建议用以下方式导出干净的 ONNXfrom ultralytics import YOLO model YOLO(yolov8s-seg.pt) model.export( formatonnx, opset12, imgsz640, simplifyTrue, dynamicFalse )这里opset12很关键opset 太高某些算子可能会发生变化RKNN 解析就容易报错。simplifyTrue会调用 onnxsim 对模型做简化把一些冗余节点删掉转换成功率会提高。dynamicFalse保证输出是固定 shapeNPU 不支持动态 shape。导出后先用 onnxruntime 在 PC 端跑一遍确认输出维度和数值正常再转 rknn。这一步必须做因为很多时候问题不是出在 RKNN而是模型导出的 ONNX 本身就有问题。3.4 第三个大坑分割后处理流程怎么拆分割的后处理比检测复杂很多它不是一个简单的 NMS 就能搞定的。在你的模型拿到原始输出后后处理流程应该是这样的从输出中分离 box、cls 和 mask 系数。先跑检测那套流程做 DFL 解码、置信度过滤、NMS得到最终的检测框。对每个保留的检测框从 mask 系数里取出对应的那一行。把 mask 系数和 proto 做矩阵乘得到一个原始 mask 特征图。对 mask 特征图做 sigmoid 和阈值化得到二值 mask。用检测框的坐标从原图中裁剪出对应区域把 mask 上采样到裁剪区域的尺寸。关键点在于分割后处理里有两个操作很容易出错一是在 NMS 之前就要把检测框的置信度算出来否则后面分不清哪些框该保留二是掩码系数要和检测框一一对应排序不能乱。我这里直接用 numpy 做矩阵乘160x160 的 proto 乘上 32 维系数速度很快基本可以忽略不计。实战建议是写一个SegPostProcessor类把检测和分割的后处理分离先确认检测结果对了再去对分割结果不要一上来就调 mask不然你根本分不清问题出在检测头还是分割头。3.5 实测速度表现与优化建议YOLOv8s-seg 在 INT8 量化下NPU 推理时间大约 55ms整体帧率含后处理只有 12 FPS 左右。这个速度比检测模型慢了不少主要原因是分割头增加了额外的卷积和 proto 分支计算量确实更大。如果你想进一步提升速度可以从这几个方向入手第一输出分辨率不要用 640可以降到 512 或者 416。分割任务本身对分辨率比较敏感但很多场景下 512 已经够用速度能提升 30% 以上。第二把分割头的 proto 分支输出降为 80x80 或更低这需要在模型结构上做裁剪省掉 proto 分支最后几个卷积层代价是 mask 边缘会更粗糙。第三用多线程把 NPU 推理和 CPU 后处理流水线化而不是串行执行。我实测过把输入从 640 降到 512NPU 推理时间从 55ms 降到 35ms 左右降幅非常可观。如果你的项目允许降低一些精度这个方案是性价比最高的。4. 板端部署细节与性能优化4.1 推理代码最小范例在 RK3588 板端部署我推荐用 rknn-toolkit-lite2 的 Python 接口开发效率高性能也够用。如果是产品化项目再考虑 C API但初期原型验证用 Python 完全没问题。一个最小可运行的板端推理脚本大概长这样import cv2 import numpy as np from rknnlite.api import RKNNLite rknn RKNNLite() rknn.load_rknn(yolov8s-seg.rknn) rknn.init_runtime(core_maskRKNNLite.NPU_CORE_0_1_2) img cv2.imread(test.jpg) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) img cv2.resize(img, (640, 640)) img img.astype(np.uint8)[None, ...] outputs rknn.inference(inputs[img]) # 通过 data_formatnhwc 参数可以省去一次 transpose推理更快这里有一个细节rknn.inference默认处理的数据格式是nchw如果你的输入是 HWC 排列需要传data_formatnhwc这样可以省去输入前的一次 transpose 操作对推理速度有微小但稳定的提升。我在板子上验证过单帧能快 1-2ms。4.2 多核 NPU 与内存管理RK3588 的 NPU 有三个核心通过core_mask参数可以控制使用哪些核心。默认情况下如果不设置可能只跑一个核心速度损失非常大。一定要显式设置为NPU_CORE_0_1_2三个核全开。内存方面RKNN 在推理时会在 NPU 和 CPU 之间搬运数据如果你的图像比较大内存占用会激增。我遇到过连续推理多个小时之后内存泄漏的问题追踪下来发现是反复加载模型导致的。正确的用法是模型加载一次然后重复调用inference用完再rknn.release()。长稳测试时建议在循环里监控内存占用watch -n 1 free -m如果发现内存持续增长检查是否在循环中重复创建了 RKNNLite 实例或者有没释放的 numpy 数组。4.3 与 CPU 后处理的流水线化真正做实时视频流推理时不能把 NPU 推理和后处理串在同一个线程里。NPU 推理 55ms后处理 20ms串行就是 75ms 一帧帧率只有 13 FPS。但如果用两个线程NPU 在做第 N1 帧推理时CPU 同时处理后处理第 N 帧的耗时吞吐量能接近单帧最长耗时的倒数而不是加起来。实现方式可以用 Python 的threading加双缓冲队列或者更简单点用concurrent.futures.ThreadPoolExecutor。我这里分享一个简单的生产者消费者思路# 推理线程 def infer_worker(): while True: frame input_queue.get() outputs rknn.inference(inputs[frame]) res_queue.put(outputs) # 后处理线程 def post_worker(): while True: outputs res_queue.get() results post_process(outputs) show_results(results)用这种方式整体吞吐按照最耗时的环节计算而不是所有环节相加项目实测能提升 20%-30% 的有效帧率。4.4 输入预处理细节很多人把模型转换搞定后卡在输入预处理上。YOLO 系模型在训练时通常会对输入做 letterbox也就是保持宽高比缩放到 640x640剩下的位置用灰色填充。如果你直接把图片 resize 到 640x640会破坏目标的长宽比导致检测精度下降。在板端做 letterbox 时用 OpenCVdef letterbox(im, new_shape(640, 640), color(114, 114, 114)): shape im.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, dh new_shape[1] - new_unpad[0], new_shape[0] - new_unpad[1] dw, dh dw // 2, dh // 2 if shape[::-1] ! new_unpad: im cv2.resize(im, new_unpad, interpolationcv2.INTER_LINEAR) top, bottom dh, dh new_shape[0] - new_unpad[1] left, right dw, dw new_shape[1] - new_unpad[0] im cv2.copyMakeBorder(im, top, bottom, left, right, cv2.BORDER_CONSTANT, valuecolor) return im这个 letterbox 的填充值 114 是 YOLO 训练时的默认填充值不要改成 0否则精度会有细微下降。推理完检测框坐标映射回原图时记得把 letterbox 的偏移量减回去不然框的位置会偏。5. 常见问题与排查技巧实录5.1 高频问题速查表问题现象可能原因解决办法转换时报不支持的算子ONNX 中包含了 NPU 不友好的算子简化模型、替换上采样方式、把 sigmoid 等操作移到后处理转换成功但推理结果全为 0输入数据格式不对确认 nchw/nhwc确认输入尺寸和 letterbox 对齐速度远低于预期算子回退 CPU / 核心没全开打开 verbose 日志查回退算子设置 core_maskNPU_CORE_0_1_2精度比原始模型低很多量化数据集覆盖率不够收集 100 张以上代表性图片重新量化检查归一化参数模型加载失败PC 端工具和板端 runtime 版本不匹配统一到同一个版本号长时间运行内存增长循环中重复加载模型未释放模型只加载一次用完调 rknn.release()5.2 转换失败时先别急着怀疑工具链我见过很多人在 RKNN 转换报错时第一反应就是工具链有 bug然后去 GitHub 提 issue。但绝大多数情况下问题出在模型本身。正确的排查顺序是先用 onnxruntime 在 PC 端加载转换前的 ONNX 模型输入一张假图片确认输出能出来且维度正确。这步通过后再转 RKNN如果还报错再看具体的算子日志。还有一个很实用的技巧把报错的那一层算子截图或者记下来去 rknn_model_zoo 的 GitHub issues 里搜大部分已知算子问题都有现成的解决方案。不要自己闷头试RK3588 部署 YOLO 系列的人太多了前人踩过的坑大概率你也躲不掉。5.3 量化精度偏差的处理思路RK3588 的 NPU 主打 INT8 量化但量化本身会引入精度损失。如果量化后的模型在你的数据集上表现很差优先检查两件事一是量化用的校准图片是否和目标场景一致二是输入归一化方式是否和训练时一致。校准图片的选择很关键。我见过有人用 20 张日常照片做校准结果到了工业检测场景精度崩得一塌糊涂。校准集应该覆盖目标场景下的各种光照、角度、目标大小分布至少 100 张多一些更好。另外如果模型对量化特别敏感可以考虑混合量化让某些敏感层保持 FP16虽然速度会降一些但精度能保住。5.4 算子回退日志怎么看这是一个非常实用的排障技巧。转换模型时打开 verbose 日志后会看到类似layer xxx not support, fallback to cpu的警告。算子一旦回退 CPUNPU 推理时还要频繁和 CPU 做数据交互速度会明显下降。如果发现某个算子回退优先考虑在模型层面改掉它。以 YOLOv8-seg 为例最常见的是Resize算子和Sigmoid算子。Sigmoid其实可以在后处理里用 numpy 实现转换前把模型末尾的 sigmoid 层裁掉Resize则按我前面说的方案处理。把这些算子从网络结构里“消灭”掉转换日志会干净很多推理速度也会有直观提升。5.5 避坑清单汇总结合我的实际经验列出几个最容易被忽略的细节板端推理前先确认固件里的 NPU 驱动版本老固件可能不支持新工具链生成的 rknn 文件。导出 ONNX 时类别数、输入尺寸一定要固定不要在部署过程中频繁修改每次改动都要重新量化和验证。用 Python 推理时不要每次调用 inference 前都对图片做完整预处理缓存提前把 letterbox 和归一化放到后台线程。如果板子是带散热风扇的型号长时间跑 NPU 推理前检查一下散热NPU 温度过高会触发降频推理速度会瞬时暴跌。不要在 Windows 上跑完整的 RKNN 转换流程官方对 Linux 支持最完善WSL2 虽然能用但偶尔会有库冲突费时费力。6. 更进一步分割模型的扩展玩法YOLOv8-seg 跑通之后它的输出不仅能拿来做实例分割还能用来做一些很有意思的下游任务。最典型的例子是人形检测加分割之后的姿态估计或者目标计数你不再只有一个框而是有目标在图像中的精细轮廓可以直接算目标面积、占空比甚至是触摸检测和物料分拣。我自己的一个项目里在 RK3588 上用 YOLOv8-seg 分割传送带上的物料然后根据掩码面积估算物料大小效果挺好的。NPU 推理时间 55ms 左右掩码后处理 10ms整体能跑到 15 FPS 以上对产线实时监控来说已经够用了。这一类玩法对于那些想要在边缘设备上做“非标准检测”的应用场景比如垃圾分拣、农作物病害识别参考价值很大。所以如果只把 YOLOv8-seg 当成一个“能出 mask 的检测器”其实低估了它。分割输出的连续性给了你更多判断维度这正是纯检测模型给不了的。部署的时候多花点心思把分割头调好后面做业务逻辑会顺手很多。7. 写在最后的实操心得RK3588 的 NPU 部署做久了你会发现真正的难点从来不是跑通一个模型的 demo而是把模型、算子、量化和后处理打磨到能在真实场景里稳定运行。YOLOv8 和 YOLOv5 的对比没什么神秘之处结构决定速度精度靠量化集和微调来兜底。YOLOv8-seg 的分割部署则更需要耐心因为要从输出张量里把 mask 和检测信息剥离出来再自己拼装后处理流程每一步都要求你理解透模型的结构。我个人在实际操作中的体会是遇到算子不支持的时候不要硬刚换个思路把操作挪出 NPU 反而更高效。RK3588 的 CPU 是 8 核 A76/A55性能一点都不弱很多 NPU 不擅长的工作交给 CPU 做整体系统效率才是最高的。硬件既然给了你两种计算单元就要学会让它们各司其职。最后再分享一个小技巧做板端部署时一定要在开发板上长期挂着串口日志或者远程日志记录每次推理的耗时和异常情况。很多问题不是一跑就崩而是跑久了才暴露出来的。有一份完整的日志排查起来会轻松很多。
返回列表