深入了解 PPQ 量化(1)

Dig deeper into PPQ.


Tensor Quantization Config

PPQ 使用 Tensor Quantization Config 类来描述数值量化的所有细节。正如我所说的那样,所有 PPQ 的逻辑都围绕这一抽象展开。Tensor Quantization Config 是 PPQ 所有逻辑的基石。

在这里我直接向你展示它的完整定义:

class TensorQuantizationConfig(Serializable):
  def __init__(
      self,
      policy: QuantizationPolicy,
      rounding: RoundingPolicy,
      num_of_bits: int,
      quant_min: int,
      quant_max: int,
      scale: Any,
      offset: Any,
      observer_algorithm: str,
      detail: Any = None,
      inplace: bool = False,
      state: QuantizationStates = QuantizationStates.INITIAL
  ):
      assert num_of_bits <= 32, 'Cannot quantize a tensor with more than 32 bits.'
      assert num_of_bits >= 2, 'Cannot quantize a tensor with less than 2 bits.'

      self._policy = policy
      self._num_of_bits = num_of_bits
      self._scale = scale
      self._offset = offset
      self.state = state
      self._rounding = rounding
      self._quant_min = quant_min
      self._quant_max = quant_max
      self.observer_algorithm = observer_algorithm
      self.inplace = inplace
      self.detail = {} if detail is None else detail
      self._father_config = self # union-find
      self._hash = self.__create_hash()
      super().__init__()
              

Tensor Quantization Config 描述了所有量化计算相关的具体细节,其中几乎每一个属性都是至关重要的,并将对量化逻辑产生深远的影响。

  • num_of_bits:用来指定量化位宽,可以设置为 2 ~ 32 位。
  • rounding: 一个 RoundingPolicy 枚举实例,用来确定量化的取整方式,例如可以直接向上取整,ROUND_HALF_UP,ROUND_HALF_EVEN 等等。
  • policy: 一个 QuantizationPolicy 位图实例,用来确定量化策略,例如可以设置为“逐层线性非对称量化”,“逐通道线性对称量化”等等,任何合理的组合均可以出现。
  • quant_min: 一个整数值,用来确定量化最小值,如 TensorRT 平台 8 bit 量化最小值 -127,DSP 平台 8 bit 量化最小值 0。请保持与后端对齐。
  • quant_max: 一个整数值,用来确定量化最大值,如 TensorRT 平台 8 bit 量化最大值 127,DSP 平台 8 bit 量化最大值 255。请保持与后端对齐。
  • scale: 一个浮点值(逐层),或者一个浮点向量(逐通道),用来确定量化中的 scale。
  • offset: 一个整数值(逐层),或者一个整数向量(逐通道),用来确定量化中的 offset(有时也叫zero_point),仅非对称量化需要使用该信息,用于在非对称量化中表示偏移量。
  • observer_algorithm: 一个字符串,用来表示一个作用在该 Tensor 上的校准算法。
  • detail: 一个字典,由 PPQ 系统管理,在任何时候你都不可以手动修改其中的值。
  • inplace: 一个布尔型,如果为真,则 PPQ 会尝试原地量化当前 tensor(不分配额外的显存)。
  • state: 一个 QuantizationStates 枚举实例,用来确定当前的量化状态,这直接决定了执行器会不会对当前的 Tensor 做出量化行为。
  • father_config(dominator): 一个指针,指向自己或者其他Tensor Quantization Config,PPQ 使用该属性表示联合定点的相关逻辑。

在 PPQ 中,Tensor Quantization Config 并不是绑定在 variable 上面的,而是绑定在 operation 之上的,事实上这也是唯一正确的量化设计。下图向你展示了 Tensor Quantization Config 与 Operation 的关系:

Image
Operation Executing with Quantization.

如上图中那样,PPQ 在执行每一个算子时,都会从算子信息中取得其对应的 Tensor Quantization Config,并对算子的输入和输出进行量化,这也是 PPQ 量化模拟的核心逻辑。

与一般的量化模拟器不同,PPQ 并不会在网络结构中插入量化节点,而是通过一种类似于 hook 的形式直接将量化操作添加到算子的执行逻辑中,这两种方法各有优劣,但我希望让你注意到以下的问题:

  • 对于上图中的 Var 1 来说,Conv 1 和 Conv 2 可以分别对其进行量化,Conv 1 执行对输出的量化,而 Conv 2 执行对输入的量化。这一特性对于混合精度量化而言是必须的,例如在 Conv 1 使用 8 bit 精度量化而 Conv 2 使用 4 bit 精度量化,则 Var 1 必须量化两次。
  • PPQ 会通过一些手段来消除冗余的量化操作,对于上图中的 Conv 1 和 Conv 2,如果量化位宽一致,PPQ 总是会屏蔽下游算子的输入量化。
  • Tensor Quantization Config 是被算子维护的,每一个算子都维护了一系列对应的 Tensor Quantization Config。在 PPQ 中你可以通过 Var 1.config的形式访问到 Tensor Quantization Config,但请返回值只是其上下游算子的一个镜像。

RoundingPolicy

正如我们之前提到的那样,取整策略对于量化而言是重要的,虽然我们总是使用x=round(y)的形式对它们进行处理,但实际上这些函数背后隐藏的逻辑比我们想象的复杂许多。

其核心逻辑是,由于计算机总是无法精确的表达小数,并且对于 x.5 这样的数我们必须存在一致的取整逻辑,因此我们有完整的一套取整逻辑:

  • ROUND_HALF_EVEN:四舍五入取整,x.5 向最近偶数取整。
  • ROUND_HALF_UP:四舍五入取整,x.5 向上取整。
  • ROUND_HALF_DOWN: 四舍五入取整,x.5 向下取整。
  • ROUND_HALF_TOWARDS_ZERO: 四舍五入取整,x.5 向零取整。
  • ROUND_HALF_FAR_FORM_ZERO: 四舍五入取整,x.5 向正负无穷取整。
  • ROUND_TO_NEAR_INT: 已废弃,不要使用。
  • ROUND_UP: 直接向上取整

可以访问wiki来获取详细的取整计算方法:https://en.wikipedia.org/wiki/Rounding


QuantizationPolicy

QuantizationPolicy 在 PPQ 中用来描述量化策略,它是一些 QuantizationProperty 枚举的组合位图。在 PPQ 中我们支持的 QuantizationProperty 包括:

  • PER_TENSOR:逐层量化。
  • PER_CHANNEL:逐通道量化。
  • LINEAR: 线性量化。
  • EXPONENTIAL: 指数量化。
  • SYMMETRICAL: 对称量化。
  • ASYMMETRICAL: 非对称量化。
  • POWER_OF_2: Power-of-2 量化。

你可以通过组合 QuantizationProperty 的方式来确定一种特定的 QuantizationPolicy,例如: policy = QuantizationProperty.ASYMMETRICAL + QuantizationProperty.LINEAR + QuantizationProperty.PER_CHANNEL 确定了一种非对称线性逐通道量化策略。

并不是所有组合都是合理的,例如 PER_TENSOR 与 PER_CHANNEL 不能同时出现在你的 QuantizationPolicy 里,PPQ 已经做好了类似的限制,如果你错误地组装了 QuantizationPolicy,PPQ 会向你抛出错误。


QuantizationStates

PPQ 中最为复杂的属性之一,同样地是一个枚举类型,用来表示 Tensor Quantization Config 的当前的状态,例如量化是否生效。PPQ 使用整整 11 种状态描述量化,枚举值包括:

  • INITIAL:量化参数刚刚被初始化,当前 config 不生效,数据不能被使用。
  • BAKED:只针对参数量化,表示参数已经被静态量化,当前 config 不生效,数据可以直接使用。
  • OVERLAPPED: 只针对 activation 量化,表示数据流的量化由其他 config 管理(通常而言这是由于后端图融合导致的),当前 config 不生效。
  • DEACTIVATED: 表示当前 config 不生效。
  • ACTIVATED: 表示当前 config 生效,量化可以进行。
  • DEQUANTIZED: 表示当前 config 处于解量化状态(不生效),解量化是 PPQ 种的一个系统操作。
  • SOI: 表示 tensor 与 Shape or index 相关,不量化。
  • PASSIVE: 表示对应的 tensor 被动量化且生效,被动量化如 bias, clip value 等,他们不具有独立的 scale 与 offset。
  • PASSIVE_INIT: 表示这一路输入被动量化,并且刚刚初始化不能被使用
  • PASSIVE_BAKED: 被动量化且静态量化,当前 config 不生效,数据可以直接使用
  • FP32: 表示这一路输入直接为 FP32 浮点数。

在任何时候,Tensor Quantization Config 只能拥有唯一一个 QuantizationState,同时按照严格的逻辑进行状态转换,在网络执行期间与网络导出期间,PPQ 都将检查并使用 QuantizationStates 来确定量化状态。

Image
PPQ Quantization States

上图为你更进一步地解释了 QuantizationState 的迁移情况,你也可以通过观察 PPQ 导出的量化 config 文件来进一步地进行分析。 总而言之,PPQ 使用复杂的状态信息来保证量化的正确性,避免与Shape或Index相关的计算被错误地量化,同时保证与后端的计算具有一致性,即在后端进行图融合的情况下,PPQ 能够保证量化计算一致。


图融合与联合定点

图融合是神经网络推理引擎常用的优化技巧,常见的融合操作包括 Conv-Relu 融合,Conv-Add 融合,Conv-Conv 融合等。若非必要,PPQ 不会试图直接在原图上进行真实的图融合操作,而是使用一些特殊手段对定点信息进行修正,从而使得修正后的定点信息能与硬件融图之后的计算过程保持一致,例如典型的 Conv-Relu 融图中,由于 Conv-Relu 融合后,Conv 算子的输出由 Relu 代替了,因此 PPQ 必须屏蔽掉 Conv 算子的输出量化,才能够保证与硬件执行结果一致。否则 PPQ 将在 Conv 的输出与 Relu 的输出上分别执行两次量化。

Image
PPQ Graph Merge & Joint Quantization

为了表达这一图融合,PPQ 会用 Relu.Output 的量化信息覆盖 Conv.Output 的量化信息,同时将 Conv.Output 的状态置为 OVERLAPPED。此时视作 Relu.Output 与 Conv.Output 联合定点,定点信息共享,此时 Relu.Output 的定点信息称为主定点信息。类似地我们还对 Conv-Clip, Conv-Add, Conv-Concat 等组合形式执行同样的操作。你需要注意,在默认配置下,图中一半以上的Tensor Quantization Config将参与图融合与联合定点。

在执行联合定点时,PPQ 将利用 Tensor Quantization Config 中的 dominator 属性,确定联合定点的主定点信息,该属性由并查集维护,确保更大规模的递归联合定点可以实现,同时主定点信息只存在一个。