拓冰建站拓冰建站
首页 / 资讯中心 / 正文

YOLOv8s INT8量化部署掉点24个点?从校准集到混合精度的完整排查实战

1. 现象收敛从 mAP 掉 24 个点开始1.1 部署环境和复现条件前几天接了一个部署任务目标平台是地平线 J6m模型是 YOLOv8s任务要求挺明确用工具链把训练好的权重量化到 INT8 后跑在 BPU 上验证集 mAP 不能比 FP32 掉超过 3 个点。听起来是个常规 PTQ 项目但第一版量化模型跑出来直接把我干沉默了——FP32 在 GPU 上评估是 0.458 mAP0.5:0.95上板 INT8 只有 0.218等于掉了 24 个点。这不是精度小波动这是模型基本废了。先说下复现环境。模型是 COCO 预训练后拿业务数据微调过的 YOLOv8s输入分辨率 640x640导出 ONNX 时把训练时的 decode 头拆开保留原始输出NMS 放在后处理做。部署工具链是地平线 OpenExplorer 这套流程大致是 PyTorch 权重导出 ONNX再用 hb_mapper 做浮点模型检查、PTQ 量化、编译成可以在 J6m 上跑的二进制。第一版我用的是工具里默认的校准配置随机从训练集抽了 200 张图batch 大小 4其他参数基本没动。复现之后就发现这个精度下降不是均匀掉点。FP32 模型在同样的场景里小目标、遮挡目标、晚上逆光目标都能框出来只是置信度参差不齐INT8 模型则是对低对比度目标直接漏检行人在十米开外几乎全丢车尾灯晚上也丢。更典型的是同一个视频序列里FP32 框的位置稳定INT8 的框会跳同一辆车相邻两帧的置信度能从 0.6 跌倒 0.2。这种表现基本可以判断量化误差影响到了某些对数值分布特别敏感的层而且不是单纯的“整体变模糊”。1.2 先复现不要急着调参遇到精度崩掉我的一贯原则是先复现完整链路再把变量一个个固定住。这里有个特别容易犯的错看到 INT8 掉点就直接去调模型结构比如换 Neck、把检测头改成解耦更彻底的结构或者干脆去搞 QAT。这些方向不是没用但第一步应该是把问题拆开FP32 ONNX 在 PC 仿真/板端浮点跑mAP 是多少INT8 量化仿真不上板mAP 是多少INT8 编译上板后 mAP 又是多少我当时实际的链路结果大概是这样阶段mAP0.5:0.95mAP0.5直观表现PyTorch FP32 验证0.4580.687正常ONNX FP32 仿真0.4510.681基本正常ONNX INT8 量化仿真0.2810.512小目标漏检严重J6m 上板 INT80.2180.441漏检更明显置信度抖动这个表格说明两件事第一ONNX 导出和浮点仿真这一步几乎没有损失问题不在这里第二INT8 量化仿真的精度已经掉了说明误差是在量化环节引入的不是板端编译“跑坏了”。板端比仿真再低 6 个点那是后处理或者输入预处理在板端实现不一致导致的这个后面说。2. ONNX 导出和预处理不一致是第一层陷阱2.1 把 FP32 跑成不过板前参考线很多人会跳过浮点模型在部署链路上跑通这一步直接做 INT8。我这次一开始也差点这么干后来复盘发现FP32 环节的价值不是帮你“保证精度”而是帮你划一条参考线。FP32 在 PC 仿真阶段跑出 0.451说明 ONNX 本身结构完整、算子支持、权重没有损坏。如果这一步就掉点问题集中在导出方式比如 training 和 export 模式没有切换正确损失了 batchnorm 折叠后的行为或者导出的 output 顺序、shape 和部署代码里的解析逻辑对不上。YOLOv8s 的结构里有很多 SiLU 激活函数以及 C2f、SPPF 这种多次 concat 的结构。导出 ONNX 时如果打开了某些优化开关比如把 SiLU 替换成近似形式或者把某些常量折叠掉虽然模型能跑但数值会有微小变化。这个变化在 FP32 下通常可接受但会成为 INT8 量化的额外误差来源。所以我在做量化前会固定使用统一的 ONNX 导出参数并且导出的 ONNX 先在 torchscript / onnxruntime 上跑一遍随机张量对比 pytorch 输出最大误差控制在 1e-4 量级。2.2 输入归一化方式的核对预处理不一致是我这次排查过程中第一个确认的坑。YOLOv8s 在 Ultralytics 训练框架里的标准预处理是把图像 resize 到 640x640 并做 letterbox 填充然后除以 255 做归一化最后转成 NCHW 的 float 张量。但在部署时地平线工具链一般不会让你在 BPU 上跑原始除以 255 的浮点操作因为 BPU 是定点计算输入图像通常是 uint8 格式需要一个“量化到 int8 输入层”的过程。这里容易错的是模型实际学到的输入分布是什么如果训练时用的是“除以 255 再减 mean 再除 std”那么部署配置里的归一化参数必须和训练一致如果训练时只是简单除以 255部署配置里就要用 data_scale 的方式做定点缩放而不是套用一套 ImageNet 的 mean/std 上去。我第一版部署配置里写的是norm_type: data_scalescale_value: 0.00392156862745098这本身没错。但我后来发现板端的前处理代码里图像读出来后先做了 BGR 到 RGB 转换然后再 letterbox。而训练时 Ultralytics 默认是 BGR模型实际上吃的是原始 BGR 顺序。就这么一个通道顺序问题导致所有图像的 R 通道和 B 通道内容互换模型虽然没崩但 mAP 直接掉了 5 个点。查这类问题最快的方式不是盯 mAP而是跑一张固定测试图把 FP32 仿真的网络中间层输出和板端输出做逐层对比。通道顺序错了往往第一个卷积层的输出分布就能看出来。2.3 踩过的一个预处理坑再补一个很隐蔽的坑letterbox 的填充值。YOLO 系列训练时通常用灰色 114 做填充这个 114 是在归一化之后除以 255 得到的约 0.447。如果部署代码里 letterbox 填充的是 0那么图像边缘大量填充区域和训练分布不一致对带 padding 的小目标影响特别明显。这个坑我在别的项目里踩过当时一批边界位置的检测框置信度整体偏低后来发现就是 fill value 的问题。所以预处理这块我后来养成了固定习惯读图、resize、letterbox、颜色通道、归一化全部收敛到一个函数并且用一个真实样例图把训练预处理和部署预处理后的输入张量打印出来逐元素对比。这一步排干净了再谈量化精度才有意义。3. 校准集选不好后面全白搭重新做 PTQ 校准的实操3.1 校准集的分布偏差排完预处理问题INT8 量化仿真精度从 0.218 回到了 0.281 左右但这还是不合格。下一个鬼问题出在 PTQ 的校准集上。地平线工具链的 PTQ 校准原理可以简化理解为喂入一批代表性图片统计每一层激活值的 min/max 或者分布直方图然后按某种优化策略找到每个 tensor 的 scale。校准集的作用是“让量化器知道真实数据长什么样”。如果校准集本身不具代表性量化 scale 就会偏向训练集某个局部特征验证集上自然一塌糊涂。我第一版用的是训练集里随机抽的 200 张图而且是从服务器日志里直接抽帧没做去重。这些图大部分是白天干线场景夜间和雨天占比极低小目标密集的城区路口也不多。结果就是模型白天场景精度还能看一到夜间和雨天直接崩行人和两轮车漏检严重。这类问题在日志上很有特点——它不是所有类别均匀掉点而是某几个特定类别掉得特别狠。我统计了各类别 AP 变化发现 bus 掉了将近 30 个点motorbike 掉了 22 个点而 daytime 的大目标是正常下降。这基本就是校准集类别不均衡的直接反应。3.2 重新设计校准集第二次我老老实实重新采集了校准集。规则很简单但也花了不少功夫总数控制在 800 张而不是越多越好。按场景比例分配白天 50%、夜间/弱光 25%、雨天/逆光 15%、隧道/地下车库 10%。每个场景内按目标尺度分层保证大目标、中目标、小目标都有覆盖。做了简单去重避免同一辆车连续帧重复出现导致校准集倾向性太大。特意加入业务 road 上容易漏的 hard case比如被遮挡的行人、远处的小车、密集两轮车。这里要说一个反直觉点校准图片并不是越多越好。校准集数量过大量化器反而会过度拟合到“所有样本的均值分布”忽略了极端值数量过少又覆盖不了动态范围。我们实际测试下来在这个业务场景下 800 张是一个比较稳的点再扣到 300 张会掉 2 到 3 个点加到 1500 张基本没有额外收益反而编译时间和校准时间长了不少。校准集换完之后INT8 量化仿真精度从 0.281 升到了 0.341mAP0.5 从 0.512 升到了 0.578。有提升但离目标还差很远。而且这时候掉点模式变了不再集中在特定类别而是整体均匀下降。这说明校准集问题基本解决剩下的问题出在模型结构对量化的敏感度上。3.3 校准参数和量化感知节点的配合在校准集之外我还踩过一个参数配置的坑。工具链在量化时会有一些“量化感知”相关配置项比如哪些算子需要特殊处理、哪些层需要强制为某种量化精度、是否允许对某些激活层做 extra clip。刚开始我把这些配置全部保持默认后来发现有些结构的 pooling 层或者 concat 节点在默认配置下会用比较保守的量化方式误差不大但拖累整体精度。不同版本工具链的字段不一样我建议关注两个事情第一校准过程中的 batch size 不要设得过大我这边设为 4 比较稳太大反而会导致统计直方图不够平滑第二如果配置文件里有“允许对激活层做截断”的选项可以先打开试一下因为 YOLOv8s 很多激活值存在明显的长尾分布直接按 max 定 scale 会让绝大多数有效数值都挤在很小的量化桶里做一点截断反而能提升精度。校准完成后可以用工具链生成每层校准的量化误差报告别只看最终 mAP。有时候整体 mAP 涨了但某个关键中间层的 cosine similarity 反而更差这种就要警惕过拟合校准集需要在下一轮校准里调整图片。4. 用逐层敏感度定位“头重脚轻”的 DFL 回归分支4.1 为什么 YOLOv8s 的检测头对 INT8 这么敏感校准集救回来一部分精度后剩下的问题基本锁定在模型本身。YOLOv8s 是一个 anchor-free 检测器检测头分成分类分支和回归分支。回归分支里有一个叫 DFLDistribution Focal Loss的结构它不是直接回归框的宽高而是对一组预设的离散 bin 做 softmax 加权求和最后得到坐标偏移。这里的问题在于DFL 分支里的输出特征数值分布非常“尖”。很多 bin 的权重集中在某一个值附近softmax 之后概率分布有一个明确的峰值。这种分布对量化误差特别敏感——如果量化后的 scale 稍微大了一点或者某个关键 bin 的概率被舍入到相邻 bin最终解码出来的框坐标就会偏移好几个像素。在 640x640 输入下几个像素的偏移对远处小目标来说就是 IoU 从 0.7 跌到 0.3mAP 自然崩。另外YOLOv8s 的检测头最终输出层之前通常有几个 3x3 卷积这些卷积的输入特征图来自不同尺度的 Neck 融合数值范围比 backbone 中间层大得多。如果这些层被一刀切地用 INT8 量化误差会在最后的输出层被放大。我之前掉点 24 个点的第一版很可能就是这部分在作祟。当时只知道模型掉精度没有工具去定位。后来养成了习惯每次做完校准先把量化后每一层相对 FP32 的 cosine similarity 拉出来看一遍而不是只看最终指标。4.2 逐层 cosine similarity 排序地平线工具链支持做逐层精度分析类似量化敏感度分析。原理不复杂把 FP32 模型和 INT8 模型的中间激活值逐一拉出来计算每个节点输出的 cosine similarity。越接近 1说明量化对这个节点的影响越小如果某个节点跌到 0.99 以下那基本就是重点怀疑对象。我这边跑完一轮分析后结果非常有指向性low level 和 backbone 大部分节点 cosine similarity 都在 0.999 以上属于正常量化损失但检测头后段的若干卷积节点尤其是和 DFL 分支直接相关的那几个节点sim 值跌到了 0.97 左右。最离谱的是靠近输出的某个 concat 节点fp32 和 int8 的激活分布直接对不上。看到这个现象基本可以确定“头重脚轻”——问题集中在 head 而不是 backbone。这时候我建议的做法是不要急着把所有 head 层都设置成 float而是先把最敏感的节点挑出来。我当时把 cosine similarity 排序取了最后 8 个敏感节点逐一查看它们所在的子图发现其中 5 个都在回归分支另外 3 个在检测头的第一个 3x3 卷积附近。这说明这个模型的主干和 Neck 已经被量化得足够好唯独检测头的数值分布不适合 INT8。4.3 混合精度方案把这几个层 keep_float定位到节点之后解决方案就清晰了对这些敏感节点做 mixed precision也就是让它们在编译时保持 FP32 计算模型里其他层仍然走 INT8。我当时的做法是在量化配置里增加混合精度节点列表大致是这种形式quantization: mixed_quant: float_nodes: - /model.22/reg/.../DFL/Conv - /model.22/reg/.../Conv具体字段名不同版本有差异以你自己下载的工具链 release note 为准。但思路是一样的把那些对量化特别敏感的层挑出来让它们在 BPU 上用浮点方式跑或者落到 CPU 上跑。这里有个取舍点keep_float 的节点越多精度越接近 FP32但代价是计算图切分次数变多可能增加内存搬运甚至整体耗时。所以我通常不会一次性把所有 head 层全加上而是先加最敏感的几个跑一遍精度和耗时再看没有没提升逐轮迭代。我这轮把 5 个敏感回归分支节点设为 float 后量化仿真精度从 0.341 升到了 0.412mAP0.5 从 0.578 升到了 0.648。接着我又试着把检测头第一个 3x3 卷积也加进去精度只涨了 0.4 个点但耗时增加明显最后果断撤回。4.4 尝试 int16 激活的取舍除了 keep_float我也顺手试了工具链里的 int16 激活选项。J6m 这类 BPU 通常支持多种量化精度混合int16 的精度比 int8 高能处理动态范围更大的激活值但代价是计算量和内存带宽开销会涨。实测下来在 DFL 分支附近用 int16 激活量化仿真精度大概提到了 0.39 左右比纯 INT8 好但和直接 keep_float 的 0.41 比还差一点而且编译时间明显变长。我当时的判断是这个模型的耗时预算不宽裕与其用 int16 覆盖一大片区域不如精准地把几个最敏感的节点用 float 处理。所以最后方案选择了 float 节点 INT8 主干混合精度而不是大范围 int16。如果你手上的部署平台对耗时比较敏感且 keep_float 的数量太多int16 是一个折中方案。但一定记住别指望它帮你救回所有掉点int16 适合“数值范围大但分布还算连续”的层不适合 DFL 这种“分布极尖”的层。5. 修改后的精度验证与上板实测5.1 验证集和场景集分开测混合精度改完后我先跑了一遍完整的评估流程。这里我强调“完整”是因为只跑 mAP 一个指标很容易骗人。我把评估拆成两块第一块是和训练对齐的标准验证集用来和 PyTorch FP32 做横向对比这是项目验收的硬指标。第二块是单独挑出来的场景集专门覆盖夜间、雨天、小目标密集、遮挡这些业务难点这块不参与验收但能反映真实路况下的可用性。混合精度方案在标准验证集上最终达到了 0.438 mAP0.5:0.95和 FP32 的 0.458 只差 2 个点mAP0.5 从 0.687 降到 0.665勉强在 3 个点的预算内。场景集上的表现也好很多夜间漏检率明显下降小目标的框虽然置信度仍然偏低但至少不再大面积丢失。这里多提一句别只看 mAP 平均数要按置信度阈值画 PR 曲线。有时候 mAP 差不多但 PR 曲线的形状差很多说明模型的输出概率校准出了问题。上板模型如果在后处理里用固定置信度阈值比如 0.4那即使 mAP 达标也可能因为置信度普遍偏低而表现为“漏检严重”。我后来在后处理里把置信度阈值从 0.4 降到 0.3召回率涨了 6 个点误报只多了 1 个点这个修正比任何模型改动都短平快。5.2 编译配置和板端耗时的观察量化仿真通过后我把模型编译到 J6m 上跑。上板后的 mAP 比量化仿真又低了大概 3 个点这属于正常现象因为 PC 仿真是比较理想的计算环境板端会有内存布局、算子切分、并行调度等因素影响。但如果上板比仿真低超过 5 个点就要查以下三个方向第一模型编译时有没有做层级优化或者算子替换。有时候工具链的优化器会把某些小算子合并如果合并后改变了数值计算的顺序INT8 下可能引入额外误差。第二后处理实现是否和 PC 评估时一致。我这次上板掉点里有一半就出在这里。PC 评估时用的 NMS 实现是 Ultralytics 默认的而板端后处理是同事另外写的 C 代码里面对每个类别的 confidence threshold、IOU threshold、最大检测数量都重新调过参数。查了半天最后发现是板端代码把 score 乘了一个 0.95 的缩放因子导致低置信度目标直接被阈值过滤掉。第三输出张量的解析顺序。YOLOv8s 的输出通常是一个大的二维数组每列代表一个 anchor/refine point顺序如果搞错模型精度再高也没用。5.3 上板后还是掉点从后处理里找问题我见过很多项目模型改了半天没效果最后发现是板端代码里用了一个有 bug 的 NMS 实现。比如只按类别循环 NMS没有跨类别抑制或者对同一类的框没有按 score 排序导致输出框奇奇怪怪。我的建议是在上板前做一个“后处理对拍”把同一组输入图像同时喂给 PC 端模型和板端模型导出两边的 raw output再用同一套后处理代码分别解析对比最终框的坐标和置信度。如果 raw output 一致但最终框不一致问题肯定在后处理。如果 raw output 本身不一致再去查模型编译和输入预处理。我们这次把后处理修好之后上板 mAP 从 0.218 恢复到 0.426量化仿真 0.438 和板端 0.426 之间的差距只有 1.2 个点基本符合预期。6. 复盘J6m 上部署 YOLOv8s 的 INT8 检查清单6.1 排查链路把这次整个排查过程压缩成一条链路大概是这样先确认 ONNX 导出没问题FP32 在部署链路上不掉点。再确认输入预处理完全对齐包括 resize、letterbox、通道顺序、填充值、归一化参数。接着检查校准集是否覆盖目标场景是否类别均衡是否做了去重。然后用逐层敏感度分析定位最敏感的量化节点优先处理检测头和 DFL 分支。混合精度不要一次开太多从最敏感节点开始逐轮迭代。上板后如果精度比量化仿真低超过 5 个点优先怀疑编译优化和后处理不要再去折腾模型结构。这个顺序很重要。我见过同事头疼了一周最后发现只是部署代码里把图像通道顺序搞反了也见过有人花大量时间调模型结构结果发现是 NMS 阈值设错了。先排除部署链路的问题再谈模型本身才是 INT8 部署的正确姿势。6.2 一些心法再分享几个比较虚但对工作很实用的心得。第一量化精度排查一定要每次只改一个变量。跑一次精度的成本不高但如果同时换了校准集、混合精度、预处理三个东西结果变好了你也不知道是哪个起了作用结果变差了你更没法定位。我这次就是先锁死预处理重做校准集确认提升有限后才开始动混合精度每一步都有日志和对应数值。第二养成保留量化报告的习惯。每轮校准完都会生成一些中间统计信息别直接删除。有时候回退到某个历史版本时需要对比这些报告是唯一线索。第三对 YOLOv8 这类检测模型heads 的量化敏感度通常高于 backbone这是一个通用规律但不代表每次都必须把 head 设成 float。先跑敏感度分析用数据说话别凭感觉。最后一点也是我踩过多次之后才真正记住的上板模型精度不达标时先不要怀疑 BPU 算错了绝大多数情况下是“你在 PC 上定义的模型”和“你在板端喂给模型的输入”根本就不是同一个东西。把两边的输入张量和原始输出拉出来对拍一次几分钟能解决的问题别拖成几个通宵。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门