QConfig 详解

定义及原理

QConfig 的定义

qconfig(Quantization Configuration)指的是量化配置,是深度学习模型量化过程中的关键参数集合,模型的量化方式由 qconfig 决定,在准备 qat / calibration 模型之前,需要先给模型设置 qconfig。

注意

因历史原因,Plugin 中有不同 qconfig 的定义和用法,早期版本的 qconfig 将在不久的将来被废弃,我们只推荐您使用此文档中介绍的 qconfig 用法。

一个 qconfig 对象可以设置 input / weight / output 三个关键字,分别表示算子输入/权重/输出的量化配置,prepare 模型时会根据这些配置决定是否要在对应位置插入 FakeQuantize / FakeCast 节点,None 表示不插入任何节点。

import torch
from horizon_plugin_pytorch.quantization.qconfig import QConfig
from horizon_plugin_pytorch.quantization.fake_quantize import FakeQuantize
from horizon_plugin_pytorch.quantization.fake_cast import FakeCast
from horizon_plugin_pytorch.quantization.observer_v2 import MinMaxObserver
from horizon_plugin_pytorch.dtype import qint8

qconfig = QConfig(
    input=None,
    weight=FakeQuantize.with_args(
        observer=MinMaxObserver,
        dtype=qint8,
        qscheme=torch.per_channel_symmetric,
        ch_axis=0,
    ),
    output=FakeCast.with_args(dtype=torch.float16),
    # activation=xxx 早期用法,作用与 output 关键字一致,当前仍兼容,但建议您使用 output 关键字。
)

FakeQuantize 的定义

FakeQuantize 是伪量化节点,会对输入进行量化反量化操作,插入伪量化可以在浮点模型的前向中模拟量化产生的误差。horizon_plugin_pytorch 支持 FakeQuantize / PACTFakeQuantize / _LearnableFakeQuantize 三种伪量化,我们只推荐您使用基于统计的 FakeQuantize,可以满足绝大部分需求。标准流程不对 PACTFakeQuantize 和 _LearnableFakeQuantize 两种方法做详细说明,如果一定有需求,请在阅读相关论文后再使用。

# 基于统计的方法
from horizon_plugin_pytorch.quantization.fake_quantize import FakeQuantize
# https://arxiv.org/pdf/1805.06085
from horizon_plugin_pytorch.quantization.pact_fake_quantize import PACTFakeQuantize
# https://arxiv.org/pdf/1902.08153
from horizon_plugin_pytorch.quantization._learnable_fake_quantize import _LearnableFakeQuantize

可以调用 FakeQuantize 的 with_args 方法得到构造器,并按上一节的代码示例用它构造 qconfig。with_args 的参数包括 FakeQuantize 和 observer 支持配置的参数,理论上可以配置所有 FakeQuantize 和 observer 类 __init__ 方法声明中的参数,但为了屏蔽无关紧要的细节,我们只推荐您配置 observer 相关参数。

不同 observer 的参数不同,下面列出常用 observer 构造 FakeQuantize 的例子,其他 observer 的具体用法见校准章节。

import torch
from horizon_plugin_pytorch.quantization.qconfig import QConfig
from horizon_plugin_pytorch.quantization.fake_quantize import FakeQuantize
from horizon_plugin_pytorch.quantization.observer_v2 import MinMaxObserver, FixedScaleObserver, MSEObserver
from horizon_plugin_pytorch.dtype import qint8

# MinMaxObserver 的 __init__ 方法包含很多参数,with_args 方法可以控制这些参数。
# 我们只推荐您设置 fq_constructor_1 示例中的几个参数。
# def __init__(
#     self,
#     averaging_constant: float = 0.01,
#     ch_axis: int = -1,
#     dtype: Union[torch.dtype, QuantDType] = qint8,
#     qscheme: torch.qscheme = torch.per_tensor_symmetric,
#     quant_min: int = None,
#     quant_max: int = None,
#     is_sync_quantize: bool = False,
#     factory_kwargs: Dict = None,
# ) -> None:

fq_constructor_1 = FakeQuantize.with_args(
    observer=MinMaxObserver,   # 适用于 qat 阶段的 input / output / weight 和 calibration 阶段的 weight。
    averaging_constant=0.01,   # calibration 后进行 qat 时,可将 input / output 的 averaging_constant 置为 0 以固定 scale。
    dtype=qint8,  # 量化类型,考虑算子的支持情况进行设置。
    qscheme=torch.per_channel_symmetric,  # 只有 weight 支持 per channel 量化。
    ch_axis=0,  # per channel 量化时指定 channel。
)

# 同理,您也可以查看 FixedScaleObserver 和 MSEObserver 的 __init__ 方法了解有哪些可以设置的参数。
fq_constructor_2 = FakeQuantize.with_args(
    observer=FixedScaleObserver,  # 固定 scale,无论何种情况都不会变。
    dtype=qint8,  # 量化类型,考虑算子的支持情况进行设置。
    scale=INPUT_ABS_MAX / 128,  # 设定的 scale 值,一般设为绝对值最大值除以量化类型最大值。
)

fq_constructor_3 = FakeQuantize.with_args(
    observer=MSEObserver,   # 适用于 calibration 阶段的 input / output。
    dtype=qint8,  # 量化类型,考虑算子的支持情况进行设置。
)

qconfig = QConfig(
    weight=fq_constructor_x,
    ...
)

FakeCast 的定义

FakeCast 是伪转换节点,会将输入转换为 float32 类型,如果数据类型是 float16,那么还会在中间模拟转 float16 产生的截断误差,此节点主要用于标志需要浮点计算的算子。

使用 FakeCast 构造 qconfig 的方法与 FakeQuantize 类似,但只有 dtype 一个参数。

import torch
from horizon_plugin_pytorch.quantization.qconfig import QConfig
from horizon_plugin_pytorch.quantization.fake_cast import FakeCast

qconfig = QConfig(
    input=FakeCast.with_args(dtype=torch.float16), # 考虑算子的支持情况进行设置。
    ...
)

构造 QConfig

在构造 Qconfig 时,有两种方法供您选择:

  • 按照上文介绍的定义方法,直接构造 QConfig 对象。这种方法比较灵活,可以配置任何可配置的参数,但同时要求也比较高,需要您对 QConfig 有一定的理解。

  • 使用 get_qconfig 接口构造 Qconfig。此接口较直接构造 QConfig 对象的方法更简单易用,但不够灵活,高级用法和需求无法使用此接口实现。

import torch
from horizon_plugin_pytorch.quantization import get_qconfig
from horizon_plugin_pytorch.quantization.observer_v2 import MinMaxObserver
from horizon_plugin_pytorch.quantization.qconfig import QConfig
from horizon_plugin_pytorch.quantization.fake_quantize import FakeQuantize
from horizon_plugin_pytorch.dtype import qint8

# qconfig_1 / qconfig_2 / qconfig_3 / qconfig_4 等价。
qconfig_1 = QConfig(
    weight=FakeQuantize.with_args(
        observer=MinMaxObserver,
        averaging_constant=0.01,
        dtype=qint8,
        qscheme=torch.per_channel_symmetric,
        ch_axis=0,
    ),
    output=FakeQuantize.with_args(
        observer=MinMaxObserver,
        averaging_constant=0,
        dtype=qint8,
        qscheme=torch.per_tensor_symmetric,
        ch_axis=-1,
    ),
)

qconfig_2 = QConfig(
    weight=FakeQuantize.with_args(
        observer=MinMaxObserver,
        qscheme=torch.per_channel_symmetric,
        ch_axis=0,
    ),
    output=FakeQuantize.with_args(
        observer=MinMaxObserver,
        averaging_constant=0,
    ),
)

qconfig_3 = get_qconfig(
    observer=MinMaxObserver,   # 输入输出 observer 类型,只支持 horizon_plugin_pytorch.quantization.observer_v2 中的 MinMaxObserver 和 MSEObserver,默认值为 MinMaxObserver。
    in_dtype=None,  # 输入数据类型,考虑算子的支持情况进行设置。None 表示 QConfig 的 input 关键字为 None,默认值为 None。
    weight_dtype=qint8,  # 权重数据类型,考虑算子的支持情况进行设置。None 表示 QConfig 的 weight 关键字为 None,默认值为 qint8。
    out_dtype=qint8,  # 输出数据类型,考虑算子的支持情况进行设置。None 表示 QConfig 的 output 关键字为 None,默认值为 qint8。
    fix_scale=True,   # 是否固定输入输出 scale。
)

qconfig_4 = get_qconfig(fix_scale=True)

通过 QconfigSetter 进行 qconfig 配置

QconfigSetter 根据模型的计算图,按设定的规则自动设置 qconfig,是我们最推荐的设置 qconfig 方法。使用 QconfigSetter 依赖 prepare 过程的图模式,用法如下:

from horizon_plugin_pytorch.quantization import prepare, PrepareMethod, get_qconfig
from horizon_plugin_pytorch.quantization.qconfig_setter import *

qat_model = prepare(
    model,
    example_inputs=example_inputs,  # 图模式需要提供模型输入,用于获取计算图
    qconfig_setter=QconfigSetter(
        reference_qconfig=get_qconfig(),  # 用于提供 observer 的 qconfig
        templates=[<Templates>],  # 用户配置的模板
        enable_optimize=True,  # 开启默认的各项优化
        save_dir="./qconfig_setting",  # qconfig 配置结果(qconfig.pt 文件)和 changelog 的保存路径
    ),
    method=PrepareMethod.JIT_STRIP,  # QconfigSetter 依赖计算图
)

模板说明

您可配置的模板如下:

  1. ModuleNameTemplate(必要,需要覆盖到全部量化算子):通过 module name 指定 dtype 配置或量化阈值。

  2. ConvDtypeTemplate(必要):指定 Conv 类算子的 input 和 weight dtype。

  3. MatmulDtypeTemplate(必要):指定 Matmul 算子的 input dtype。

  4. SensitivityTemplate(可选): 根据敏感度将 top-n 的算子配置为高精度。

  5. LoadFromFileTemplate:加载 qconfig.pt 文件,用于复现之前的量化配置。此时 enable_optimize 必须为 False,否则配置结果的正确性无法保证,部署时可能存在 cpu 算子

这些模板按配置顺序生效,前面模板的配置可被后面的模板覆盖。

ModuleNameTemplate 详解

  1. module name 可以为算子名或 prefix。当一个 ModuleNameTemplate 中不同的 module name 存在覆盖关系时,越长的名字优先级越高,例如:

    ModuleNameTemplate(
        {
            "": qint8,  # 全局 qint8
            "head": qint16,  # 优先级高过全局配置,head 最终配置为 int16
            "head.conv0": torch.float16,  # 优先级高过 head 的配置,head.conv 最终配置为输出 float16
        }
    )
  2. 可以指定算子的 threshold(前提是算子中存在对应的伪量化节点),此时量化 scale 的计算方式为 scale = threshold / -qdtype.min,例如:

    ModuleNameTemplate(
        {
            "quant": {"dtype": qint8, "threshold": 1.0},  # quant 的量化 scale 为 1.0/128
        }
    )
  3. dtype 和 threshold 默认配置到算子的输出上,可通过指定 key 的方式配置 input 或 weight,在算子有多个输入时,可使用 None 做占位符,例如:

    ModuleNameTemplate(
        {
            "conv0": {"input": qint8, "weight": qint16},
            "conv1": {"dtype": {"input": qint16}, "threshold": {"weight": 1.0}},
            "matmul0": {"dtype": {"input": [qint16, None]}, "threshold": {"input": [1.0, None]}},  # 将 matmul0 的第一个输入配置为 int16 且固定 scale 为 1.0/32768,第二个输入不做配置
        }
    )

场景示例

  1. 全 int8:

    QconfigSetter(
        reference_qconfig=get_qconfig(),
        templates=[
            ModuleNameTemplate({"": qint8}),  # 全部算子配置为 int8 输出
            ConvDtypeTemplate(input_dtype=qint8, weight_dtype=qint8),  # conv 的 input 和 weight 配置为 int8
            MatmulDtypeTemplate(input_dtypes=qint8),  # matmul 两个输入均配置为 int8
        ],
    )
  2. feature int16, weight int8:

    QconfigSetter(
        reference_qconfig=get_qconfig(),
        templates=[
            ModuleNameTemplate({"": qint16}),  # 全部算子配置为 int16 输出
            ConvDtypeTemplate(input_dtype=qint16, weight_dtype=qint8),  # conv 的 input 配置为 int16, weight 配置为 int8
            MatmulDtypeTemplate(input_dtypes=qint16),  # matmul 两个输入均配置为 int16
        ],
    )
  3. gemm 算子双 int8,其他算子 fp16:

    QconfigSetter(
        reference_qconfig=get_qconfig(),
        templates=[
            ModuleNameTemplate({"": torch.float16}),  # 全部算子配置为 fp16 输出
            ConvDtypeTemplate(input_dtype=qint8, weight_dtype=qint8),  # conv 的 input 和 weight 配置为 int8
            MatmulDtypeTemplate(input_dtypes=qint8),  # matmul 两个输入均配置为 int8
        ],
    )
  4. gemm 算子双 int8,其他算子 int16, 并将高敏感度 gemm 配置为 int16:

    QconfigSetter(
        reference_qconfig=get_qconfig(),
        templates=[
            ModuleNameTemplate({"": qint16}),  # 全部算子配置为 int16 输出
            ConvDtypeTemplate(input_dtype=qint8, weight_dtype=qint8),  # conv 的 input 和 weight 配置为 int8
            MatmulDtypeTemplate(input_dtypes=qint8),  # matmul 两个输入均配置为 int8
            SensitivityTemplate(  # 若敏感度较高的 feat 或 weight 配置为 int8,则将其修改为 int16
                    sensitive_table=...,
                    topk_or_ratio=...,
                ),
        ],
    )

默认优化 pass 说明

除您可配置的模板外,QconfigSetter 还集成了一系列优化和合法化模板,在本章节进行说明。

  1. CanonicalizeTemplate: 按算子类型对 dtype 配置进行合法化,当前默认规则有:

    • Gemm 类算子不支持 float 输入(含 weight)。

    • 插值类算子:默认使用 int8 (当前 nash-e/m/p/h 平台上的插值类算子虽支持了 fp16 和 int16,但性能较差,所以模板默认仍使用了 int8。若插值算子需要使用其他 dtype,请使用 ModuleNameTemplate(..., freeze=True) 将它的上一个算子的输出配置为需要的 dtype)。

    • DPP、RPP 等特殊算子仅支持 int8。

    • 其他算子的通用规则:算子的 input dtype 和 output dtype 不能同时存在 qint 和 float。

  2. EqualizeInOutScaleTemplate:对于 relu,concat,stack 算子,应该在算子之后统计 scale,否则精度或性能存在损失。为此:

    • 将前面算子的 output dtype 配置为 float32。

    • Relu,concat,stack 算子在 export hbir 时,在 input 处插入伪量化,scale 复用 output scale。

  3. FuseConvAddTemplate:硬件支持 conv + add 的 fuse,为此:

    • 将 conv 的 output dtype 配置为 float32。

    • 将 add 对应的 input dtype 配置为 float32。

  4. GridHighPrecisionTemplate:根据经验,grid sample 的 grid 计算过程用 qint8 精度不够,因此自动将相关算子配置为高精度。

  5. InternalQuantsTemplate:模型分段部署场景下,会在分段点处插入 QuantStub,用于记录此处的 dtype 和 scale,此类 QuantStub 的 dtype 配置必须和输入保持一致。

  6. OutputHighPrecisionTemplate:当 Gemm 类算子作为模型输出时,将其配置为高精度输出。

  7. PropagateTemplate:对于拆分为子图实现的算子,存在经验性配置,如 LayerNormSoftmax 内部小算子应该使用高精度。

  8. SimpleIntPassTemplate:性能优化,对于 op0->op1->op2 此类计算图,若以下条件同时成立,则将 op1 输出类型修改为 int:

    • op2 需要 int 输入。

    • op0 可以输出 int。

    • op1 当前输出为 float16,且属于以下类型:

      • cat,stack。

      • mul_scalar。

      • 无精度风险的查表算子(即在 fp16 上默认使用查表实现的算子)。

  9. SimplifyTemplate:删除多余的量化节点配置(将对应的 dtype 修改为 None)。