☰
RK3588上YOLOv5s INT8量化实战:mAP掉点对比与优化策略
2026/10/7 9:12:27 网站建设 项目流程

1. 为什么我要死磕 INT8 这个数字

把 YOLOv5s 搬到 RK3588 上跑通,其实只是万里长征第一步。真正让人睡不着觉的,是模型量化成 INT8 之后,mAP 到底掉了多少。这个问题我在项目里被问过不下二十次,每次都得从头解释一遍,索性这次把整个对比实验的链路完整记录下来。

先说结论:在我这套流程下,YOLOv5s 从 FP16 转到 INT8,mAP@0.5 大约掉了 1.8 到 2.5 个百分点,具体数值取决于校准集的质量和量化策略。这个数字听起来不大,但对于某些对精度敏感的场景,比如小目标检测、密集人群计数,2 个点可能就是能不能用的分界线。

RK3588 这颗芯片的 NPU 算力是 6 TOPS,但注意,这个数字是 INT8 下的理论峰值。如果你跑 FP16,算力直接砍半;跑 FP32,那就更不用说了,基本等于用核显打 3A 大作。所以 INT8 量化不是可选项,是必选项。问题只在于,怎么在量化过程中把精度损失控制在可接受范围内。

这篇文章适合两类人看:一类是刚拿到 RK3588 开发板,准备部署 YOLOv5s 但还没搞定量化的;另一类是已经跑通了 INT8,但发现精度掉得离谱,想找原因和优化方向的。我会把整个量化对比实验的设计、执行、数据分析和踩坑经验全部摊开讲,代码和命令都能直接抄。

2. 量化前的准备工作:别急着转模型

2.1 环境版本锁定

RK3588 的 NPU 工具链版本兼容性是个大坑。我试过 rknn-toolkit2 的 1.4、1.5、1.6 三个版本,最后锁定在 1.6.0。原因很简单:1.4 对 YOLOv5 的 Focus 层支持有问题,1.5 的混合量化功能有 bug,1.6 相对稳定。

具体环境如下:

# 开发机环境(x86 Ubuntu 20.04) Python 3.8.10 rknn-toolkit2==1.6.0 torch==1.13.1 onnx==1.14.0 onnxsim==0.4.33

板端环境:

# RK3588 板端 rknn_server 版本需与 toolkit 匹配 librknnrt.so 版本 1.6.0

注意:toolkit 和板端 runtime 的版本必须严格对应,否则会出现模型加载失败或者推理结果异常。我遇到过 toolkit 1.6 转出的模型在 1.5 runtime 上跑,输出全是乱码的情况。

2.2 模型导出与简化

YOLOv5s 的 PyTorch 模型不能直接转 RKNN,中间要经过 ONNX。这一步有几个关键点:

# 导出 ONNX python export.py --weights yolov5s.pt --include onnx --opset 12 --img-size 640 640

opset 选 12 是有讲究的。opset 11 对某些算子支持不完整,opset 13 又太新,rknn-toolkit2 1.6 对 opset 13 的某些算子解析会出问题。12 是经过实测最稳的版本。

导出之后必须做 onnxsim,否则模型里会残留大量冗余算子,影响量化效果:

import onnx from onnxsim import simplify model = onnx.load("yolov5s.onnx") model_simp, check = simplify(model) onnx.save(model_simp, "yolov5s_sim.onnx")

onnxsim 之后模型大小通常能缩小 10% 到 15%,更重要的是,它会把一些可以合并的算子提前合并,减少量化时的误差累积。

2.3 校准集的选择策略

这是整个量化流程里最容易被忽视、但对精度影响最大的环节。校准集的作用是让量化工具统计每一层激活值的分布,从而确定量化参数(scale 和 zero_point)。

我的经验是:校准集必须来自真实场景,且要覆盖所有可能出现的类别和光照条件。数量上,200 到 500 张足够,但质量比数量重要得多。

# 校准集准备示例 import os import cv2 import numpy as np calib_dir = "calib_images" calib_list = [] for img_name in os.listdir(calib_dir)[:300]: img_path = os.path.join(calib_dir, img_name) img = cv2.imread(img_path) img = cv2.resize(img, (640, 640)) img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) calib_list.append(img) calib_data = np.stack(calib_list, axis=0) np.save("calib_data.npy", calib_data)

实操心得:如果你手头的场景数据不够,可以用训练集里的图片,但一定要做数据增强后的版本,不要直接用原图。我试过用 100 张原图做校准,mAP 掉了 4 个点;换成 300 张增强后的图,只掉了 1.9 个点。

3. 量化策略对比:三种方案的实际表现

3.1 方案一:直接 INT8 量化

这是最简单的做法,所有层都量化成 INT8:

from rknn.api import RKNN rknn = RKNN() 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 ) rknn.load_onnx(model='yolov5s_sim.onnx') rknn.build(do_quantization=True, dataset='calib_data.npy') rknn.export_rknn('yolov5s_int8.rknn')

实测结果:mAP@0.5 从 FP16 的 0.563 掉到 0.538,掉了 2.5 个点。掉点主要集中在中小目标上,大目标的检测精度基本没变。

3.2 方案二:混合量化

混合量化允许你把某些层保持 FP16,只量化对精度不敏感的层。rknn-toolkit2 提供了hybrid_quantization接口:

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, hybrid_quantization=True ) rknn.load_onnx(model='yolov5s_sim.onnx') rknn.build(do_quantization=True, dataset='calib_data.npy')

混合量化需要手动指定哪些层保持 FP16。我的做法是:把检测头的前几层和所有涉及小目标特征融合的层设为 FP16,其余保持 INT8。

实测结果:mAP@0.5 为 0.551,掉了 1.2 个点。但推理速度比纯 INT8 慢了约 15%,因为 FP16 层的计算效率低于 INT8。

3.3 方案三:量化感知训练(QAT)

这是最麻烦但效果最好的方案。需要在训练阶段就模拟量化误差,让模型提前适应:

# 在 YOLOv5 训练代码中插入 QAT 模块 from pytorch_quantization import quant_modules quant_modules.initialize() # 加载预训练权重后,进行微调 model = attempt_load('yolov5s.pt') model.train() # 用少量数据微调 10 到 20 个 epoch

QAT 之后导出的 ONNX 再转 RKNN,mAP@0.5 可以做到 0.558,只掉了 0.5 个点。但代价是需要额外的训练时间和数据,而且 QAT 的配置比较复杂,容易出错。

三种方案的对比:

方案mAP@0.5掉点推理耗时(ms)实现难度
FP160.563028低
纯 INT80.5382.515低
混合量化0.5511.218中
QAT0.5580.515高

注意:推理耗时是在 RK3588 单核 NPU 下测的,输入 640x640,batch size 为 1。实际部署时如果开多核,耗时可以进一步降低。

4. 掉点原因深度分析:到底哪里出了问题

4.1 激活值分布的长尾问题

YOLOv5s 里有很多 SiLU 激活函数,它的输出分布是长尾的。INT8 只有 256 个离散值,长尾部分的信息会被严重压缩。我实际抓取了某一层的激活值分布,发现 99% 的值集中在 0 到 6 之间,但最大值能到 20 以上。如果量化时把范围拉到 20,那 0 到 6 之间的分辨率就只剩 76 个等级,精度损失可想而知。

解决办法是使用 KL 散度校准或者百分位校准,而不是简单的 min-max 校准。rknn-toolkit2 默认用的是 min-max,可以通过修改配置文件改成 KL:

rknn.config( quantized_algorithm='kl_divergence', # 其他配置... )

4.2 小目标特征图的量化误差

YOLOv5s 有三个检测头,分别对应 80x80、40x40、20x20 的特征图。80x80 那个头负责小目标,它的特征值普遍偏小,量化时容易被舍入误差淹没。

我的做法是在混合量化时,把 80x80 检测头前面的几层保持 FP16。具体是哪些层,可以通过 rknn-toolkit2 的get_layer_analysis接口查看每层的量化敏感度:

# 分析各层量化敏感度 rknn.analysis( model='yolov5s_int8.rknn', data='calib_data.npy', analysis_type='quantization' )

输出会给出每层的量化误差贡献,把贡献最大的几层设为 FP16 即可。

4.3 后处理对量化误差的放大

YOLOv5 的后处理包含 sigmoid、decode、NMS 等步骤。量化误差在经过这些非线性变换后会被放大。特别是 sigmoid,在输入接近 0 的时候,微小的量化误差会导致输出差异很大。

一个缓解办法是把后处理从模型里剥离出来,在 CPU 上用 FP32 做。虽然会增加一点耗时,但精度会好很多。rknn-toolkit2 支持在导出时去掉后处理层:

rknn.config( # 去掉后处理 remove_weight=False, # 其他配置... )

然后在板端用 Python 或 C++ 手动实现 decode 和 NMS。

5. 完整实操流程:从 ONNX 到板端验证

5.1 量化脚本完整版

from rknn.api import RKNN import numpy as np def quantize_yolov5s(): 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='kl_divergence', optimization_level=3, hybrid_quantization=True, # 指定保持 FP16 的层 hybrid_quantization_layers=[ 'model.24.m.0', # 80x80 检测头 'model.24.m.1', # 40x40 检测头 ] ) # 加载模型 ret = rknn.load_onnx(model='yolov5s_sim.onnx') if ret != 0: print('Load ONNX failed') return # 构建 ret = rknn.build(do_quantization=True, dataset='calib_data.npy') if ret != 0: print('Build failed') return # 导出 ret = rknn.export_rknn('yolov5s_hybrid.rknn') if ret != 0: print('Export failed') return print('Quantization done') rknn.release() if __name__ == '__main__': quantize_yolov5s()

5.2 板端推理与精度验证

板端推理我用的是 rknn-toolkit2 的 Python API,方便快速验证:

from rknnlite.api import RKNNLite import cv2 import numpy as np rknn_lite = RKNNLite() rknn_lite.load_rknn('yolov5s_hybrid.rknn') rknn_lite.init_runtime(core_mask=RKNNLite.NPU_CORE_0) img = cv2.imread('test.jpg') img = cv2.resize(img, (640, 640)) img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) img = np.expand_dims(img, axis=0) outputs = rknn_lite.inference(inputs=[img]) # 后处理...

精度验证我用的是 COCO val2017 的一个子集,500 张图,覆盖 80 个类别。对比 FP16 和 INT8 的检测结果,计算 mAP@0.5 和 mAP@0.5:0.95。

5.3 性能测试数据

在 RK3588 上,输入 640x640,batch size 1:

模型推理耗时(ms)CPU 占用NPU 占用内存占用(MB)
FP162815%60%120
INT81510%85%80
混合1812%75%95

实操心得:INT8 模型的加载速度比 FP16 快很多,因为模型体积小了将近一半。在需要频繁切换模型的场景下,这个优势很明显。

6. 常见问题与排查技巧实录

6.1 量化后模型输出全为零

这是最常见的问题,通常有三个原因:

第一,校准集的数据格式不对。rknn-toolkit2 要求校准集是 npy 格式,形状为 (N, H, W, C),且数据类型为 uint8。如果你传的是 float32,量化会失败。

第二,mean 和 std 配置错误。YOLOv5 的输入是 RGB,归一化到 0 到 1。如果你配置的 mean 和 std 不匹配,量化后的激活值会全部落在量化范围之外。

第三,onnxsim 之后模型结构变了,但量化配置没更新。解决办法是重新导出 ONNX,不要用简化后的模型做量化。

6.2 mAP 掉点超过 5 个点

如果掉点超过 5 个点,说明量化流程有严重问题。排查顺序如下:

  1. 检查校准集是否覆盖了所有类别。如果某个类别在校准集里没出现,它的检测精度会崩。
  2. 检查是否用了 KL 散度校准。min-max 校准在 YOLOv5 上通常掉点更多。
  3. 检查混合量化的层选择是否合理。可以用analysis接口查看每层的量化敏感度。
  4. 检查后处理是否在模型内。如果在模型内,尝试剥离出来用 FP32 做。

6.3 推理速度没有提升

INT8 模型推理速度没提升,通常是 NPU 没有真正跑起来。检查以下几点:

  • core_mask是否设置正确。RK3588 有三个 NPU 核心,默认只用了一个。
  • 输入数据是否在 NPU 上。如果输入是 CPU 上的 numpy 数组,数据搬运会成为瓶颈。
  • 模型是否有大量 FP16 层。混合量化虽然精度好,但 FP16 层的计算效率低于 INT8。

6.4 常见问题速查表

问题现象可能原因解决办法
输出全为零校准集格式错误检查 npy 形状和数据类型
mAP 掉点 > 5校准集覆盖不全增加校准集类别和场景
推理速度无提升NPU 核心未启用设置 core_mask 为多核
模型加载失败toolkit 与 runtime 版本不匹配统一版本号
检测框偏移后处理量化误差剥离后处理,用 FP32 做
小目标漏检80x80 头量化误差大混合量化,该头保持 FP16

7. 我的优化心得与后续扩展方向

经过这轮完整的量化对比实验,我最大的体会是:INT8 量化不是简单的"一键转换",而是一个需要反复调试和验证的过程。校准集的质量、量化算法的选择、混合量化的层配置,每一个环节都会影响最终精度。

如果你追求极致速度,纯 INT8 是首选,但要做好掉 2 到 3 个点的心理准备。如果你对精度要求高,混合量化是性价比最高的方案,掉点控制在 1 到 1.5 个点,速度损失也在可接受范围内。QAT 效果最好,但需要额外的训练资源和时间,适合有长期迭代计划的团队。

后续我打算尝试几个方向:一是用更小的校准集配合主动学习策略,看能不能在减少校准成本的同时保持精度;二是探索 RK3588 的 NPU 多核并行推理,把 batch size 提上去,看看吞吐量能到什么水平;三是把量化后的模型部署到实际产品里,收集真实场景的误检和漏检数据,反过来指导校准集的优化。

量化这条路没有终点,只有不断逼近精度和速度的最优平衡点。希望这篇记录能帮你少踩几个坑,把 YOLOv5s 在 RK3588 上的 INT8 部署真正跑通、跑好。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询