4、激活量化:动态量化与静态量化的实现与权衡

聊完权重量化,咱们得聊聊另一个大头——激活量化

权重是模型学到的参数,训练完就固定了。但激活值不一样,它是每层计算出来的中间结果。你想想看,输入一张猫的图片和输入一张狗的图片,中间层的激活值分布能一样吗?

这就带来了一个核心问题:激活值的范围是动态变化的

所以,激活量化比权重量化要棘手得多。我刚开始做量化的时候,在权重量化上顺风顺水,一到激活量化就翻车。模型直接输出乱码,跟中了邪似的。

目前主流的做法分两种:动态量化静态量化。咱们一个一个拆开看。

4.1 动态量化:实时计算,灵活但慢

动态量化,说白了就是每次推理时,实时统计当前 batch 的激活值范围,然后当场做量化。

它的流程是这样的:

  1. 拿到当前层的激活值(比如一个 4D 张量)
  2. 统计它的 min 和 max(或者用其他校准方法)
  3. 根据这个范围计算 scale 和 zero_point
  4. 用这个 scale 做量化计算

代码实现其实不复杂:

import torch

def dynamic_quantize_activation(x, num_bits=8):
    # x: 输入激活值张量
    min_val = x.min()
    max_val = x.max()
    
    # 计算 scale 和 zero_point
    qmin = 0
    qmax = 2 ** num_bits - 1
    scale = (max_val - min_val) / (qmax - qmin)
    zero_point = qmin - min_val / scale
    zero_point = torch.round(zero_point).clamp(qmin, qmax)
    
    # 量化
    x_q = torch.round(x / scale + zero_point).clamp(qmin, qmax)
    
    # 反量化(模拟量化效果)
    x_deq = (x_q - zero_point) * scale
    
    return x_deq, scale, zero_point

嗯,这里要注意:动态量化每次都要做 min/max 统计。这个操作本身是有开销的。我在项目中遇到过,对于小模型(比如 MobileNet),动态量化的额外开销甚至能抵消掉量化带来的加速收益。

适用场景:

  • 模型结构不稳定,经常改
  • 输入分布变化剧烈(比如 NLP 任务中不同长度的句子)
  • 对精度要求极高,不能接受校准误差
  • 推理框架支持动态量化(PyTorch 原生支持较好)

4.2 静态量化:离线校准,快但需要调

静态量化就不一样了。它提前用一批校准数据跑一遍模型,统计出每层激活值的典型范围。然后把这个范围固定下来,推理时直接用。

我个人习惯用 500-1000 张图片做校准。太少统计不准,太多浪费时间。

校准过程的核心是收集激活值的直方图:

import torch
import numpy as np

class CalibrationCollector:
    def __init__(self, num_bins=2048):
        self.histograms = {}  # 每层一个直方图
        self.num_bins = num_bins
        
    def collect(self, layer_name, activation):
        # 将激活值展平并统计直方图
        act_flat = activation.detach().cpu().numpy().flatten()
        hist, edges = np.histogram(act_flat, bins=self.num_bins)
        
        if layer_name not in self.histograms:
            self.histograms[layer_name] = {'hist': hist, 'edges': edges}
        else:
            # 合并多个 batch 的直方图
            self.histograms[layer_name]['hist'] += hist

    def get_scale(self, layer_name, quantile=0.999):
        # 根据分位数确定量化范围
        hist = self.histograms[layer_name]['hist']
        edges = self.histograms[layer_name]['edges']
        
        total = hist.sum()
        cumsum = np.cumsum(hist)
        threshold_idx = np.searchsorted(cumsum, total * quantile)
        
        return edges[threshold_idx]  # 返回 max_val

这里有个坑,我曾经踩过:校准数据必须能代表真实推理时的数据分布。有一次我用 ImageNet 的验证集做校准,结果部署时用户上传的都是自拍照片,激活值分布完全不一样,模型精度直接掉了 5 个点。

避坑指南:

我曾经犯过一个低级错误——校准数据里全是白天拍的图片,结果模型在夜间场景下直接崩了。后来我学乖了,校准数据集至少要覆盖 80% 以上的真实场景。

4.3 动态 vs 静态:一张表说清楚

对比维度 动态量化 静态量化
校准方式 实时统计 离线校准
推理速度 较慢(有统计开销) 快(无额外开销)
精度损失 小(自适应) 略大(固定范围)
部署复杂度 低(无需校准数据) 高(需要校准流程)
硬件适配 CPU 友好 GPU/NPU 友好
典型框架 PyTorch 动态量化 TensorRT、ONNX Runtime

4.4 知识体系:激活量化的核心逻辑

为了让你更直观地理解,我画了一张图:

激活量化核心逻辑 输入激活值 量化方式? 动态量化 实时统计 min/max 每次推理都计算 静态量化 离线校准数据 固定 scale/zero_point 量化后的激活值 权衡点:精度 vs 速度 vs 部署复杂度

4.5 实际项目中的选择建议

说了这么多,到底该选哪个?我根据经验给几个建议:

  • CPU 部署 + 追求精度:选动态量化。PyTorch 的 torch.quantization.quantize_dynamic 一行代码搞定,省心。
  • GPU/NPU 部署 + 追求速度:选静态量化。TensorRT 的 INT8 校准流程虽然麻烦,但推理速度能翻倍。
  • 模型经常更新:选动态量化。每次更新都要重新校准,太折腾了。
  • 输入分布极其稳定:选静态量化。比如人脸识别,输入都是对齐后的人脸,分布很固定。

我的小技巧:

如果你不确定选哪个,可以先用动态量化跑一遍,记录下每层激活值的 min/max 范围。然后把这些范围作为静态量化的初始值。这样既保证了精度,又享受了静态量化的速度。我在一个 OCR 项目里就是这么干的,效果出奇的好。

最后说一句,没有银弹。动态量化和静态量化各有各的适用场景。关键是要理解你的模型、你的数据、你的部署环境。多试几次,你就能找到最适合的方案。


专注资料整理