目录
1. 介绍
2. 基本概念
2.1 映射函数
2.2 量化参数
2.3 校准
2.4 对称与非对称量化
2.5 Per-Tensor and Per-Channel
2.6 PTQ
2.7 QAT
2.8 敏感性分析
2.6 退火学习率
3. 几点建议
4. 总结
1. 介绍
Practical Quantization in PyTorch | PyTorchQuantization is a cheap and easy way to make your DNN run faster and with lower memory requirements. PyTorch offers a few different approaches to quantize your model. In this blog post, we’ll lay a (quick) foundation of quantization in deep learning, and then take a look at how each technique looks like in practice. Finally we’ll end with recommendations from the literature for using quantization in your workflows.https://pytorch.org/blog/quantization-in-practice/#fundamentals-of-quantization
本文介绍了量化的基本概念和实践应用:
- 映射函数:将浮点值映射到整数空间,常用的映射函数是线性变换。
- 量化参数:包括比例因子 ( S ) 和零点 ( Z ),用于确定输入数据的范围和偏移。
- 校准:确定量化过程中所需的缩放因子和零点,常用方法包括 MaxMin、Percentile、Entropy、MSE 和 Diffs。
- 对称与非对称量化:对称量化无需计算零点偏移,而非对称量化则需要。
- Per-Tensor 和 Per-Channel:Per-Tensor 使用相同的量化参数,而 Per-Channel 则为每个通道使用不同的量化参数。
- PTQ(训练后静态量化):适用于大型模型,通过模块融合和定期校准来减少量化误差。
- QAT(量化感知训练):通过在训练过程中模拟量化误差来提高小型模型的量化精度。
- 敏感性分析:确定哪些层对量化最敏感,并保留这些层的 FP32 精度。
- 退火学习率:通过动态调整学习率来帮助模型更好地收敛。
2. 基本概念
2.1 映射函数
映射函数是一个将值从浮点映射到整数空间的函数。
常用的映射函数是线性变换,由下式给出:
其中,r是输入,S和Z是量化参数。
可以将量化后的模型重新转换为浮点空间,反函数由下式给出:
,它们的差值构成了量化误差。
2.2 量化参数
映射函数有两个参数:比例因子 S 和零点 Z。
比例因子 S
S 就是输入范围与输出范围的比率:
,:输入的裁剪范围,即允许输入数据的边界值。这个范围定义了输入数据中哪些值会被保留并映射到量化后的输出空间,而超出这个范围的值会被剪切(截断)。
例如,对于一个输入范围 [-5.0, 10.0],所有小于-5.0的值会被剪切为-5.0,所有大于10.0的值会被剪切为10.0,以确保输入数据在量化过程中不会因为极端值而导致量化精度的显著下降。
零点 Z
Z 为偏差量,以确保输入空间中的 0 完美映射到量化空间中的 0。
1). 偏移调整:零点作为偏移量,将缩放后的数据移动到量化范围内。例如,在无符号量化中,零点可以将负值移动到正值范围内。
2). 对称量化:在对称量化中,零点通常为零,因为浮点范围和量化范围是对称的。
3). 非对称量化:在非对称量化中,零点用于调整量化范围,使其适应非对称的浮点数据分布。
2.3 校准
校准(Calibration)的目的是为了确定量化过程中所需的缩放因子(scale factor)和零点(zero point),以便将浮点数转换为整数表示。
Vitis AI 中,3.2.3 局部量化设置 2.Method,提供了不同的校准方法:maxmin、percentile、entropy、mse、diffs。
- MaxMin:使用校准数据的最大值和最小值来确定范围。这是最简单的方法,但容易受到异常值的影响。
- Percentile:基于数据的分位数来确定范围,通常使用 99.9% 分位数来避免异常值的影响。
- Entropy:使用信息熵(如 KL 散度)来最小化原始浮点值和量化值之间的信息损失。这种方法可以最大化保留信息,但计算复杂度较高。
- MSE(Mean Squared Error):通过最小化原始值和量化值之间的均方误差来确定范围。这种方法在保留模型精度方面表现良好。
- Diffs:基于数据的差异来确定范围,具体实现可能因工具而异。
量化观察器:
import torch
from torch.quantization.observer import MinMaxObserver, MovingAverageMinMaxObserver, HistogramObserver
C, L = 3, 4
# 使用正态分布生成两个随机张量作为输入数据
normal = torch.distributions.normal.Normal(0,1)
inputs = [normal.sample((C, L)), normal.sample((C, L))]
print(inputs)
observers = [MinMaxObserver(), MovingAverageMinMaxObserver(), HistogramObserver()]
for obs in observers:
for x in inputs: obs(x)
print(obs.__class__.__name__, obs.calculate_qparams())
执行结果:
[tensor([[-0.1551, 0.4171, 0.0281, 0.8844],
[-0.0766, 1.4027, 0.1924, 0.8369],
[ 0.7786, 1.0915, 0.4398, -1.8102]]),
tensor([[-1.2902, -1.3943, -1.6080, 0.1695],
[-0.9307, -0.5508, 0.6164, -2.2461],
[-1.1094, -0.3126, 0.5751, 0.6137]])]
MinMaxObserver (tensor([0.0143]), tensor([157], dtype=torch.int32))
MovingAverageMinMaxObserver (tensor([0.0126]), tensor([144], dtype=torch.int32))
HistogramObserver (tensor([0.0125]), tensor([141], dtype=torch.int32))
上述结果中,含缩放因子(tensor([0.0143])),零点(tensor([157], dtype=torch.int32))。
通过这个例子,可以看到不同的观察器在处理相同的数据时,可能会生成不同的量化参数。这有助于理解不同观察器的行为和它们在量化过程中可能产生的影响。
2.4 对称与非对称量化
对称量化方案
对称量化方案(Symmetric quantization schemes),将输入范围集中在 0 附近,无需计算零点偏移。范围计算如下:
对于倾斜信号(如非负激活),这可能会导致量化分辨率不佳,因为剪切范围包含永远不会出现在输入中的值。
非对称量化方案
非对称量化方案(Asymmetric quantization schemes),将输入空间的最小和最大观测值分配给。范围计算如下:
对于量化非负激活非常有用(如果输入张量从不为负,则不需要输入范围包含负值)。
import torch
import matplotlib.pyplot as plt
import numpy as np
# 从帕累托分布中生成的激活值样本
act = torch.distributions.pareto.Pareto(1, 10).sample((1, 1024))
# 从正态分布中生成的权重样本,并将其展平
weights = torch.distributions.normal.Normal(0, 0.12).sample((3, 64, 7, 7)).flatten()
def get_range(x, scheme):
if scheme == 'asymmetric':
return x.min().item(), x.max().item()
elif scheme == 'symmetric':
beta = torch.max(x.max(), x.min().abs())
return -beta.item(), beta.item()
# 计算直方图、边界、以及直方图中非零部分的25%和95%分位数。
def prepare_data(data, scheme):
boundaries = get_range(data, scheme)
hist, bin_edges = np.histogram(data.numpy(), bins=100, density=True)
ymin, ymax = np.quantile(hist[hist > 0], [0.25, 0.95])
return hist, bin_edges, boundaries, ymin, ymax
# 准备激活和权重数据
act_asymmetric = prepare_data(act, 'asymmetric')
act_symmetric = prepare_data(act, 'symmetric')
weights_asymmetric = prepare_data(weights, 'asymmetric')
weights_symmetric = prepare_data(weights, 'symmetric')
# 绘图循环
fig, axs = plt.subplots(2, 2, figsize=(12, 8))
titles = ["Activation, Asymmetric-Quantized", "Activation, Symmetric-Quantized",
"Weights, Asymmetric-Quantized", "Weights, Symmetric-Quantized"]
data_list = [act_asymmetric, act_symmetric, weights_asymmetric, weights_symmetric]
for ax, data, title in zip(axs.flatten(), data_list, titles):
hist, bin_edges, boundaries, ymin, ymax = data
ax.hist(bin_edges[:-1], bin_edges, weights=hist)
ax.vlines(x=boundaries, ls='--', colors='purple', ymin=ymin, ymax=ymax)
ax.set_title(title)
plt.tight_layout()
plt.show()
结果:
2.5 Per-Tensor and Per-Channel
Per-Tensor:整个张量使用相同的比例因子 S 和零点 Z。
Per-Channel:每个通道使用一组比例因子 S 和零点 Z。
Per-Channel 可以减少量化误差,因为异常值只会影响它所在的通道,而不是整个张量。
2.6 PTQ
训练后静态量化(Post-Training Static Quantization)。
PTQ 方法非常适合大型模型(>10M),但在较小的模型中准确性会受到影响。
模块融合将多个顺序模块(例如: [Conv2d, BatchNorm, ReLU] )合并为一个。融合模块意味着编译器只需要运行一个内核而不是多个;这可以通过减少量化误差来加快速度并提高准确性。
静态量化模型可能需要定期重新校准,以保持对分布漂移的鲁棒性。
2.7 QAT
量化感知训练(Quantization-aware Training)。
QAT 通过将量化误差包含在训练损失中来解决小型模型(<10M)量化数值精度的损失。
所有权重和偏差都存储在 FP32 中,正常进行反向传播。然而,在前向传播中,量化是通过 FakeQuantize 模块进行内部模拟的。FakeQuantize 对数据进行量化并立即反量化,从而添加类似于量化推理期间可能遇到的量化噪声。因此,最终的损失考虑了任何预期的量化误差。
QAT 比 PTQ 具有更高的准确度。
在 QAT 中重新训练模型的计算成本可能是数百个 epoch。
import torch
from torch import nn
backend = "fbgemm" # 在x86 CPU上运行。如果在ARM上运行,请使用 "qnnpack"。
m = nn.Sequential(
nn.Conv2d(2,64,8),
nn.ReLU(),
nn.Conv2d(64, 128, 8),
nn.ReLU()
)
"""融合模块"""
torch.quantization.fuse_modules(m, ['0','1'], inplace=True) # 融合第一对Conv-ReLU
torch.quantization.fuse_modules(m, ['2','3'], inplace=True) # 融合第二对Conv-ReLU
"""插入量化和去量化节点"""
m = nn.Sequential(torch.quantization.QuantStub(),
*m,
torch.quantization.DeQuantStub())
"""准备进行量化感知训练"""
m.train()
m.qconfig = torch.quantization.get_default_qconfig(backend)
torch.quantization.prepare_qat(m, inplace=True)
"""训练循环"""
n_epochs = 10
opt = torch.optim.SGD(m.parameters(), lr=0.1)
loss_fn = lambda out, tgt: torch.pow(tgt-out, 2).mean()
for epoch in range(n_epochs):
x = torch.rand(10,2,24,24) # 生成随机数据
out = m(x) # 通过模型传递数据
loss = loss_fn(out, torch.rand_like(out)) # 计算损失
opt.zero_grad() # 清除梯度
loss.backward() # 反向传播
opt.step() # 更新参数
"""转换为量化模型"""
m.eval() # 设置为评估模式
torch.quantization.convert(m, inplace=True) # 转换模型为量化版本
2.8 敏感性分析
并非所有层对量化的响应都相同,有些层对精度下降比其他层更敏感。
确定最小化精度下降的最佳层组合非常耗时。
进行一次一个的敏感性分析,可以确定哪些层最敏感,并保留这些层的 FP32 精度。
实验中,仅跳过 2 个卷积层(MobileNet v1 中总共 28 个)即可获得接近 FP32 的精度。
2.6 退火学习率
退火学习率(Annealing learning rate)是一种动态调整学习率的方法,灵感来自于物理中的退火过程。退火是指通过逐渐降低温度来减少系统的能量,从而达到更稳定的状态。在机器学习中,退火学习率计划(Annealing learning rate schedule)通过逐渐降低学习率来帮助模型更好地收敛,避免陷入局部最优解。
常见的 Annealing learning rate schedule 方法包括:
- 指数衰减(Exponential Decay):学习率按指数函数逐渐减小。
- 余弦退火(Cosine Annealing):学习率按余弦函数周期性减小。
- 分段衰减(Step Decay):学习率在特定的训练轮次后按固定比例减小。
3. 几点建议
需要注意的几点:
1. 大型模型(参数超过1000万)对量化误差更具鲁棒性。
2. 从预训练的32位量化模型比从头训练INT8模型提供更好的准确性。
3. 通过运行时剖析模型,可以帮助识别在推理中造成瓶颈的层。
4. 动态量化是一个简单的第一步,特别是如果你的模型有很多线性或递归层。
5. 对于权重量化,使用带有MinMax观察器的对称通道量化。对于激活量化,使用带有移动平均MinMax观察器的仿射张量量化。
6. 使用 SQNR 等指标来识别哪些层最容易受到量化误差的影响。关闭这些层的量化。
7. 使用量化感知训练(QAT)进行微调,训练时间约为原始训练计划的10%,并采用从初始训练学习率的1%开始的退火学习率计划。
4. 总结
量化技术在深度学习模型优化中具有重要作用,通过合理选择量化方法和参数,可以在不显著降低模型精度的情况下,显著提高模型的推理速度和内存效率。
本文记录的概念和建议能帮助在今后的实际应用中更好地利用量化技术。