
1. 为什么我要死磕 INT8 这个数字把 YOLOv5s 搬到 RK3588 上跑通其实只是万里长征第一步。真正让人睡不着觉的是模型量化成 INT8 之后mAP 到底掉了多少。这个问题我在项目里被问过不下二十次每次都得从头解释一遍索性这次把整个对比实验的链路完整记录下来。先说结论在我这套流程下YOLOv5s 从 FP16 转到 INT8mAP0.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 的混合量化功能有 bug1.6 相对稳定。具体环境如下# 开发机环境x86 Ubuntu 20.04 Python 3.8.10 rknn-toolkit21.6.0 torch1.13.1 onnx1.14.0 onnxsim0.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 640opset 选 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, axis0) np.save(calib_data.npy, calib_data)实操心得如果你手头的场景数据不够可以用训练集里的图片但一定要做数据增强后的版本不要直接用原图。我试过用 100 张原图做校准mAP 掉了 4 个点换成 300 张增强后的图只掉了 1.9 个点。3. 量化策略对比三种方案的实际表现3.1 方案一直接 INT8 量化这是最简单的做法所有层都量化成 INT8from rknn.api import RKNN rknn RKNN() rknn.config( mean_values[[0, 0, 0]], std_values[[255, 255, 255]], target_platformrk3588, quantized_dtypeasymmetric_quantized-8, quantized_algorithmnormal, optimization_level3 ) rknn.load_onnx(modelyolov5s_sim.onnx) rknn.build(do_quantizationTrue, datasetcalib_data.npy) rknn.export_rknn(yolov5s_int8.rknn)实测结果mAP0.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_platformrk3588, quantized_dtypeasymmetric_quantized-8, quantized_algorithmnormal, optimization_level3, hybrid_quantizationTrue ) rknn.load_onnx(modelyolov5s_sim.onnx) rknn.build(do_quantizationTrue, datasetcalib_data.npy)混合量化需要手动指定哪些层保持 FP16。我的做法是把检测头的前几层和所有涉及小目标特征融合的层设为 FP16其余保持 INT8。实测结果mAP0.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 个 epochQAT 之后导出的 ONNX 再转 RKNNmAP0.5 可以做到 0.558只掉了 0.5 个点。但代价是需要额外的训练时间和数据而且 QAT 的配置比较复杂容易出错。三种方案的对比方案mAP0.5掉点推理耗时ms实现难度FP160.563028低纯 INT80.5382.515低混合量化0.5511.218中QAT0.5580.515高注意推理耗时是在 RK3588 单核 NPU 下测的输入 640x640batch size 为 1。实际部署时如果开多核耗时可以进一步降低。4. 掉点原因深度分析到底哪里出了问题4.1 激活值分布的长尾问题YOLOv5s 里有很多 SiLU 激活函数它的输出分布是长尾的。INT8 只有 256 个离散值长尾部分的信息会被严重压缩。我实际抓取了某一层的激活值分布发现 99% 的值集中在 0 到 6 之间但最大值能到 20 以上。如果量化时把范围拉到 20那 0 到 6 之间的分辨率就只剩 76 个等级精度损失可想而知。解决办法是使用 KL 散度校准或者百分位校准而不是简单的 min-max 校准。rknn-toolkit2 默认用的是 min-max可以通过修改配置文件改成 KLrknn.config( quantized_algorithmkl_divergence, # 其他配置... )4.2 小目标特征图的量化误差YOLOv5s 有三个检测头分别对应 80x80、40x40、20x20 的特征图。80x80 那个头负责小目标它的特征值普遍偏小量化时容易被舍入误差淹没。我的做法是在混合量化时把 80x80 检测头前面的几层保持 FP16。具体是哪些层可以通过 rknn-toolkit2 的get_layer_analysis接口查看每层的量化敏感度# 分析各层量化敏感度 rknn.analysis( modelyolov5s_int8.rknn, datacalib_data.npy, analysis_typequantization )输出会给出每层的量化误差贡献把贡献最大的几层设为 FP16 即可。4.3 后处理对量化误差的放大YOLOv5 的后处理包含 sigmoid、decode、NMS 等步骤。量化误差在经过这些非线性变换后会被放大。特别是 sigmoid在输入接近 0 的时候微小的量化误差会导致输出差异很大。一个缓解办法是把后处理从模型里剥离出来在 CPU 上用 FP32 做。虽然会增加一点耗时但精度会好很多。rknn-toolkit2 支持在导出时去掉后处理层rknn.config( # 去掉后处理 remove_weightFalse, # 其他配置... )然后在板端用 Python 或 C 手动实现 decode 和 NMS。5. 完整实操流程从 ONNX 到板端验证5.1 量化脚本完整版from rknn.api import RKNN import numpy as np def quantize_yolov5s(): rknn RKNN(verboseTrue) # 配置 rknn.config( mean_values[[0, 0, 0]], std_values[[255, 255, 255]], target_platformrk3588, quantized_dtypeasymmetric_quantized-8, quantized_algorithmkl_divergence, optimization_level3, hybrid_quantizationTrue, # 指定保持 FP16 的层 hybrid_quantization_layers[ model.24.m.0, # 80x80 检测头 model.24.m.1, # 40x40 检测头 ] ) # 加载模型 ret rknn.load_onnx(modelyolov5s_sim.onnx) if ret ! 0: print(Load ONNX failed) return # 构建 ret rknn.build(do_quantizationTrue, datasetcalib_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_maskRKNNLite.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, axis0) outputs rknn_lite.inference(inputs[img]) # 后处理...精度验证我用的是 COCO val2017 的一个子集500 张图覆盖 80 个类别。对比 FP16 和 INT8 的检测结果计算 mAP0.5 和 mAP0.5:0.95。5.3 性能测试数据在 RK3588 上输入 640x640batch size 1模型推理耗时msCPU 占用NPU 占用内存占用MBFP162815%60%120INT81510%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 个点说明量化流程有严重问题。排查顺序如下检查校准集是否覆盖了所有类别。如果某个类别在校准集里没出现它的检测精度会崩。检查是否用了 KL 散度校准。min-max 校准在 YOLOv5 上通常掉点更多。检查混合量化的层选择是否合理。可以用analysis接口查看每层的量化敏感度。检查后处理是否在模型内。如果在模型内尝试剥离出来用 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 头量化误差大混合量化该头保持 FP167. 我的优化心得与后续扩展方向经过这轮完整的量化对比实验我最大的体会是INT8 量化不是简单的一键转换而是一个需要反复调试和验证的过程。校准集的质量、量化算法的选择、混合量化的层配置每一个环节都会影响最终精度。如果你追求极致速度纯 INT8 是首选但要做好掉 2 到 3 个点的心理准备。如果你对精度要求高混合量化是性价比最高的方案掉点控制在 1 到 1.5 个点速度损失也在可接受范围内。QAT 效果最好但需要额外的训练资源和时间适合有长期迭代计划的团队。后续我打算尝试几个方向一是用更小的校准集配合主动学习策略看能不能在减少校准成本的同时保持精度二是探索 RK3588 的 NPU 多核并行推理把 batch size 提上去看看吞吐量能到什么水平三是把量化后的模型部署到实际产品里收集真实场景的误检和漏检数据反过来指导校准集的优化。量化这条路没有终点只有不断逼近精度和速度的最优平衡点。希望这篇记录能帮你少踩几个坑把 YOLOv5s 在 RK3588 上的 INT8 部署真正跑通、跑好。