第 4 章 校准与后训练量化(PTQ)
第 4 章 校准与后训练量化(PTQ)
后训练量化(Post-Training Quantization,PTQ)是成本最低的量化方式:模型已经训练好了,我们不碰训练过程,只做一次"校准",然后把权重和激活换成低精度表示。第 2、3 章解决的是"怎么量化",这一章解决的是"范围从哪来"——尤其是激活的范围,因为激活没有现成的数值可查,必须从数据里统计出来。
原书仓库 ch4/ 目录的配套脚本非常齐全:range_estimation_demo.py、calibration_stability.py、llm_calibration_builder.py、ptq_bitwidth_analysis.py、ch4_equalization_multi_arch.py、validate_calibration.py。
4.1 范围估计的三种基本方法
range_estimation_demo.py 用 BERT 和 ResNet 的激活做了三种范围估计方法的对照:AbsMax、Percentile、MSE-Optimal。这三个方法构成了所有校准算法的地基。
4.1.1 AbsMax:最简单,但怕异常值
# 来自 ch4/range_estimation_demo.py(整理)
class AbsMaxObserver:
"""最简单的范围估计:使用绝对最大值。"""
def __init__(self):
self.max_val = 0.0
def observe(self, tensor: torch.Tensor):
self.max_val = max(self.max_val, tensor.abs().max().item())
def compute_range(self) -> float:
return self.max_valAbsMax 把所有校准样本里看到的绝对值最大值当作范围。它实现为零成本,但缺陷在第 2.4 节就预告过:一个异常值就能把范围撑爆,让 99% 的正常值挤进一两格。
4.1.2 Percentile:牺牲极端值,保住主体
# 来自 ch4/range_estimation_demo.py(整理)
class PercentileObserver:
"""用百分位裁剪范围。"""
def __init__(self, percentile: float = 99.99):
self.percentile = percentile
self.values = []
def observe(self, tensor: torch.Tensor):
self.values.append(tensor.abs().flatten().cpu())
def compute_range(self) -> float:
all_vals = torch.cat(self.values)
return torch.quantile(all_vals, self.percentile / 100).item()Percentile 主动放弃最极端的 0.01%(或你设定的比例),换取主体数据的精度。它的代价是:被截掉的那部分数据会产生 overload error(过载误差)。第 2.4 节的框架在这里派上用场——Percentile 是在 granular error 和 overload error 之间做一次显式的交换。
4.1.3 MSE-Optimal:直接最小化重建误差
# 来自 ch4/range_estimation_demo.py(核心逻辑整理)
class MSEOptimalObserver:
"""最小化量化 MSE 的范围估计。"""
def observe(self, tensor):
# 把绝对值数据累积成直方图
hist = torch.histc(tensor.abs().flatten(), bins=2048, min=0, max=max_val)
# 记录当前最大范围
self.max_val = max(self.max_val, tensor.abs().max().item())
def compute_range(self) -> float:
# 在 [0.5*max, max] 之间枚举候选范围
candidates = torch.linspace(0.5 * self.max_val, self.max_val, 200)
best_mse, best_range = float('inf'), self.max_val
for r in candidates:
scale = r / 127
clipped = torch.clamp(bin_centers, 0, r)
quantized = torch.round(clipped / scale) * scale
mse = ((squared_error * self.histogram).sum() / self.histogram.sum())
if mse < best_mse:
best_mse, best_range = mse, r
return best_rangeMSE-Optimal 的思路更聪明:与其拍脑袋定范围,不如枚举一堆候选范围,对每个范围模拟一遍量化-反量化,选重建误差最小的那个。它以 max_val 的 50%~100% 为搜索空间,用直方图加权的方式逼近真实分布的重建误差。这是 TensorRT 熵校准(KL-divergence) 和许多现代校准器的近亲——核心都是"用真实数据分布,找重建误差最小的截断点"。
4.1.4 三种方法的对照结论
range_estimation_demo.py 的 run_calibration_comparison 输出一张表,对比每种方法的 clipped_pct(被裁剪比例)、mse、snr_db。典型结论:
- 对 BERT 这种含异常值的激活:AbsMax 的 MSE 最差(范围被异常值撑爆),Percentile 明显更好,MSE-Optimal 通常最优。
- 对 ResNet 这种相对干净的分布:三者差距缩小,AbsMax 也堪用。
snr_db(信噪比)随 MSE 降低而提高,是衡量量化质量的直观指标。
4.2 校准集的构建:不只是"随便喂几批数据"
范围估计的前提是有数据。llm_calibration_builder.py 研究了一个容易被忽视的问题:校准集怎么构建才算合格。它把这个问题拆成几个维度:
# 来自 ch4/llm_calibration_builder.py 的 LLMCalibrationBuilder(结构)
class LLMCalibrationBuilder:
def __init__(self, model_id, ...):
# 加载模型、tokenizer、采样器
...
def build(self):
# 1. 领域覆盖:文本要覆盖目标任务所在的领域
# 2. 长度覆盖:短文本和长文本都要有(影响 KV cache 与注意力分布)
# 3. 内容多样性:避免全部是同一主题
# 4. 数量:样本太少统计不稳,太多浪费时间
...关键结论:
- 领域要匹配:用代码语料校准一个做代码补全的模型、用新闻语料校准一个做新闻摘要的模型,激活分布才真实。跨领域校准会导致范围估计偏差。
- 样本量要够但不用太多:
calibration_stability.py专门研究了这个问题——它增量式地喂入 1000 条文本,逐批重算 scale,观察 scale 随样本量的收敛曲线。结论是:scale 会在几百条样本后趋于稳定,再多边际收益递减。这个"稳定点"就是你该用的校准集大小。 - 长度要覆盖:LLM 的激活分布和序列长度强相关,校准集要包含长短不一的样本。
4.3 跨层均衡(Cross-Layer Equalization)
ch4_equalization_multi_arch.py 在四种架构(ResNet、MobileNetV2、BERT、ViT)上做了同一个实验:跨层均衡。
问题背景:conv → relu → conv 这样的结构里,前一层的输出是后一层的输入。如果前一层权重范围很大、后一层范围很小,那么量化误差会被"接力放大"。跨层均衡的思想是:在两层之间重新分配缩放,让两层的范围更接近,整体量化误差更小。
# 来自 ch4/ch4_equalization_multi_arch.py 的均衡核心(结构)
def compute_equalization_scale(w1, w2, ...):
# 找到让 w1 和 w2 范围更均衡的缩放因子 s
# w1' = w1 / s, w2' = w2 * s (数学上等价,但量化误差不同)
...
def apply_equalization(weight, scale, ...):
# 应用均衡缩放
...数学上,w1 * w2 = (w1/s) * (w2*s),输出完全不变;但量化后,Q(w1/s) * Q(w2*s) 的误差和 Q(w1) * Q(w2) 不同。跨层均衡就是找那个让整体量化误差最小的 s。实验结果显示:均衡后,四种架构的量化精度都有提升,其中 MobileNetV2 和 ViT 提升最明显——这两类架构存在明显的通道间范围不平衡。
4.4 位宽选择:8bit 还是 4bit?
ptq_bitwidth_analysis.py 用一个可调位宽的模拟实验,画出了位宽 vs 量化质量的曲线:
# 来自 ch4/ptq_bitwidth_analysis.py(核心)
def simulate_ptq_quality(bits, weight_shape=(1000, 1000), ...):
# 在不同位宽下模拟 PTQ,计算量化后的误差/质量
...典型结论:
- 8bit → 4bit 的误差跳跃:位宽每减 1bit,理论上的量化噪声翻倍。但从实验曲线看,从 8bit 降到 4bit 时,误差的恶化不是线性的——8bit 到 6bit 通常还能接受,6bit 到 4bit 会快速恶化(尤其对激活)。
- 权重 vs 激活的位宽不对称:权重通常比激活更能承受低位宽(权重是静态的,可以用细粒度缩放补偿;激活是动态的,低位宽误差大)。
- 没有免费的 4bit:想用 4bit 权重,要么接受质量下降,要么上 QAT(第 5 章),要么用更聪明的 LLM 量化方法(第 7 章的 GPTQ/AWQ)。
4.5 校准验证:上线前必须做的事
validate_calibration.py 是这一章最"工程化"的脚本——它回答了"我怎么知道校准做得好不好"。核心做法是:
- 校准后跑一遍评估集,对比量化前后模型的输出/指标差距。
- 观察哪些层/哪些算子误差最大,定位是范围估计的问题还是某层本身太敏感。
- 把误差分解到不同位宽、不同粒度,找到性价比最高的配置。
一句话:校准不是"跑完就完",而是"跑完要验证"。量化前后的质量差距如果超出业务容忍线,就要回到前面 4.1-4.4 的方法里去调整范围估计、校准集、位宽或粒度。
4.6 本章小结
PTQ 的完整决策链条:
graph LR
A[校准集构建] --> B[范围估计]
B --> C[粒度选择]
C --> D[位宽选择]
D --> E[跨层均衡]
E --> F[校准验证]
F -->|不达标| A- 范围估计三件套:AbsMax(快但怕异常值)、Percentile(牺牲极端值换主体精度)、MSE-Optimal(枚举候选范围选最优)。
- 校准集四要素:领域匹配、数量收敛(几百条就够)、长度覆盖、内容多样。
- 跨层均衡:重分配相邻层的缩放,白赚量化精度,输出数学等价。
- 位宽权衡:8bit 是安全区,4bit 要付出代价(QAT 或更好的算法)。
- 验证闭环:量化后必须跑评估,不达标就回炉。
下一章,当 PTQ 撑不住的时候,我们进入量化感知训练(QAT)——让模型在训练过程中就学会"忍受"量化误差。
作者: itech001 来源: 公众号:AI人工智能时代(the-ai-era) 网站: https://www.theaiera.top/ 关注每日最新AI新闻和技术博客,主页有更多的文章的AI 技术参考:https://www.theaiera.top
本文首发于 AI人工智能时代,转载请注明出处。