ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

YOLOv8s INT8量化精度掉点排查:从mAP 0.31到0.73的实战复盘

YOLOv8s INT8量化精度掉点排查:从mAP 0.31到0.73的实战复盘 上周我是被一条凌晨三点发来的消息炸醒的J6M板卡上YOLOv8s 的 INT8 模型在测试集上的 mAP0.5 只有 0.31而浮点模型在模拟器上是 0.75掉点直接超过一半。同事第一反应是训练集出了问题重训了三版完全无效又怀疑是板上内存对齐或者 DMA 搬运的问题折腾了一个多小时也没有结论。我接手之后花了四天时间把问题从预处理一路查到校准集再到逐层敏感性分析最后用混合精度配置把 mAP 追回 0.73。这篇文章把整个过程完整复盘一遍所有涉及的关键点和踩坑细节都会写出来对在 Horizon J6M 上做 YOLOv8s INT8 部署的团队或者说任何端侧 NPU 平台的部署工程师都有参考价值。1. 项目背景J6M 上为什么要上 INT81.1 部署平台与模型选型地平线征程 6 系列是面向智能驾驶量产场景设计的芯片J6M 是这个系列里定位中高阶的型号片上集成了 BPUBrain Processing Unit加速器。BPU 对量化模型特别友好INT8 是它的原生计算精度算力利用率能拉到很高相比之下FP16 或 FP32 在 BPU 上要么算力有限要么某些算子根本不支持只能落到 CPU 上去跑。所以要在 J6M 上把 YOLOv8s 这种量级的模型跑到实时帧率INT8 量化基本是绕不开的标准路线。选 YOLOv8s 的原因也很实际它在 YOLOv8 系列里属于 small 版本参数量约 11.2M输入 640x640 时浮点算力大概在 28.6 GFLOPs。这个体量放进 J6M 的 BPU 上INT8 量化后还有非常充裕的算力余量给后处理和别的任务同时它背后有大量开源资料团队上手速度快训练、调参都方便。在真正动工之前我们其实也对比过 YOLOv8n 和 YOLOv8m前者精度在项目场景下有点不够后者又会在 INT8 之后出现更大的量化损失综合下来 YOLOv8s 是当时的最优解。1.2 INT8 量化和浮点部署的差异先把 INT8 量化用通俗的方式讲清楚。浮点模型里的权重和激活值通常都是 FP32数值范围很宽精度很高。INT8 量化做的事情就是把这些浮点数值映射到 8 位整数区间里一般是对称量化映射到 [-128, 127]非对称量化映射到 [0, 255]。这个映射过程一定有精度损失就像把一张 1600 万色的照片压成 256 色颜色信息肯定丢只是看你能不能接受。量化误差主要有三个来源第一是权重裁剪误差权重里如果有个别特别大的离群值量化时必须把它裁掉否则整个动态范围会被拉大第二是激活值映射误差激活值的分布往往不是均匀的很多值集中在一个很窄的区间而映射时只能用一个全局的 scale 去覆盖集中区域的数值被过度压缩细节就没了第三是敏感算子放大误差有些算子本身误差不大但会让后续层的输出偏差越来越大这就像多米诺骨牌第一张倒下去之后后面全倒了。这也是为什么在 J6M 上做 INT8 部署不能天真地以为“量化只损失一到两个点”。如果校准集选得不好、预处理没对齐、某些敏感算子被无差别量化mAP 掉一半都是有可能的。这次的掉点问题就是多重因素叠加的结果。2. 问题现象精度掉点到底有多严重2.1 量化前后的精度对比数据先把最直观的数据摆出来。我们用的是 COCO 风格的数据集评估指标取 mAP0.5 和 mAP0.5:0.95。浮点模型在测试集上的表现是很正常的模型版本mAP0.5mAP0.5:0.95备注PyTorch 浮点模型0.7520.458训练后评估ONNX 导出浮点模型0.7480.453验证导出无损J6M INT8 初次部署0.3120.141掉点 58%修正预处理后0.4630.235有明显回升重构校准集 校准策略0.5860.307还差一截混合精度 DFL 拆出0.7350.434接近浮点水平从表格里可以看得很清楚这不是“略降一点点”而是直接崩了。关键点在于这个精度损失不是单点原因而是三层问题叠在一起预处理没对齐、校准集代表性不足、敏感算子被无差别量化。每修一层精度就往回收一点最后才恢复到一个可以上线的水平。2.2 第一轮排查预处理与数据通路拿到问题之后我第一件事不是看工具链日志而是先确认输入数据的预处理到底对不对。在端侧部署模型预处理是个极其容易被忽略但又影响巨大的坑。YOLOv8 官方训练时用的是 RGB 输入归一化方式是直接除以 255没有 mean/std 偏移。而 J6M 的 ISP 输出和 SDK 的图像输入习惯上经常是 NV12 或 BGR 格式很多从 OpenCV 走出来的同学也天然喜欢用 BGR。如果没有人仔细盯着模型训练时的预处理和板端推理时的预处理是否完全一致只要通道顺序反了量化模型的精度就会崩得非常彻底。怎么确认这个问题我是这样做的从测试集里抽三张图先用 Python 侧的浮点 ONNX 模型跑一遍再把同一张图放到板卡上跑 INT8 模型把第一层卷积的输出 feature map 用调试工具 dump 出来逐通道算余弦相似度。结果很明显三个通道的相似度分别是 0.18、0.21、0.23几乎对不上。而 FP32 模型在板端跑同样三张图相似度在 0.99 以上。这说明不是模型结构问题而是输入数据分布从一开始就偏了。把预处理统一改成 RGB 输入、除以 255 之后再次对比第一层卷积输出的余弦相似度三个通道都到了 0.97 以上。跑完整测试集mAP0.5 从 0.31 升到 0.46。这一步解决的是“数据通路错位”的问题相当于把地基修正了。2.3 第二轮排查校准数据集与量化参数0.46 还是不可接受。第二步我盯上了校准过程。第一次量化的时候图省事直接拿 COCO val2017 里的 1000 张图片做校准集但这个项目实际场景是厂区安防特点是行人密集、互相遮挡多、小目标多、光照条件复杂。COCO 校验集虽然类别覆盖广但场景分布和真实业务差别非常大。量化在校准阶段要做的事是统计模型每层激活值的分布范围然后决定 INT8 映射的 scale 和 zero point。校准数据如果不代表真实输入分布统计出来的量化参数就是偏的。可以类比成给一个人量体裁衣你用别人的尺码去做西装穿在自己身上当然不合身。这次调整做了三件事。第一校准集从 COCO 换成“真实场景抽帧 公开数据混合”的组合总数 800 张覆盖白天、夜间、逆光、阴雨、远距离和近距离。第二把校准时的 batch_size 从 8 改成 4迭代数从 32 改成 200相当于用 800 张图片做统计样本量足够了。第三校准策略从默认的 KL 散度尝试了多种方式包括 max、percentile99.99%和 MSE。实测下来这个项目上用 percentile 99.99% 比 KL 更稳特别是对激活值里存在长尾分布的情况。这一轮改完后mAP0.5 从 0.46 升到 0.58小目标的召回也有肉眼可见的提升。3. 根因定位敏感层与量化策略3.1 逐层误差分析流程到了 0.58 这个水平靠粗调预处理和校准集已经榨不出太多收益了。接下来必须精确到“哪一层在量化时损失最大”。逐层误差分析的思路并不复杂准备好两个模型一个是浮点 ONNX 作为参考一个是工具链量化后的模型然后在同一张输入图上分别跑推理逐层 dump 中间输出计算每一层的余弦相似度和相对误差。余弦相似度越接近 1说明这一层量化后的输出和浮点输出越一致如果某层余弦相似度低于 0.9基本就能判定它是敏感层。具体操作流程大概是这样的准备一张或一小批典型测试图不要太多两三张就够保证通过若干层后能看到差异即可。用浮点模型跑一次把每一层的输出保存下来。用量化模型跑同一次同样保存每层输出。按层名对齐逐层计算余弦相似度。把相似度最低的 20 层列出来结合模型结构图判断它们的共同特征。我在 J6M 上做完这一步之后很快发现一个规律误差排名靠前的层高度集中在检测头的后半段尤其是 DFL 相关的层和最终输出层。Backbone 和 Neck 部分虽然也有量化误差但普遍还是可控的。这个结果直接指向了 YOLOv8s 结构里一个非常典型的量化敏感点。3.2 DFL 与检测头最容易被 INT8“干掉”的算子YOLOv8s 的检测头和传统 YOLO 的 anchor-based 结构不太一样。它的回归分支不是直接回归边界框的宽高和中心点坐标而是使用 DFLDistribution Focal Loss思想把每个坐标预测成一个离散的概率分布再对这个分布做积分得到最终坐标。DFL 在模型里通常会体现为一个卷积加 Softmax 再积分的组合。问题就出在这个 Softmax 加积分的过程上Softmax 对输入的微小变化非常敏感量化引入的误差经过 Softmax 的指数运算会被放大导致输出的概率分布变形最终框的位置偏移几个像素甚至更多。在我们这个项目里DFL 层的余弦相似度只有 0.86是所有中间层里误差最大的一层。而且 DFL 的误差会直接传导到最后的坐标输出所以它的影响不是“一个层的误差”而是“整个检测头都被污染”。处理 DFL 敏感问题有两种方案。第一种是把 DFL 从模型里拆出去放到后处理流程里用 CPU 浮点计算。DFL 的计算量相对整个模型来说非常小CPU 开销完全可以接受但精度提升立竿见影。第二种是在工具链的混合精度配置里把 DFL 相关层指定为 FP16 或 FP32 保留精度让它们跳过 INT8 量化。两种方案可以结合使用实际项目中我两个都试过效果都很明显。3.3 其他容易踩坑的算子除了 DFLYOLOv8s 部署到 J6M 的 INT8 量化时还有几个算子要特别留意。第一个是 SiLU 激活函数。YOLOv8 在 Backbone 和 Neck 里大量使用 SiLU。SiLU 在 x0 附近比较平滑负半轴也不是完全饱和很多激活值会集中在 0 附近的窄区间里。这个分布特征在 INT8 量化后很容易出现“多个浮点值被映射到同一个整数”的问题信息损失会集中在特征提取最关键的阶段。如果误差分析发现 SiLU 输出层的余弦相似度比较低可以优先考虑把这一层做混合精度保护。第二个是上采样层。YOLOv8s 的 Neck 部分用到了上采样。虽然这类算子一般不会被单独量化但插值系数本身是浮点小数在量化后的特征图上做插值遇到放大倍数和特征图尺寸不整的情况误差会被小幅放大。通常不至于造成灾难性影响但如果项目特别看重小目标检测这个点还是值得检查一下。第三个是 C2f 模块内部的 split 和 concat 组合。split 之后不同分支的特征图分布各异concat 时会让后续层的动态范围变大量化参数如果照顾不到所有分支就会牺牲部分分支的精度。这类问题不像 DFL 那么明显但累积起来也可能让 mAP 掉两三个点。第四个容易被忽略的是第一层卷积。第一层卷积的输入直接和预处理强相关如果预处理和校准集的分布没有严格对齐第一层卷积输出的动态范围就会和浮点模型统计到的动态范围不一致后面的层全都会被带偏。这其实就呼应了我们在 2.2 里犯过的那个错。4. 解决实操把精度一点一点找回来4.1 校准数据集重构方案校准集重构是这次精度恢复过程中收益最大的一步。我把我实际用的方案写成一段可操作的建议供参考。首先是数量。不要少于 500 张推荐 800 到 1000 张。太少统计出来的分布不稳定太多校准时间拉长而且边际收益递减。其次是来源。纯公开数据集比如 COCO很难覆盖真实场景的分布最好的做法是从业务场景里抽帧。我们当时从现场录像里按时间均匀抽帧一天 24 小时每个时段都有覆盖再叠加一部分公开数据去补充类别多样性。第三是类别均衡。如果模型训练时是 80 类但真实业务里只有其中 10 类常见校准集里这 10 类要保证充分出现不能完全按 COCO 的自然频率来。另外要特别注意一点校准集不要和验证集共用图片否则你测出来的精度会偏乐观量化参数似乎表现得很好但一到现场就现原形。校准集选完之后可以做一个简单的分布检查比如统计所有图片的亮度均值、方差、目标尺寸分布尽量贴近真实推理时的输入分布。4.2 混合精度量化配置示例校准集解决的是“量化参数是否可靠”的问题混合精度解决的是“敏感层是否适合 INT8”的问题。在 J6M 上通过工具链的配置文件可以指定某些层保持 FP16/FP32。不同版本的工具链参数名可能略有差异但这个思路是通用的。当时我用的简化配置长这样model: type: onnx path: ./models/yolov8s.onnx input_shape: [1, 3, 640, 640] calibration: data_source: ./calib_set.txt batch_size: 4 num_samples: 800 strategy: percentile percentile: 99.99 quantization: precision: int8 keep_float_layers: - model.22.dfl - model.22.cv2.0.2 - model.22.cv2.1.2 - model.22.cv3.0.2 - model.22.cv3.1.2这里的 keep_float_layers 指定的是检测头里 DFL 模块和回归分支、分类分支末端卷积层保持浮点精度。为什么要保留这些层而不是全部 INT8因为它们直接输出的是坐标偏移和类别概率对最终检测结果影响最大而且这些层的通道数少、计算量小用浮点跑也不会拖慢整体速度。这是一个很有性价比的做法等于把 INT8 的算力优势用在关键特征提取上把浮点精度用在结果输出上。需要注意的是不同工具链对层名的表达方式不同有些用的是算子名有些用的是模型图中节点名。建议先在工具链的模型可视化工具里找到敏感层的真实名称再填进配置里不要靠猜。4.3 模型结构微调与后处理拆分混合精度配置改完之后精度已经回到 0.70 以上但我还不满意。主要原因是我还想把 DFL 彻底从模型里拆出去让模型输出换回 DFL 解码之前的特征图然后把这个解码逻辑放到后处理代码里用浮点实现。这样有几个好处模型本身结构变得干净INT8 量化的敏感层直接少了一块后处理虽然要写一点代码但 DFL 的 CPU 计算开销比模型推理小一个数量级完全不影响实时性。DFL 解码逻辑如果用 Python/NumPy 来示意大致是这样import numpy as np def dfl_decode(dist): # dist shape: (batch, 4*16, height, width) 或类似 b, c, h, w dist.shape reg_max 16 project np.arange(reg_max, dtypenp.float32) out np.zeros((b, 4, h, w), dtypenp.float32) for i in range(4): d dist[:, i * reg_max:(i 1) * reg_max, :, :] # 取第i个坐标的分布 d np.exp(d - d.max(axis1, keepdimsTrue)) # 数值稳定 d d / d.sum(axis1, keepdimsTrue) # softmax out[:, i, :, :] np.sum(d * project.reshape(1, reg_max, 1, 1), axis1) return out实际工程里建议用 C 实现但逻辑完全一样。拆出去之后模型的 ONNX 输出节点要改到 DFL 之前的卷积输出然后用板上推理拿到特征图在后处理里完成 DFL 解码和 NMS。这一改量化误差对检测结果的影响就被隔离了。4.4 最终精度恢复结果完成以上所有调整之后我重新在测试集上做了完整评估。修正预处理、重构校准集、混合精度保护敏感层、DFL 拆分到后处理这四步全部叠加之后mAP0.5 从最初 INT8 部署的 0.31 回到了 0.735mAP0.5:0.95 回到了 0.434和浮点模型的 0.748 / 0.453 相比差距已经压缩到了很小的范围。对项目来说这个精度完全满足上线要求。这个结果也验证了一个经验INT8 量化掉点绝大多数时候不是“芯片不行”或者“工具链不行”而是模型准备、预处理、校准数据、量化策略这些环节里有某一个或多个细节没做到位。把这几个环节逐项排查精度是可以一步一步追回来的。5. 排查清单与速查表5.1 一页纸排查清单这次项目踩完坑之后我把整套排查思路整理成了一份一页纸清单后面再遇到 INT8 精度问题就直接按顺序过一遍效率高很多。先查预处理。确认模型训练时的输入格式和板端预处理完全一致包括通道顺序RGB/BGR、归一化方式除以 255 还是 mean/std、输入尺寸是否 letterbox、padding 值是否一致。这一步是最基础但也最容易出错的地方。再查校准集。校准集要覆盖真实场景分布数量 500 到 1000 张类别均衡。不要直接用训练集或验证集。调整校准策略。KL 不是万能的。如果激活值有长尾分布试一下 percentile比如 99.9% 或 99.99%可能有惊喜。做逐层误差分析。找到余弦相似度最低的那批层分析它们的功能和结构特征。对敏感层做混合精度保护或者把敏感算子拆到模型外部的后处理里。如果以上所有手段用完精度还不够再考虑 QAT量化感知训练在训练阶段就把量化误差模拟进去让模型自己去适应。QAT 成本高但往往是最兜底的手段。5.2 常见问题速查表现象可能原因解决手段mAP 断崖式下跌超过 50%预处理不一致RGB/BGR、mean/std 错误统一预处理重新校准小目标检测质量明显下降校准集小目标样本不足增加小目标密集场景的校准图片框的位置偏移但分类基本正常DFL 层被 INT8 量化DFL 拆到后处理或保持 FP16置信度虚高或过低不稳定检测输出层量化动态范围过大输出层保护 使用 percentile 校准个别场景概率性漏检校准集场景覆盖不全补充该场景的抽帧图片所有层均匀误差偏大校准集数量不够或分布偏移增加数量重构校准集某个算子性能异常低算子没有被 BPU 支持落到了 CPU检查算子映射替换或改写这张表不限于 J6M 平台换到其他端侧 NPU只要量化原理类似排查思路都是通用的。5.3 个人实操心得最后分享几条纯实操经验。第一不要听信“INT8 只掉一两个点”这种说法每个模型、每个平台、每个场景都要自己实测而且要在最接近真实业务的测试集上测。第二精度排查顺序不能乱先预处理再校准集最后才去碰敏感层。跳过前面两步直接做混合精度配置往往是在错误的地基上盖楼。第三工具链日志里每个算子是跑在 BPU 还是 CPU 上一定要检查一遍。没有被 BPU 支持的算子会偷偷落到 CPU 上去性能掉一截不说精度也可能和预期不一致。第四建议在调试阶段保留一版把所有中间输出都 dump 出来的调试模型出了问题能立刻定位到层不用重新搭建调试环境。这次在 J6M 上部署 YOLOv8s INT8 的排查经验说到底可以浓缩成一句话量化掉点不可怕可怕的是不去系统地拆解问题而是盲目怀疑工具链或者模型结构。预处理、校准集、敏感层、混合精度这四个环节只要逐一过关大部分精度问题都能被追回来。希望这篇记录能帮你少走几步弯路。
返回列表