量化是一系列方法,用于降低表示模型权重和/或激活值的数字精度。不是使用标准的32位浮点数($FP32$),这些值被映射到低精度格式,最常见的是8位整数($INT8$),但也日益包括像$FP8$这样的低位浮点格式。主要目的是提升性能:低精度算术在支持硬件上更快,需要更少的内存带宽,功耗更低,并使模型尺寸更小。但是,这种压缩本质上是有损的;面临的难题是如何在最大化性能收益的同时,尽量减小对模型精度的影响。此处将介绍控制这一过程的基本原理。量化映射其核心是,量化将一个实数$R$从高精度范围$[R_{min}, R_{max}]$(例如,观察到的FP32值)映射到低精度整数范围$[Q_{min}, Q_{max}]$(例如,有符号INT8的[-128, 127])内的量化值$Q$。这种映射通常是仿射的,由两个参数定义:比例因子 ($S$) 和 零点 ($Z$)比例因子 ($S$): 一个正实数,表示量化的步长。它决定了量化整数值每增加一个单位时实数的变化量。其计算基于选定的范围: $$S = \frac{R_{max} - R_{min}}{Q_{max} - Q_{min}}$$零点 ($Z$): 量化范围$[Q_{min}, Q_{max}]$内的一个整数值,它对应于实数值0.0。它确保实数值0.0可以被精确表示,这在涉及填充或偏置的操作中通常很重要。其计算方式为: $$Z = Q_{min} - \text{round}\left(\frac{R_{min}}{S}\right)$$ 请注意,舍入函数确保$Z$是一个整数。如果0.0在$[R_{min}, R_{max}]$范围内并且我们希望它被精确表示,那么$Z$应该精确映射到0.0。量化函数将实数值$R$映射到其量化整数表示$Q$: $$Q = \text{clamp}\left( \text{round}\left(\frac{R}{S}\right) + Z, Q_{min}, Q_{max} \right)$$ round函数通常四舍五入到最近的整数(平局时任意或一致处理,例如,四舍五入到偶数)。clamp函数确保结果保持在目标整数范围$[Q_{min}, Q_{max}]$内。相应的反量化函数将整数$Q$映射回近似实数值$R'$: $$R' = S \times (Q - Z)$$ 重要的是,$R' \approx R$。差值$R - R'$是量化误差,是潜在精度下降的来源。在整个模型中最小化此误差是量化方法的核心目的。映射方案:对称量化与非对称量化实数范围$[R_{min}, R_{max}]$的选择会影响$S$和$Z$的计算,并引出两种主要的映射方案:对称量化在对称量化中,实数值范围以零为中心,意味着$R_{min} = -R_{max}$。最大绝对值$\max(|R_{min}|, |R_{max}|)$决定了范围。对于有符号整数(例如,INT8 [-128, 127]): 范围通常被选择为对称的,例如$[-127, 127]$,舍弃一个值(例如-128)以保持对称性。实数零(0.0)直接映射到整数零(0),使零点$Z=0$。比例因子计算为$S = R_{max} / Q_{max}$。 量化函数简化为$Q = \text{clamp}(\text{round}(R/S), Q_{min}, Q_{max})$。对于无符号整数(例如,UINT8 [0, 255]): 对称性不直接适用,但一种简化的方式可能固定$R_{min} = 0$。此时$S = R_{max} / Q_{max}$且$Z=0$。对称量化常用于权重,因为它们的分布通常以零为中心。其优势在于计算效率,因为计算过程中无需处理零点偏移$Z$(它默认为0)。然而,如果实际数据分布严重偏斜,强制采用对称范围可能导致在分布中较少出现的值产生更大的量化误差。非对称量化非对称量化使用实际观察到的最小值和最大值$R_{min}$和$R_{max}$,不强制对称。$S$和$Z$均按前述方法计算。实数值0.0精确映射到整数$Z$。这种方案可以更准确地表示输入范围,特别是对于不以零为中心的数据分布,例如ReLU等激活函数的输出(它们总是非负的)。其代价是计算复杂性增加,因为在算术运算中必须考虑零点$Z$(例如,在乘法后调整结果)。digraph QuantizationSchemes { rankdir=LR; node [shape=box, style=rounded, fontname="sans-serif", fontsize=10]; edge [fontname="sans-serif", fontsize=10]; subgraph cluster_sym { label = "对称量化"; bgcolor="#e9ecef"; node [fillcolor="#a5d8ff"]; s_fp_axis [label="FP32 范围\n[-R_abs, +R_abs]"]; s_int_axis [label="INT8 范围\n[-127, 127] (Z=0)"]; s_fp_axis -> s_int_axis [label=" S = R_abs / 127 \n Q = round(R/S) "]; s_fp_0 [shape=point, label="", xlabel="0.0"]; s_int_0 [shape=point, label="", xlabel="0"]; s_fp_0 -> s_int_0 [style=dashed, arrowhead=none, color="#1c7ed6"]; } subgraph cluster_asym { label = "非对称量化"; bgcolor="#e9ecef"; node [fillcolor="#b2f2bb"]; a_fp_axis [label="FP32 范围\n[R_min, R_max]"]; a_int_axis [label="INT8 范围\n[-128, 127]"]; a_fp_axis -> a_int_axis [label=" S = (R_max-R_min) / 255 \n Z = -round(R_min/S) - 128 \n Q = round(R/S) + Z "]; a_fp_0 [shape=point, label="", xlabel="0.0"]; a_int_z [shape=point, label="", xlabel="Z"]; a_fp_0 -> a_int_z [style=dashed, arrowhead=none, color="#37b24d"]; } }浮点范围到INT8的对称和非对称映射比较。对称量化强制范围以0.0为中心,映射到整数0。非对称量化使用实际范围,将0.0映射到计算出的零点Z。量化粒度:逐张量与逐通道另一个需要考虑的是量化参数($S$和$Z$)应用时的粒度:逐张量量化: 计算并应用一对$(S, Z)$值到整个张量内的所有元素(例如,卷积层中的所有权重,或激活张量中的所有元素)。这是最简单的方法,它能最大限度地减少存储和管理量化参数的开销。但是,如果值分布在张量不同部分(例如,卷积滤波器中的不同输出通道)之间差异很大,使用单个范围$[R_{min}, R_{max}]$可能不是最优的,可能裁剪重要值或未能充分利用张量某些子集的量化范围。逐通道(或逐轴)量化: 沿着特定维度或轴为张量的切片计算单独的$(S, Z)$对。对于卷积层权重,这最常沿输出通道轴进行(形状为[output_channels, input_channels, kH, kW]的权重,axis=0)。这使得量化范围能更紧密地适应每个通道内值的特定分布,通常会比逐张量量化带来显著更好的精度,特别是对于深度卷积网络。其代价是计算过程中需要存储和使用的$S$和$Z$参数数量增加。其他粒度,如逐组量化(对通道进行分组),也作为中间选项存在。逐张量和逐通道之间的选择通常取决于具体的层类型、其对量化噪声的敏感性以及目标硬件高效处理不同比例因子和零点的能力。校准:确定量化范围量化的有效性取决于选择合适的裁剪范围$[R_{min}, R_{max}]$。这个范围直接决定了比例因子$S$以及可能的零点$Z$。如果范围过窄,超出范围的值将被裁剪,从而引入显著误差。如果范围过宽,量化步长会变大($S$变大),降低范围内值的精度。确定最优范围的过程称为校准。它通常包括:检测: 修改模型或执行环境,以观察FP32权重和激活值的运行时统计信息(最小值、最大值、直方图)。数据正向传递: 使用检测过的模型在一个小型、具有代表性的数据集(“校准数据集”)上运行推理。这个数据集理想情况下应反映模型在部署时将遇到的数据特征。统计信息收集: 收集需要量化的每个张量的观察范围或分布。范围确定: 使用特定算法,根据收集到的统计信息选择最终的$[R_{min}, R_{max}]$。常见方法包括:最小/最大值: 直接使用校准运行中观察到的最小值和最大值。这很直接,但对异常值高度敏感。单个异常大或小的值会显著扩大范围,从而降低大部分值的精度。移动平均最小/最大值: 对多个校准批次的最小/最大值进行平滑处理,以减少对任何单个批次中异常值的敏感性。百分位数: 排除少量最低和最高值(例如,0.01%或0.1%),以便在存在异常值时使范围确定更准确。熵(KL散度): 选择使原始FP32分布与量化和反量化后的分布之间Kullback-Leibler(KL)散度最小化的范围$[R_{min}, R_{max}]$。这种方法旨在保留原始分布的信息量。均方误差(MSE): 选择使原始FP32值及其量化-反量化等效值之间均方误差最小化的范围。校准主要与**训练后量化(PTQ)有关,PTQ将预训练的FP32模型转换为低精度格式,而无需重新训练。在量化感知训练(QAT)**中,模拟量化操作在训练期间被插入到模型中,使模型能够将其权重适应降低的精度,并有效地隐式学习最优范围。然而,即使是QAT也可能受益于校准步骤提供的初始范围估计。INT8 概述INT8(8位整数)量化是目前最广泛采用的低精度格式。表示: 使用8位表示整数。有符号INT8通常涵盖[-128, 127]范围,而无符号UINT8涵盖[0, 255]。映射: 可使用对称或非对称映射,参数通过校准或QAT确定。硬件支持: 大多数现代CPU(例如AVX-VNNI、ARM Neon点积)、GPU(例如NVIDIA Tensor Core、AMD Matrix Core)和专用AI加速器(例如TPU、NPU)都提供了专用的INT8计算指令,对于矩阵乘法和卷积等操作,与FP32相比,它们提供了显著的加速(通常是2-4倍或更多)。算术运算: 像$Y = WX + B$这样的计算是使用整数算术执行的。如果$W$和$X$是INT8,累加($W \times X$)通常以更高精度(例如INT32)完成,以避免溢出。然后,结果会重新缩放(再量化)回INT8或反量化为FP32,适当地纳入比例因子($S_W$, $S_X$, $S_Y$)和零点($Z_W$, $Z_X$, $Z_Y$)。再量化步骤$R' = S \times (Q - Z)$涉及整数乘以分数比例因子$S$,通常通过带有位移和加法的定点算术实现。新型格式:FP8尽管INT8带来了显著优势,但研究和硬件发展正推动实现更低的精度,特别是8位浮点格式(FP8)。与INT8不同,FP8保留了浮点数的指数-尾数结构,可能为某些应用(包括训练)在动态范围和精度之间提供更好的平衡。目前还没有单一的FP8标准;但由NVIDIA定义并得到其他行业厂商支持的两个主要变体是:E5M2: 1个符号位,5个指数位,2个尾数位(+1个隐式前导位)。这种格式的动态范围与FP16相同(5个指数位),但精度显著降低(总精度位只有3个,而FP16有11个)。E4M3: 1个符号位,4个指数位,3个尾数位(+1个隐式前导位)。这种格式的动态范围比E5M2或FP16窄(4个指数位),但提供略高的精度(总精度位4个)。{"data":[{"type":"bar","x":["FP32","FP16","BF16","INT8","FP8 E5M2","FP8 E4M3"],"y":[8,5,8,0,5,4],"name":"指数位","marker":{"color":"#339af0"}},{"type":"bar","x":["FP32","FP16","BF16","INT8","FP8 E5M2","FP8 E4M3"],"y":[23,10,7,7,2,3],"name":"尾数/整数位","marker":{"color":"#51cf66"}},{"type":"bar","x":["FP32","FP16","BF16","INT8","FP8 E5M2","FP8 E4M3"],"y":[1,1,1,1,1,1],"name":"符号位","marker":{"color":"#adb5bd"}}],"layout":{"title":{"text":"数值格式的比特分配","font":{"size":14}},"barmode":"stack","xaxis":{"title":{"text":"格式","font":{"size":12}}},"yaxis":{"title":{"text":"比特数量","font":{"size":12}}},"legend":{"font":{"size":10}},"margin":{"l":40,"r":10,"t":40,"b":40},"height":300}}机器学习中不同数值格式的比特分配。浮点格式将比特专用于指数和尾数,以此确定范围和精度。INT8使用所有非符号位来表示大小。FP8变体用指数位(范围)换取尾数位(精度)。FP8 与 INT8 的权衡:动态范围: FP8,特别是E5M2,可以表示比INT8更宽的值范围,这对于具有大动态范围的值可能有利,包括训练期间的梯度。精度: 在其可表示范围内,INT8提供均匀精度。FP8在接近零时精度较高,但对于大数量级数字精度较低。E4M3提供比E5M2略高的精度。硬件复杂性: 浮点算术单元通常比整数单元更复杂。然而,专用硬件(如NVIDIA Hopper/Blackwell的Transformer Engine或AMD的MI300)实现了高效的FP8运算。零点表示: INT8(通过非对称量化)和FP8都可以精确表示零。特殊值: FP8可以表示$\pm \infty$和NaN,这与标准INT8不同。FP8需要仔细管理比例因子,类似于FP16混合精度训练,通常是动态调整的。编译器在为不同张量选择合适的FP8格式(E4M3与E5M2)以及使用硬件专用FP8指令生成高效代码方面起着重要作用。掌握这些基本原理、映射方案、校准、粒度以及INT8和FP8的特性,是设计和实现利用低精度计算性能优势的编译器与运行时方法的前提,我们将在后续章节中讨论这些方法。