ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

地平线J6m部署YOLOv8s INT8量化精度骤降排查与优化实践

2026/9/20 19:00:09 拓冰建站 浏览量
地平线J6m部署YOLOv8s INT8量化精度骤降排查与优化实践 1. 问题背景与排查思路拆解1.1 项目场景与精度下降现象地平线 J6m 这颗芯片在边缘侧推理场景里算是比较热门的选择算力够用、功耗控制得也不错很多做智能座舱、辅助驾驶、工业质检的团队都在用它跑视觉模型。我这次接到的任务是把一个已经训练好的 YOLOv8s 检测模型部署到 J6m 上走的是 INT8 量化路线目标是拿到比 FP16 更低的延迟和更高的吞吐。模型转换链路是标准的PyTorch 权重导出 ONNX再用地平线工具链做量化编译最后生成板端可执行的 hbm 模型。整个流程跑下来没有报错板端也能正常出框但问题出在精度上——mAP 从 FP32 的 0.72 左右掉到了 0.51掉了二十多个点这个幅度已经不能用“量化正常损失”来解释了。正常 INT8 量化在检测模型上掉 1 到 3 个点是可以接受的掉 20 个点说明某个环节出了结构性问题。我先做了几组对照实验来定位问题范围。第一组把同一份 ONNX 在 PC 端用 ONNX Runtime 跑 FP32 推理结果和 PyTorch 对齐说明导出环节没问题。第二组用工具链的校准集做量化后在 PC 端模拟器上跑 INT8 推理精度就已经掉了说明问题出在量化阶段而不是板端部署阶段。第三组把量化配置里的某些层强制回退到 FP16精度有明显回升这就把嫌疑范围缩小到了特定算子或特定结构上。排查这类问题我的习惯是先建立一条“基准链路”也就是确保 FP32 全链路对齐然后再逐段替换成 INT8看哪一段引入的误差最大。很多人一上来就盯着量化参数调其实方向错了得先确认模型结构本身在转换后有没有变形。1.2 为什么 INT8 量化对检测模型特别敏感要理解这次精度下降得先搞清楚 INT8 量化到底在做什么。简单说量化就是把 FP32 的浮点数值映射到 8 位整数区间用一个 scale 和一个 zero_point 来描述这个映射关系。对于权重来说这个映射是静态的校准阶段就能确定对于激活值来说映射依赖输入数据的分布所以需要校准集来统计。检测模型和分类模型不一样它的输出包含分类分支和回归分支。分类分支对量化相对鲁棒因为最终取的是 argmax数值上的小扰动不太影响类别判定。但回归分支就麻烦了尤其是 YOLOv8 用的 DFLDistribution Focal Loss结构它把边界框回归建模成一个离散分布每个边对应一组 logits最后通过期望计算得到连续坐标。这个期望计算对 logits 的数值精度非常敏感一旦量化误差让分布形状发生偏移回归出来的框就会明显跑偏。另外YOLOv8 的检测头里还有 Sigmoid、Concat、Reshape 这些操作它们本身不引入量化误差但会影响量化工具对前后算子的融合策略。如果融合没做好中间就会插入额外的量化/反量化节点每一次转换都会带来精度损失。J6m 的工具链在这方面有自己的融合规则不是所有 ONNX 算子组合都能被完美识别。还有一个容易被忽略的点是校准集的选择。很多人随便拿几十张训练集图片就当校准集用了但校准集的数据分布必须和实际推理场景接近否则统计出来的激活范围会偏导致量化后的数值大量落在有效区间之外。我这次一开始用的校准集就是从训练集里随机抽的后来换成验证集里更有代表性的样本精度就有改善虽然没完全解决问题但说明校准集确实是个变量。1.3 排查路线从 ONNX 到板端的逐段验证我的排查路线分四步走。第一步确认 ONNX 模型本身的结构和数值正确性用 Netron 看计算图用 ONNX Runtime 跑数值对齐。第二步检查量化配置重点看哪些层被量化、哪些层被跳过、校准算法用的是什么。第三步在 PC 模拟器上做逐层误差分析找出误差最大的那几个算子。第四步针对问题算子做结构调整或量化策略调整重新编译验证。这条路线的好处是每一步都有明确的输入输出不会陷入“改了参数看结果”的盲目试错。下面我按这个顺序把每个环节的细节展开讲。2. 核心细节解析与实操要点2.1 ONNX 导出环节的隐藏陷阱ONNX 导出看起来是最简单的一步但其实坑不少。YOLOv8 的官方导出脚本用的是ultralytics库默认导出的 ONNX 是 opset 17输入是1x3x640x640。这里第一个要注意的是动态轴的问题。如果你导出时开了 dynamic batchONNX 里会有dynamic_axes标记地平线工具链对动态 shape 的支持有限可能会导致某些算子无法融合。我建议部署到 J6m 时先把 batch 固定成 1等精度对齐后再考虑动态 batch。第二个坑是 DFL 结构的导出方式。YOLOv8 的检测头里DFL 模块在 PyTorch 里是一个softmax加conv再加sum的组合导出到 ONNX 后可能变成SoftmaxConvReduceSum或者被优化成MatMul形式。不同导出方式对量化的友好程度不一样。我实测下来如果 DFL 被导出成Softmax后面直接接Conv量化工具会把 Softmax 的输出当成激活值来统计而 Softmax 输出的分布非常集中量化 scale 会偏小导致后续 Conv 的输入精度损失严重。第三个坑是后处理。有些导出脚本会把 NMS 也导进 ONNX这样模型输出就是最终框。但地平线工具链对 NMS 的支持有自己的实现如果你在 ONNX 里带了 NMS工具链可能会用自定义算子替换替换过程中量化策略可能不一致。我的做法是 ONNX 只导出到检测头输出NMS 放在板端用 CPU 做这样量化范围可控。检查 ONNX 结构我一般用两个工具Netron 看图结构ONNX Runtime 跑数值。Netron 里重点看有没有多余的QuantizeLinear/DequantizeLinear节点如果有说明导出时已经带了量化信息需要清理掉重新导出。ONNX Runtime 那边我会写一个简单的 Python 脚本把 PyTorch 的输出和 ONNX 的输出做逐元素对比确保 max diff 在 1e-4 以内。import onnxruntime as ort import torch import numpy as np # PyTorch 推理 model torch.load(yolov8s.pt) model.eval() dummy torch.randn(1, 3, 640, 640) with torch.no_grad(): pt_out model(dummy).numpy() # ONNX Runtime 推理 sess ort.InferenceSession(yolov8s.onnx) onnx_out sess.run(None, {images: dummy.numpy()})[0] # 数值对比 diff np.abs(pt_out - onnx_out) print(fmax diff: {diff.max():.6f}, mean diff: {diff.mean():.6f})这段脚本跑下来如果 max diff 超过 1e-3就说明导出有问题得回去检查导出参数。我这次跑出来是 2e-5说明导出环节是干净的。2.2 量化配置的关键参数解读地平线工具链的量化配置一般是一个 YAML 文件里面有几个关键字段需要重点关注。第一个是calibration_type常见的有kl、max、percentile几种。KL 散度校准适合激活分布比较集中的模型max 校准简单粗暴但对异常值敏感percentile 是折中方案。YOLOv8 的激活分布在不同层差异很大我建议先用percentile跑一版再对比kl的效果。第二个是per_channel开关。权重量化默认是 per-channel 的也就是每个输出通道有独立的 scale这对精度有好处。但激活量化通常是 per-tensor 的因为激活值在推理时是动态的没法提前知道每个通道的范围。如果你发现某一层的激活量化误差特别大可以考虑把这层加到skip_layers里让它保持 FP16。第三个是skip_layers列表。这个列表里的层不会被量化保持浮点精度。我这次排查发现把 DFL 相关的几个 Conv 层加进去后精度从 0.51 回升到了 0.63说明这些层确实是精度瓶颈。但全部跳过又会导致速度下降太多所以需要在精度和速度之间找平衡。第四个是calibration_dataset的配置。校准集一般准备 100 到 500 张图片太少统计不准太多没必要。图片要从实际推理场景里选覆盖不同的光照、角度、目标尺度。我这次用了 200 张验证集图片比之前随机抽的 50 张训练集图片效果好很多。还有一个细节是input_type和input_shape的配置。J6m 工具链要求输入是 NHWC 格式而 ONNX 默认是 NCHW转换过程中会插入 transpose 操作。如果 transpose 的位置不对可能会影响前后算子的融合。我建议在导出 ONNX 时就把输入改成 NHWC或者在量化配置里明确指定 transpose 的位置。2.3 DFL 结构对量化的影响机制DFL 是这次问题的核心值得单独拿出来讲。YOLOv8 的边界框回归不是直接预测四个坐标值而是预测每个边的离散分布。具体来说每个边预测 16 个 logits经过 Softmax 变成概率分布然后用0, 1, 2, ..., 15做加权求和得到期望值也就是最终的坐标偏移。这个结构对量化的敏感性来自两个方面。第一Softmax 的输出是概率值范围在 0 到 1 之间而且大部分值集中在很小的区间里。量化工具在校准时会统计 Softmax 输出的范围如果大部分值都在 0.01 到 0.1 之间scale 就会设得很小导致量化后的分辨率不够概率分布的细节丢失。第二加权求和那一步涉及乘加运算如果 logits 的量化误差导致概率分布偏移期望值就会偏离真实值而且这个偏差会随着分布宽度的增加而放大。我做过一个实验把 DFL 的 Softmax 输出单独拿出来看量化前后的差异。量化前某个边的概率分布峰值在 index 7 附近量化后峰值跑到了 index 5期望值差了将近 2 个像素。对于小目标来说2 个像素的偏差可能就导致框完全偏出目标。解决思路有几个方向。一是把 DFL 的 Softmax 和后续的加权求和整体作为一个自定义算子不让量化工具拆开处理。地平线工具链支持自定义算子注册但需要写一些配置代码。二是把 DFL 相关的层全部跳过量化保持 FP16代价是这部分计算会慢一些但 DFL 的计算量本身不大对整体延迟影响有限。三是在训练阶段做量化感知训练QAT让模型提前适应量化误差但这次项目时间紧没来得及做 QAT。我最后采用的是第二种方案把 DFL 的四个输出分支对应的 Conv 层加到skip_layers里同时把 Softmax 也跳过。这样精度回升到了 0.68虽然还没到 FP32 的 0.72但已经可以接受了。剩下的 4 个点损失主要来自 backbone 和 neck 部分的量化这部分用 percentile 校准后还能再找回 1 到 2 个点。2.4 校准集构建的实操经验校准集这块我踩过坑值得多说几句。一开始我图省事直接从训练集里随机抽了 50 张图片结果量化后精度只有 0.51。后来分析发现训练集里的图片分布很不均匀有些场景的图片特别多有些场景几乎没有导致校准统计出来的激活范围偏向某些特定场景。第二次我改用验证集因为验证集的分布更接近实际测试场景。但验证集只有 200 张我全用了。效果有改善精度到了 0.55但还是不够。第三次我做了一个改进把验证集按场景分类每个场景抽等量的图片保证校准集的分布均衡。同时我还在校准前对图片做了和推理时一样的前处理包括 resize、归一化、通道转换确保输入分布一致。这里有个细节容易被忽略校准时的前处理必须和推理时完全一致。如果你在校准时用了 letterbox 填充推理时也必须是 letterbox如果你在校准时把图片归一化到 0 到 1推理时也得一样。我见过有人校准时用 0 到 255 的原始像素推理时用 0 到 1 的归一化值量化 scale 直接偏了 255 倍精度不掉才怪。另外校准集的数量也不是越多越好。我试过用 500 张和 200 张的效果差不多但校准时间多了不少。一般来说100 到 300 张足够覆盖主要场景了。如果模型特别大或者场景特别复杂可以适当增加但没必要超过 500 张。还有一个技巧是校准集的多样性。除了不同场景还要覆盖不同的目标尺度、不同的光照条件、不同的遮挡程度。我这次在校准集里特意加入了一些小目标密集的图片和一些低光照图片因为这些场景下激活值的分布和普通场景差异很大如果不覆盖量化后这些场景的精度会掉得特别厉害。3. 实操过程与核心环节实现3.1 环境准备与工具链版本确认动手之前先把环境理清楚这一步很多人会跳过但版本不匹配导致的玄学问题特别多。我这次用的工具链是地平线官方发布的版本具体版本号在hb_mapper的--version里能看到。ONNX 的版本是 1.14ONNX Runtime 是 1.15PyTorch 是 2.0。这几个版本之间的兼容性我提前查过没有已知的冲突。工具链安装完之后先跑一个官方的示例模型确认整个流程能走通。官方示例一般是一个简单的分类模型量化后精度损失很小。如果示例都跑不通那说明环境有问题先解决环境再搞自己的模型。我这次跑示例花了大概半小时确认没问题后才开始处理 YOLOv8s。环境准备里还有一个容易忽略的点是 Python 环境。地平线工具链对 Python 版本有要求一般是 3.8 到 3.10。我建议用 conda 建一个独立环境避免和系统里的其他包冲突。依赖包也要按官方文档的版本装不要图省事直接pip install最新版版本不匹配的报错往往很难排查。3.2 ONNX 模型导出与结构检查导出 ONNX 我用的是ultralytics库自带的导出脚本命令很简单yolo export modelyolov8s.pt formatonnx imgsz640 opset17 simplifyTrue这里simplifyTrue会调用onnx-simplifier做图优化把一些冗余算子合并掉。但要注意simplify有时候会把 DFL 的结构改掉导致量化工具识别不了。我建议先导出一版不 simplify 的用 Netron 看清楚原始结构再导出一版 simplify 的做对比。如果 simplify 后结构变化太大就用手动方式做必要的优化。导出完成后用 Netron 打开 ONNX 文件重点看几个地方。第一输入节点的 shape 是不是1x3x640x640数据类型是不是 float32。第二检测头的输出是不是三个分支分别对应 stride 8、16、32。第三DFL 部分的结构是不是Softmax后面接Conv或者MatMul。第四有没有多余的Cast、Reshape、Transpose节点这些节点会影响融合。我这次导出的 ONNX 里DFL 部分是一个Softmax接一个ConvConv 的权重是1x16x1x1相当于对 16 个通道做加权求和。这个结构本身没问题但量化工具会把 Softmax 的输出当成激活值来统计这就是精度损失的来源。检查完结构后跑一遍 ONNX Runtime 的数值对齐确认和 PyTorch 的输出一致。我前面给的脚本可以直接用注意输入名称要和 ONNX 里的输入节点名称一致。如果 max diff 在 1e-4 以内就可以进入下一步了。3.3 量化配置编写与校准执行量化配置我写了一个 YAML 文件核心内容如下model: input_type: nhwc input_shape: [1, 640, 640, 3] output_names: [output0, output1, output2] calibration: calibration_type: percentile percentile: 99.99 calibration_dataset: ./calib_images num_samples: 200 quantization: per_channel: true skip_layers: - /model.22/cv2.0/cv2.0.2/Conv - /model.22/cv2.1/cv2.1.2/Conv - /model.22/cv2.2/cv2.2.2/Conv - /model.22/cv3.0/cv3.0.2/Conv - /model.22/cv3.1/cv3.1.2/Conv - /model.22/cv3.2/cv3.2.2/Conv这里的skip_layers就是 DFL 相关的 Conv 层。层名称可以从 Netron 里复制注意要写完整的路径。calibration_type我选了percentilepercentile值设成 99.99意思是把激活值范围的第 99.99 百分位作为量化上限这样可以过滤掉极端异常值。校准集的准备我写了一个脚本把验证集图片按场景分类每个场景抽等量图片统一 resize 到 640x640做 letterbox 填充归一化到 0 到 1最后保存成 numpy 数组或者图片文件。工具链一般支持直接读图片文件夹但要求图片格式和推理时一致。执行量化的命令是hb_mapper makertbin --config yolov8s_config.yaml --model-type onnx这个过程会先做校准统计每一层的激活范围然后生成量化模型。校准时间取决于模型大小和校准集数量YOLOv8s 加 200 张图片大概跑了 10 分钟左右。跑完后会生成一个量化后的 ONNX 或者 hbm 文件还有一个精度报告。精度报告里会列出每一层的量化误差重点看cosine_similarity和max_abs_error这两个指标。如果某一层的 cosine similarity 低于 0.99说明这层的量化误差比较大可以考虑加到skip_layers里。我这次报告里 DFL 相关层的 cosine similarity 只有 0.95 左右其他层都在 0.99 以上这就验证了我的判断。3.4 板端部署与精度验证量化完成后把生成的 hbm 模型推到板端用地平线的推理接口加载。板端推理的代码框架官方有示例主要是初始化模型、准备输入、执行推理、解析输出这几步。输入数据要和校准时一致NHWC 格式float32 类型归一化到 0 到 1。精度验证我用的是 COCO 验证集的一个子集大概 500 张图片覆盖不同的目标类别和尺度。评测指标用 mAP0.5 和 mAP0.5:0.95。FP32 的基准是 0.72 和 0.52量化后如果 mAP0.5 能到 0.68 以上mAP0.5:0.95 能到 0.48 以上就算达标了。我这次最终跑出来是 mAP0.5 为 0.68mAP0.5:0.95 为 0.47比 FP32 分别掉了 4 个点和 5 个点。这个损失在 INT8 量化里算是正常范围尤其是检测模型。如果业务对精度要求特别高可以考虑混合量化也就是部分层 INT8、部分层 FP16但速度会有所下降。板端推理的延迟我也测了INT8 比 FP16 快了大概 40%单帧推理时间从 12ms 降到了 7ms 左右。这个提升对于实时性要求高的场景很有价值比如 30fps 的视频流FP16 可能勉强够用INT8 就有足够的余量了。4. 常见问题与排查技巧实录4.1 精度下降问题的速查表排查精度问题我整理了一个速查表按可能性从高到低排列遇到问题可以按这个顺序查。问题现象可能原因排查方法解决思路精度掉 20 个点以上模型结构在转换后变形Netron 对比 ONNX 和原始模型结构重新导出关闭 simplify精度掉 10 到 20 个点DFL 或 Softmax 层量化误差大看精度报告里的 cosine similarity把这些层加入 skip_layers精度掉 5 到 10 个点校准集分布不匹配检查校准集和推理输入的前处理重新构建校准集精度掉 3 到 5 个点量化参数不合适对比不同 calibration_type换用 percentile 或 kl精度掉 1 到 3 个点正常量化损失无需排查可接受或做 QAT 进一步优化板端和模拟器精度不一致板端前处理或后处理有差异对比板端和 PC 端的输入输出统一前处理和后处理逻辑这个表是我踩了多次坑之后总结的基本上覆盖了常见的精度问题。实际排查时先看精度掉了多少然后按表里的顺序查能省不少时间。4.2 几个容易忽略的细节问题第一个细节是输入数据的 layout。ONNX 默认是 NCHW但 J6m 工具链要求 NHWC。如果你在量化配置里写了input_type: nhwc但 ONNX 模型本身是 NCHW工具链会自动插入 transpose。这个 transpose 的位置很关键如果插在了量化节点之后可能会导致量化 scale 不匹配。我建议在导出 ONNX 时就把输入改成 NHWC或者在量化配置里明确指定 transpose 的位置。第二个细节是输出节点的选择。YOLOv8 的 ONNX 输出可能有多个节点包括检测头的原始输出和后处理后的输出。如果你把后处理也导进了 ONNX量化工具可能会对 NMS 部分做量化而 NMS 涉及排序和阈值比较量化后行为可能不一致。我的做法是只导出检测头的原始输出NMS 放在板端用 CPU 做。第三个细节是校准集的图片格式。工具链一般支持 JPG、PNG 等常见格式但要求图片的通道顺序和模型输入一致。如果你用 OpenCV 读图片默认是 BGR 顺序而模型训练时用的是 RGB这就会导致颜色通道错位量化 scale 偏掉。我建议在校准脚本里显式做 BGR 到 RGB 的转换确保和训练时一致。第四个细节是量化后的模型大小。INT8 量化后模型大小应该比 FP32 小 4 倍左右如果发现模型大小没怎么变说明量化没生效可能配置写错了或者工具链没识别到。我这次量化后模型从 22MB 降到了 6MB 左右符合预期。4.3 混合量化的取舍经验混合量化是精度和速度之间的折中方案。我的经验是优先把以下几类层保持 FP16一是 DFL 相关的层前面已经讲过原因二是检测头的最后几层因为这些层直接决定输出精度三是模型里数值范围特别大的层比如某些激活函数输出范围很宽的层。哪些层可以放心用 INT8 呢backbone 里的卷积层一般没问题因为特征图的数值分布比较稳定neck 里的上采样和 concat 层也可以量化这些操作本身不引入数值误差还有一些 BN 层通常会被融合到卷积里不需要单独考虑。混合量化的配置方法是在skip_layers里列出要保持 FP16 的层。但要注意跳过的层越多速度越慢。我这次跳过了 6 个 DFL 相关的 Conv 层速度比全 INT8 慢了大概 15%但精度回升了 17 个点这个 trade-off 是值得的。如果业务对速度要求特别高可以考虑只跳过最关键的 2 到 3 层其他层用 INT8。具体跳过哪几层可以看精度报告里的 cosine similarity优先跳过 similarity 最低的那几层。我这次试过只跳过 3 层精度是 0.64速度比跳过 6 层快了 8% 左右也算是一个可选的方案。4.4 从这次排查中总结的实操心得这次排查花了大概三天时间踩了不少坑也积累了一些经验。第一个心得是遇到精度问题不要急着调量化参数先确认模型结构在转换后有没有变形。我一开始就是直接调calibration_type从max换到kl再换到percentile精度只波动了 1 到 2 个点根本问题没解决。后来用 Netron 仔细看结构才发现 DFL 部分的量化策略有问题。第二个心得是校准集的质量比数量重要。我试过用 500 张随机图片效果不如 200 张精心挑选的图片。校准集要覆盖实际推理场景的主要变化包括光照、角度、目标尺度、遮挡程度等。如果场景特别复杂可以分场景构建多个校准集分别量化后对比效果。第三个心得是精度报告要认真看。工具链生成的精度报告里有很多有用信息比如每一层的 cosine similarity、max abs error、量化前后的数值范围对比。这些信息能帮你快速定位问题层而不是盲目试错。我这次就是通过精度报告发现 DFL 层的 similarity 只有 0.95其他层都在 0.99 以上才确定了问题方向。第四个心得是板端验证不能省。PC 模拟器的结果和板端有时候会有差异尤其是涉及自定义算子或者特殊融合策略的时候。我这次在 PC 模拟器上精度是 0.68板端跑出来也是 0.68说明一致性没问题。但如果模拟器和板端不一致就要检查板端的前处理和后处理逻辑看看有没有和 PC 端不一样的地方。第五个心得是保留一份完整的排查记录。我这次把每一步的配置、命令、输出、精度结果都记下来了包括失败的尝试。这样后面再遇到类似问题可以直接翻记录不用重新踩坑。而且记录本身也能帮你理清思路避免在多个变量之间来回切换。4.5 后续可优化的方向这次排查解决了主要问题但还有一些优化空间。第一个方向是 QAT也就是量化感知训练。在训练阶段就模拟量化误差让模型提前适应这样量化后的精度损失会更小。QAT 一般能比 PTQ 多找回 2 到 3 个点但需要重新训练模型时间成本比较高。第二个方向是自定义算子。地平线工具链支持注册自定义算子可以把 DFL 的 Softmax 和加权求和整体作为一个算子避免量化工具拆开处理。这样既能保持 INT8 的速度又能避免精度损失。但自定义算子的开发需要熟悉工具链的算子接口有一定的学习成本。第三个方向是校准算法的改进。除了 percentile 和 kl还有一些更高级的校准算法比如基于熵的校准、基于梯度的校准等。这些算法在某些模型上效果更好但需要额外的实现和验证。如果项目对精度要求特别高可以尝试这些方法。第四个方向是模型结构的调整。如果 DFL 结构对量化太敏感可以考虑在训练阶段换一种回归方式比如直接回归坐标值而不是用分布。但这样会改变模型结构需要重新训练和验证适合新项目而不是已有项目的优化。这次排查让我对边缘侧 INT8 量化有了更深的理解尤其是检测模型和分类模型的差异。分类模型量化后精度损失通常很小但检测模型因为回归分支的存在对量化误差更敏感。DFL 这种结构虽然提升了检测精度但也增加了量化的难度。后续再做类似项目我会在模型选型阶段就考虑量化友好性尽量选择对量化不敏感的结构。