量化 (quantization)是将浮点值映射到低精度整数范围的过程。此映射需要两个参数 (parameter):比例 (S) 因子和零点 (Z)。比例定义了量化的步长,表示量化值每增加一个单位时实际值的差异。零点确保实数零能正确映射到量化值。对于仿射(非对称)量化方案,它们的关系通常是:
实际值=S×(量化值−Z)
对于对称量化,零点 Z 通常隐式为零或固定值。在整个编译过程中正确处理这些比例和零点参数,对于保持模型精度同时实现低精度计算的性能优势非常重要。编译器负责管理、传播和优化涉及这些参数的计算。
IR中量化 (quantization)参数 (parameter)的表示
编译器中间表示 (IR) 需要有机制将量化参数与张量关联起来。常见方法包括:
- 张量类型属性: 像MLIR这样的现代多级IR允许定义自定义类型。量化张量类型可以直接将比例、零点、存储类型(例如
i8)和量化方案(仿射/对称,逐张量/逐通道)作为类型属性嵌入 (embedding)。这使得量化信息成为张量定义的一个组成部分。
// 逐张量仿射量化张量的MLIR类型示例
!quant.uniform<i8:f32, 0.0039:128> // <存储类型:表示类型, 比例:零点>
- 元数据: 较简单的IR可能会将比例和零点信息作为元数据附加到张量值或操作中。这种方法结构性较差,可能难以保持一致性验证。
- 显式量化/反量化操作: IR可以通过专门的
quantize 和 dequantize 操作隐式表示量化参数,这些操作接受浮点张量并生成量化张量(反之亦然),将参数嵌入到操作本身中。优化过程随后将这些操作移动、结合或消除。
对于高级ML编译器而言,通常优先使用类型属性(如MLIR中所示),因为它允许更强的类型检查以及在方言语义中定义的更有原则的传播规则。
传播与折叠
当编译器优化计算图时,必须正确传播量化 (quantization)参数 (parameter)。
- 恒等操作: 重塑、转置或切片等操作通常会保留其输入张量的量化参数。编译器会确保输出张量继承相同的比例和零点。
- 逐元素操作(参数相同): 对于两个输入具有相同比例和零点的逐元素加法或减法,输出通常可以保持相同的参数。然而,溢出处理可能需要进行调整或检查。
- 连接: 沿着轴连接张量时,所有输入张量必须具有相同的比例和零点,才能使操作在量化域中有效,而无需立即重新量化。编译器必须验证这一点或插入适当的变换。
- 常量折叠: 编译器可以折叠涉及常量和量化参数的操作。例如,对常量张量应用
dequantize 操作的折叠,仅涉及直接计算浮点表示。如果参数匹配,折叠像 quantize(dequantize(x)) 这样的序列可能会简化为 x,但受潜在的精度差异影响。
重新量化 (quantization):处理不匹配的参数 (parameter)
许多操作,特别是逐元素加法或乘法,会产生其有效比例和零点与输入不同的结果,或者它们结合了具有不同量化参数的输入。考虑将两个张量 Ta 和 Tb 相加,它们的参数分别为 (Sa,Za) 和 (Sb,Zb),目标是得到参数为 (Sc,Zc) 的输出 Tc。
理想的浮点加法是:
实际值c=实际值a+实际值b=Sa(qa−Za)+Sb(qb−Zb)
我们想用量化值 qc 来表示这个结果:
Sc(qc−Zc)=Sa(qa−Za)+Sb(qb−Zb)
求解 qc 得到:
qc=Zc+ScSa(qa−Za)+ScSb(qb−Zb)
此计算涉及浮点比率(Sa/Sc, Sb/Sc),无法在典型的低精度硬件上仅使用整数算术直接执行。使用主要通过整数操作计算 qc 的过程称为重新量化。
编译器通过使用定点算术来近似缩放因子(Sa/Sc, Sb/Sc)来实现重新量化。这通常涉及:
- 将比例比率表示为一个整数乘数 M 和一个右位移 s,使得 M/2s≈Sin/Sout。
- 使用更宽的整数累加器执行计算(例如,8位输入使用32位整数)。
- 应用整数乘数 M。
- 应用右位移 s(高效地除以2的幂)。
- 添加输出零点 Zc。
- 将结果钳制到目标量化类型的有效范围(例如,INT8为 [−128,127])。
编译器的作用是:
- 确定适当的输出比例和零点 Sc,Zc(通常根据校准期间观察到的激活范围来选择)。
- 预先计算重新量化步骤的整数乘数 (M) 和位移 (s)。
- 将必要的整数算术指令(乘法、位移、加法、钳制)插入到计算图或内核代码中。
计算 M 和 s 有不同的策略,通常需要平衡精度和计算成本(例如,Google的gemmlowp库方法)。
结合比例/零点计算
显式插入 dequantize、requantize 和 quantize 操作会带来开销。一项重要优化是将这些与参数 (parameter)相关的计算直接结合到主要计算内核中。
- 反量化 (quantization)结合: 内核可以在主要计算期间隐式处理比例和零点,而不是将输入反量化为浮点数然后执行操作(如卷积)。例如,卷积 Y=Conv(X,W) 可以实现为:
Sy(qy−Zy)≈∑(Sx(qx−Zx)×Sw(qw−Zw))
内核使用整数算术(例如INT8点积)累加到更宽的整数(例如INT32)。最终的缩放因子(SxSw/Sy)和零点调整仅在累加后应用一次,通常使用上面描述的定点重新量化技术。
- 激活函数 (activation function)结合: 量化操作后应用的激活函数(ReLU、sigmoid等)通常可以结合。对于ReLU,重新量化中固有的钳制有时可以实现
max(0, x) 部分。更复杂的函数可能直接在量化值上使用查找表。
- 偏置 (bias)加法结合: 添加浮点偏置需要对其进行适当缩放,以匹配累加器的比例,然后进行添加和重新量化。此缩放可以预先计算并结合到内核的最后阶段。
结合将反量化、计算以及重新量化/量化到一个高效的低精度内核中。
硬件影响
目标硬件显著影响比例和零点的处理方式。
- 专用指令: CPU(AVX-VNNI、ARM点积)和GPU(Tensor Cores、Matrix Cores)通常具有直接在低精度整数上执行结合乘加操作的指令(例如,INT8乘法累加到INT32)。编译器必须针对这些指令以获得最佳性能。
- 2的幂次比例: 某些硬件或软件库可能偏好或要求比例为2的幂次。这简化了重新量化 (quantization)乘法(Sa/Sc),使其变为高效的位移操作。此约束可以在量化过程本身中强制执行,或由编译器在降级时处理。
- 逐通道与逐张量支持: 硬件能力可能会决定是否能高效支持逐通道量化(卷积滤波器每个输出通道使用不同的比例/零点)。逐通道量化通常能获得更好精度,但需要更复杂的参数 (parameter)管理,并可能需要专门的硬件支持。
代码生成
最终,编译器将高级操作和相关的量化 (quantization)参数 (parameter)转换为可执行代码。这涉及:
- 为重新量化步骤生成整数算术(乘法、加法、位移)序列。
- 在可用时发出硬件特定的低精度指令。
- 管理比例和零点常量的存储和加载,如果架构允许,可能会将它们直接作为立即值嵌入 (embedding)到指令中。
- 为在量化值上操作的复杂函数生成查找表。
有效管理比例和零点对于面向低精度执行的编译器来说是一项复杂但重要的任务。它需要在IR中进行仔细表示,精巧的传播和变换规则,结合技术,以及对目标硬件能力的了解,以平衡性能提升和精度保持的目标。