一、背景:为什么大家几乎都会从 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 当成“精度万能补丁”。

十二、工程调优路径建议

推荐流程:

  1. 全 int8 → 测性能
  2. 全 int16 → 测精度上限
  3. GlobalFakequantSwitch 定位问题
  4. 结构分析敏感模块
  5. 构建统一 int8 base
  6. prefix 升级
  7. 单层剔除
  8. 构建精度-性能 Pareto 曲线

十三、常见误区

❌ 误区 1:int16 一定比 int8 精度高

实际很多 backbone 层 int8 几乎无损。

❌ 误区 2:回退法可以长期维护

回退法适合探测上限,不适合工程维护。

❌ 误区 3:忽略输出 dtype 传播

输出 dtype 会影响下游模块。

十四、最终总结

量化优化不是:

  • 从 int16 往下退
  • 从 int8 往上加

而是:

设计一个清晰、可迁移、可验证的量化拓扑结构。

当我们做到:

  • base 可替换
  • prefix 可迁移
  • 最终 dtype 可验证
  • FakeQuant 可局部控制

我们就掌握了 QAT 的量化配置体系。

标签: none

添加新评论