1. 为什么选择 RK3588 跑 YOLOv8:算力账与落地场景
RK3588 这颗芯片在边缘视觉圈子里火起来不是没有道理的。它内置的 NPU 标称 6 TOPS 算力,支持 INT8 量化推理,配合三核 Cortex-A76 加五核 Cortex-A55 的 CPU 架构,跑 YOLOv8n 这种轻量级检测模型时,单帧推理能压到 20ms 以内。我实测过一块 8GB 内存的 RK3588 开发板,YOLOv8n 输入 640x640,INT8 量化后单核 NPU 推理稳定在 18-25ms 区间,三核并行还能再往下压。这个性能放在智能安防、工业质检、车载辅助这些场景里,已经完全够用了。
很多人第一次接触这个流程时,最大的困惑不是"能不能跑",而是"从哪开始"。PyTorch 训练出来的权重是浮点模型,RK3588 的 NPU 只认 RKNN 格式,中间要经过 ONNX 导出、量化校准、模型转换、板端部署这一整条链路。每一步都有坑,而且坑和坑之间是串联的——前面一步没做对,后面直接报错,排查起来非常痛苦。
这篇文章面向的是已经有一定深度学习基础、手里有 RK3588 开发板、想把 YOLOv8 真正跑起来的工程师。我会把从训练到部署的完整链路拆开讲,重点放在那些官方文档里不会写、但实际调试中一定会遇到的问题上。整个流程涉及的核心工具链包括:PyTorch 训练框架、ONNX 作为中间格式、RKNN-Toolkit2 做模型转换和量化、板端 RKNPU2 运行时做推理。
先给一个全局的链路图景,让你心里有数:
| 阶段 | 输入 | 输出 | 核心工具 |
|---|---|---|---|
| 模型训练 | 自定义数据集 | PyTorch 权重 .pt | ultralytics |
| 格式导出 | .pt | .onnx | torch.onnx.export |
| 模型转换 | .onnx | .rknn | RKNN-Toolkit2 |
| 板端部署 | .rknn | 推理结果 | RKNPU2 Runtime |
这条链路上,训练和导出在 PC 上完成,转换可以在 PC 或板端完成,部署一定在板端。量化校准需要一批有代表性的图片,这一步直接决定最终精度掉多少。
提示:RKNN-Toolkit2 的版本和板端 RKNPU2 驱动的版本必须匹配,否则会出现模型加载失败或推理结果异常。建议在开始之前先确认板端固件版本,再选择对应版本的 Toolkit。
2. 训练环境的搭建与 YOLOv8 模型训练要点
2.1 环境配置的版本陷阱
YOLOv8 的训练环境看起来简单,pip install ultralytics就能跑,但真正要导出 ONNX 并保证后续转换顺利,版本控制非常关键。我踩过最深的坑是 PyTorch 版本和 ONNX opset 的兼容性问题。ultralytics 在不同版本里对 ONNX 导出的默认 opset 设置不一样,opset 太高 RKNN-Toolkit2 不支持,太低又会导致某些算子导出失败。
我目前验证过比较稳的组合是:
- Python 3.8 或 3.10
- PyTorch 2.0.1 或 2.1.0
- ultralytics 8.0.x 系列
- onnx 1.14.0
- onnxruntime 1.15.1
安装命令大致如下:
conda create -n yolov8_rknn python=3.10 conda activate yolov8_rknn pip install torch==2.1.0 torchvision==0.16.0 --index-url https://download.pytorch.org/whl/cu118 pip install ultralytics==8.0.200 pip install onnx==1.14.0 onnxruntime==1.15.1 pip install onnxsimonnxsim这个工具后面会用到,它的作用是对导出的 ONNX 模型做图优化,把一些冗余的算子合并掉,能显著减少 RKNN 转换时的算子兼容问题。
2.2 自定义数据集训练的关键参数
YOLOv8 训练自己的数据集,数据组织格式是 YOLO 标准的 txt 标注,每行是class_id x_center y_center width height,坐标都归一化到 0-1。目录结构长这样:
dataset/ images/ train/ val/ labels/ train/ val/ data.yamldata.yaml里配置好路径和类别名。训练命令本身不复杂:
yolo detect train data=dataset/data.yaml model=yolov8n.pt epochs=200 imgsz=640 batch=16但有几个参数对后续部署影响很大,必须提前想清楚。第一个是imgsz,训练时的输入尺寸最好和部署时保持一致,RK3588 上常用 640x640,如果你训练用 640 部署用 320,精度会掉得莫名其妙。第二个是rect参数,默认训练会做矩形推理,但导出 ONNX 时如果输入是动态尺寸,RKNN 转换会麻烦很多,建议训练时就固定成正方形输入。
关于freeze参数,如果你想在预训练权重基础上微调,可以冻结 backbone 的前若干层,减少训练时间。但要注意,冻结层数太多会导致模型对自定义数据集的适应能力下降,我一般只冻结前 5 层左右。
训练完成后,损失函数曲线可以用 ultralytics 自带的工具画出来,观察是否过拟合。如果验证集 loss 在后期开始上升,说明该早停了。这些细节看起来和部署无关,但模型质量直接决定量化后的精度底线。
2.3 训练完成后的模型自检
训练结束后别急着导出,先在 PC 上用验证集跑一遍,确认 mAP 正常。然后拿几张实际场景的图片做推理测试,看看检测框是否合理。我遇到过训练指标很好看但实际推理一塌糊涂的情况,原因是标注数据里有一批图片的标注框偏移了,训练时被平均掉了,验证集又恰好没覆盖到。
yolo detect val model=runs/detect/train/weights/best.pt data=dataset/data.yaml yolo detect predict model=runs/detect/train/weights/best.pt source=test_images/确认无误后,best.pt就是后续流程的起点。
3. 从 PyTorch 到 ONNX:导出环节的隐藏细节
3.1 导出命令与 opset 选择
ultralytics 提供了直接的导出接口:
yolo export model=best.pt format=onnx opset=12 simplify=True这里opset=12是我反复验证后比较稳妥的选择。RKNN-Toolkit2 对 opset 11 到 13 的支持最好,opset 12 在算子覆盖和兼容性之间平衡得不错。simplify=True会调用 onnxsim 做图优化,这一步能去掉很多无用的 Identity 节点和常量折叠。
导出完成后你会得到一个best.onnx文件。但别以为这就完事了,直接拿这个 ONNX 去转 RKNN,大概率会遇到算子不支持或者输出节点不对的问题。
3.2 检查 ONNX 模型的输入输出
用 Netron 打开 ONNX 文件,或者用代码打印输入输出信息:
import onnx model = onnx.load("best.onnx") for inp in model.graph.input: print("Input:", inp.name, [d.dim_value for d in inp.type.tensor_type.shape.dim]) for out in model.graph.output: print("Output:", out.name, [d.dim_value for d in out.type.tensor_type.shape.dim])YOLOv8 导出的 ONNX 通常有多个输出头,对应不同尺度的检测结果。RKNN 转换时需要明确指定输出节点,如果输出节点选错了,板端推理出来的结果维度对不上,后处理直接崩。
3.3 动态维度改成静态
YOLOv8 默认导出的 ONNX 输入是动态 batch 和动态尺寸,这对 RKNN 转换很不友好。RKNN 更偏好固定输入尺寸。可以在导出时指定:
yolo export model=best.pt format=onnx opset=12 simplify=True imgsz=640 batch=1这样导出的 ONNX 输入就是固定的1x3x640x640。如果已经导出了动态模型,也可以用 onnx 的 API 手动改:
import onnx from onnx import shape_inference model = onnx.load("best.onnx") model = shape_inference.infer_shapes(model) # 手动设置输入维度为固定值 model.graph.input[0].type.tensor_type.shape.dim[0].dim_value = 1 model.graph.input[0].type.tensor_type.shape.dim[2].dim_value = 640 model.graph.input[0].type.tensor_type.shape.dim[3].dim_value = 640 onnx.save(model, "best_fixed.onnx")3.4 一个容易被忽略的坑:输出节点命名
YOLOv8 导出的 ONNX 输出节点名字通常是output0这种自动生成的。RKNN 转换时如果不指定输出节点,它会自己推断,有时候推断出来的输出顺序和你想的不一样。我建议在导出后手动确认输出节点名称,并在 RKNN 转换配置里显式指定。
注意:ONNX 模型导出后,务必用 onnxruntime 在 PC 上跑一遍推理,确认输出结果和 PyTorch 一致。这一步是后面所有工作的基准,如果这里就不对,后面全是白费功夫。
4. RKNN 模型转换与 INT8 量化实战
4.1 RKNN-Toolkit2 环境搭建
RKNN-Toolkit2 是瑞芯微官方提供的模型转换工具,运行在 PC 上(x86 Linux 环境)。安装方式是从官方仓库下载 whl 包:
pip install rknn_toolkit2-1.5.2-cp310-cp310-linux_x86_64.whl版本选择要和板端 RKNPU2 驱动匹配。我用的板端固件是 1.5.0 版本,Toolkit 用 1.5.2,兼容性没问题。如果你用的是更新的固件,Toolkit 也要相应升级。
安装完成后可以验证:
from rknn.api import RKNN print("RKNN Toolkit2 loaded")4.2 转换脚本的完整写法
RKNN 转换的核心是一个 Python 脚本,配置模型路径、目标平台、量化方式等。下面是一个我实际使用的完整脚本:
from rknn.api import RKNN rknn = RKNN(verbose=True) # 配置模型预处理 rknn.config( mean_values=[[0, 0, 0]], std_values=[[255, 255, 255]], target_platform='rk3588', quantized_dtype='asymmetric_quantized-8', optimization_level=3 ) # 加载 ONNX 模型 ret = rknn.load_onnx(model='best_fixed.onnx') if ret != 0: print("Load ONNX failed") exit(ret) # 构建 RKNN 模型,指定量化校准数据集 ret = rknn.build(do_quantization=True, dataset='calibration.txt') if ret != 0: print("Build RKNN failed") exit(ret) # 导出 RKNN 模型 ret = rknn.export_rknn('best.rknn') if ret != 0: print("Export RKNN failed") exit(ret) rknn.release()这里有几个关键点需要展开说。
mean_values和std_values是预处理参数。YOLOv8 训练时输入是 0-1 归一化的 RGB 图像,所以这里 mean 设为 0,std 设为 255,表示把 0-255 的输入除以 255。如果你在板端前处理里已经做了归一化,这里就要相应调整,否则会出现重复归一化导致精度暴跌。
quantized_dtype选asymmetric_quantized-8是 INT8 非对称量化,这是 RK3588 NPU 支持最好的量化方式。对称量化在某些层上会有精度损失。
optimization_level=3是最高优化级别,会做一些算子融合和内存优化,但偶尔也会引入问题。如果转换后精度异常,可以降到 2 试试。
4.3 量化校准数据集的选择
calibration.txt是一个文本文件,每行是一张校准图片的路径。这些图片必须来自你的实际应用场景,数量在 100 到 500 张之间比较合适。太少会导致量化参数估计不准,太多会拖慢转换速度。
我一般从训练集里随机抽 200 张,再额外加 50 张验证集里的图片,确保覆盖各种光照和场景。校准图片的预处理方式必须和板端推理时完全一致,包括 resize、归一化、通道顺序。
# calibration.txt 示例 ./calib/img_001.jpg ./calib/img_002.jpg ./calib/img_003.jpg4.4 量化精度损失的定位方法
INT8 量化后精度下降是正常的,一般 mAP 掉 1-3 个点是可接受的。如果掉超过 5 个点,就要排查原因。常见的排查路径:
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 精度暴跌 | 预处理参数不匹配 | 检查 mean/std 和板端前处理 |
| 部分类别失效 | 校准集不均衡 | 增加该类别图片到校准集 |
| 输出全乱 | 输出节点选错 | 用 Netron 确认输出节点 |
| 转换报错 | 算子不支持 | 查看 verbose 日志定位算子 |
RKNN-Toolkit2 提供了仿真推理功能,可以在 PC 上模拟板端推理结果:
rknn.load_rknn('best.rknn') rknn.init_runtime(target='rk3588') outputs = rknn.inference(inputs=[test_img])把仿真结果和 ONNX 推理结果对比,就能定位是量化问题还是后处理问题。
提示:如果某些算子 RKNN 不支持,可以尝试在 ONNX 导出时用
simplify做算子替换,或者手动修改模型结构把不支持的算子换掉。YOLOv8 里常见的 SiLU 激活函数在旧版 Toolkit 里支持不好,升级到 1.5.x 后基本没问题。
5. 板端部署:RKNPU2 运行时与推理代码
5.1 板端环境确认
RK3588 板端需要确认 RKNPU2 驱动和运行时库已经安装。一般官方固件里都带了,可以用以下命令检查:
cat /sys/kernel/debug/rknpu/version如果输出类似RKNPU driver version: 0.9.2就说明驱动正常。运行时库是librknnrt.so,通常在/usr/lib/目录下。
Python 端需要安装rknn_toolkit_lite2,这个包是专门给板端用的轻量级推理接口:
pip install rknn_toolkit_lite2-1.5.2-cp310-cp310-linux_aarch64.whl5.2 板端推理代码的完整实现
板端推理的核心流程是:加载 RKNN 模型、初始化运行时、前处理、推理、后处理。下面是一个完整的 Python 示例:
import numpy as np import cv2 from rknnlite.api import RKNNLite class YOLOv8RKNN: def __init__(self, model_path): self.rknn = RKNNLite() ret = self.rknn.load_rknn(model_path) if ret != 0: raise RuntimeError("Load RKNN failed") ret = self.rknn.init_runtime(core_mask=RKNNLite.NPU_CORE_0_1_2) if ret != 0: raise RuntimeError("Init runtime failed") self.input_size = (640, 640) def preprocess(self, img): img_resized = cv2.resize(img, self.input_size) img_rgb = cv2.cvtColor(img_resized, cv2.COLOR_BGR2RGB) img_input = np.expand_dims(img_rgb, axis=0) return img_input def inference(self, img): input_data = self.preprocess(img) outputs = self.rknn.inference(inputs=[input_data]) return outputs def postprocess(self, outputs, conf_thres=0.25, iou_thres=0.45): # YOLOv8 输出解码 predictions = np.squeeze(outputs[0]).T scores = np.max(predictions[:, 4:], axis=1) predictions = predictions[scores > conf_thres, :] scores = scores[scores > conf_thres] if len(scores) == 0: return [] class_ids = np.argmax(predictions[:, 4:], axis=1) boxes = predictions[:, :4] # 转换 xywh 到 xyxy boxes_xyxy = np.zeros_like(boxes) boxes_xyxy[:, 0] = boxes[:, 0] - boxes[:, 2] / 2 boxes_xyxy[:, 1] = boxes[:, 1] - boxes[:, 3] / 2 boxes_xyxy[:, 2] = boxes[:, 0] + boxes[:, 2] / 2 boxes_xyxy[:, 3] = boxes[:, 1] + boxes[:, 3] / 2 # NMS indices = cv2.dnn.NMSBoxes( boxes_xyxy.tolist(), scores.tolist(), conf_thres, iou_thres ) results = [] for i in indices: results.append({ 'box': boxes_xyxy[i], 'score': scores[i], 'class_id': class_ids[i] }) return results def release(self): self.rknn.release()core_mask=RKNNLite.NPU_CORE_0_1_2表示使用三个 NPU 核心并行推理,这是 RK3588 的独有优势。如果你的模型比较小,单核就够用,可以改成NPU_CORE_0减少功耗。
5.3 后处理中的坐标还原
YOLOv8 的输出是相对于输入尺寸 640x640 的归一化坐标,需要还原到原图尺寸。这一步很容易出错,尤其是图片做了 letterbox 填充的情况下。如果前处理用了 letterbox,后处理必须做对应的逆变换,否则检测框会偏移。
我一般在前处理时记录缩放比例和填充偏移量:
def letterbox(self, 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, dh = new_shape[1] - new_unpad[0], new_shape[0] - new_unpad[1] dw /= 2 dh /= 2 img_resized = cv2.resize(img, new_unpad, interpolation=cv2.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_padded = cv2.copyMakeBorder( img_resized, top, bottom, left, right, cv2.BORDER_CONSTANT, value=color ) return img_padded, r, (left, top)后处理时用r和(left, top)把坐标还原回去。这个细节不做,检测框会整体偏移,而且偏移量随图片长宽比变化,非常隐蔽。
5.4 性能实测与优化
在 RK3588 上跑 YOLOv8n,我实测的数据如下:
| 配置 | 单帧耗时 | 帧率 |
|---|---|---|
| 单核 NPU | 28ms | 35 FPS |
| 三核 NPU | 18ms | 55 FPS |
| 三核 + 零拷贝 | 15ms | 66 FPS |
零拷贝是指用 RKNN 的inference接口直接传入 numpy 数组,避免内存拷贝。如果追求极致性能,可以用 C++ 接口配合 DMA 缓冲区,还能再快一点。
CPU 占用方面,前处理和后处理是瓶颈。如果帧率要求高,可以把 resize 和归一化用 RGA 硬件加速,RK3588 有独立的 RGA 模块专门做图像缩放和格式转换。
6. 调试过程中最容易卡住的几个问题
6.1 模型加载失败:版本不匹配
最常见的报错是load_rknn failed或者init_runtime failed。九成以上是 Toolkit 版本和板端驱动版本不匹配。排查方法是分别打印两边的版本号:
# 板端 cat /sys/kernel/debug/rknpu/version # PC 端 python -c "from rknn.api import RKNN; print(RKNN().version)"两个版本号的主版本号必须一致,次版本号可以有小差异。如果差太多,要么升级板端固件,要么降级 Toolkit。
6.2 推理结果全为零或全为同一类别
这种情况通常是量化校准出了问题。检查校准集图片是否和推理时的输入分布一致。我遇到过一次,校准集用的是白天场景,实际推理是夜间红外图像,量化参数完全不对,输出全是背景。
解决办法是把实际场景的图片加入校准集,重新转换。校准集要覆盖各种光照、角度、目标尺度。
6.3 检测框偏移或尺寸不对
前面提到的 letterbox 逆变换问题。还有一种可能是 RKNN 输出的坐标格式和预期不一致。RKNN 转换后输出可能是[1, 84, 8400]或者[1, 8400, 84],取决于转换时的配置。用rknn.inference打印输出 shape 确认一下,然后在后处理里做对应转置。
6.4 多线程推理时的资源竞争
如果你在板端开了多个线程同时推理,要注意 RKNN 运行时不是线程安全的。每个线程需要独立的 RKNNLite 实例,或者用锁串行化。我一般用单线程推理加队列的方式,避免资源竞争。
注意:RKNN 模型加载后,
init_runtime只需要调用一次。重复调用会导致内存泄漏,长时间运行后板子会卡死。
7. 从能跑到好用:几个实战优化技巧
7.1 模型剪枝与通道裁剪
如果 YOLOv8n 还是太慢,可以考虑对模型做剪枝。用 ultralytics 训练时加prune参数,或者在 ONNX 层面用工具做通道裁剪。剪枝后的模型需要重新微调,精度会掉一些,但推理速度能提升 20%-30%。
7.2 输入分辨率的选择
640x640 是精度和速度的平衡点。如果场景里目标比较大,可以降到 416x416,速度能提升近一倍,精度掉 2-3 个点。如果目标很小,比如远距离检测,反而要升到 800x800 以上,但 RK3588 的 NPU 对非 640 尺寸的支持需要额外验证。
7.3 多模型并行
RK3588 有三个 NPU 核心,可以同时跑三个模型。比如一个跑检测,一个跑分类,一个跑 OCR。用core_mask分别指定核心,互不干扰。这个特性在复杂视觉系统里非常有用。
7.4 温度与功耗管理
RK3588 满负荷跑 NPU 时发热不小,长时间运行需要加散热片。如果板子温度超过 80 度,NPU 会降频,推理速度明显下降。可以在代码里监控温度:
cat /sys/class/thermal/thermal_zone0/temp返回值除以 1000 就是摄氏度。超过 75 度就要考虑加风扇或者降低推理频率。
7.5 模型加密与授权
如果部署到商业产品里,RKNN 模型可以加密。RKNN-Toolkit2 支持在导出时设置加密密钥,板端加载时需要提供相同密钥。这个功能在防止模型被逆向时有用,但会增加一点加载时间。
整个流程走下来,从训练到部署,顺利的话两三天能跑通,不顺利的话在量化精度和板端调试上卡一两周也正常。关键是要有耐心,每一步都做验证,不要跳步。ONNX 导出后在 PC 上验证,RKNN 转换后用仿真验证,板端部署后先用单张图片验证,确认无误再上视频流。这个逐级验证的习惯,能帮你省下大量排查时间。