返回专辑
·Johan·5 分钟阅读

fbgemm PTQ 与 per-channel scale

PTQ 校准数据不代表部署分布时 INT8 掉点惨烈——校准集、per-channel 和 sensitive layer fp16 保留要一起调。

fbgemm PTQ 与 per-channel scale

1. 校准集不覆盖部署分布,INT8 掉点惨烈

模型 fp32 训好,PTQ 量化后 val acc 掉 5pt+——常见根因是 calibration 数据不代表部署分布。校准集只有白天场景,部署有雨夜;或 calib 只有 50 batch,scale 估计不稳,per-tensor 量化时一个 outlier activation 拉偏整层 scale。INT8 部署不是「convert 一行就完」,是校准集、per-channel scale、敏感层保留三件事一起调。掉点大时先查 calib 覆盖,再查 sensitive layer,最后才考虑 QAT。

量化误差来源:权重舍入、activation 动态范围截断、accumulate 精度(INT32 累加后 requant)。per-channel weight scale 让每层每个 output channel 独立 scale,Conv 默认如此;activation 仍常 per-tensor,敏感层 keep fp32 可绕过 activation 截断。PTQ 假设 fp32 权重已收敛,用 calib forward 统计 activation 的 min/max 或 moving average 估 scale 。部署 slice 与 calib slice 错位时,某 slice 上 activation 超出 calib 见过的范围,INT8 饱和 clipping 增多,metric 断崖式下跌——这不是「模型变差」,是 scale 契约失效。

2. PyTorch PTQ 流程

python
model.eval()
model.qconfig = torch.quantization.get_default_qconfig('fbgemm')  # x86
# mobile CPU 用 'qnnpack'
torch.quantization.prepare(model, inplace=True)
for x, _ in calib_loader:  # 100–500 batch,覆盖部署场景
    model(x)
torch.quantization.convert(model, inplace=True)

fbgemm 用于 server x86,qnnpack 用于 mobile ARM——backend 与部署硬件必须一致,否则 latency 测了也白测。calib 须覆盖部署 slice(白天/夜/雨/室内/室外),缺 night 场景 calib 时 night slice 掉点往往最大。calib 时 model.eval() 固定 BN stats,与训练一致。

prepare 插入 observer 记录 activation 分布;convert 把 float module 换成 quantized module。calib loader 宜 shuffle 关闭、顺序与部署预处理一致——归一化 mean/std 错一位,scale 全偏。动态量化(仅 Linear)几乎不用在 CV deploy;静态 PTQ 才是检测/分割常态。x86 服务器上 fbgemm 对 Conv+ReLU 融合 kernel 成熟;ARM 上 qnnpack 对 depthwise 友好。若训练用 SyncBN、deploy 用 fused BN,convert 前须确认 BN 已 fold 进 Conv 或 stats 已固定,否则 calib 与 deploy graph 不一致。

3. 敏感层 skip 与 QAT

first conv(input 分布特殊,RGB 与归一化边界)和 last fc/head 常 keep fp32:

python
model.fc.qconfig = None  # 该层不量化

也可对 named module 遍历,按 skip list 设 qconfig = None。QAT:prepare_qat + fake quant modules 继续训 5–10 epoch,权重与 scale 联合适应,通常优于纯 PTQ;成本是额外训练与 QAT 超参(LR 常为 finetune 的 1/10)。Mobile 部署若 PTQ 掉点 2pt 内可接受,优先 PTQ 省工期。

敏感层判定可自动化:对每层做 量化误差 sensitivity 扫描——单独 keep fp32 其余 INT8,看 val 恢复幅度。first conv 输入动态范围受 RAW/RGB 与归一化影响,last head logits 小幅扰动即改变 argmax/mAP。中间层若某 channel activation 峰度极高(如 attention softmax 前),也可列入 skip。QAT 时 fake quant 在 forward 插入 round-trip,backward 用 STE;须用小 LR,否则 fp32 权重被「拉」向错误 scale 的局部最优。PTQ 与 QAT 的取舍:工期紧、掉点容忍 1–2pt → PTQ;关键 slice 掉点 >3pt → QAT 或 partial fp32

4. 取舍与失败模式

PTQ:零训练成本,calib 代表性好时够用。QAT:掉点更小,需再训。敏感层 fp32:latency 略增,acc 恢复明显——first/last 是常见 skip list,中间某层 activation 动态范围极大时也可单独 skip。report 量化前后 val acc/mAP diff 部署端 latency 实测,不用 FLOPs 估算——INT8 加速依赖 kernel 与 memory bandwidth。

失败:calib 不覆盖部署分布;backend 选错;convert 后未 model.eval() 导致 BN 漂移;ONNX 导出未带 quant 节点,部署端又做一遍 PTQ 导致 double quant。另一类是 per-tensor activation scale 被 outlier batch 拉偏: calib 仅 50 batch 且含一张过曝图,整层 scale 变大,多数正常图量化分辨率下降。缓解:增加 calib 量、用 percentile clipping(如 99.9%)估 scale,或对 activation 做 per-channel(部分框架支持)。latency 验收必须在目标 SoC 上测——笔记本 x86 INT8 加速 3×,同一模型在嵌入式 ARM 可能仅 1.3×,因 memory bound。

5. 案例:缺 night calib 导致 night mAP 崩

某检测模型,白天 calib 500 batch,白天 val mAP fp32 0.78 / INT8 0.77。night val fp32 0.65 / INT8 0.52——差 13pt。补 100 batch night calib 后 night INT8 0.61;first conv + last head keep fp32 后 0.63。说明 night 掉点一半是 scale 估计、一半是 sensitive 层。

复盘时按 slice 画 fp32 vs INT8 的 score 分布:night slice 上 INT8 高分框数量锐减,多为 activation clipping 而非 NMS 阈值问题。补 night calib 后 scale 更接近 night activation 范围;first conv fp32 保留输入动态范围;last head fp32 避免 bbox 回归量化误差放大。若仍不满足业务阈值,再上 QAT 5 epoch,通常还能回收 1–2pt——成本与 deadline 需在项目初期写进 deploy checklist。

6. 验收

  • calib 覆盖部署 slice;缺场景故意 ablation 看掉点幅度。
  • 量化前后 val metric diff 在业务阈值内(如 mAP 掉 <1pt aggregate,关键 slice 单独报)。
  • 目标硬件上测 latency 和 throughput,确认 INT8 真有加速。
  • 对比 per-channel vs per-tensor;敏感层 skip 前后 acc 变化。
  • ONNX/ORT 导出后抽样 batch 对齐 fp32 与 INT8 输出,排除 convert bug。

INT8 PTQ 的第一道闸是 calib 500 batch 覆盖部署场景。改 calib 或 skip list 时单变量 ablation,才分得清是 scale 问题还是层敏感问题。

← 全部文章

johan's blog