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)。

    • 插值类算子:在不同 march 下有不同的限制。

    • 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)。