4. 训练后量化(PTQ):从原理到实战调优

训练后量化,圈里人常叫 PTQ。说白了,就是模型训练完了,我们直接拿它做量化,不再动训练过程。这招在端侧部署里特别实用——你想想看,很多场景下我们只有训练好的权重,没有原始训练环境,PTQ 就成了唯一选择。

我个人习惯把 PTQ 看作「模型瘦身手术」。手术做得好,模型跑得快还几乎不掉精度;做得不好,那掉点起来真是让人头疼。这一章,我把 PTQ 的完整流程、校准数据怎么选、误差怎么分析调优,全部分享给你。

4.1 PTQ 标准流程

PTQ 的流程其实不复杂,但每一步都有坑。我画了个流程图,你先看个全貌:

PTQ 标准流程 加载预训练模型 准备校准数据集 插入量化/反量化节点(Observer) 前向推理收集统计量 计算量化参数(Scale/ZeroPoint) 转换并导出量化模型 关键点 校准数据需覆盖真实分布 Observer 位置决定精度 统计量收集要足够充分

流程看着简单,但每个环节都有讲究。我一个个拆开说。

4.2 校准数据集的选择

校准数据集,是 PTQ 里最容易被低估的一环。很多人随便拿几百张图就跑,结果量化后模型在某些场景下崩得一塌糊涂。

校准集的核心要求:要能代表真实推理时的数据分布。不是越多越好,而是越「像」越好。

校准集类型 适用场景 推荐数量 注意事项
训练集子集 通用场景,分布稳定 500~2000 张 需随机采样,避免类别偏差
验证集 精度验证,分布已知 全部或随机子集 注意不要过拟合到验证集
真实场景采集 特定部署场景(如夜间、雨天) 200~500 张 覆盖极端情况,如过曝、欠曝
合成数据 隐私受限或数据获取困难 1000+ 张 需验证分布一致性
我的经验:做 AI ISP 量化时,我习惯从训练集里抽 1000 张,再从真实场景补 200 张极端光照的图。这样既保证了分布覆盖,又不会让校准集太大拖慢流程。

校准集数量怎么定?我见过有人用 100 张图就跑通了,也见过用 5000 张反而效果更差的。关键在于:校准集要能「激活」模型的所有量化敏感路径。太少,统计量不稳定;太多,边际收益递减,还浪费时间。

一般经验值:分类任务 500~1000 张,检测任务 1000~2000 张,超分或 ISP 任务 500~1000 张就够了。

4.3 量化误差分析

量化误差,说白了就是「信息丢失」。FP32 的精度被压缩到 INT8,那丢失的信息去哪了?我们得找到它。

误差主要来自三个地方:

  1. 截断误差:超出量化范围的值被硬截断。比如某个激活值跑到 200,但 INT8 最大只能表示 127,那 73 就丢了。
  2. 舍入误差:浮点数映射到离散整数时的精度损失。这个一般影响小,但累积起来也够呛。
  3. 跨层误差传播:前一层的误差被后一层放大。这是最隐蔽的,也是调优的重点。

怎么分析?我常用的方法是 逐层对比:

# 伪代码:逐层余弦相似度对比
for layer in model.layers:
    fp32_output = forward_fp32(layer, input)
    int8_output = forward_int8(layer, input)
    cos_sim = cosine_similarity(fp32_output, int8_output)
    if cos_sim < 0.99:
        print(f"警告:{layer.name} 相似度 {cos_sim:.4f},需要关注")
关键指标:余弦相似度低于 0.99 的层,基本就是量化敏感层。低于 0.95 的,必须调优。

我记得有一次做 ISP 去噪模型的量化,发现前面几层卷积的相似度都在 0.98 以上,但到了中间一个残差块,突然掉到 0.89。查了半天,原来是那个层的激活值分布特别宽,最大值跑到 300 多,INT8 根本装不下。这就是典型的截断误差问题。

4.4 量化调优实战

找到问题层之后,怎么调?我总结了几个实战技巧:

4.4.1 调整校准策略

不同的校准策略,对极端值的处理方式不同:

校准策略 原理 适用场景 精度表现
MinMax 取实际最小/最大值 分布均匀,无极端值 中等,易受 outlier 影响
Percentile 取指定百分位(如 99.9%) 存在少量极端值 较好,鲁棒性高
KL 散度 最小化量化前后分布差异 分布不对称,有长尾 优秀,但计算稍慢
MSE 最小化均方误差 对精度要求极高 优秀,需调参
避坑:我曾经在一个 ISP 项目里用 MinMax 校准,结果因为几张过曝的校准图,导致整个模型的量化参数被拉偏,精度掉了 3 个点。后来换成 Percentile(99.9%),问题就解决了。校准集里的 outlier 真的很要命。

4.4.2 混合精度量化

不是所有层都适合 INT8。有些层对精度特别敏感,强行量化反而得不偿失。这时候可以「网开一面」:

  • 敏感层保留 FP16 或 FP32:比如第一层卷积、最后的全连接层、注意力机制中的 softmax。
  • 逐层指定量化位宽:有些框架支持 INT8 + INT4 混合,进一步压缩。
  • 按通道量化:每个输出通道独立算 scale,精度比逐层量化高不少。
# 示例:在 TensorRT 中指定混合精度
import tensorrt as trt

# 创建配置
config = builder.create_builder_config()
config.set_memory_pool_limit(trt.MemoryPoolType.WORKSPACE, 1 << 30)

# 设置精度约束
config.set_flag(trt.BuilderFlag.INT8)
config.set_flag(trt.BuilderFlag.FP16)  # 允许回退到 FP16

# 逐层精度控制(通过 inspector 分析后手动指定)
precision_constraints = {
    "conv_1": trt.DataType.FLOAT,      # 第一层保留 FP32
    "attention_softmax": trt.DataType.FLOAT,  # softmax 用 FP32
    "res_block_3": trt.DataType.HALF,  # 敏感残差块用 FP16
}
# 实际通过 per-layer precision override API 设置

4.4.3 量化感知训练(QAT)兜底

如果 PTQ 调了半天,精度还是差 1~2 个点,那就别硬撑了。这时候该上 QAT 了。但 QAT 不是本章重点,我只说一句:PTQ 调优的终点,就是 QAT 的起点。先用 PTQ 找到问题层,再用 QAT 微调那几个层,效率最高。

4.5 实战案例:ISP 去噪模型 PTQ 调优

拿我之前做的一个 ISP 去噪模型举例。模型不大,8 层卷积加 2 个残差块,FP32 的 PSNR 是 38.5dB。

第一轮 PTQ:直接用 MinMax 校准,500 张训练集子集。结果 PSNR 掉到 36.2dB,掉了 2.3 个点。逐层分析发现,第 5 层和第 7 层卷积的余弦相似度只有 0.93 和 0.91。

第二轮调优:换成 Percentile(99.9%) 校准,校准集加了 200 张真实夜间场景。PSNR 回升到 37.8dB,还差 0.7dB。

第三轮:把第 5 层和第 7 层改成 FP16 混合精度。PSNR 到了 38.3dB,只差 0.2dB。这个精度损失在端侧完全可以接受。

最终结论:PTQ 调优不是一蹴而就的。校准策略 + 校准集 + 混合精度,三板斧下来,大部分模型都能做到 0.5dB 以内的精度损失。

4.6 本章小结

PTQ 的核心就三件事:选对校准集、找对问题层、用对调优策略。校准集要覆盖真实分布,别偷懒;误差分析要逐层做,别只看最终精度;调优时先改校准策略,再考虑混合精度,最后才上 QAT。

嗯,这一章的内容就到这里。PTQ 是端侧部署的入门功夫,但入门功夫练好了,后面 QAT 和量化算子优化才能走得顺。你自己动手跑一遍流程,踩几个坑,就全明白了。