模型量化(Quantization)是端侧推理加速的关键技术之一,它将浮点数(FP32)权重和激活值转换为低比特整数(如INT8),显著减少了模型大小并提高了计算效率。然而,量化方式的选择——特别是对称量化(Symmetric Quantization)和非对称量化(Asymmetric Quantization)——对模型精度以及在特定移动端NPU(Neural Processing Unit)上的性能有着决定性的影响。
1. 技术背景:对称与非对称量化的原理
量化过程本质上是将浮点范围 $[R_{min}, R_{max}]$ 映射到整数范围 $[Q_{min}, Q_{max}]$,通过一个比例因子(Scale, $S$)和一个零点(Zero Point, $Z$)来实现:
$$\text{Q} = \text{round}(\text{FP} / S) + Z$$
$$\text{FP} = (Q – Z) \times S$$
A. 对称量化(Symmetric Quantization)
在对称量化中,浮点范围是对称于零点的,即 $R_{min} = -R_{max}$。因此,零点 $Z$ 通常被设定为0。这使得量化计算更加简单,因为无需处理零点偏移,尤其适用于权重,也常用于某些专为对称输入优化的DSP或NPU。
特点:
* 零点 $Z=0$。
* 适用于分布中心接近0的权重。
* 在某些老旧或设计简单的NPU上,运算速度更快。
B. 非对称量化(Asymmetric Quantization)
非对称量化允许浮点范围 $[R_{min}, R_{max}]$ 不对称,零点 $Z$ 用于精确表示浮点数0,即使它不在范围中心。这对于激活值(尤其是ReLU输出,其范围总是 $[0, R_{max}]$)至关重要,因为它可以最大限度地保留精度。
特点:
* 零点 $Z \neq 0$(通常)。
* 更精确地拟合数据分布,尤其适用于激活值。
* 计算中引入了零点偏移项,计算复杂度稍高(但在现代NPU上通常被优化)。
2. NPU 优化考量:性能差异的根源
为什么要在两者之间选择?因为不同的NPU硬件对这两种量化方式的支持和优化程度不同:
- 硬件指令集: 一些NPU或DSP的乘加指令(MAC operations)专门针对对称 INT8 (如 $[-127, 127]$) 进行了高度优化,当采用对称量化时,可以避免额外的零点处理,从而提升速度。
- 精度损失: 对于 ReLU 等非负激活层,使用非对称量化通常能获得更高的精度,因为它能更有效地利用整个整数范围。
- 兼容性: 如果你的模型需要兼容更广泛的硬件(包括一些只支持简单 INT8 乘法的DSP),对称量化可能更安全。
在实际部署中,通常会采用权重对称量化(Weight Symmetric)和激活非对称量化(Activation Asymmetric)的混合策略,但这并非绝对,具体需查阅目标 NPU 的开发文档。
3. 实操演示:在 PyTorch 中配置量化策略
我们以 PyTorch 的 Post-Training Quantization (PTQ) 为例,展示如何通过配置 qconfig 来切换量化策略。
import torch
import torch.nn as nn
import torch.quantization
# 假设我们有一个简单的模型(例如MobileNetV2的片段)
class SimpleModel(nn.Module):
def __init__(self):
super().__init__()
self.conv = nn.Conv2d(3, 16, 3, padding=1)
self.relu = nn.ReLU()
def forward(self, x):
return self.relu(self.conv(x))
# 1. 准备模型和数据
model = SimpleModel().eval() # 必须设置为eval模式
example_input = torch.randn(1, 3, 224, 224)
# --- 配置量化策略示例 ---
# 策略 A: 偏向传统的非对称量化 (常用于激活层,例如使用 MinMax Observer)
# weights: 对称量化 (Symmetric, 默认行为)
# activations: 非对称量化 (Asymmetric, 使用 Histogram Observer)
def get_asymmetric_qconfig():
return torch.quantization.QConfig(
activation=torch.quantization.observer.HistogramObserver.with_args(
quant_min=0, quant_max=255, dtype=torch.quint8, qscheme=torch.per_tensor_affine # per_tensor_affine 即非对称
),
weight=torch.quantization.default_weight_observer
)
# 策略 B: 强制使用对称量化 (在精度损失可接受前提下,针对特定NPU优化)
# weights: 对称量化
# activations: 强制使用对称量化 (使用 L2Norm MinMax Observer)
def get_symmetric_qconfig():
return torch.quantization.QConfig(
activation=torch.quantization.observer.MinMaxObserver.with_args(
quant_min=-128, quant_max=127, dtype=torch.qint8, qscheme=torch.per_tensor_symmetric # per_tensor_symmetric 即对称
),
weight=torch.quantization.default_weight_observer
)
# --- 应用量化流程 ---
# 1. 应用非对称策略
model_asym = model.train().eval() # 重新初始化模型
model_asym.qconfig = get_asymmetric_qconfig()
quantized_model_asym = torch.quantization.prepare(model_asym)
# 2. 校准 (Calibration): 使用少量数据运行模型以收集统计信息
# 在实际应用中,这里需要运行完整的校准数据集
_ = quantized_model_asym(example_input)
# 3. 转换模型
quantized_model_asym = torch.quantization.convert(quantized_model_asym)
print("--- 非对称量化 (Asymmetric) 配置完成 ---")
# 4. 应用对称策略
model_sym = model.train().eval() # 重新初始化模型
model_sym.qconfig = get_symmetric_qconfig()
quantized_model_sym = torch.quantization.prepare(model_sym)
_ = quantized_model_sym(example_input)
quantized_model_sym = torch.quantization.convert(quantized_model_sym)
print("--- 对称量化 (Symmetric) 配置完成 ---")
4. 总结与选择建议
选择量化策略时,必须结合目标NPU的特性:
- 优先考虑精度: 如果目标NPU对零点处理开销不大(现代NPU通常如此),优先选择非对称量化(Activation Asymmetric),以保证最高的模型精度。
- 性能瓶颈分析: 如果目标是性能有限的NPU或旧版DSP,且文档表明其INT8/INT16乘法针对对称输入有特殊优化,那么尝试对称量化(Symmetric)并接受轻微的精度损失,可能会带来显著的推理速度提升。
- 测试是王道: 最终的性能优化必须通过在目标移动设备上运行基准测试(Benchmark)来确定,对比使用不同量化策略导出的 TFLite 或 NCNN/MNN 模型在实际 NPU 上的延迟和功耗表现。
汤不热吧