摘要: 模型在 GPU 上精度正常,到了 J6 做 INT8 量化却开始掉点,很多人的第一反应是换校准数据、调量化参数,甚至直接上 QAT。但实际工程中,问题可能早在 ONNX、图优化、量化转换或编译部署阶段就已经出现。本文结合 J6 工具链,把 PyTorch 模型到 J6 HBM 的精度变化明确拆成 4 段,通过逐阶段验证定位第一处误差,再决定到底该修模型、调 PTQ,还是进入 QAT。
1. PTQ 掉点了,真的是 PTQ 的锅吗?
做端侧量化的同学几乎都见过这个开场白:
PyTorch FP32 正常
ONNX FP32 正常
PTQ INT8 开始掉点
很多人第一反应是继续调 Calibration,或者干脆进入 QAT。但这种方法多半会陷入误区,因为模型从训练环境到真正运行在 征程6 上,中间远不止一次“FP32 → INT8”。
征程6 工具链把这件事拆成了多个独立检查点:模型检查、性能验证、量化转换、编译、板端部署。每相邻两段之间都可能产生误差。"PTQ 掉点"这个描述其实太粗了,它到底掉在哪一段,才决定下一步该做什么。
图 1:征程6 PTQ 推荐使用流程。图中模型检查、性能上限验证、配置生成和精度调优本身就是多个独立检查点。
所以现在碰到掉点,先把问题拆成四段:
| 阶段 | 核心问题 |
|---|---|
| 第一段:PyTorch → ONNX | 模型还没进入 征程6 工具链之前是否已经产生偏差 |
| 第二段:ONNX → Optimized Float | 图优化或模型转换是否改变浮点模型行为 |
| 第三段:Float → INT8 | 真正的量化误差究竟在哪里 |
| 第四段:Quantized BC → HBM → J6 | 编译、输入和板端运行是否保持一致 |
这四段里,哪一段第一次明显出问题,就优先查哪一段。
这也是整篇文章唯一需要记住的原则。
第一段:PyTorch → ONNX
还没进入 征程6,误差可能已经出现了
这一段其实和 征程6 本身没有直接关系,却同样关键。
训练模型导出:
model.pth
↓
model.onnx
然后执行一次:
session.run(...)
模型能跑、Shape 对、结果看着也正常,于是默认:
ONNX 没问题。
实际上,“ONNX 能执行”和“ONNX 与 PyTorch 一致”完全是两个概念。
真实项目里最容易出现的问题往往也并不复杂,例如:
RGB / BGR
Resize
Normalize
0~255 / 0~1
HWC / CHW
Sigmoid
Decode
NMS
尤其是 PyTorch 验证程序和 ONNX 推理程序分别维护一套 preprocess() 时,很容易出现一种比较讨厌的 Bug:
程序完全正常,模型也有输出,但结果已经悄悄变了。
因此尽量让 PyTorch 和 ONNX 使用同一份已经完成前处理的数据,先比较原始模型 Tensor,再跑完整验证集。
例如最简单的输出检查:
diff = np.abs(torch_output - onnx_output)
print("max diff :", diff.max())
print("mean diff:", diff.mean())
单张图片主要用于 Debug,真正判断模型有没有发生变化,还是要重新计算 Accuracy、Precision、Recall、mAP 等完整指标。
检测模型还有一个额外的坑
检测模型比分类模型更容易把这种差异藏起来。
假设:
PyTorch confidence = 0.281
ONNX confidence = 0.267
检测阈值:
conf = 0.25
两个模型最终都会画出这个框。
肉眼看:
PyTorch:√
ONNX: √
但数值实际上已经开始发生变化。
如果后面的 INT8 再产生一点误差:
INT8 confidence = 0.243
最终立刻变成:
| 模型 | 最终结果 |
|---|---|
| PyTorch | TP |
| ONNX | TP |
| INT8 | FN |
最后看到的是:
INT8 漏检。
但真正的问题并不是从 INT8 才突然发生。
所以第一段的结论很简单:
PyTorch 和 ONNX 没对齐之前,不要急着讨论 PTQ。
第二段:ONNX → Optimized Float
“只是图优化”也应该验证一次
如果 PyTorch 和 ONNX 已经基本一致,下一步才进入 征程6 工具链。
这一阶段我以前比较容易忽略:
Original ONNX
↓
Optimized Float
因为直觉上会认为:
图优化都是等价变换,不应该影响结果。
大多数时候确实如此。
但工程上需要区分两个概念:
“理论上应该一样”
和:
“实际验证过一样”
征程6 PTQ 转换过程中会产生不同阶段的模型产物,例如原始浮点模型、优化后的浮点模型以及后续量化模型。这恰好给了我们逐级验证的机会。
如果出现:
Original ONNX 正常
Optimized Float 异常
那就暂时不用继续研究 Calibration。
因为此时:
INT8
QAT
Calibration Data
Quantization Scale
都还不是第一嫌疑人。
征程6 官方的一致性分析同样采用这种“先定位阶段”的思路。QAT 链路中会明确区分 qat.pt、qat_export.pt、qat.bc、quantized.bc、nv12_quantized.bc 和最终 HBM,而不是只比较训练模型和板端模型。
*图 2:征程6 QAT/定点链路中,不同阶段模型本身就是一致性定位的重要依据。
虽然这张图来自 QAT 一致性教程,但它体现的排错方法同样适用于 PTQ:
A → B → C → D
不要只检查:
A ↔ D
而应该找到:
B 从什么时候开始不像 A?
C 从什么时候开始不像 B?
这会让问题空间小很多。
第三段:Float → INT8
到这里,才真正轮到“量化问题”
前两段确认正常以后,现在终于可以比较:
Float
↓
INT8
如果明显掉点发生在这里,那么“PTQ 精度问题”这个判断才真正成立。
这时不要看“Calibration 再加多少张图?”,而是先看三个方向。
3.1 Calibration 数据有没有代表性
校准集最大的价值不是“数量足够多”,而是能不能覆盖真实输入分布。
如果模型实际运行场景和校准数据差异很大,那么量化得到的 Activation Range 本身就可能不合理。
所以:
1000 张随机数据
不一定天然优于:
200 张分布覆盖更完整的数据
3.2 Activation Range 有没有被异常值拉开
假设某层绝大部分激活都集中在:
[-2, 3]
但极少量数据跑到了:
[-40, 35]
量化范围被扩大以后,有限的 INT8 表示区间需要覆盖更宽的数值范围,正常数据区域的量化精度自然会受到影响。
这也是实际项目中比较值得观察数据分布的原因。
3.3 是整个网络敏感,还是只有几个节点敏感?
这一点尤其重要。
如果模型大部分节点量化都很稳定,只有少数节点出现明显误差,那么它已经从:
“这个模型不适合 INT8”
变成了:
“这几个节点需要处理。”
征程6 QAT 调优指南同样非常强调敏感度分析和混合精度,而不是全局简单地从 INT8 切换到高精度。征程6E/M 主要采用 INT8+INT16 混合精度思路,而 征程6P/H 可以进一步利用 FP16 等配置处理难量化结构。
*图 3:J6E/M QAT 精度调优思路中,会分别从 Float、Calibration、QAT 等阶段继续缩小量化问题范围,而不是直接把整个模型切到更高精度。
官方 PTQ Debug 工具的设计也很类似:通过量化误差、敏感度和数据分布继续判断问题来自 Weight 还是 Activation,并进一步决定是否使用高精度、调整网络或进入 QAT。
*图 4:PTQ 精度 Debug 的定位流程。真正有价值的并不是“模型掉了多少点”,而是继续判断误差来源及敏感节点。
到这里,如果确定:
PyTorch → ONNX 正常
ONNX → Float 正常
Float → INT8 明显下降
那么才值得认真讨论:
Calibration
混合精度
敏感节点
QAT
第四段:Quantized BC → HBM → 征程6
模型没坏,也可能“部署坏了”
第四段是特别容易被忽略的一段。
假设:
quantized.bc 正常
HBM / J6 异常
这时候如果继续回去调整 QAT,很可能又找错方向。
征程6 的模型从定点 HBIR 到实际板端部署,还需要经过 compile,并且图像输入场景还可能涉及 NV12 前处理节点、Stride、Padding 等问题。官方 征程6 一致性教程专门把 convert、NV12 节点插入以及 compile 分成独立阶段排查。
其中非常实用的一个工具就是 hb_verifier 。征程6 的 hb_verifier 支持 ONNX、HBIR、HBM 之间进行一致性比较,可以用来快速判断问题究竟发生在模型转换的哪一段。
图 5:征程6 compile 阶段的一致性定位。quantized.bc* 正常而 HBM 异常时,可以先通过 hb_verifier 判断 compile 前后是否已经产生偏差。
这一阶段如果模型本身已经对齐,排查重点就应该转向:
Input Data
NV12 / RGB
Stride
Padding
Preprocess
Postprocess
Runtime
而不是:
Learning Rate
FakeQuant
Observer
QAT Epoch
也就是说:
板端精度异常,不一定意味着模型量化异常。
这是第四段最重要的结论。
5. 四段全部检查完,什么时候才值得上 QAT?
经过前面四段以后,QAT 的位置其实已经很清楚了。
| 当前状态 | 是否优先进入 QAT |
|---|---|
| PTQ 已达到业务精度 | 没必要 |
| PyTorch → ONNX 已经掉点 | 否 |
| Optimized Float 出现明显偏差 | 否 |
| BC / HBM 一致性异常 | 否 |
| 板端输入或后处理不一致 | 否 |
| Float → INT8 存在明确量化损失 | 可以 |
| Calibration 调优后仍明显掉点 | 可以 |
| 少数量化敏感节点影响整体精度 | 更值得尝试 |
征程6 官方 QAT 调优本身也不是“打开 QAT 再训练几轮”这么简单。针对不同 征程6 平台,需要根据硬件能力选择 INT8、INT16、FP16 等混合精度策略,并通过模型检查、Calibration、敏感度和 Debug 工具不断缩小问题。
所以我现在更愿意把 QAT 定义成:
确认模型确实不适应当前量化误差以后,再让模型通过训练主动适应这种误差。
而不是:
PTQ 掉点
↓
QAT
6. 最终目标:把“四段排查”变成自动回归
如果只部署一个模型,人工检查四段问题还可以接受。
但真实项目往往是:
v1 → v2 → v3 → v4 → ...
模型每迭代一次,都重新手动检查 PyTorch、ONNX、BC、HBM,很快就会变成重复工作。
所以我更希望最后把这套流程固定下来:
PyTorch Validation
↓
Export ONNX
↓
ONNX Regression
↓
Optimized Float Check
↓
PTQ / QAT
↓
Quantized BC Validation
↓
hb_verifier
↓
HBM / J6 Regression
↓
Report
最终先看一个简单的结果:
| Check | Result |
|---|---|
| PyTorch Baseline | PASS |
| PyTorch ↔ ONNX | PASS |
| ONNX ↔ Optimized Float | PASS |
| Float ↔ INT8 | FAIL |
| BC ↔ HBM | - |
| J6 Regression | - |
这样看到的第一件事情就是:
第三段 FAIL。
而不是打开几十个 Log,再凭经验猜到底哪里出了问题。
7. 写在最后
现在再遇到 PTQ 掉点,不要第一时间问:
“QAT 怎么调?”
而是把整个过程拆成四段:
第一段
PyTorch → ONNX
↓
第二段
ONNX → Optimized Float
↓
第三段
Float → INT8
↓
第四段
Quantized BC → HBM → J6
然后只做一件事情:
找到第一处明显发生变化的位置。
第一段错了,就查导出和前后处理;第二段错了,就查 Float 图转换;第三段错了,才真正研究 Calibration、敏感层和 QAT;第四段错了,则重点检查 compile、输入格式和板端 Runtime。
把问题这样拆开以后,很多原本看起来很玄学的“量化掉点”,其实都会变成一个范围非常有限的工程问题。
模型部署最怕的不是掉点,而是把所有掉点都叫作量化误差。