模型量化基础:量化原理、定点与浮点、量化参数

3.1 为什么要量化:从连续世界到离散表示

深度学习模型在训练阶段通常使用 FP32(32位浮点数) 存储权重和激活值,以获得足够的数值精度和梯度稳定性。但在推理部署阶段,我们面对的是另一组约束:内存带宽有限、算力芯片(尤其是边缘设备)对浮点运算支持较弱、功耗预算紧张。模型量化正是为了解决这一矛盾而生的——它把连续分布的浮点数值映射到离散的整数集合上,用更少的比特位表达相近的信息。

量化的本质是有损压缩,但优秀的量化方案能让精度损失控制在可接受范围内。以 INT8 量化为例,模型体积直接缩小到原来的 1/4,推理速度在支持整数指令集的硬件上可提升 2~4 倍。对于 7B 甚至更大规模的模型,这种压缩带来的部署收益是决定性的。

核心直觉:量化不是“把数字变小”,而是“用更少的离散台阶去近似连续的数值斜坡”。台阶越密(比特数越多),近似越准;台阶越稀,压缩越狠。

3.2 定点数与浮点数:两种数字世界观

要理解量化,必须先厘清定点数(Fixed-Point)与浮点数(Floating-Point)的差异。浮点数用科学计数法表示数值,由符号位、指数位和尾数位组成,能动态适应从 1e-38 到 3.4e38 的巨大范围,但代价是每一位的“分辨率”不均匀——数值越大,相邻两个可表示数之间的间隔越大。

定点数则固定小数点的位置,用整数部分和小数部分的固定位宽来表示数值。例如 Q7.8 格式表示 7 位整数、8 位小数,总共 16 位。定点数的优势在于:均匀分辨率、硬件实现简单(整数加减乘除即可)、可预测的精度。缺点也很明显:动态范围有限,超出范围就会饱和溢出。

特性 FP32 浮点 INT8 定点(量化后)
位宽 32 位(1符号 + 8指数 + 23尾数) 8 位(1符号 + 7数值)
数值范围 约 ±3.4e38 -128 ~ 127
分辨率 不均匀,大数间隔大 均匀,固定步长
硬件成本 需要浮点运算单元(FPU) 整数ALU即可,功耗低
典型用途 训练、高精度科学计算 推理部署、边缘计算

ONNX 量化主要面向定点整数格式(INT8、UINT8、INT16 等),因为整数运算在 CPU、GPU、NPU 上都有极高的吞吐优化。

3.3 量化参数:scale 与 zero-point 的数学含义

将浮点张量 r 映射到整数张量 q,需要两个关键参数:缩放因子 scale(s) 和 零点 zero-point(z)。映射公式如下:

// 浮点 → 整数(量化)
q = round(r / s) + z

// 整数 → 浮点(反量化)
r = (q - z) * s

其中:

  • scale(s):一个正浮点数,表示整数中“1”代表的实际浮点步长。它由浮点数据的动态范围决定:s = (rmax - rmin) / (qmax - qmin)。
  • zero-point(z):整数域中对应浮点 0 的值。它保证了浮点 0 能被精确表示,这对卷积神经网络中大量存在的 padding 操作至关重要——padding 值通常是 0,如果零点不准确,padding 区域会引入噪声。
示例:假设某层权重浮点范围是 [-2.0, 6.0],要量化到 INT8(范围 -128~127)。
scale = (6.0 - (-2.0)) / (127 - (-128)) = 8.0 / 255 ≈ 0.03137
zero-point = round(-(-2.0) / 0.03137) - 128 = round(63.75) - 128 = 64 - 128 = -64
于是浮点值 0 对应整数 q = round(0 / 0.03137) + (-64) = -64,反量化 (-64 - (-64)) * 0.03137 = 0,精确无误。

根据 zero-point 是否为 0,量化分为两种对称形式:

  • 对称量化(Symmetric):z = 0,浮点范围对称于原点(如 [-a, a])。实现简单,适合权重分布近似对称的情况。
  • 非对称量化(Asymmetric):z ≠ 0,能更充分利用整数范围,适合激活值(通常经过 ReLU 后非负)这类分布偏斜的数据。
量化映射关系:浮点连续轴 → 整数离散轴 浮点 r -2.0 0 3.0 6.0 rmin rmax 整数 q -128 -64 (z) 32 127 q = round(r / s) + z s = (rmax - rmin) / (qmax - qmin) z = round(-rmin / s) + qmin

3.4 量化粒度:张量级、通道级与逐元素

scale 和 zero-point 并非只能对整个张量统一设置。根据共享参数的范围,量化粒度分为三个层级:

  1. 逐张量量化(Per-Tensor):整个权重张量共用一个 scale 和 zero-point。实现最简,但若张量内数值分布差异大(如深度可分离卷积的 depthwise 层),精度损失明显。
  2. 逐通道量化(Per-Channel):对卷积核的每个输出通道分别计算 scale 和 zero-point。这是目前 CNN 量化的主流选择,能显著提升精度,ONNX Runtime 和 TensorRT 均支持。
  3. 逐组量化(Per-Group):将通道进一步细分为小组(如每组 128 个元素),常用于大模型权重量化(如 GPTQ、AWQ),在精度与存储之间取得平衡。
常见误区:初学者常误以为“量化就是直接除以 255”。实际上,正确的 scale 必须基于实际数据范围(而非固定 255)计算,且要考虑零点偏移。直接截断到 [-128, 127] 而不做零点校正,会导致浮点 0 无法精确表示,padding 区域产生系统性偏差。

3.5 量化误差的来源与缓解策略

量化误差主要来自三个方面:

  • 舍入误差(Rounding Error):round() 操作不可避免,均匀分布在 ±0.5 个 scale 步长内。
  • 截断误差(Clipping Error):超出 [qmin, qmax] 范围的浮点值被强制截断,若数据分布有长尾,误差会很大。
  • 异常值(Outliers):个别极大或极小的值会拉宽动态范围,导致 scale 变大、整体分辨率下降。

缓解策略包括:MinMax 校准(直接取 min/max)、百分位裁剪(忽略 0.1% 极端值)、KL 散度校准(TensorRT 采用,寻找信息损失最小的阈值)。在 ONNX 生态中,可使用 onnxruntime.quantization 工具库配合校准数据集完成这些操作。

# ONNX Runtime 静态量化示例(伪代码)
from onnxruntime.quantization import quantize_static, QuantType
from onnxruntime.quantization.calibrate import CalibrationDataReader

# 准备校准数据读取器(通常使用验证集子集)
calib_reader = YourCalibrationDataReader(calibration_data)

# 执行静态量化
quantize_static(
    model_input="model_fp32.onnx",
    model_output="model_int8.onnx",
    calibration_data_reader=calib_reader,
    quant_format=QuantType.QInt8,          # 使用 INT8
    per_channel=True,                       # 逐通道量化
    weight_type=QuantType.QInt8,
    activation_type=QuantType.QInt8
)

3.6 案例复盘:一个典型的 INT8 量化流程

假设我们有一个训练好的 ResNet-18 分类模型(FP32,约 45MB),目标部署到 ARM 边缘设备。完整流程如下:

  1. 数据准备:从验证集抽取 500 张图片作为校准集,覆盖各类别。
  2. 校准:运行模型前向,统计每层激活值的 min/max 分布,计算 scale 和 zero-point。
  3. 量化:将权重和激活值转换为 INT8,生成量化后的 ONNX 模型。
  4. 验证:在测试集上对比 FP32 与 INT8 的 Top-1 精度。通常损失应 < 1%。
  5. 部署:使用 ONNX Runtime 的 EP(Execution Provider) 加载 INT8 模型,在 ARM 上测试延迟。
实测数据参考:某 ResNet-18 模型,FP32 精度 69.8%,INT8 量化后精度 69.2%(损失 0.6%),模型体积从 45MB 降至 11.5MB,推理延迟从 12.3ms 降至 4.1ms(ARM Cortex-A76 单线程)。
若采用逐张量量化,精度可能降至 67.5%;改用逐通道后恢复至 69.2%——可见量化粒度的重要性。

量化不是银弹。对于极深或注意力机制密集的模型(如 Transformer),直接 INT8 量化可能损失较大,此时需考虑混合精度(部分层保持 FP16)或更高级的量化感知训练(QAT)。理解 scale 与 zero-point 的数学本质,是后续学习 QAT、GPTQ 等进阶技术的地基。