精度损失诊断工具:量化前后问题定位三板斧
量化做完,模型跑起来,发现精度掉了。这时候别急着调参,先搞清楚问题出在哪。我见过太多人一上来就调量化参数,调了半天发现是某个算子炸了——方向错了,白费功夫。
这一章,咱们就聊聊怎么用工具给量化模型做「体检」。说白了,就是三件事:看算子精度、看激活值分布、找敏感层。这三板斧用好了,问题在哪基本就清楚了。
1. 用 torch.profiler 盯算子精度
torch.profiler 这玩意儿,很多人只拿它看性能。其实它也能帮我们对比量化前后每个算子的输出差异。我个人习惯在量化前后各跑一次推理,把每个算子的输出都存下来,然后逐层算误差。
具体怎么做?看代码:
import torch
import torch.profiler
def compare_operator_accuracy(fp32_model, int8_model, sample_input):
# 注册钩子,收集每层输出
fp32_outputs = {}
int8_outputs = {}
def hook_fn(name, storage):
def hook(module, input, output):
storage[name] = output.detach().cpu()
return hook
# 给 fp32 模型注册钩子
for name, module in fp32_model.named_modules():
module.register_forward_hook(hook_fn(name, fp32_outputs))
# 给 int8 模型注册钩子
for name, module in int8_model.named_modules():
module.register_forward_hook(hook_fn(name, int8_outputs))
# 分别跑推理
with torch.no_grad():
_ = fp32_model(sample_input)
_ = int8_model(sample_input)
# 逐层算误差
errors = {}
for name in fp32_outputs:
if name in int8_outputs:
mse = torch.mean((fp32_outputs[name] - int8_outputs[name]) ** 2).item()
errors[name] = mse
return errors
跑完这段代码,你会得到一个字典,里面是每个算子的误差值。排序一下,误差最大的那几个算子,就是重点怀疑对象。
2. 量化前后激活值分布对比(直方图)
光看误差数值还不够。你想想看,量化本质上是把浮点数映射到整数空间。如果激活值的分布变了,那精度损失几乎是必然的。
我习惯把量化前后每层的激活值画成直方图,叠在一起看。这样一眼就能看出分布有没有偏移、有没有截断。
import matplotlib.pyplot as plt
import numpy as np
def plot_activation_distribution(fp32_acts, int8_acts, layer_name):
plt.figure(figsize=(10, 6))
# 展平成一维
fp32_flat = fp32_acts.flatten()
int8_flat = int8_acts.flatten()
# 画直方图
plt.hist(fp32_flat, bins=100, alpha=0.5, label='FP32', density=True)
plt.hist(int8_flat, bins=100, alpha=0.5, label='INT8', density=True)
plt.title(f'Activation Distribution - {layer_name}')
plt.xlabel('Value')
plt.ylabel('Density')
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()
看直方图时,重点关注三点:
- 分布偏移:量化后的分布整体左移或右移,说明 scale 或 zero_point 没算对
- 分布截断:两边被切掉一块,说明 clip 范围设窄了
- 分布变形:形状变了,可能是量化噪声太大
我记得有一次,一个卷积层的激活值分布量化后整体右移了 0.3。查了半天,发现是 calibration 数据没选好,导致 scale 偏大。换了一组数据,问题就解决了。
3. 定位敏感层(Layer-wise Sensitivity Analysis)
这一步是关键。不是所有层对量化都敏感。有些层量化后精度几乎不变,有些层一量就崩。我们的目标就是找出那些「一碰就碎」的层。
怎么做?逐层量化,逐层看精度变化。说白了就是:
- 先把模型全部量化
- 然后逐层回退到 FP32
- 看每回退一层,精度提升多少
提升越大的层,说明它对量化越敏感。
def layerwise_sensitivity_analysis(model, quantized_model, val_loader):
sensitivity = {}
# 先跑全量化模型的精度
full_int8_acc = evaluate(quantized_model, val_loader)
# 逐层回退
for name, module in quantized_model.named_modules():
if hasattr(module, 'weight_fake_quant'):
# 保存量化状态
orig_state = module.weight_fake_quant.state_dict()
# 回退到 FP32
module.weight_fake_quant.disable()
# 评估精度
acc = evaluate(quantized_model, val_loader)
# 计算敏感度
sensitivity[name] = acc - full_int8_acc
# 恢复量化状态
module.weight_fake_quant.enable()
return sensitivity
核心结论:敏感度越高的层,越值得用更高精度(如 FP16)或更精细的量化策略(如 per-channel)。
我做过一个项目,模型有 50 多层。逐层分析后发现,只有 3 层是敏感层。把这 3 层单独用 FP16 跑,其他层用 INT8,最终精度只掉了 0.1%,推理速度却快了 3 倍。
知识结构总览
下面这张图,把整个诊断流程串起来了:
三种工具对比
| 诊断方法 | 核心工具 | 输出结果 | 适用场景 |
|---|---|---|---|
| 算子精度对比 | torch.profiler + 钩子 | 每层 MSE / KL 散度 | 快速定位误差大的算子 |
| 激活值分布对比 | matplotlib 直方图 | 分布偏移 / 截断 / 变形 | 分析量化参数是否合理 |
| 逐层敏感度分析 | 逐层回退 + 精度评估 | 敏感度排序 | 确定哪些层需要特殊处理 |