4. 权重量化与激活量化:量化策略与实战经验

大家好,我是你们的讲师。今天我们来聊聊量化里最核心的两个角色——权重和激活。很多人觉得量化就是把 float32 转成 int8,其实没那么简单。权重怎么量化?激活为什么更难搞?动态和静态又是什么?这些坑,我一个个给你讲透。

4.1 权重量化策略:从对称到非对称

先说权重。权重量化,说白了就是给模型参数找个合适的“刻度尺”。我个人习惯把策略分成三类:对称量化、非对称量化,还有逐通道量化。

4.1.1 对称量化

对称量化,就是让零点固定在 0。公式很简单:

scale = max(|W|) / 127
q = round(W / scale)

为什么用 127?因为 int8 的范围是 [-128, 127],对称量化只用到 [-127, 127],留一个 -128 给特殊情况。我在项目中遇到过,对称量化对权重分布比较对称的层效果很好,比如卷积层。但遇到某些全连接层,权重分布偏了,效果就差点。

4.1.2 非对称量化

非对称量化引入了零点(zero point)。公式变成:

scale = (max(W) - min(W)) / 255
zero_point = round(-min(W) / scale)
q = round(W / scale + zero_point)

你想想看,非对称量化能覆盖整个 [0, 255] 范围,对偏态分布更友好。嗯,这里要注意:非对称量化计算量更大,因为每次都要处理零点偏移。

4.1.3 逐通道量化

这是我最常用的策略。每个输出通道单独算 scale 和 zero_point。为什么?因为不同通道的权重分布可能差很多。我曾经在一个 MobileNet 模型上试过,逐层量化掉点 2%,换成逐通道量化只掉 0.5%。差距很明显。

重要经验: 逐通道量化虽然增加了存储开销(每个通道多存一个 scale),但精度损失最小。在 TensorRT 中,这是默认的权重量化方式。

4.2 激活量化挑战:分布不均与异常值

激活量化比权重难多了。为什么?因为激活值分布变化大,而且经常有异常值。

4.2.1 分布不均的问题

激活值通常不是均匀分布的。拿 ReLU 来说,输出全是非负数,而且大部分值集中在 0 附近。如果直接用 min-max 量化,很多量化区间都浪费了。

我记得有一次量化一个 Transformer 模型,激活值分布像长尾一样。大部分值在 0 到 1 之间,但偶尔冒出个 100 多的异常值。直接量化的话,小值全被压缩到 0 附近,精度直接崩了。

4.2.2 异常值处理

怎么处理异常值?我建议用百分位法。比如取 99.9% 分位点作为最大值,而不是用全局最大值。这样能避免被几个异常值带偏。

# 伪代码示例
import numpy as np

def percentile_quantize(activations, percentile=99.9):
    max_val = np.percentile(activations, percentile)
    min_val = np.min(activations)
    scale = (max_val - min_val) / 255
    zero_point = round(-min_val / scale)
    return scale, zero_point

小技巧: 我习惯在 calibration 阶段收集激活值的直方图,然后手动调整百分位。一般 99.9% 到 99.99% 之间效果最好。

4.3 动态量化与静态量化

这两个概念很多人搞混。我简单说:动态量化是运行时算 scale,静态量化是提前算好。

4.3.1 动态量化

动态量化,每次推理都根据当前输入计算 scale。好处是精度高,因为 scale 是“量身定制”的。坏处是慢,因为多了计算 scale 的开销。

我一般在 CPU 上做动态量化。PyTorch 里直接调用 torch.quantization.quantize_dynamic 就行。适合那些对延迟不敏感的场景。

4.3.2 静态量化

静态量化,用 calibration 数据集提前算好 scale。推理时直接用,没有额外开销。这是 TensorRT 的默认方式。

静态量化的关键在 calibration 数据集。数据集要能代表真实分布。我曾经用 ImageNet 的 500 张图做 calibration,效果就很好。但有人只用 10 张图,结果量化后精度掉了 5%。

对比项 动态量化 静态量化
计算时机 推理时 推理前
精度 较高 略低(依赖 calibration)
延迟 较高
适用场景 CPU、小模型 GPU、大模型、生产环境

避坑指南: 我曾经在生产环境里用了动态量化,结果延迟翻了一倍。后来换成静态量化,延迟降下来了,但精度掉了 0.3%。嗯,这就是 trade-off。

4.4 混合精度量化

混合精度量化,就是不同层用不同精度。比如敏感层用 float16,不敏感层用 int8。这招很实用。

4.4.1 哪些层该用高精度?

我总结了几类敏感层:

  • 第一层和最后一层(输入输出层)
  • 注意力机制中的 QKV 投影层
  • 残差连接中的加法层
  • 激活值范围特别大的层

举个例子,我在量化 BERT 模型时,把 embedding 层和最后的分类层保留 float16,中间层全用 int8。最终精度只掉了 0.1%,但模型大小减少了 60%。

4.4.2 如何实现混合精度?

在 TensorRT 里,可以通过设置每层的精度来实现:

# TensorRT 混合精度配置示例
import tensorrt as trt

builder = trt.Builder(network)
config = builder.create_builder_config()
config.set_flag(trt.BuilderFlag.INT8)
config.set_flag(trt.BuilderFlag.FP16)

# 手动指定某些层用 FP16
for layer in network:
    if layer.name in ["embedding", "classifier"]:
        layer.precision = trt.float16
    else:
        layer.precision = trt.int8

我的建议: 先用逐层精度分析工具跑一遍,看看每层对最终精度的贡献。然后从最不敏感的层开始量化,逐步推进。这样能最大化压缩比。

4.5 总结与实战建议

好了,我们来捋一捋今天的内容:

  • 权重量化:对称量化简单,非对称量化灵活,逐通道量化最实用
  • 激活量化:注意分布不均和异常值,用百分位法处理
  • 动态 vs 静态:动态精度高但慢,静态快但依赖 calibration
  • 混合精度:敏感层用高精度,不敏感层用低精度

最后说一句,量化没有银弹。每个模型、每个任务都要单独调。我建议你从逐通道量化 + 静态量化开始,然后根据精度损失情况,逐步引入混合精度。这样最稳妥。

下一章我们讲 TensorRT 的 INT8 校准器,到时候会深入 calibration 的细节。记得提前跑一下今天给的代码示例。