构建浮点模型

量化原理

模型量化的核心思想是:用低精度的数值(例如 INT8)来表示和计算原本用高精度数值(例如 FP32)训练的模型参数和激活值。

量化一个张量的基本步骤如下:

  • 统计该张量的数值范围,从而确定与之对应的缩放系数(scale)。
  • 使用缩放系数对张量进行缩放,使缩放后的数值范围落在低精度 dtype 可以表示的区间,然后 cast 到低精度的 dtype。 以 int8 量化为例,计算过程为:quantized_x=clip(round(x/scale),128,127)quantized\_x = clip(round(x / scale), -128, 127)

静态量化 VS 动态量化

静态量化需要在数据集上统计模型各处激活值的分布,为每处激活计算固定的缩放系数(此过程称为“校准”或“calibration”);动态量化不需要校准过程,而是在模型推理过程中实时统计当前张量的数值范围,实时计算缩放系数并完成到低精度的转换。

注解

当前工具链仅支持静态量化,为此工具需要在模型中插入统计节点,详见 Prepare说明

量化感知训练(Quantized Awareness Training)

为了弥补模型量化带来的精度损失,我们可以在模型 forward 过程中模拟低精度计算,在此条件下对模型进行 finetune 以提升模型在低精度计算下的精度指标,此过程称为量化感知训练。

注解

模拟低精度计算的过程通过在模型中插入伪量化节点实现,详见 Prepare说明FakeQuantize 的定义

支持算子范围

基于 BPU 对算子的限制,我们仅支持 工具链算子支持约束列表 章节所列出的算子以及基于 BPU 限制而内部特殊定义的特殊算子。

构建浮点模型

在获取浮点模型后,我们需要对浮点模型进行必要的改造,以使其能够支持量化相关操作。模型改造必要的操作包括:

  • 在模型输入前插入 QuantStub 节点,在模型输出后插入 DequantStub 节点。
  • 对于模型 forward 逻辑中存在的次数可变的循环(如时序模型中逐帧的计算),使用 horizon_plugin_pytorch.fx.jit_scheme.dynamic_block 接口标记(此接口的具体作用在 Prepare说明 介绍)。
注意

插入 QuantStubDequantStub 时需要遵循以下原则: 

  • 仅关注需要量化的数据(即浮点类型的数据),其他原生的定点型或布尔型数据不需要插入 Stub。
  • QuantStubDequantStub 之间的计算,即为工具进行量化的范围。通常模型的辅助头和 loss 等并不需要部署,因此无需量化,需要确保这些逻辑不在量化范围。
  • 需要特别注意,模型中可能存在一些定点类型与浮点类型之间的计算,此时 torch 会自动将计算过程和输出 cast 为浮点。但是对于量化工具来说,此处也是“量化与非量化的边界”,您需要先将定点输入手动 cast 为浮点,插入 QuantStub,再进行与另一浮点数的计算。同理,量化的浮点值在 cast 为定点之前,需要先经过 DequantStub

改造模型时需要注意:

  • 插入的 QuantStubDequantStub 节点必须注册为模型的子模块,否则将无法正确处理它们的量化状态。
  • 多个输入仅在数值分布相同时可以共享 QuantStub,否则请为每个输入定义单独的 QuantStub
  • 若需要将上板时输入的数据来源指定为 "pyramid",请手动设置对应 QuantStubscale 参数为 1/128
  • 您也可以使用 torch.quantization.QuantStub,但是仅有 horizon_plugin_pytorch.quantization.QuantStub 支持通过参数手动固定 scale。
注解

在此提供一个简易的多输入多输出示例,注意每个 QuantStub 负责一个输入 Tensor 的量化。

class Net(torch.nn.Module):
    def __init__(self):
        self.quantx = QuantStub()
        self.quanty = QuantStub()
        self.dequant = DequantStub()
        ...

    def forward(self, x, y):
        x = self.quantx(x)
        y = self.quanty(y)
        ...
        ret_0 = self.dequant(ret_0)
        ret_1 = self.dequant(ret_1)
        return ret_0, ret_1

改造后的模型可以无缝加载改造前模型的参数,因此若已有训练好的浮点模型,直接加载即可,否则需要正常进行浮点训练。

完成改造后,不要立即开始校准和量化感知训练,优先走通完整部署流程,确认模型是否包含 CPU 算子

注意

由于模型上板时的输入图像数据一般为 centered_yuv444 格式,因此模型训练时需要把图像转换成 centered_yuv444 格式(注意下面代码中对 rgb2centered_yuv 的使用)。

如果无法转换成 centered_yuv444 格式进行模型训练,请在模型部署时在输入上插入适当的颜色空间转换节点。(注意,该方法可能导致模型精度降低)

构建量化友好模型

浮点模型变为定点模型的过程存在一定的精度误差,越是量化友好的浮点模型,其量化感知训练越容易,量化后的精度也越高。一般而言,有以下几种情况会导致模型变得量化不友好:

  1. 使用有精度风险的算子。例如:softmax,layernorm 等,这类算子一般底层由查表或多个 op 拼接实现,容易发生掉点问题。

  2. 一次 forward 中多次调用同一算子。同一算子多次调用,对应的输出分布存在差异,但只会统计一组量化参数,当多次调用的输出分布差异过大时,量化误差会变大。

  3. add,cat 等多输入算子的不同输入差异过大,可能造成较大误差。

  4. 数据分布不合理。plugin 采用的是均匀对称量化,所以 0 均值的均匀分布最好,应尽量避免长尾和离群点。同时,数值范围需要与量化 bit 相匹配,如果使用 int8 量化分布为 [-1000, 1000] 均匀分布的数据,那么精度显然也是不够的。 例如,下面三个分布图,从左到右对量化的友好性依次递减,模型中大部分数值的分布应当为中间这种分布。在实际使用中,可以用 debug 工具查看模型 weight 和 feature map 的分布是否量化友好。 因为模型冗余性的存在,有些看起来分布非常量化不友好的 op 并不会显著降低模型的最终精度,需要结合实际的 qat 训练难度和最后达到的量化精度综合考虑。

    data_distribution

那么如何使得模型更加量化友好呢?具体来说:

  1. 尽量少使用值域不受限的非线性算子如 reciprocal、exp、pow 等,此类算子输出范围可能过大,导致量化精度低。

  2. 保证多次调用的共享算子每次调用的输出分布差异不要太大,或者将共享算子拆开分别单独使用。

  3. 避免多输入算子不同输入的数值范围差异过大。

  4. 使用 int16 量化数值范围和误差都非常大的 op。可通过 debug 工具找到这类 op。

  5. 通过调大 weight decay,增加数据增强等方式防止模型过拟合。过拟合模型容易出现较大数值,且对输入非常敏感,轻微的误差可能导致输出完全错误。

  6. 使用 BN。

  7. 对模型输入做关于 0 对称的归一化。

需要注意的是,qat 自身具有一定的调整能力,量化不友好并不代表不能量化,很多情况下,即使出现上面的不适合量化的现象,仍然可以量化得很好。因为上述建议也可能会导致浮点模型精度下降,所以应当在 qat 精度无法达标时再尝试上述建议,尤其是 1 - 5 条建议,最后应当是在浮点模型精度和量化模型精度中找一个平衡点。