精度损失诊断工具:量化前后问题定位三板斧

量化做完,模型跑起来,发现精度掉了。这时候别急着调参,先搞清楚问题出在哪。我见过太多人一上来就调量化参数,调了半天发现是某个算子炸了——方向错了,白费功夫。

这一章,咱们就聊聊怎么用工具给量化模型做「体检」。说白了,就是三件事:看算子精度、看激活值分布、找敏感层。这三板斧用好了,问题在哪基本就清楚了。

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
我的经验:别只看 MSE。有时候 MSE 很小,但分布已经偏了。我建议同时算一下 KL 散度,两个指标一起看,更靠谱。

跑完这段代码,你会得到一个字典,里面是每个算子的误差值。排序一下,误差最大的那几个算子,就是重点怀疑对象。

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()
注意:直方图的 bin 数量别设太少。我试过 bin=20,结果分布细节全丢了,根本看不出问题。建议至少 100 个 bin。

看直方图时,重点关注三点:

  • 分布偏移:量化后的分布整体左移或右移,说明 scale 或 zero_point 没算对
  • 分布截断:两边被切掉一块,说明 clip 范围设窄了
  • 分布变形:形状变了,可能是量化噪声太大

我记得有一次,一个卷积层的激活值分布量化后整体右移了 0.3。查了半天,发现是 calibration 数据没选好,导致 scale 偏大。换了一组数据,问题就解决了。

3. 定位敏感层(Layer-wise Sensitivity Analysis)

这一步是关键。不是所有层对量化都敏感。有些层量化后精度几乎不变,有些层一量就崩。我们的目标就是找出那些「一碰就碎」的层。

怎么做?逐层量化,逐层看精度变化。说白了就是:

  1. 先把模型全部量化
  2. 然后逐层回退到 FP32
  3. 看每回退一层,精度提升多少

提升越大的层,说明它对量化越敏感。

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 倍。

知识结构总览

下面这张图,把整个诊断流程串起来了:

精度损失诊断流程 量化后模型 算子精度对比 激活值分布对比 逐层敏感度分析 MSE / KL 散度 分布偏移 / 截断 敏感度排序 定位问题层 → 制定修复策略

三种工具对比

诊断方法 核心工具 输出结果 适用场景
算子精度对比 torch.profiler + 钩子 每层 MSE / KL 散度 快速定位误差大的算子
激活值分布对比 matplotlib 直方图 分布偏移 / 截断 / 变形 分析量化参数是否合理
逐层敏感度分析 逐层回退 + 精度评估 敏感度排序 确定哪些层需要特殊处理
避坑指南:我曾经犯过一个错——只看了算子精度对比,没看分布。结果发现一个卷积层 MSE 很小,但分布已经严重偏移。后来加上直方图,才真正定位到问题。所以这三板斧,建议一起用,别偷懒。