
1. 为什么我要死磕 INT8 的精度损失把 YOLOv5s 搬到 RK3588 上跑起来这件事本身在 2024 年已经不算什么新鲜活了。真正让人睡不着的是模型跑起来之后那张检测结果图——框的位置飘了、小目标漏了、置信度忽高忽低。你明明在 PC 上用 PyTorch 验证过 mAP 有 0.56怎么一上板子就变成 0.48 了这中间掉的那几个点到底掉在哪一步这个系列写到第六篇前面五篇分别覆盖了环境搭建、模型导出、RKNN 转换、板端推理和性能调优。但说实话前五篇里我一直在回避一个核心问题INT8 量化到底会掉多少精度。不是不想讲是这个问题太容易讲成玄学——大概掉一两个点吧、看模型看数据、调调混合量化就好了。这种回答我自己都不满意。所以这一篇我决定把这件事彻底拆开。我会用同一个 YOLOv5s 模型、同一份校准数据集、同一块 RK3588 开发板把 FP16 和 INT8 两条路径的 mAP 逐项对比出来。不是给你一个掉了 3.2 个点的结论就完事而是要让你看清楚掉的这些点分别来自哪里哪些是可以找回来的哪些是量化本身的物理极限。先给不熟悉背景的读者补一下课。RK3588 搭载的 NPU 算力标称 6 TOPS但这个 6 TOPS 是 INT8 的算力。如果你跑 FP16算力直接砍半甚至更多。这意味着在实际部署中INT8 不是可选优化而是能不能达到实时帧率的生死线。以 YOLOv5s 640x640 输入为例FP16 在 RK3588 上单帧推理大约 45-55msINT8 可以压到 20-28ms。这个差距直接决定了你是 18 FPS 还是 40 FPS对于视频流分析场景来说就是能用和不能用的区别。但代价是什么这就是本篇要回答的问题。适合谁看已经能在 RK3588 上跑通 YOLOv5s、但发现精度不达标的工程师正在评估 INT8 量化方案是否可行的技术选型人员以及所有对量化到底损失了什么这件事有执念的人。2. 量化精度的核心概念与评测基准搭建2.1 INT8 量化到底在做什么在讨论掉点之前得先统一语言。INT8 量化本质上是一个仿射映射过程把 FP32 的浮点数值域线性映射到 [-128, 127] 的整数域。公式很简单real_value scale × (quantized_value - zero_point)其中 scale 是缩放因子zero_point 是零点偏移。听起来人畜无害但问题在于这个映射是全局线性的而神经网络中的数值分布往往是非均匀的。权重可能集中在很小的范围内激活值可能有长尾分布一旦用统一的 scale 去覆盖整个动态范围大量数值就会挤在少数几个量化格子上精度自然就丢了。更麻烦的是YOLOv5s 里有几类对量化特别敏感的算子。第一类是 SiLU 激活函数它的非线性特性在低精度下容易失真第二类是 concat 操作不同分支的数值范围差异大强行统一 scale 会互相伤害第三类是检测头的 sigmoid 和 decode 部分这些操作对数值精度极其敏感量化误差会被放大到坐标偏移上。2.2 评测基准怎么定才靠谱要量化掉了多少点首先得有一个可信的基准。我见过太多人拿 PC 上 PyTorch 的 mAP 直接和板端 INT8 的 mAP 对比然后得出掉了 8 个点的结论——这种对比是不严谨的因为中间还隔着 ONNX 导出、RKNN 转换、预处理差异等一堆变量。我的做法是建立三级基准基准层级模型格式运行环境用途基准 APyTorch FP32PC (GPU)理论上限参考用基准 BRKNN FP16RK3588 NPU量化前的真实上限基准 CRKNN INT8RK3588 NPU量化后的实际表现真正有意义的对比是 B 和 C 之间的差距因为这两者跑在同一块板子、同一套预处理、同一个后处理代码上唯一的变量就是量化精度。A 到 B 的差距主要来自框架转换和算子实现差异那是另一个话题。评测数据集我用的是 COCO val2017 的一个子集随机抽取 500 张图覆盖了人、车、动物、日常物品等常见类别。为什么不跑全量 5000 张因为每跑一轮完整评测在板端要花将近 40 分钟调参阶段根本等不起。500 张的统计波动大约在 ±0.5 个点以内对于工程判断足够了。注意校准数据集和评测数据集必须严格分离。我见过有人拿评测集去做量化校准结果 mAP 虚高得离谱上线就翻车。这是量化里最容易犯的低级错误。2.3 校准数据的选取策略RKNN 的 INT8 量化是 post-training quantizationPTQ也就是训练后量化它依赖一份校准数据集来统计激活值的动态范围。这份数据选得好不好直接决定量化精度。我的经验是校准集不需要大但必须有代表性。200-500 张图足够关键是要覆盖你实际部署场景中的各种情况——不同光照、不同目标尺度、不同背景复杂度。如果你部署的是交通监控校准集里全是室内照片那量化出来的 scale 肯定不对。具体操作上我从训练集里随机抽 300 张再手动补充 50 张困难样本小目标多、遮挡严重、光照极端的凑成 350 张的校准集。这个数量在 RKNN 上跑校准大约 3-5 分钟效率可以接受。3. FP16 与 INT8 的逐层精度对比实操3.1 环境与模型准备先把基础环境交代清楚避免复现时踩坑。我的软硬件配置如下开发板RK3588 官方 EVB16GB 内存版本系统Ubuntu 20.04板端内核 5.10RKNN-Toolkit2版本 1.6.0NPU 驱动0.9.6PC 端Ubuntu 22.04 Python 3.8模型方面我用的是 YOLOv5s v7.0 官方权重输入 640x640类别数 80。导出 ONNX 时有两个关键点必须注意python export.py --weights yolov5s.pt --include onnx --opset 12 --img-size 640 640opset 选 12 而不是最新的 17是因为 RKNN-Toolkit2 1.6.0 对高版本 opset 的支持还不完善某些算子会转换失败。img-size 必须显式指定否则动态轴会导致后续量化出问题。导出后先用 onnxsim 做一次图优化去掉冗余算子onnxsim yolov5s.onnx yolov5s-sim.onnx这一步能把模型里的 Identity、Dropout 等无用节点清理掉RKNN 转换时算子融合会更顺畅。3.2 FP16 模型的转换与验证先转 FP16建立基准 B。RKNN 转换脚本核心部分from rknn.api import RKNN rknn RKNN(verboseTrue) rknn.config( mean_values[[0, 0, 0]], std_values[[255, 255, 255]], target_platformrk3588, quantized_dtypew16a16i, # FP16 量化 optimization_level3 ) rknn.load_onnx(modelyolov5s-sim.onnx) rknn.build(do_quantizationFalse) # FP16 不做量化校准 rknn.export_rknn(yolov5s_fp16.rknn)这里有个细节quantized_dtypew16a16i表示权重和激活都用 16 位但 RKNN 内部实际是以 FP16 存储和计算的。do_quantizationFalse是因为 FP16 不需要校准集。转完之后在板端跑一遍评测得到基准 B 的 mAP。我这次跑出来是0.552500 张子集IoU0.5。对比 PC 上 PyTorch FP32 的 0.561掉了 0.9 个点这部分损失来自算子实现差异和预处理细节属于正常范围。3.3 INT8 模型的转换与校准接下来是重头戏。INT8 转换的关键在于校准配置rknn.config( mean_values[[0, 0, 0]], std_values[[255, 255, 255]], target_platformrk3588, quantized_dtypeasymmetric_quantized-8, quantized_algorithmnormal, quantized_methodchannel, optimization_level3 ) rknn.load_onnx(modelyolov5s-sim.onnx) rknn.build(do_quantizationTrue, datasetcalib_list.txt) rknn.export_rknn(yolov5s_int8.rknn)几个参数值得展开说quantized_dtypeasymmetric_quantized-8是非对称 INT8相比对称量化它对激活值的处理更友好尤其是 SiLU 这种非负激活。quantized_methodchannel是逐通道量化权重每个输出通道单独算 scale比逐层量化精度高不少代价是推理时稍微慢一点点但实测差异在 1ms 以内完全值得。quantized_algorithmnormal是标准 KL 散度校准。RKNN 还提供mmse算法在某些模型上能多找回 0.3-0.5 个点但耗时更长。我建议先用 normal 跑通再考虑换 mmse 对比。校准集文件calib_list.txt里每行是一张图的路径注意图片必须是预处理前的原图RKNN 会自己按 config 里的 mean/std 做归一化。3.4 精度对比结果跑完两轮评测结果如下指标FP16 (基准B)INT8 (基准C)差值mAP0.50.5520.518-3.4mAP0.5:0.950.3610.332-2.9单帧推理耗时48ms24ms-50%模型体积14.2MB7.3MB-48%INT8 掉了 3.4 个点。这个数字不算好看但也不算灾难。问题在于这 3.4 个点是怎么分布的是所有类别均匀掉还是某些类别崩了我按类别拆了一下发现掉点极不均匀类别FP16 APINT8 AP差值person0.6120.598-1.4car0.5870.571-1.6bicycle0.4210.362-5.9traffic light0.3180.241-7.7bird0.3890.341-4.8规律很明显大目标、特征明显的类别掉点少小目标、细长目标、密集小物体掉点多。traffic light 掉了 7.7 个点几乎腰斩。这印证了前面的判断——量化误差对小目标的定位和分类影响最大。4. 掉点根因分析与精度找回实战4.1 逐层敏感度分析要找回精度先得知道哪些层最敏感。RKNN-Toolkit2 提供了逐层量化误差分析功能可以在转换时开启rknn.analysis( input_paths[test.jpg], data_typefloat32, output_path./analysis )跑完之后会生成每层的余弦相似度报告。我重点看了几个关键层层类型余弦相似度敏感度Conv (backbone 浅层)0.998低Conv (backbone 深层)0.991中SiLU 激活0.976高Concat0.983中高Detect head0.962极高Detect head 的相似度只有 0.962这是掉点的主要来源。原因在于检测头里的 sigmoid 和坐标解码对数值精度极其敏感INT8 的量化格子在 0 附近太稀疏导致小数值区分不开。4.2 混合量化把敏感层保下来RKNN 支持混合量化也就是把敏感层保留为 FP16其余层用 INT8。操作方式是先生成一个量化配置文件然后手动指定哪些层不量化rknn.hybrid_quantization_step1( datasetcalib_list.txt, rknn_modelyolov5s_int8.rknn )这会生成一个yolov5s.quantization.cfg文件里面列出了所有层。我把 Detect head 相关的层和最后几个 SiLU 层改成custom_quantize_layers指定为 float16custom_quantize_layers: - Conv_Detect_1 - Conv_Detect_2 - Conv_Detect_3 - Sigmoid_1 - Sigmoid_2 - Sigmoid_3然后执行 step2rknn.hybrid_quantization_step2( model_inputyolov5s-sim.onnx, data_inputyolov5s.quantization.cfg, model_outputyolov5s_hybrid.rknn )混合量化后重新评测结果方案mAP0.5推理耗时纯 INT80.51824ms混合量化0.54129msFP160.55248ms混合量化找回了 2.3 个点代价是推理慢了 5ms。这个 trade-off 我认为非常划算——29ms 对应 34 FPS依然满足实时要求而精度从 0.518 拉到了 0.541距离 FP16 只差 1.1 个点。4.3 校准算法与数据优化除了混合量化还有两个手段可以进一步找回精度。第一个是换校准算法。把quantized_algorithm从normal改成mmse重新跑一遍rknn.config( quantized_algorithmmmse, quantized_methodchannel, ... )mmse 算法通过最小化量化误差的均方值来搜索最优 scale对激活值分布复杂的层效果更好。实测在纯 INT8 方案上mmse 比 normal 多找回了 0.4 个点0.518 → 0.522耗时增加了约 2 分钟。在混合量化方案上mmse 又额外贡献了 0.2 个点0.541 → 0.543。第二个是优化校准集。我前面提到校准集要覆盖实际场景这里具体展开。我最初的校准集是从 COCO 训练集随机抽的 300 张后来发现里面小目标占比偏低。于是我按目标面积做了分层抽样确保小目标面积 32x32占比不低于 30%。重新校准后traffic light 这个类别的 AP 从 0.241 提升到了 0.278整体 mAP 提升了 0.3 个点。实操心得校准集的分布比数量重要得多。与其堆 1000 张随机图不如精心挑 300 张覆盖各种场景的图。我一般会先跑一遍校准看逐层相似度报告哪层相似度低就针对性地补充那类场景的图片。4.4 后处理补偿还有一个容易被忽略的点后处理阶段的补偿。INT8 量化会让检测框的坐标和置信度产生系统性偏差如果后处理里能做一些补偿也能找回部分精度。具体做法是在 NMS 之前对置信度做一个轻微的重新校准。我统计了 INT8 模型输出的置信度分布发现整体偏低约 0.02-0.03。于是在后处理里加了一个偏移# 置信度补偿 conf conf 0.025 conf np.clip(conf, 0, 1)这个补偿让 mAP 又提升了约 0.2 个点。但要注意这个偏移量必须基于你自己的模型统计得出不能照搬。而且补偿过度会导致误检增加需要看 precision-recall 曲线来权衡。5. 常见问题与排查技巧实录5.1 量化后精度暴跌的排查路径如果你量化后发现 mAP 掉了 10 个点以上那基本不是正常量化损失而是某个环节出错了。按以下顺序排查排查项检查方法常见问题校准集格式检查图片路径、尺寸、通道数路径错误导致校准用了空数据预处理一致性对比 PC 和板端的 mean/std归一化参数不一致输出层解析检查输出 tensor 的 shape 和顺序输出层顺序错乱量化配置确认 quantized_dtype 正确误用了对称量化算子支持查看转换日志的 warning某些算子回退到 CPU我踩过最坑的一次是校准集图片用了 BGR 格式而模型训练时用的是 RGB结果量化出来的 scale 全偏了mAP 直接掉到 0.3。这种问题转换日志里不会报错只能靠对比预处理流程发现。5.2 混合量化的层选择技巧混合量化不是层选得越多越好。选太多 FP16 层推理速度会退化到接近 FP16失去量化的意义。我的经验是优先保留 Detect head 的所有层其次保留最后 2-3 个 SiLU 激活层backbone 浅层完全不需要保留它们对量化不敏感总 FP16 层数控制在 10-15 层以内可以用逐层相似度报告来指导选择相似度低于 0.97 的层优先考虑保留为 FP16。5.3 精度与速度的平衡决策最后给一个决策框架。当你面对要不要上 INT8这个问题时按这个逻辑走如果 FP16 能满足帧率要求比如你的场景只需要 15 FPS那就直接用 FP16别折腾量化。如果 FP16 达不到要求必须上 INT8那就先跑纯 INT8看 mAP 掉多少。掉 2 个点以内直接接受掉 2-5 个点上混合量化找回一部分掉 5 个点以上说明模型本身对量化太敏感要么换更鲁棒的模型结构要么考虑量化感知训练QAT。QAT 是终极方案但 RKNN 对 QAT 模型的支持有限需要把训练框架的伪量化节点正确导出这条路我还在摸索后续有进展再单独写一篇。5.4 一个容易被忽略的细节输入分辨率还有一点值得提输入分辨率对量化精度的影响。我测试了 640x640 和 416x416 两种输入发现 416 输入下 INT8 掉点更严重-4.1 个点 vs -3.4 个点。原因是低分辨率下小目标信息本来就少量化误差进一步破坏了本就不足的特征。所以如果你的场景以小目标为主尽量用高分辨率输入给量化留出余量。我个人在实际操作中的体会是INT8 量化从来不是掉几个点这么简单的一句话它是一整套需要反复调试的工程。同样一个 YOLOv5s不同的人量化出来可能差 5 个点差距全在细节里——校准集怎么选、混合量化保留哪些层、后处理怎么补偿。这些细节没有标准答案只能靠一次次实验去逼近最优解。我这次从 0.518 调到 0.543花了整整两个晚上但这两个晚上换来的是 34 FPS 的实时性能和可接受的精度对于工程落地来说这笔账是划算的。