在RK3588上实现YOLOv8n高效推理:从模型优化到GPU加速后处理全解析
在RK3588上实现YOLOv8n高效推理:从模型优化到GPU加速后处理全解析
- 一、背景介绍:为什么选择RK3588与YOLOv8n?
- 二、性能数据
- 三、参考链接
- 四、完整实现流程详解
- 4.1 安装`ultralytics`
- 4.2 安装`rknn-toolkit2`
- 4.3 导出`ONNX`模型
- 4.3.1 方案一 导出NMS之前的Box【RK3588上int8精度有问题,*放弃该方案*】
- 4.3.2 方案二 导出box decoding之前的feature map
- 4.4 生成RKNN模型,用CPU模拟计算
- 4.4.1 下载测试及量化图片
- 4.4.2 模型转换脚本
- 4.5 在RK3588上运行,后处理用numpy
- 4.6 用`pyopencl`加速后处理
- 4.6.1 循环执行一个空的Kernel,避免GPU休眠
- 4.6.2 NPU推理+GPU后处理完整代码
一、背景介绍:为什么选择RK3588与YOLOv8n?
在边缘计算设备上部署目标检测模型时,我们通常会面临两个关键挑战:计算资源受限和实时性要求。RK3588作为新一代旗舰级AIoT芯片,其内置的NPU(神经网络处理单元)提供了6TOPS的算力,非常适合部署轻量级目标检测模型。
YOLOv8n(YOLO You Only Look Once的第八代nano版本)是当前最先进的轻量级检测模型之一,在保持较高精度的同时,模型大小仅6MB左右。但直接将PyTorch模型部署到嵌入式设备会面临两个主要问题:
- 后处理耗时严重:模型输出的原始检测框需要经过复杂的解码和非极大值抑制(NMS),在CPU上执行这些操作会成为性能瓶颈
- 硬件适配困难:原始模型需要转换为硬件专用格式(如RKNN)才能充分利用NPU加速
本文将详细介绍如何通过模型优化、量化压缩和GPU加速后处理等技术,在RK3588上实现YOLOv8n的高效部署。
二、性能数据
方案 | FP16(ms) | INT8(ms) |
---|---|---|
numpy后处理 | 第一次 Infer:90 PostProc:308 E2E:399 BoxCount:34 第十次 Infer:90 PostProc:308. E2E:399.s BoxCount:34 | 第一次 Infer:50 PostProc:307 E2E:358 BoxCount:30 第十次 Infer:43 PostProc:309 E2E:352 BoxCount:30 |
pyopencl GPU后处理 | 第一次 Infer:89 PostProc:50 E2E:139 BoxCount:31 第十次 Infer:81 PostProc:4 E2E:86 BoxCount:31 | 第一次 Infer:49 PostProc:49 E2E:98 BoxCount:30 第十次 Infer:43 PostProc:4 E2E:47 BoxCount:30 |
从上表可以看出两个重要结论:
- 量化加速效果显著:INT8量化使推理时间减少45%(从90ms到43ms)
- 后处理优化空间巨大:使用GPU加速后处理,端到端时间降低78%
YOLOv8的输出解码包含以下主要步骤:
- 特征图解析:处理3个不同尺度的输出层(80x80, 40x40, 20x20)
- DFL解码:使用分布聚焦损失解码边界框坐标
- Sigmoid计算:对分类置信度进行激活
- NMS筛选:过滤重叠检测框
三、参考链接
- 基于BoxMOT的目标检测与跟踪全流程详解
- 在RK3588上使用PyOpenCL加速NMS:原理、实现与性能对比
四、完整实现流程详解
4.1 安装ultralytics
git clone https://github.com/mikel-brostrom/ultralytics.git
cd ultralytics
git checkout 8e17ff56a9db8933a1962b88e05547dd2cce9c48
export PYTHONPATH=$PWD:$PYTHONPATH
wget https://huggingface.co/Ultralytics/YOLOv8/resolve/main/yolov8n.pt
pip3.10 install matplotlib
4.2 安装rknn-toolkit2
git clone https://github.com/airockchip/rknn-toolkit2.git
cd rknn-toolkit2/
pip3.10 uninstall rknn-toolkit2 -y
pip3.10 install -r rknn-toolkit2/packages/arm64/arm64_requirements_cp310.txt
pip3.10 install rknn-toolkit2/packages/arm64/rknn_toolkit2-2.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
cp rknpu2/runtime/Linux/librknn_api/aarch64/librknnrt.so /usr/lib/aarch64-linux-gnu/
pip3.10 install rknn-toolkit-lite2/packages/rknn_toolkit_lite2-2.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
4.3 导出ONNX
模型
4.3.1 方案一 导出NMS之前的Box【RK3588上int8精度有问题,放弃该方案】
cat> export_onnx.py<<-'EOF'
from ultralytics import YOLO
# 加载模型
model = YOLO("yolov8n.pt")
# 将模型导出为 ONNX 格式
path = model.export(format="onnx") # 返回导出模型的路径
EOF
python3.10 export_onnx.py
4.3.2 方案二 导出box decoding之前的feature map
cat> export_onnx.py<<-'EOF'
from ultralytics import YOLO
import importlib
def forward(self,x):y=[]for i in range(self.nl):t1 = self.cv2[i](x[i])t2 = self.cv3[i](x[i])y.append(t1)y.append(t2)return ydef patch_func(old_func: str, new_func: callable):items=old_func.split('.')parent='.'.join(items[:-2])m=importlib.import_module(parent)m=getattr(m,items[-2])func_name=items[-1]setattr(m,func_name,new_func)patch_func("ultralytics.nn.modules.head.Detect.forward",forward)
# 加载模型
model = YOLO("yolov8n.pt",task="detect")
import torch
dummy_input = torch.randn(1, 3, 640, 640)
torch.onnx.export(model.model, dummy_input, "yolov8n.onnx", verbose=False, opset_version=11)
EOF
python3.10 export_onnx.py
4.4 生成RKNN模型,用CPU模拟计算
4.4.1 下载测试及量化图片
wget -O img.jpg https://raw.githubusercontent.com/hi20240217/csdn_images/refs/heads/main/img.jpg
4.4.2 模型转换脚本
cat> gen_yolov8n_rknn.py<<-'EOF'
import os
import urllib
import traceback
import time
import sys
import numpy as np
import cv2
from rknn.api import RKNN
from math import expimport cv2
import numpy as npCLASSES = ['person', 'bicycle', 'car', 'motorcycle', 'airplane', 'bus', 'train', 'truck', 'boat', 'traffic light','fire hydrant', 'stop sign', 'parking meter', 'bench', 'bird', 'cat', 'dog', 'horse', 'sheep', 'cow','elephant', 'bear', 'zebra', 'giraffe', 'backpack', 'umbrella', 'handbag', 'tie', 'suitcase', 'frisbee','skis', 'snowboard', 'sports ball', 'kite', 'baseball bat', 'baseball glove', 'skateboard', 'surfboard','tennis racket', 'bottle', 'wine glass', 'cup', 'fork', 'knife', 'spoon', 'bowl', 'banana', 'apple','sandwich', 'orange', 'broccoli', 'carrot', 'hot dog', 'pizza', 'donut', 'cake', 'chair', 'couch','potted plant', 'bed', 'dining table', 'toilet', 'tv', 'laptop', 'mouse', 'remote', 'keyboard', 'cell phone','microwave', 'oven', 'toaster', 'sink', 'refrigerator', 'book', 'clock', 'vase', 'scissors', 'teddy bear','hair drier', 'toothbrush']import math# --------- 简化数据结构 ---------
# 这里改用普通Python字典,后续在OpenCL中用struct替代
def create_detect_box(class_id, score, xmin, ymin, xmax, ymax):return {'class_id': class_id,'score': score,'xmin': xmin,'ymin': ymin,'xmax': xmax,'ymax': ymax}# --------- 计算IOU ---------
def iou(box1, box2):xmin = max(box1['xmin'], box2['xmin'])ymin = max(box1['ymin'], box2['ymin'])xmax = min(box1['xmax'], box2['xmax'])ymax = min(box1['ymax'], box2['ymax'])inner_width = xmax - xmininner_height = ymax - yminif inner_width <= 0 or inner_height <= 0:return 0.0inner_area = inner_width * inner_heightarea1 = (box1['xmax'] - box1['xmin']) * (box1['ymax'] - box1['ymin'])area2 = (box2['xmax'] - box2['xmin']) * (box2['ymax'] - box2['ymin'])union = area1 + area2 - inner_areaif union <= 0:return 0.0return inner_area / union# --------- NMS ---------
def nms(detect_boxes, nms_thresh):# 手写简单的降序排序(可移植到OpenCL的并行排序算法)# 这里仍保持Python写法,移植时实现对应的排序内核for i in range(len(detect_boxes)):max_idx = ifor j in range(i+1, len(detect_boxes)):if detect_boxes[j]['score'] > detect_boxes[max_idx]['score']:max_idx = jif max_idx != i:detect_boxes[i], detect_boxes[max_idx] = detect_boxes[max_idx], detect_boxes[i]keep = []suppressed = [False] * len(detect_boxes)for i in range(len(detect_boxes)):if suppressed[i]:continuekeep.append(detect_boxes[i])for j in range(i+1, len(detect_boxes)):if suppressed[j]:continueif detect_boxes[i]['class_id'] != detect_boxes[j]['class_id']:continueif iou(detect_boxes[i], detect_boxes[j]) > nms_thresh:suppressed[j] = Truereturn keep# --------- Sigmoid改写 ---------
def sigmoid(x):# 为避免math.exp溢出,限定输入范围if x < -40.0:return 0.0elif x > 40.0:return 1.0else:return 1.0 / (1.0 + math.exp(-x))# --------- 后处理 ---------
def postprocess(out, meshgrid, strides,img_h, img_w,input_img_w, input_img_h,head_num, map_size,object_thresh, nms_thresh,class_num):detect_result = []output = [o.reshape(-1) for o in out