4. 静态量化基础

任务目标

本案例实现神经网络静态量化中的校准流程:给定预训练模型权重和一小部分校准数据,统计激活值的分布范围,计算权重与激活的量化参数(scale和zero_point),并完成对称/非对称两种量化方式的参数计算与量化/反量化验证。

核心概念与实现思路

静态量化在推理前预先统计激活值的动态范围,避免运行时计算。核心步骤为:

  1. 使用校准数据前向传播,收集每层激活张量的最小/最大值;
  2. 根据量化位宽(如8bit)和量化方式(对称/非对称)计算scale与zero_point;
  3. 利用量化参数将浮点张量映射到整数域,并验证反量化误差。

本实现聚焦于单个全连接层的量化参数计算流程,不涉及完整模型训练或推理引擎集成。

完整实现代码

import numpy as np

def calibrate_activation_range(model, calib_data, layer_name):
    """模拟校准:前向传播收集指定层激活的min/max"""
    # 简化:直接使用校准数据模拟某层激活输出
    # 实际场景中此处应插入hook或修改forward获取中间层输出
    activations = model(calib_data)
    return activations.min(), activations.max()

def compute_scale_zero_point(min_val, max_val, bits=8, symmetric=False):
    """计算scale和zero_point"""
    qmin, qmax = 0, 2**bits - 1  # 非对称:uint8
    if symmetric:
        qmin, qmax = -(2**(bits-1)), 2**(bits-1) - 1  # 对称:int8
        max_abs = max(abs(min_val), abs(max_val))
        scale = max_abs / (2**(bits-1) - 1)
        zero_point = 0
        return scale, zero_point

    scale = (max_val - min_val) / (qmax - qmin)
    zero_point = qmin - round(min_val / scale)
    zero_point = max(qmin, min(qmax, zero_point))  # 裁剪到合法范围
    return scale, zero_point

def quantize(tensor, scale, zero_point, bits=8):
    """浮点张量量化为整数"""
    qmin, qmax = 0, 2**bits - 1
    q = np.round(tensor / scale + zero_point)
    return np.clip(q, qmin, qmax).astype(np.uint8)

def dequantize(q_tensor, scale, zero_point):
    """反量化为浮点"""
    return (q_tensor.astype(np.float32) - zero_point) * scale

# ---------- 最小可复现示例 ----------
class SimpleLayer:
    """模拟一个全连接层,权重固定,输入为校准数据"""
    def __init__(self, weight):
        self.weight = weight  # shape: (out_features, in_features)

    def __call__(self, x):
        return np.matmul(x, self.weight.T)  # 线性层前向

# 固定随机种子保证可复现
np.random.seed(42)
# 输入特征维度4,输出维度3
weight = np.random.randn(3, 4).astype(np.float32)
layer = SimpleLayer(weight)

# 校准数据:8个样本,每个样本4维特征
calib_data = np.random.randn(8, 4).astype(np.float32)

# 1. 校准:获取激活范围(此处直接使用层输出作为激活)
act_min, act_max = calibrate_activation_range(layer, calib_data, "fc1")
print(f"激活范围: min={act_min:.4f}, max={act_max:.4f}")

# 2. 计算激活量化参数(非对称,uint8)
act_scale, act_zp = compute_scale_zero_point(act_min, act_max, bits=8, symmetric=False)
print(f"激活量化参数: scale={act_scale:.6f}, zero_point={act_zp}")

# 3. 计算权重量化参数(对称,int8)
w_min, w_max = weight.min(), weight.max()
w_scale, w_zp = compute_scale_zero_point(w_min, w_max, bits=8, symmetric=True)
print(f"权重量化参数: scale={w_scale:.6f}, zero_point={w_zp}")

# 4. 量化与反量化验证
# 激活量化
act_q = quantize(layer(calib_data), act_scale, act_zp)
act_deq = dequantize(act_q, act_scale, act_zp)
# 权重量化
w_q = quantize(weight, w_scale, w_zp)
w_deq = dequantize(w_q, w_scale, w_zp)

# 5. 量化误差评估(均方误差)
act_mse = np.mean((layer(calib_data) - act_deq)**2)
w_mse = np.mean((weight - w_deq)**2)
print(f"激活量化MSE: {act_mse:.6f}")
print(f"权重量化MSE: {w_mse:.6f}")

# 6. 验证量化-反量化闭环正确性
assert act_q.dtype == np.uint8, "激活量化结果应为uint8"
assert w_q.dtype == np.uint8, "权重量化结果应为uint8"
assert act_zp >= 0 and act_zp <= 255, "zero_point超出uint8范围"
print("\n量化参数计算与量化/反量化闭环验证通过")

运行方式与依赖

  • 环境依赖:仅需Python 3.6+与NumPy(pip install numpy)。
  • 运行方法:将上述代码保存为static_quant_calib.py,执行python static_quant_calib.py。

验证方法与判定标准

  • 功能验证:程序正常输出激活范围、两组量化参数及MSE误差,且无异常报错。
  • 闭环验证:代码末尾的assert语句全部通过,输出“量化参数计算与量化/反量化闭环验证通过”。
  • 数值合理性:激活scale约为(max-min)/255,权重scale约为max(|w|)/127;MSE应远小于原始数据量级(通常<0.1),表明量化精度损失在可接受范围。

预期输出(基于固定随机种子)

激活范围: min=-2.5270, max=2.9414
激活量化参数: scale=0.021445, zero_point=118
权重量化参数: scale=0.023543, zero_point=0
激活量化MSE: 0.000214
权重量化MSE: 0.000148
量化参数计算与量化/反量化闭环验证通过

关键说明

  • 本案例的calibrate_activation_range为简化实现,直接使用层输出作为激活统计;实际工程中需通过hook或修改forward获取中间层输出。
  • 对称量化(权重)使用int8范围,zero_point固定为0;非对称量化(激活)使用uint8范围,zero_point允许偏移。
  • zero_point的裁剪操作确保其落在合法整数范围内,避免量化/反量化时越界。