1. 为什么要在RK3588上折腾YOLOv5量化
手里这块RK3588板子标称6TOPS算力,第一次拿到的时候我也觉得这数字挺唬人。但真把YOLOv5的PyTorch权重直接扔上去跑,帧率惨不忍睹——FP16精度下勉强十几帧,换成原始FP32模型更是卡成幻灯片。问题出在哪?6TOPS这个数字是有前提的:它指的是NPU在INT8精度下的理论峰值算力。你拿FP32模型去跑,等于让一台为整数运算优化的专用引擎去干浮点活,算力利用率可能连20%都不到。
这就是量化存在的意义。把FP32的权重和激活值映射到INT8整数域,模型体积缩小到原来的四分之一,推理速度提升2到4倍,而精度损失在YOLOv5这种检测任务上通常可以控制在1到2个mAP百分点以内。对于RK3588这颗芯片来说,量化不是可选项,是必选项——你不做量化,NPU基本等于白买。
但量化这件事,坑比想象中多。我见过太多人拿着YOLOv5的.pt文件,用RKNN-Toolkit2一路默认参数转下去,结果要么转换报错,要么转出来了但检测框乱飞,要么精度掉得亲妈都不认识。问题往往不在工具本身,而在于对量化流程的理解不够——混合量化怎么配、校准集怎么选、量化感知训练要不要做、RKNN的量化粒度是什么级别,这些细节决定了最终模型能不能用。
这篇文章面向的是手里有RK3588开发板、想把YOLOv5真正跑起来的开发者。不管你是刚拿到板子的新手,还是已经跑通过但精度不达标的进阶用户,下面这些从实际项目中摔打出来的经验,应该能帮你少走几天弯路。我会从模型导出开始,一步步讲到RKNN量化配置、板端部署验证,以及那些官方文档里不会写的踩坑记录。
2. 从PyTorch到ONNX:导出环节的隐藏陷阱
2.1 为什么不能直接拿.pt文件去量化
RKNN-Toolkit2支持的模型格式里,ONNX是最稳妥的中间表示。有人可能会想,能不能跳过ONNX直接转RKNN?理论上RKNN-Toolkit2确实支持PyTorch的torchscript格式,但实际用下来,torchscript那条路对YOLOv5这种包含大量自定义算子的模型来说,兼容性远不如ONNX。而且ONNX作为中间层,你可以用onnxsim做图优化,用Netron可视化检查结构,出问题了也容易定位。
另一个常见误区是拿YOLOv5官方仓库的export.py直接导出就完事。官方脚本默认导出的ONNX是动态batch的,输入维度是[1,3,640,640]但batch维度标记为动态。RKNN-Toolkit2在量化阶段对动态shape的支持有限,虽然新版本有所改善,但为了省事,建议导出时就固定batch=1。
2.2 导出命令与参数取舍
YOLOv5的导出脚本参数不少,但真正影响后续量化的就几个。我常用的命令是这样的:
python export.py --weights yolov5s.pt --include onnx --img 640 --batch 1 --opset 12 --simplify这里逐个说下取舍逻辑。--opset 12是我实测下来和RKNN-Toolkit2兼容性最好的版本,opset 11在某些算子映射上会出问题,opset 13以上又可能引入RKNN还不支持的新算子。--simplify会调用onnxsim做常量折叠和算子融合,这一步很关键——它能消掉一些冗余的Transpose和Reshape,减少后续量化时的算子兼容问题。
--img 640这个不用多说,YOLOv5的标准输入尺寸。但如果你实际部署时输入分辨率不是640,比如用320或者1280,那导出时就要对应改掉。RKNN量化后的模型输入尺寸是固定的,后期改不了。
导出完成后,强烈建议用Netron打开ONNX文件看一眼。重点检查三处:输入节点的shape是不是[1,3,640,640]、输出节点是不是三个检测头(对应不同尺度)、有没有出现RKNN不支持的算子比如GridSample或者NonMaxSuppression。YOLOv5的NMS通常是在后处理里做的,不在模型图内,但有些导出配置会把NMS嵌进去,那就麻烦了。
2.3 输出节点命名与后续量化的关系
YOLOv5导出ONNX后,输出节点默认叫output、output1、output2之类的名字。这些名字在RKNN量化配置里会用到——你需要告诉RKNN哪些节点是输出,量化时对这些节点的处理方式可能不同。如果输出节点名字混乱或者有重复,量化脚本可能报错。
我习惯在导出后用onnx工具重命名输出节点,改成有意义的名称比如detect_80、detect_40、detect_20,对应三个不同尺度的检测头。这样在写RKNN量化配置时一目了然,不容易搞混。
另外要注意的是,YOLOv5的ONNX输出是未经过sigmoid和decode的原始特征图。这意味着量化时这些输出节点的数值范围可能比较大,需要在校准集里覆盖足够多的场景,否则量化后的输出偏差会被后续的decode放大。
3. RKNN量化配置:混合量化的参数怎么调
3.1 量化数据集的选择与预处理
RKNN-Toolkit2做量化时需要一个校准数据集,通常是一批图片。这批图片的质量直接决定量化后的精度。我见过有人随便拿几十张图就去量化,结果模型在特定场景下完全失效。校准集的核心原则是:分布要覆盖你实际部署时可能遇到的所有场景。
具体来说,如果你做的是安防场景,校准集里就要包含白天、夜晚、逆光、雨天等各种光照条件;如果是工业检测,就要覆盖不同缺陷类型和背景。数量上,官方建议100到200张,但我实测下来,200到500张效果更稳。太少会导致量化参数估计不准,太多则量化时间线性增长。
预处理方面,校准图片的尺寸要和模型输入一致,也就是640x640。但不要直接resize,而是用letterbox方式保持宽高比填充。因为YOLOv5训练时用的就是letterbox,量化校准如果用了不同的预处理方式,会导致激活值分布偏移。
# 校准集预处理示例 import cv2 import numpy as np 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, dh = new_shape[1] - new_unpad[0], new_shape[0] - new_unpad[1] dw, dh = dw // 2, dh // 2 img = cv2.resize(img, new_unpad, interpolation=cv2.INTER_LINEAR) top, bottom = dh, dh left, right = dw, dw img = cv2.copyMakeBorder(img, top, bottom, left, right, cv2.BORDER_CONSTANT, value=color) return img这段代码和YOLOv5训练时的letterbox逻辑一致,确保校准集和训练集的预处理对齐。
3.2 混合量化的分层策略
RKNN-Toolkit2支持混合量化,也就是可以对不同层设置不同的量化精度。默认情况下是全INT8,但有些层对精度敏感,强行INT8会导致较大误差。哪些层需要保持FP16?我的经验是重点关注三类:第一层卷积(直接处理输入图像,数值范围大)、检测头附近的卷积(输出直接影响检测结果)、以及任何包含特殊激活函数的层。
在RKNN的量化配置里,可以通过hybrid_quantization参数指定哪些层用FP16。但手动指定层名很麻烦,更实用的做法是先全INT8量化一版,跑精度测试,找出误差最大的层,再针对性调整。
# RKNN量化配置示例 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', quantized_algorithm='normal', optimization_level=3 )这里几个参数值得展开说。mean_values和std_values是归一化参数,YOLOv5训练时用的是0到1归一化,所以这里mean设0、std设255,把输入从0-255映射到0-1。quantized_dtype选asymmetric_quantized-8,非对称量化比对称量化在激活值分布偏斜时效果更好。optimization_level=3会启用更激进的图优化,但偶尔会导致算子融合后精度下降,如果发现精度异常可以降到2试试。
3.3 量化算法normal与mmse的实测对比
RKNN-Toolkit2提供两种量化算法:normal和mmse。normal是标准的min-max量化,速度快但精度一般;mmse(最小均方误差)会迭代优化量化参数,精度更好但耗时更长。
我在YOLOv5s上做过对比测试,用同一批500张校准图:
| 量化算法 | 量化耗时 | mAP@0.5 | 模型大小 |
|---|---|---|---|
| normal | 约3分钟 | 0.352 | 3.7MB |
| mmse | 约18分钟 | 0.371 | 3.7MB |
mmse比normal高了近2个mAP点,代价是量化时间多了5倍。对于精度要求高的场景,这时间花得值。但如果只是做原型验证,normal也够用。
还有一个细节:mmse算法对校准集数量更敏感。校准集少于100张时,mmse的优势不明显甚至可能更差,因为迭代优化需要足够的样本估计分布。
4. 板端部署:从RKNN模型到实际推理
4.1 RKNN模型在板端的加载与初始化
量化完成得到.rknn文件后,下一步是把它放到RK3588板子上跑起来。板端推理需要用到RKNN Runtime库,通常板子厂商的SDK里已经包含了。加载模型的代码不复杂,但有几个初始化参数会影响性能。
from rknnlite.api import RKNNLite rknn_lite = RKNNLite() ret = rknn_lite.load_rknn('yolov5s_quantized.rknn') ret = rknn_lite.init_runtime(core_mask=RKNNLite.NPU_CORE_0_1_2)core_mask这个参数很关键。RK3588的NPU有三个核心,可以单独使用也可以组合。NPU_CORE_0_1_2表示三个核心都用上,理论算力最高。但实际测试下来,多核并行的效率取决于模型能否被均匀切分。YOLOv5s这种规模的模型,三核并行的加速比大概在2.2到2.5倍之间,达不到理想的3倍。如果跑的是更小的模型,可能双核就够了,三核反而因为调度开销导致延迟增加。
另一个容易忽略的点是init_runtime的耗时。每次程序启动都要重新初始化NPU,这个过程大概需要几百毫秒。如果是长时间运行的服务,建议把RKNNLite对象做成全局单例,避免反复初始化。
4.2 输入输出的内存布局与数据搬运
RKNN推理的输入输出都是numpy数组,但数据布局和OpenCV读进来的图片不一样。OpenCV默认是HWC格式,而RKNN需要的是NHWC或者NCHW,具体取决于模型导出时的设置。YOLOv5的ONNX通常是NCHW,所以需要做transpose。
img = cv2.imread('test.jpg') img = letterbox(img, (640, 640)) img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) img = img.transpose(2, 0, 1) # HWC -> CHW img = np.expand_dims(img, axis=0) # CHW -> NCHW outputs = rknn_lite.inference(inputs=[img])数据搬运本身不复杂,但要注意类型转换。RKNN的输入期望是uint8或者float32,取决于量化配置。如果模型量化时用了mean/std归一化,板端推理时输入可以直接是uint8的0-255,归一化会在NPU内部完成。这样省去了CPU上的浮点运算,对帧率提升有帮助。
输出方面,YOLOv5的三个检测头输出shape分别是[1,255,80,80]、[1,255,40,40]、[1,255,20,20]。这些是原始特征图,需要经过sigmoid、decode、NMS才能得到最终检测框。后处理在CPU上做,这部分耗时在YOLOv5s上大概占整体延迟的30%到40%。如果追求极致帧率,可以考虑把后处理也放到NPU上,但实现复杂度高,一般项目没必要。
4.3 实测帧率与6TOPS算力的真实利用率
在RK3588上跑量化后的YOLOv5s,实测数据如下:
| 配置 | 推理延迟 | 帧率 | CPU占用 |
|---|---|---|---|
| 单核NPU | 28ms | 35FPS | 15% |
| 双核NPU | 16ms | 62FPS | 18% |
| 三核NPU | 12ms | 83FPS | 22% |
这是640x640输入、batch=1的结果。83FPS对应每帧12ms,换算成算力利用率:YOLOv5s的INT8计算量大约是4.5GOPS,12ms完成意味着实际算力约375GOPS,也就是0.375TOPS。相比标称的6TOPS,利用率只有6%左右。
这个数字看起来很低,但其实是正常的。6TOPS是NPU的理论峰值,实际利用率受限于内存带宽、算子调度、后处理开销等多个因素。YOLOv5s本身计算量不大,瓶颈更多在数据搬运而非计算。如果你跑的是YOLOv5l或者更大的模型,算力利用率会更高,可能达到15%到20%。
想提升利用率,有几个方向:增大batch size(但会增加延迟)、使用更高效的算子实现、把后处理也卸载到NPU。不过对于大多数应用场景,83FPS已经足够用了。
5. 精度掉点排查:从mAP下降到检测框偏移
5.1 量化前后精度对比的正确方法
量化后精度掉了多少,不能只看一两个样本的检测结果,要用标准评估流程。我通常的做法是:在PC上用ONNX模型跑一遍验证集,记录mAP;然后在板子上用RKNN模型跑同一个验证集,再记录mAP。两者对比才能反映真实的量化损失。
但这里有个坑:板端推理的后处理代码必须和PC端完全一致。我遇到过有人PC端用YOLOv5官方NMS,板端自己写了个简化版NMS,结果mAP差异很大,还以为是量化的问题。后处理的置信度阈值、NMS的IoU阈值、最大检测数这些参数都要对齐。
另一个细节是输入预处理。PC端跑ONNX时可能用了归一化到0-1,板端如果直接用0-255输入,虽然RKNN内部会做归一化,但如果mean/std配置错了,结果就会偏。建议在量化配置里明确写好mean和std,板端推理时输入原始uint8数据,让NPU统一处理。
5.2 常见精度问题与对应解决方案
量化后精度问题大致分三类,每类的表现和解决方法不同。
第一类是整体mAP下降但检测框位置基本正确。这通常是量化误差累积导致的置信度偏移。解决方法:增加校准集数量、改用mmse量化算法、对检测头附近的层使用FP16混合量化。
第二类是特定类别检测失效。比如人检测正常但车检测全丢。这往往是因为校准集里该类别的样本太少,量化参数对该类别的激活值分布估计不准。解决方法:在校准集里增加该类别的样本比例,确保每个类别至少有20到30张。
第三类是检测框位置偏移或大小异常。这通常是输出层的量化误差被decode放大了。YOLOv5的输出是相对偏移量,量化误差在乘以anchor尺寸后会被放大。解决方法:对输出层使用FP16,或者调整anchor配置使其更匹配实际数据分布。
5.3 用混合量化拯救掉点严重的层
当全INT8量化精度不达标时,混合量化是最后的救命稻草。但怎么找到需要FP16的层?我的方法是二分排查:先把所有层设为INT8,跑精度;然后把模型从中间切成两半,前半部分FP16后半部分INT8,跑精度;再反过来。通过几次二分就能定位到敏感层所在的区间。
RKNN-Toolkit2的混合量化配置可以通过修改量化配置文件实现。具体来说,在量化时指定hybrid_quantization参数,传入一个列表,列出需要保持FP16的层名。层名可以从Netron里查看ONNX模型得到。
需要注意的是,FP16层的比例不宜过高。如果超过30%的层都用FP16,模型体积会显著增大,推理速度也会下降,量化的意义就大打折扣了。一般来说,控制在10%到20%之间比较合理。
6. 那些官方文档不会告诉你的实操细节
6.1 校准集制作中的常见错误
校准集制作看似简单,但有几个坑我踩过不止一次。第一个坑是图片格式不统一。有些是JPEG有些是PNG,有些是灰度图有些是RGB。RKNN在读取校准图片时如果遇到不支持的格式会直接跳过,导致实际参与量化的图片数量少于预期。建议统一转成RGB的JPEG格式。
第二个坑是图片尺寸。虽然RKNN会自动resize到模型输入尺寸,但resize的方式是直接缩放还是letterbox,会影响激活值分布。我建议在校准集制作阶段就手动做好letterbox,保存成640x640的图片,这样RKNN读取时不需要再做resize,避免引入额外的插值误差。
第三个坑是校准集和验证集重叠。有人图省事,直接拿验证集的图片做校准,然后又在验证集上评估精度。这样得到的精度是虚高的,因为模型已经“见过”这些图片的分布了。校准集和验证集必须严格分开,最好来自不同的数据批次。
6.2 RKNN-Toolkit2版本选择的经验
RKNN-Toolkit2的版本更新很频繁,不同版本之间的量化行为和API都有差异。我用过1.4.0、1.5.0、1.6.0和2.0.0几个版本,感受是:1.5.0比较稳定,量化精度和速度平衡得不错;1.6.0引入了一些新特性但偶尔有bug;2.0.0改动较大,API有 breaking change,老代码迁移需要时间。
选择版本的原则是:跟板子端Runtime库的版本匹配。RKNN模型是版本相关的,用1.5.0工具量化出来的模型,在1.6.0的Runtime上可能加载失败。所以先确认板子SDK里带的Runtime版本,然后选择对应的Toolkit版本。
如果非要用新版本Toolkit,记得在量化配置里检查API是否有变化。比如2.0.0版本里config函数的参数名和1.x有所不同,直接套用老代码会报错。
6.3 多核NPU调度的实际表现
RK3588的三个NPU核心并不是完全独立的,它们共享内存带宽。当三个核心同时跑推理时,内存带宽可能成为瓶颈,导致加速比达不到3倍。我实测下来,YOLOv5s在三核下的加速比大约是2.4倍,YOLOv5m大约是2.6倍,模型越大加速比越高,因为计算密度更大,内存带宽的相对瓶颈没那么明显。
另一个发现是,多核调度对延迟的改善不是线性的。单核到双核延迟从28ms降到16ms,降了43%;双核到三核从16ms降到12ms,只降了25%。如果应用对延迟极度敏感,双核可能是性价比最高的选择。
还有一点:多核并行时,如果多个推理任务同时提交,RKNN Runtime会自动做负载均衡。但如果只有一个任务,它会默认用指定的核心组合。所以如果你的应用是单路视频流,指定三核能获得最低延迟;如果是多路视频流,每路指定一个核心可能整体吞吐更高。
6.4 模型加密与部署安全
实际产品部署时,RKNN模型可能需要加密,防止被提取。RKNN-Toolkit2支持模型加密,在导出时设置加密密钥,板端加载时需要提供相同的密钥。这个功能用起来简单,但有个坑:加密后的模型加载速度会变慢,因为需要先解密再加载。如果对启动时间敏感,需要权衡。
另外,加密密钥的管理是个问题。硬编码在代码里容易被逆向,放在配置文件里又增加了泄露风险。对于安全要求高的场景,建议结合板子的安全启动机制,把密钥存储在安全区域。
7. 从YOLOv5到其他模型的量化迁移思路
YOLOv5量化跑通之后,同样的流程可以迁移到其他模型上,但不同模型结构的量化友好度差异很大。YOLOv5之所以量化效果好,是因为它的结构以标准卷积为主,没有太多对量化敏感的算子。如果你要量化的是Transformer类模型或者包含大量Group Conv的模型,可能需要更多的混合量化配置。
一个通用的迁移思路是:先全INT8量化跑一遍,看精度掉多少;如果掉点在可接受范围内(比如2个mAP以内),就直接用;如果掉点严重,再用二分法定位敏感层,做混合量化。这个流程对大多数CNN模型都适用。
对于检测模型,还有一个额外注意点:后处理中的NMS对量化误差比较敏感。如果量化后检测框数量异常增多或减少,可能是NMS的输入置信度分布变了。这时候可以尝试在NMS之前加一个小的校准步骤,或者调整置信度阈值来补偿。
量化这件事,工具只是辅助,核心还是对模型结构和数据分布的理解。同样的工具,不同人用出来的效果可能差很多,差别就在这些细节里。多跑几组对比实验,多看看中间层的输出分布,比死磕参数配置更有效。