YOLOv8s地平线J6m INT8量化掉点排查实战与经验总结
2026/9/18 9:50:02 网站建设 项目流程

前一阵把 YOLOv8s 部署到地平线 J6m 上,模型转换、板端推理这些环节走得都挺顺,结果一到 INT8 量化就翻车:mAP@0.5 从浮点的 0.412 直接掉到 0.334,将近 8 个点的差距,这已经不是正常量化误差能解释的了。后来连续排查了好几天,翻了不少工具链文档,最后发现根子其实不在量化本身,而在上游好几个“看起来影响不大”的环节。今天把这段踩坑记录整理出来,希望能帮到正在 J6m 上做 YOLOv8 或者其他检测模型量化的朋友。

这个内容适合谁看?两类人最需要:一类是刚接触地平线 J6m 部署,正准备做 INT8 量化的,可以先看完避坑;另一类是已经遇到量化掉点、正在挠头找不到原因的,可以参考我的排查思路,按图索骥。文里不会贴太细的内部接口代码,但每个排查步骤、每个经验结论都会讲清楚“为什么这么做”,确保你能在项目里真正用上。

1. 先把问题框定:FP32基线、测试集和评估口径

很多朋友遇到 INT8 掉点,第一反应就是调整量化参数,什么校准方法换一遍、校准集换一遍、scale 再校一遍,折腾半天还是掉。我这次最大的教训是:排查掉点问题,第一步不是调量化,而是把基线定住

所谓基线的意思就是,你得先回答清楚一个问题:这个模型现在到底“正常”应该跑出来多少精度?如果 FP32 模型在 J6m 上本身就比训练时的精度低很多,那 INT8 掉点很可能只是把迁移过程的问题放大了,跟量化参数没太大关系。

1.1 为什么需要一个干净的 FP32 基线

我的习惯是建三组基线,缺一不可:

  1. PyTorch 原生 FP32 模型的评估结果,用固定的测试集跑出来,记为 A
  2. 导出成 ONNX 后,用 onnxruntime 在 PC 上跑同样的测试集,记为 B
  3. 地平线工具链仿真或者板端 FP32/混合精度模型的结果,记为 C

这三组结果要放在一起比:A 和 B 之间的差异,反映的是 PyTorch 到 ONNX 的导出损失;B 和 C 之间的差异,反映的是地平线工具链对模型结构重写、算子映射带来的损失;C 和最终 INT8 的差异,才真正是量化造成的损失。

如果你的 A、B 之间就有 1% 以上的差距,说明 ONNX 导出环节已经埋了雷,这时候没必要往下走,先把导出问题解决。如果 A、B 都正常,C 明显偏低,说明是工具链算子映射环节出了问题,需要去查哪些算子被转换成了低效或者不支持的实现。只有 A、B、C 基本一致,才可以放心地把矛头指向量化本身。

1.2 预处理一致性:最容易被忽视的暗坑

这是我在这次排查里踩的最大的坑,也极可能是你未来会遇到的问题。PyTorch 训练 YOLOv8 时,ultralytics 框架默认的预处理是:图像从 0-255 除以 255 变成 0-1,然后按 ImageNet 的 mean/std 做归一化,同时做 letterbox 到 640x640,letterbox 的填充值默认是 114。这一套在 PyTorch 里跑得明明白白,问题出在导出 ONNX、写校准脚本、部署预处理这几个环节,非常容易有人写简化版。

常见的不一致有这么几种:

  • 通道顺序搞反,训练时是 RGB,校准脚本里 OpenCV 读进来就是 BGR,没转回去
  • 忘了做 mean/std 归一化,只做了除以 255
  • letterbox 填充值写了 0 或者 128,训练时是 114
  • 缩放方式不一致,比如训练时按长边等比缩放,部署时直接拉伸到 640x640,目标比例变形

这些差异在 FP32 模型上可能只掉零点几个点,不明显;但在 INT8 上,因为量化本身就压缩了表示范围,前端输入的微小偏差会被逐层放大,最后体现在精度上就是好几个点的差距。所以排查时,务必把训练、校准、部署三套预处理代码摆在一起逐行对。

1.3 评估口径统一:别让后处理吃掉精度

另一个容易被忽略的“隐藏变量”是评估口径。训练的时候用 ultralytics 的 val.py 或者自定义的 mAP 脚本,后处理参数是固定的:NMS 的 IoU 阈值、置信度阈值、每个类别保留的框数量、计算 AP 时的 IoU 阈值,这些参数有一套默认值。但到了板端验证时,很多人会重新写一套后处理,参数稍一改动,mAP 就变了。

我在排查过程中就发现,自己后来写的一个板端验证脚本,NMS 的 IoU 阈值用的是 0.6,而训练时用的是 0.45,就这么个差异,mAP 就少了 1.5 个点左右。所以结论是:比较浮点和 INT8 精度时,后处理代码必须完全一样,参数必须和训练时对齐。比较的是同一个后处理逻辑下,浮点模型和量化模型输出的差异,而不是把两套后处理逻辑的性能差异也算进去。

1.4 逐类别拆解精度:找到真正的掉点区域

整体 mAP 是一个汇总指标,它会把所有类别的表现平均在一起,掩盖真实问题。这次我遇到的情况就是整体掉了近 8 个点,如果只看这个数字,会以为所有类别都在恶化。但拆到每个类别之后就发现:人、车辆这类大目标其实掉得不多,充其量一两个点;真正惨的是红绿灯、锥桶这类小目标,精度直接拦腰砍。

为什么要做这一步?因为不同类别、不同尺寸目标的掉点原因完全是两码事。大目标掉点通常指向全局性的问题,比如预处理不一致、校准集分布整体偏移;小目标掉点则往往指向局部细节,比如某些浅层特征、特定尺度的检测头对量化更敏感,或者校准集中该类别的样本太少。定位到具体类别,排查方向就不会散。

2. INT8量化链路梳理:每个环节都可能埋雷

基线打齐之后,如果确认问题出在量化环节,那就要把 J6m 上的 INT8 量化链路完整过一遍。这个过程其实不复杂,但每个环节都有可能掉链子。

2.1 J6m上YOLOv8s的部署链路概览

地平线 J6m 的部署流程,大方向是这样的:训练好的模型先导出成 ONNX,然后用地平线 OE 工具链做模型转换,转换过程中会有一个 PTQ(训练后量化)环节,工具链会跑一批校准数据,统计各层激活值的分布,算出合理的量化 scale,最终生成 INT8 的 hbm 模型文件,板端用推理运行时加载执行。

整个链路里,最容易出问题的地方不是最后的板端,而是 ONNX 导出和校准这两个环节。因为工具链是“被动”接受 ONNX 的,你给它的模型结构里有什么算子,它就尽量去映射、去量化。模型结构本身如果在导出时就带病,后续步骤再调也很难救回来。

2.2 ONNX导出环节的几个注意点

先说 ONNX 导出。我用的是 YOLOv8s 的 pytorch 权重,ultralytics 官方就提供了导出接口,一行命令就能出 ONNX,很方便,但也正因为方便,很多人忽略了里面的细节:

  • opset 版本。地平线工具链对 ONNX opset 版本是有兼容范围的,用太新的版本,某些算子转换不了,或者转换工具只能 fallback 到一个精度损失很大的实现。建议先确认工具链文档支持的 opset 范围,在这个范围内选一个稳定的版本,不要盲目追新。
  • 静态 shape 还是动态 shape。部署模型建议直接固定输入 shape 为静态,比如 640x640。动态 shape 虽然灵活,但工具链处理时往往要做 shape 推断,推断不准确就会导致某些层计算错误。有时候导出 ONNX 默认是动态维度,需要手动固定。
  • 导出后必须先验证。导出完用 onnxruntime 跑一遍,拿浮点输出和 PyTorch 的输出逐层对比,确认 max abs error 在极小范围。不要直接丢给地平线工具链,否则后面出了什么问题都不知道是哪个环节导致的。
  • DFL 解码部分是否导出到模型里。YOLOv8 的回归分支带一个 DFL(Distribution Focal Loss)解码结构,这个结构里有 softmax、conv、matmul 等操作,在量化时属于敏感区域。后面我详细讲,这里先提一句:导出时可以考虑把这部分后置到模型外面,后面单独处理。

2.3 校准集:量化效果的第一决定因素

校准集选了哪些图、选了多少张,直接决定量化 scale 准不准。我踩过的坑是把训练集里随便抽了几百张当校准集,结果训练集和实际部署场景的分布差太多了,白天、夜晚、雨天各种场景分布不均,量化出来的 scale 对测试集完全不对。

校准集准备的经验建议:

  • 数量:500 张是底线,我自己这次最终用了 1000 张,效果明显比 500 张稳。太少的话,激活值分布统计不够充分;太多的话校准时间变长,边际收益递减。
  • 分布:覆盖部署场景下所有典型的光照、天气、目标尺度和类别。比如你的场景是自动驾驶路口,那白天逆光、夜间灯光、雨天反光、远处小目标、近处大目标都得有。
  • 不要用增强过的图:翻转、裁剪、拼接这些数据增强手段在校准场景下是帮倒忙,因为量化要的是模型在真实推理分布上的激活统计,不是训练分布。
  • 排除完全冗余的图:连拍序列里前后帧高度相似的图,挑一两张代表就行,不要全塞进去,避免某一种分布被过度代表。

2.4 校准方法怎么选:max、percentile、KL还是MSE

地平线工具链里一般会提供几种量化校准方法,每种方法本质上是不同的激活值统计策略。我常用的几个:

  • MAX:直接取激活值的最大值作为量化边界。简单粗暴,但对离群点极敏感。检测网络的特征图里经常会出现个别特别大的响应值,比如某张图里一个超亮的光源,一个 MAX 就把整个量化区间拉宽了,其他正常区域的值被压缩到很少几个量化步长里,精度自然崩。所以 YOLO 这类模型我一般不建议直接用 MAX。
  • PERCENTILE:取某个分位点的值作为量化边界,把极端的尾部截断。比 MAX 稳很多,适合激活值长尾分布的模型。分位点设多少需要试,一般是 99.9% 到 99.999%,太低了会截掉有效信息,太高了跟 MAX 没区别。
  • KL:最小化量化前后分布之间的 KL 散度,TensorRT 的默认做法,被验证过很多模型上表现稳定,我也经常用。
  • MSE:最小化量化前后数值的均方误差。理论上有道理,但实际用起来有时候太“平滑”,反而效果不如 KL 和 percentile,部分模型上可以试。

我的习惯是第一版先用 KL 或者 percentile,看整体精度和逐类别精度。如果发现整体掉点但找不到明显问题,再逐个换方法对比,用数据说话。每种方法跑一次校准其实不需要太久,多试几次不吃亏。

2.5 量化粒度与预处理配置

除了校准方法,还有两个配置容易踩坑。

第一个是权重量化粒度。per-tensor 是整个张量共用一个 scale,实现简单但不同通道幅值差异大的时候误差大;per-channel 是每个通道单独一个 scale,精度更友好。J6m 工具链我印象里是支持 per-channel 的,效果也更好。如果你的模型不是特别大,建议优先考虑 per-channel。不过我实际用下来,per-channel 对 YOLOv8s 这种通道数不算极端的模型,提升幅度有限,最终还是以实测为准。

第二个是输入预处理节点。地平线工具链通常允许你配置一个输入预处理算子,比如在模型里直接做归一化。这个配置必须和校准脚本的预处理一致:校准脚本里喂给模型的,如果是已经归一化到 0-1 的输入,那模型内部的预处理节点就不要重复操作;如果模型内部做了除以 255 的操作,那校准数据就不能预先归一化。这个配置一端错了,整个量化 scale 就是错的,精度直接崩到底。

3. 精度下降根因定位方法论

基线正常、量化链路配置看着也没问题,但 INT8 还是掉点多,怎么办?这时候不能继续盲调了,需要一套系统化的定位方法。我的方法论核心是一句话:让误差自己说话——逐层对比,找到误差最先被放大的那一层。

3.1 逐层输出对比:让误差自己“说话”

地平线工具链一般都带仿真或者输出统计的功能,可以拿到量化模型中每一层的输入输出统计值。我们要做的就是:先把浮点 ONNX 跑一遍,保存每一层的输出 tensor;再用工具链的量化仿真模式跑一遍同样的输入,同样保存每一层的输出 tensor;然后把两者逐层对比。

对比指标我用的是余弦相似度和 NRMSE(归一化均方根误差)。余弦相似度对数值整体缩放不敏感,能反映“形状”层面的差异;NRMSE 对数值绝对差异敏感,能反映“幅度”层面的差异。两个指标配合看,基本能定位误差是从哪一层开始变大的。

实操中,你往往会看到这样的情况:前几层相似度都在 0.99 以上,某层之后突然掉到 0.85,再往后一路走低。这个突变点,就是量化误差被放大的源头。沿着这个点往前找,看看是什么算子,再看看这层的激活值分布有什么特点,问题基本就清楚了。

3.2 YOLOv8体系里的高频敏感算子

在 YOLOv8 这个结构里,经过这几轮排查,我发现有几个位置是误差放大高发区:

  • DFL 解码里的 softmax。DFL 是 YOLOv8 回归分支的一个核心结构,把 bbox 的每个坐标预测拆成 16 个 bin,用 softmax 转为概率后求加权和。softmax 对输入数值的绝对大小和相对差异都很敏感,一旦量化 scale 没有选准,softmax 输出的概率分布就变形了,导致回归框的位置偏移。这个在量化掉点里非常常见。
  • 检测头最后一层卷积的输出。这层输出直接决定类别得分和回归参数的原始值,如果这层激活值分布跨度大或者有离群点,量化误差会被 NMS 和后处理二次放大。有些场景下,检测头分支的特征图里存在大量接近零的背景值,量化很容易把这些背景区域处理得过于“干净”,反而把原本微弱的目标信号也吃掉了。
  • concat 和上采样的组合节点。YOLOv8 的 neck 部分有大量 concat,把不同尺度的特征图拼接起来。如果拼接的特征尺度分布差异大,一个 scale 很难同时 cover 两条分支,误差就会在 concat 之后集中体现。

定位到敏感算子之后,修复手段就不是盲目调全局参数,而是精准处理这几处。

3.3 混合量化:把关键层从INT8里摘出来

当定位到个别敏感层后,最直接有效的解决手段是混合量化,也就是对少数关键层保留 FP16 甚至 FP32,其他层继续走 INT8。地平线工具链支持算子级或者节点级的量化配置,可以给指定的节点设置不量化。

这里要慎重,因为混合量化会牺牲部分性能,所以必须遵循“先定位,再回退,只回退最少量”的原则。我这次的情况是:DFL 部分的 softmax 回退成 FP16,检测头最后一层卷积也回退成 FP16,其余层全部保持 INT8。最终精度恢复到了浮点水平的 98% 以上,板端推理帧率只降了不到 3%,完全可以接受。

如果你一开始直接用“整个模型回退到 FP16”来验证,那当然也会恢复精度,但这没有意义——性能损失太大,还不如直接用 FP16 模型。混合量化的价值就在于用最小的性能代价换回大部分精度。

3.4 从后处理视角反推精度问题

逐层对比如果找不到明显异常,问题可能不在量化模型本身,而在后处理对量化输出的“放大作用”上。具体来说,量化模型的输出置信度分布通常比浮点模型“平”一些,原本 0.6、0.7 的置信度可能变成了 0.3、0.4。如果后处理置信度阈值是 0.25,那就有一批检测框因为置信度被压到阈值以下而消失,整体召回率下降,mAP 自然就掉了。

排查方法很朴素:抓一张典型场景图,分别跑浮点模型和 INT8 模型,把后处理前的所有候选框和置信度打印出来,并排对比。看看是置信度普遍变低,还是回归框位置偏移,还是 NMS 抑制逻辑出现了差异。我之前遇到过一种情况是 NMS 的阈值在量化后需要重新调节,因为量化后置信度分布整体变化,原来调好的阈值就不匹配了。这个因素不是模型的问题,但最终也会体现在精度上,属于排查盲区。

4. 实测案例与排查速查表

前面讲了方法论,这部分把我实际遇到的几个案例拿出来拆解,再把排查经验浓缩成速查表。三个案例分别对应三类典型原因:预处理问题、校准集分布问题、敏感算子问题,覆盖了绝大多数 YOLOv8s 在 J6m 上 INT8 掉点的场景。

4.1 案例一:预处理不一致,整体掉点近10个点

当时现象是 INT8 模型 mAP 从 0.781 掉到 0.682,所有类别都掉,没有任何类别幸免。因为掉得很“均匀”,我第一反应是校准方法或者校准集的问题,结果换了 KL、percentile 都差不多。

后来静下心来打基线:PyTorch 原生 FP32 评估 0.782;导出 ONNX 用 onnxruntime 评估 0.781,基本一致;工具链仿真 FP32 评估 0.780,也正常。问题就锁定在量化环节。再往下看校准脚本,发现两个低级错误:一个是图像通道顺序没从 BGR 转回 RGB,另一个是 letterbox 填充值写成了 0,而训练时是 114。

改掉这两个预处理问题,重新校准量化,mAP 恢复到 0.769,掉点从 9.9 个点缩小到 1.3 个点。这个案例说明:INT8 本身放大了输入差异,真正的问题出在输入链路。所以每次量化掉点,预处理必须第一个排查。

4.2 案例二:校准集分布偏差,特定类别精度崩掉

另一个项目里,整体 mAP 掉了 4 个点左右,拆到逐类别后发现问题高度集中:“锥桶”这一类从 0.63 掉到了 0.31,其他类别基本正常。这个现象非常典型,说明问题一定出在跟这一类目标相关的环节上。

回看校准集,500 张图片里,锥桶样本最多只有 20 张,而且大多数是远距离小目标,特征很弱,模型在量化时根本没有足够的锥桶样本去统计激活分布,scale 就往其他高频类别偏了。重新构建校准集,保证锥桶样本站到 10% 以上,同时加入一些近距离、高清晰度的锥桶图片。重新量化后,锥桶精度恢复到 0.61,整体 mAP 也回来了。

这个案例的教训是:校准集不是你“模型训练集”的缩减版,而应该是一个专门为量化统计设计的、类别和场景均衡的集合。尤其针对小目标、稀有类别,一定要保证足够的占比。

4.3 案例三:检测头敏感层误差大,混合量化解围

第三个案例是最难定位的一类。整体掉点 6 个点,逐层对比发现浅层和中层网络都正常,但从某个检测头分支开始,输出相似度从 0.99 骤降到 0.88,误差从此一路放大。用工具链的统计工具查看后发现,两个明确的误差放大点:一个是 DFL 相关的一个 softmax 层,另一个是最后输出类别得分的卷积层。

针对这两个节点做混合量化,回退成 FP16,其他层保持 INT8。重新量化后,mAP 掉点从 6 个点缩小到 1.5 个点以内,板端推理帧率下降不到 3%。这个案例展示了定位敏感层再精准处理的有效性,也说明混合量化是精度和性能之间的关键平衡杆。

4.4 问题现象到排查项的自检速查表

问题现象优先检查项目解决建议
所有类别均匀掉点较多预处理链路(RGB/BGR、归一化、letterbox填充值)统一预处理,重新校准
特定类别掉点严重校准集中该类样本数量不足或分布不均补充该类真实样本,重新构建校准集
小目标大量漏检输入分辨率、NMS的topk数量、置信度阈值检查后处理参数,考虑提高输入分辨率
输出框位置明显偏移DFL解码部分是否被量化、后处理解码是否一致将DFL解码移出模型,用浮点计算
某一层输出相似度异常低该层算子类型、激活值分布特征定位敏感算子,对该层开启混合量化
量化前后置信度整体偏低校准方法选择、激活值量化范围尝试percentile或KL校准方法

4.5 几个容易被忽略的经验技巧

最后分享几个踩过几次坑之后总结出来的实操技巧,这些细节常规文档里不会写,但对排查掉点问题帮助很大。

技巧一:先跑小样本可视化对比。量化后不要直接跑整个测试集,先挑 10 张左右有代表性的图,把浮点模型和 INT8 模型的检测结果可视化并排对比。如果个别图差异明显,能直接看出是哪类场景、哪类目标出问题,比盲跑几千张测试集高效得多。

技巧二:确认预处理节点是否真的生效。地平线工具链里如果配置了输入预处理节点,一定要确认转换后的模型里这个节点确实生效了。有个笨办法:找个熟悉的小工具,把输入一张全零图,或者一张像素值已知的图,跑一遍模型,看第一层卷积的输入是不是你预期的预处理后的值。这个检查只花几分钟,能避免很多“校准没问题、部署就掉点”的玄学问题。

技巧三:大分辨率对量化的容忍度更高。如果模型在 640x640 上掉点明显,可以先在 960x960 或者更大分辨率下做量化评估,因为信息冗余更多,量化误差相对没那么致命。如果能通过加大分辨率找到问题源头,再回头优化小分辨率下的量化策略,定位效率会高很多。

技巧四:检测头输出尽量拆开观察。导出的 ONNX 如果分类分支和回归分支是拼接在一起的,建议修改导出脚本,把两个分支的输出拆成两路。这样在逐层对比时可以分别观察分类置信度和回归偏移的误差来源,定位更精准,排查方便很多。

这次在 J6m 上部署 YOLOv8s 的过程,让我最深的体会是:INT8 掉点问题,大部分人把锅甩给量化本身,但实际排查下来,多数问题出在量化之外。预处理的一致性、校准集的代表性和均衡性、评估口径的统一、后处理参数的匹配,这些环节任何一个没对齐,最后都会汇集成“量化掉点”这一个表象。所以我建议遇到问题先别急着调量化参数,按本文的顺序把基线打好、链路捋清、逐层定位,你会发现问题远比想象的简单。

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

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

立即咨询