一、背景:为什么大家几乎都会从 base_int16 开始?
在 QAT 项目中,只要遇到精度问题,工程师的第一反应通常是:
先上全 int16,看精度上限。
这是完全合理的。
原因:
- int16 动态范围更大
- 量化误差更小
- 更接近浮点
- 能快速验证“模型是否具备量化可行性”
如果全 int16 精度仍不好,问题往往不在 bit-width,而在:
- scale 分布异常
- observer 未收敛
- 插桩位置不合理
- 数据分布问题
因此:
base_int16 是“精度上限探测工具”。
这一步是科学且必要的。
二、工程现实:最终目标往往是性能
但真实部署环境通常是:
- 延时受限
- 带宽受限
- 片上存储受限
在这种前提下:
全 int16 基本不可能成为最终部署形态。
所以工程上更合理的路径应该是:
以 base_int8 作为默认底座对精度敏感区域做局部升级
这意味着:
- int16 用来探上限
- int8 用来做工程
这两个阶段目标不同。
三、真正的困难:从 base_int16 切回 base_int8
问题往往出现在这里。
当我们在 base_int16 下完成精度探索后,会得到大量细节信息:
- 哪些 layer 敏感
- 哪些 layer 需要 fix_scale
- 哪些模块 output 必须 int16
- 哪些 Conv / Matmul 必须 int16 输入
但当切换到 base_int8 时,会发现:
- 默认 ModuleNameTemplate 不同
- 默认 ConvDtypeTemplate 不同
- 默认 MatmulDtypeTemplate 不同
- 输出 dtype 传播链改变
结果:
相同 prefix 写法,生效行为完全不同。
这就意味着:
base_int16 的配置不能直接复制到 base_int8。
四、问题的本质:不要让 base 决定量化形态
量化系统本质是“分层覆盖系统”。
如果让 base 决定形态,你就会被 base 牵着走。
真正应该控制的是:
每个模块最终生效的 dtype 拓扑。
五、方法论框架:量化拓扑设计
整个方法可以抽象为五个阶段:
1. 精度上限探测(全 int16)
2. 敏感层识别
3. 结构分析
4. 等效拓扑构建
5. int8 工程落地
我们逐步展开。
六、第一阶段:全 int16 精度上限探测
典型配置:
ModuleNameTemplate({"": qint16})
ConvDtypeTemplate(input_dtype=qint16, weight_dtype=qint8)
MatmulDtypeTemplate(input_dtypes=qint16)
目标:
- 验证量化可行性
- 建立精度上限参考
七、第二阶段:使用 GlobalFakequantSwitch 定位问题
无论哪种路径,都建议使用:
GlobalFakeQuantSwitch.disable()
需要去量化的操作
GlobalFakeQuantSwitch.enable()
典型使用思路:
- 全局关闭 FakeQuant
- 单模块开启
- 或单模块关闭
确认:
- 精度损失是否来自 bit-width
- 是否来自 scale 更新
- 是否来自某个具体模块
这一步可以避免盲目升位宽。
八、第三阶段:基于模型结构识别敏感模块
量化配置必须依赖模型结构。
例如:
- backbone 多为线性卷积 → int8 风险低
- head 中 aggregation / attention → 敏感
必须回答:
- 哪些模块属于 backbone?
- 哪些属于 neck?
- 哪些属于 head?
- 哪些包含 matmul?
- 哪些包含 feature aggregation?
没有结构分析,就没有精准升级。
九、第四阶段:构建“等效量化拓扑”
核心思想:
默认 int8 + 精准 prefix 升级
Step 1:统一默认 base_int8
ModuleNameTemplate({"": qint8})
ConvDtypeTemplate(input_dtype=qint8, weight_dtype=qint8)
MatmulDtypeTemplate(input_dtypes=qint8)
这是性能底座。
Step 2:定义敏感模块列表
int16_modules = [
"head.anchor_encoder",
"head.lidar_shared_conv",
"head.layers",
]
Step 3:输出 dtype 升级
ModuleNameTemplate({
name: qint16 for name in int16_modules
})
Step 4:Conv 输入升级
ConvDtypeTemplate(
input_dtype=qint16,
weight_dtype=qint8,
prefix=int16_modules
)
Step 5:Matmul 输入升级
MatmulDtypeTemplate(
input_dtypes=qint16,
prefix=int16_modules
)
十、等效性的关键点
如果你在 base_int16 下:
- backbone output=int8
- head output=int16
那么你必须保证:
在 base_int8 下通过 prefix 升级后,
每个模块最终 output dtype 完全一致。
验证方法:
- 打印每层最终 dtype
- 单层剔除测试
- 对比精度曲线
十一、fix_scale 的位置
fix_scale 与 dtype 是两个维度:
- dtype 控制动态范围
- fix_scale 控制 scale 是否锁定
某些 head 模块:
- 可能必须 int16
- 也可能必须 fix_scale
但不要把 fix_scale 当成“精度万能补丁”。
十二、工程调优路径建议
推荐流程:
- 全 int8 → 测性能
- 全 int16 → 测精度上限
- GlobalFakequantSwitch 定位问题
- 结构分析敏感模块
- 构建统一 int8 base
- prefix 升级
- 单层剔除
- 构建精度-性能 Pareto 曲线
十三、常见误区
❌ 误区 1:int16 一定比 int8 精度高
实际很多 backbone 层 int8 几乎无损。
❌ 误区 2:回退法可以长期维护
回退法适合探测上限,不适合工程维护。
❌ 误区 3:忽略输出 dtype 传播
输出 dtype 会影响下游模块。
十四、最终总结
量化优化不是:
- 从 int16 往下退
- 从 int8 往上加
而是:
设计一个清晰、可迁移、可验证的量化拓扑结构。
当我们做到:
- base 可替换
- prefix 可迁移
- 最终 dtype 可验证
- FakeQuant 可局部控制
我们就掌握了 QAT 的量化配置体系。