3. 对称量化与非对称量化对比
任务目标
实现对称量化(Symmetric Quantization)与非对称量化(Asymmetric Quantization)两种方案,在相同的小型神经网络模型上对比两者的量化误差(权重/激活的均方误差)与推理性能(INT8算子耗时)。通过本案例,理解两种量化策略在动态范围利用率和计算开销上的本质差异。
核心原理与差异
对称量化将浮点范围映射到以零为中心的INT8区间 [-127, 127],缩放因子 scale = max(|x|) / 127,无需零点偏移,推理时计算量更小;非对称量化利用 [min, max] 完整范围映射到 [0, 255],引入零点 zero_point 以提升对分布偏移数据的精度,但反量化与量化时需额外处理零点。两者在权重分布接近零对称时精度接近,而激活值(如ReLU输出)通常非对称分布,此时非对称量化精度优势明显。
环境依赖
需要 numpy(数值计算)、torch(模型定义与推理)。无需GPU,CPU即可完成对比。
完整实现代码
import numpy as np
import torch
import torch.nn as nn
import time
# ---------- 1. 定义一个小型神经网络(2层全连接 + ReLU) ----------
class SmallNet(nn.Module):
def __init__(self):
super().__init__()
self.fc1 = nn.Linear(64, 32)
self.relu = nn.ReLU()
self.fc2 = nn.Linear(32, 8)
def forward(self, x):
return self.fc2(self.relu(self.fc1(x)))
torch.manual_seed(0)
model = SmallNet()
model.eval()
# 生成固定输入数据(100个样本,每个64维)
inputs = torch.randn(100, 64)
with torch.no_grad():
fp32_outputs = model(inputs).numpy()
# ---------- 2. 量化工具函数 ----------
def quantize_tensor(tensor, symmetric):
"""返回 (quantized_int8, scale, zero_point)"""
np_tensor = tensor.numpy().astype(np.float32)
if symmetric:
scale = np.max(np.abs(np_tensor)) / 127.0
zero_point = 0
q = np.clip(np.round(np_tensor / scale), -127, 127).astype(np.int8)
else:
min_val = np.min(np_tensor)
max_val = np.max(np_tensor)
scale = (max_val - min_val) / 255.0
zero_point = np.clip(round(-min_val / scale), 0, 255)
q = np.clip(np.round(np_tensor / scale) + zero_point, 0, 255).astype(np.uint8)
return q, scale, zero_point
def dequantize_tensor(q, scale, zero_point, symmetric):
if symmetric:
return q.astype(np.float32) * scale
else:
return (q.astype(np.float32) - zero_point) * scale
# ---------- 3. 量化整个模型(仅权重)并推理 ----------
def quantize_model_weights(model, symmetric):
"""返回量化后的模型状态字典(权重为int8)及每层的scale/zero_point"""
q_state = {}
params = {}
for name, param in model.named_parameters():
q, scale, zp = quantize_tensor(param.data, symmetric)
q_state[name] = q
params[name] = (scale, zp)
return q_state, params
def inference_with_quantized_weights(model, q_state, params, symmetric, input_data):
"""使用量化权重手动执行前向传播(模拟INT8推理)"""
# 获取原始权重形状并反量化
with torch.no_grad():
# 第一层
w1 = q_state['fc1.weight']
s1, zp1 = params['fc1.weight']
w1_dq = dequantize_tensor(w1, s1, zp1, symmetric)
b1 = model.fc1.bias.numpy()
# 第二层
w2 = q_state['fc2.weight']
s2, zp2 = params['fc2.weight']
w2_dq = dequantize_tensor(w2, s2, zp2, symmetric)
b2 = model.fc2.bias.numpy()
x = input_data.numpy()
# 第一层 + ReLU
h = np.maximum(0, x @ w1_dq.T + b1)
# 第二层
out = h @ w2_dq.T + b2
return out
# ---------- 4. 量化激活值(以第一层输出为例)并对比误差 ----------
def quantize_activations(model, input_data, symmetric):
"""获取第一层ReLU输出,量化-反量化后返回误差"""
with torch.no_grad():
x = model.fc1(input_data)
act = torch.relu(x).numpy()
q, scale, zp = quantize_tensor(torch.from_numpy(act), symmetric)
act_dq = dequantize_tensor(q, scale, zp, symmetric)
mse = np.mean((act - act_dq) ** 2)
return mse, act, act_dq
# ---------- 5. 执行对比实验 ----------
print("=== 对称量化 (Symmetric) ===")
q_state_sym, params_sym = quantize_model_weights(model, symmetric=True)
out_sym = inference_with_quantized_weights(model, q_state_sym, params_sym, True, inputs)
mse_weight_sym = np.mean((fp32_outputs - out_sym) ** 2)
mse_act_sym, _, _ = quantize_activations(model, inputs, symmetric=True)
print(f"权重量化后输出MSE: {mse_weight_sym:.6f}")
print(f"激活量化MSE: {mse_act_sym:.6f}")
print("\n=== 非对称量化 (Asymmetric) ===")
q_state_asym, params_asym = quantize_model_weights(model, symmetric=False)
out_asym = inference_with_quantized_weights(model, q_state_asym, params_asym, False, inputs)
mse_weight_asym = np.mean((fp32_outputs - out_asym) ** 2)
mse_act_asym, _, _ = quantize_activations(model, inputs, symmetric=False)
print(f"权重量化后输出MSE: {mse_weight_asym:.6f}")
print(f"激活量化MSE: {mse_act_asym:.6f}")
# ---------- 6. 性能对比(模拟INT8矩阵乘 vs FP32) ----------
def measure_inference_time(model, input_data, iterations=100):
with torch.no_grad():
# 预热
_ = model(input_data[:1])
start = time.time()
for _ in range(iterations):
_ = model(input_data)
return (time.time() - start) / iterations
# 模拟INT8推理(使用量化权重,但实际计算仍为浮点,仅演示流程)
def measure_quantized_inference(q_state, params, symmetric, input_data, iterations=100):
start = time.time()
for _ in range(iterations):
_ = inference_with_quantized_weights(model, q_state, params, symmetric, input_data)
return (time.time() - start) / iterations
fp32_time = measure_inference_time(model, inputs)
sym_time = measure_quantized_inference(q_state_sym, params_sym, True, inputs)
asym_time = measure_quantized_inference(q_state_asym, params_asym, False, inputs)
print(f"\nFP32 平均推理时间: {fp32_time*1000:.3f} ms")
print(f"对称量化推理时间: {sym_time*1000:.3f} ms")
print(f"非对称量化推理时间: {asym_time*1000:.3f} ms")
运行方式
将上述代码保存为 quant_compare.py,在已安装 numpy 和 torch 的Python环境中执行:
python quant_compare.py
验证方法与判定标准
- 精度对比:观察输出的MSE数值。由于本模型权重接近零对称分布,两种方案的权重量化MSE应接近;而ReLU激活值非负分布,非对称量化的激活MSE应明显小于对称量化(通常小一个数量级以上)。
- 性能对比:对称量化因无需零点偏移,在推理流程中省去零点加减操作,理论上耗时略低于非对称量化。但本案例中两者均以浮点模拟,实际耗时差异可能很小,重点观察流程正确性。
- 通过条件:程序无报错运行完毕,输出四组MSE和三个时间值;且激活MSE满足“非对称 < 对称”的预期关系,即证明两种量化方案实现正确且差异符合理论。
预期输出示例
=== 对称量化 (Symmetric) ===
权重量化后输出MSE: 0.000123
激活量化MSE: 0.045678
=== 非对称量化 (Asymmetric) ===
权重量化后输出MSE: 0.000118
激活量化MSE: 0.002345
FP32 平均推理时间: 0.082 ms
对称量化推理时间: 0.095 ms
非对称量化推理时间: 0.098 ms
以上数值仅为逻辑一致的示例,实际运行结果会因随机种子和硬件略有不同,但激活MSE的对比关系(非对称远小于对称)应稳定成立。
关键说明
- 本案例聚焦量化方案本身的算法对比,INT8推理性能通过模拟流程展示,未调用底层INT8指令集加速;真实部署时对称量化因计算图更简单,在支持INT8的硬件上通常吞吐更高。
- 权重量化误差两者接近,是因为本模型权重初始化接近零对称分布;若改用偏置分布明显的权重,非对称优势会体现在权重上。
- 激活量化误差差异显著,源于ReLU输出全为非负值,对称量化浪费了一半的动态范围。