快捷方式

量化

警告

量化处于测试阶段,可能会发生变化。

量化简介

量化是指以比浮点精度更低的位宽执行计算和存储张量的技术。量化模型在比全精度(浮点)值精度更低的张量上执行部分或全部操作。这使得模型表示更加紧凑,并且能够在许多硬件平台上使用高性能矢量化操作。PyTorch 支持 INT8 量化,与典型的 FP32 模型相比,模型大小减少了 4 倍,内存带宽需求减少了 4 倍。INT8 计算的硬件支持通常比 FP32 计算快 2 到 4 倍。量化主要是一种加速推理的技术,并且仅支持量化运算符的正向传递。

PyTorch 支持多种方法来量化深度学习模型。在大多数情况下,模型是在 FP32 中训练的,然后转换为 INT8。此外,PyTorch 还支持量化感知训练,该训练使用伪量化模块在正向和反向传递中模拟量化误差。请注意,整个计算是在浮点中进行的。在量化感知训练结束时,PyTorch 提供转换函数以将训练后的模型转换为更低精度。

在更低层面上,PyTorch 提供了一种表示量化张量并对其执行运算的方法。它们可用于直接构建模型,这些模型以更低精度执行部分或全部计算。提供了更高级别的 API,这些 API 集成了将 FP32 模型转换为更低精度而不会损失太多精度的典型工作流程。

量化 API 摘要

PyTorch 提供三种不同的量化模式:急切模式量化、FX 图模式量化(维护模式)和 PyTorch 2 导出量化。

急切模式量化是一个测试功能。用户需要手动进行融合并指定量化和反量化发生的位置,而且它只支持模块,不支持函数。

FX 图模式量化是 PyTorch 中的自动化量化工作流程,目前它是一个原型功能,自我们拥有 PyTorch 2 导出量化以来,它处于维护模式。它通过添加对函数的支持和自动化量化过程来改进急切模式量化,尽管用户可能需要重构模型以使其与 FX 图模式量化兼容(使用 torch.fx 可符号化追踪)。请注意,FX 图模式量化不能在任意模型上运行,因为模型可能无法符号化追踪,我们将把它集成到像 torchvision 这样的领域库中,用户将能够使用 FX 图模式量化来量化类似于支持领域库中模型的模型。对于任意模型,我们将提供一般性指南,但要真正实现,用户可能需要熟悉 torch.fx,尤其是在如何使模型可符号化追踪方面。

PyTorch 2 导出量化是新的全图模式量化工作流程,作为原型功能在 PyTorch 2.1 中发布。借助 PyTorch 2,我们正在转向更好的全程序捕获解决方案(torch.export),因为它与 torch.fx.symbolic_trace(14K 模型的 72.7%)相比,可以捕获更多模型(14K 模型的 88.8%),FX 图模式量化使用的程序捕获解决方案。torch.export 仍然对某些 Python 结构存在限制,并且需要用户参与才能支持导出模型中的动态性,但总体而言它比以前的程序捕获解决方案有所改进。PyTorch 2 导出量化是为 torch.export 捕获的模型而构建的,同时考虑了建模用户和后端开发人员的灵活性。主要功能包括:(1)用于配置模型量化方式的可编程 API,可以扩展到更多用例;(2)简化的用户体验,建模用户和后端开发人员只需与单个对象(Quantizer)交互,以表达他们对如何量化模型以及后端支持的意图;(3)可选的参考量化模型表示,可以使用整数运算表示量化计算,该运算更接近硬件中发生的实际量化计算。

建议量化的新用户首先尝试 PyTorch 2 导出量化,如果它不能很好地工作,用户可以尝试急切模式量化。

下表比较了急切模式量化、FX 图模式量化和 PyTorch 2 导出量化的差异

急切模式量化

FX 图模式量化

PyTorch 2 导出量化

发布状态

测试

原型(维护模式)

原型

运算符融合

手动

自动

自动

Quant/DeQuant 布置

手动

自动

自动

量化模块

支持

支持

支持

量化函数/Torch 运算

手动

自动

支持

对自定义的支持

有限支持

完全支持

完全支持

量化模式支持

训练后量化:静态、动态、仅权重

量化感知训练:静态

训练后量化:静态、动态、仅权重

量化感知训练:静态

由后端特定的量化器定义

输入/输出模型类型

torch.nn.Module

torch.nn.Module(可能需要一些重构才能使模型与 FX 图模式量化兼容)

torch.fx.GraphModule(由 torch.export 捕获)

支持三种类型的量化

  1. 动态量化(权重量化,激活以浮点读/写,并为计算量化)

  2. 静态量化(权重量化,激活量化,训练后需要校准)

  3. 静态量化感知训练(权重量化,激活量化,训练期间对量化数值建模)

请参阅我们的 PyTorch 量化简介 博客文章,以更全面地了解这些量化类型之间的权衡。

动态量化和静态量化之间的算子覆盖范围有所不同,并在下面的表格中列出。

静态量化

动态量化

nn.Linear
nn.Conv1d/2d/3d
Y
Y
Y
N
nn.LSTM

nn.GRU
Y(通过
自定义模块)
N
Y

Y
nn.RNNCell
nn.GRUCell
nn.LSTMCell
N
N
N
Y
Y
Y

nn.EmbeddingBag

Y(激活以 fp32 为单位)

Y

nn.Embedding

Y

Y

nn.MultiheadAttention

Y(通过自定义模块)

不支持

激活

广泛支持

保持不变,计算保持在 fp32

急切模式量化

有关量化流程的总体介绍,包括不同类型的量化,请查看 通用量化流程

训练后动态量化

这是最容易应用的量化形式,权重在提前量化,但激活在推理期间动态量化。这用于模型执行时间主要由从内存中加载权重而不是计算矩阵乘法所支配的情况。对于批处理大小很小的 LSTM 和 Transformer 型模型,情况就是这样。

图表

# original model
# all tensors and computations are in floating point
previous_layer_fp32 -- linear_fp32 -- activation_fp32 -- next_layer_fp32
                 /
linear_weight_fp32

# dynamically quantized model
# linear and LSTM weights are in int8
previous_layer_fp32 -- linear_int8_w_fp32_inp -- activation_fp32 -- next_layer_fp32
                     /
   linear_weight_int8

PTDQ API 示例

import torch

# define a floating point model
class M(torch.nn.Module):
    def __init__(self):
        super().__init__()
        self.fc = torch.nn.Linear(4, 4)

    def forward(self, x):
        x = self.fc(x)
        return x

# create a model instance
model_fp32 = M()
# create a quantized model instance
model_int8 = torch.ao.quantization.quantize_dynamic(
    model_fp32,  # the original model
    {torch.nn.Linear},  # a set of layers to dynamically quantize
    dtype=torch.qint8)  # the target dtype for quantized weights

# run the model
input_fp32 = torch.randn(4, 4, 4, 4)
res = model_int8(input_fp32)

要详细了解动态量化,请参阅我们的 动态量化教程

训练后静态量化

训练后静态量化(PTQ 静态)量化模型的权重和激活。它尽可能将激活融合到前一层。它需要使用代表性数据集进行校准,以确定激活的最优量化参数。训练后静态量化通常用于内存带宽和计算节省都很重要的情况,其中 CNN 是一个典型的用例。

在应用训练后静态量化之前,我们可能需要修改模型。请参阅 急切模式静态量化的模型准备

图表

# original model
# all tensors and computations are in floating point
previous_layer_fp32 -- linear_fp32 -- activation_fp32 -- next_layer_fp32
                    /
    linear_weight_fp32

# statically quantized model
# weights and activations are in int8
previous_layer_int8 -- linear_with_activation_int8 -- next_layer_int8
                    /
  linear_weight_int8

PTSQ API 示例

import torch

# define a floating point model where some layers could be statically quantized
class M(torch.nn.Module):
    def __init__(self):
        super().__init__()
        # QuantStub converts tensors from floating point to quantized
        self.quant = torch.ao.quantization.QuantStub()
        self.conv = torch.nn.Conv2d(1, 1, 1)
        self.relu = torch.nn.ReLU()
        # DeQuantStub converts tensors from quantized to floating point
        self.dequant = torch.ao.quantization.DeQuantStub()

    def forward(self, x):
        # manually specify where tensors will be converted from floating
        # point to quantized in the quantized model
        x = self.quant(x)
        x = self.conv(x)
        x = self.relu(x)
        # manually specify where tensors will be converted from quantized
        # to floating point in the quantized model
        x = self.dequant(x)
        return x

# create a model instance
model_fp32 = M()

# model must be set to eval mode for static quantization logic to work
model_fp32.eval()

# attach a global qconfig, which contains information about what kind
# of observers to attach. Use 'x86' for server inference and 'qnnpack'
# for mobile inference. Other quantization configurations such as selecting
# symmetric or asymmetric quantization and MinMax or L2Norm calibration techniques
# can be specified here.
# Note: the old 'fbgemm' is still available but 'x86' is the recommended default
# for server inference.
# model_fp32.qconfig = torch.ao.quantization.get_default_qconfig('fbgemm')
model_fp32.qconfig = torch.ao.quantization.get_default_qconfig('x86')

# Fuse the activations to preceding layers, where applicable.
# This needs to be done manually depending on the model architecture.
# Common fusions include `conv + relu` and `conv + batchnorm + relu`
model_fp32_fused = torch.ao.quantization.fuse_modules(model_fp32, [['conv', 'relu']])

# Prepare the model for static quantization. This inserts observers in
# the model that will observe activation tensors during calibration.
model_fp32_prepared = torch.ao.quantization.prepare(model_fp32_fused)

# calibrate the prepared model to determine quantization parameters for activations
# in a real world setting, the calibration would be done with a representative dataset
input_fp32 = torch.randn(4, 1, 4, 4)
model_fp32_prepared(input_fp32)

# Convert the observed model to a quantized model. This does several things:
# quantizes the weights, computes and stores the scale and bias value to be
# used with each activation tensor, and replaces key operators with quantized
# implementations.
model_int8 = torch.ao.quantization.convert(model_fp32_prepared)

# run the model, relevant calculations will happen in int8
res = model_int8(input_fp32)

要详细了解静态量化,请参阅 静态量化教程

静态量化的量化感知训练

量化感知训练 (QAT) 对训练期间的量化影响进行建模,与其他量化方法相比,可实现更高的精度。我们可以对静态、动态或仅权重量化进行 QAT。在训练期间,所有计算都在浮点中完成,fake_quant 模块通过钳位和舍入模拟量化的影响,以模拟 INT8 的影响。在模型转换后,权重和激活将被量化,并且激活将尽可能融合到前一层。它通常与 CNN 一起使用,与静态量化相比,它可以实现更高的精度。

在应用训练后静态量化之前,我们可能需要修改模型。请参阅 急切模式静态量化的模型准备

图表

# original model
# all tensors and computations are in floating point
previous_layer_fp32 -- linear_fp32 -- activation_fp32 -- next_layer_fp32
                      /
    linear_weight_fp32

# model with fake_quants for modeling quantization numerics during training
previous_layer_fp32 -- fq -- linear_fp32 -- activation_fp32 -- fq -- next_layer_fp32
                           /
   linear_weight_fp32 -- fq

# quantized model
# weights and activations are in int8
previous_layer_int8 -- linear_with_activation_int8 -- next_layer_int8
                     /
   linear_weight_int8

QAT API 示例

import torch

# define a floating point model where some layers could benefit from QAT
class M(torch.nn.Module):
    def __init__(self):
        super().__init__()
        # QuantStub converts tensors from floating point to quantized
        self.quant = torch.ao.quantization.QuantStub()
        self.conv = torch.nn.Conv2d(1, 1, 1)
        self.bn = torch.nn.BatchNorm2d(1)
        self.relu = torch.nn.ReLU()
        # DeQuantStub converts tensors from quantized to floating point
        self.dequant = torch.ao.quantization.DeQuantStub()

    def forward(self, x):
        x = self.quant(x)
        x = self.conv(x)
        x = self.bn(x)
        x = self.relu(x)
        x = self.dequant(x)
        return x

# create a model instance
model_fp32 = M()

# model must be set to eval for fusion to work
model_fp32.eval()

# attach a global qconfig, which contains information about what kind
# of observers to attach. Use 'x86' for server inference and 'qnnpack'
# for mobile inference. Other quantization configurations such as selecting
# symmetric or asymmetric quantization and MinMax or L2Norm calibration techniques
# can be specified here.
# Note: the old 'fbgemm' is still available but 'x86' is the recommended default
# for server inference.
# model_fp32.qconfig = torch.ao.quantization.get_default_qconfig('fbgemm')
model_fp32.qconfig = torch.ao.quantization.get_default_qat_qconfig('x86')

# fuse the activations to preceding layers, where applicable
# this needs to be done manually depending on the model architecture
model_fp32_fused = torch.ao.quantization.fuse_modules(model_fp32,
    [['conv', 'bn', 'relu']])

# Prepare the model for QAT. This inserts observers and fake_quants in
# the model needs to be set to train for QAT logic to work
# the model that will observe weight and activation tensors during calibration.
model_fp32_prepared = torch.ao.quantization.prepare_qat(model_fp32_fused.train())

# run the training loop (not shown)
training_loop(model_fp32_prepared)

# Convert the observed model to a quantized model. This does several things:
# quantizes the weights, computes and stores the scale and bias value to be
# used with each activation tensor, fuses modules where appropriate,
# and replaces key operators with quantized implementations.
model_fp32_prepared.eval()
model_int8 = torch.ao.quantization.convert(model_fp32_prepared)

# run the model, relevant calculations will happen in int8
res = model_int8(input_fp32)

要详细了解量化感知训练,请参阅 QAT 教程

急切模式静态量化的模型准备

目前,在进行急切模式量化之前,有必要对模型定义进行一些修改。这是因为当前量化是在模块级别进行的。具体来说,对于所有量化技术,用户需要

  1. 将任何需要输出重新量化(因此具有附加参数)的操作从函数形式转换为模块形式(例如,使用 torch.nn.ReLU 而不是 torch.nn.functional.relu)。

  2. 指定模型的哪些部分需要量化,方法是在子模块上分配 .qconfig 属性,或者指定 qconfig_mapping。例如,设置 model.conv1.qconfig = None 表示 model.conv 层不会被量化,设置 model.linear1.qconfig = custom_qconfig 表示 model.linear1 的量化设置将使用 custom_qconfig 而不是全局 qconfig。

对于量化激活的静态量化技术,用户需要额外执行以下操作

  1. 指定激活量化和反量化的位置。这是通过使用 QuantStubDeQuantStub 模块实现的。

  2. 使用 FloatFunctional 将需要针对量化进行特殊处理的张量操作包装到模块中。例如,操作 addcat 需要特殊处理来确定输出量化参数。

  3. 融合模块:将操作/模块合并到单个模块中,以获得更高的精度和性能。这是使用 fuse_modules() API 实现的,该 API 接收要融合的模块列表。我们目前支持以下融合:[Conv, Relu]、[Conv, BatchNorm]、[Conv, BatchNorm, Relu]、[Linear, Relu]

(原型 - 维护模式)FX 图模式量化

训练后量化中有多种量化类型(仅权重、动态和静态),配置是通过 qconfig_mappingprepare_fx 函数的参数)完成的。

FXPTQ API 示例

import torch
from torch.ao.quantization import (
  get_default_qconfig_mapping,
  get_default_qat_qconfig_mapping,
  QConfigMapping,
)
import torch.ao.quantization.quantize_fx as quantize_fx
import copy

model_fp = UserModel()

#
# post training dynamic/weight_only quantization
#

# we need to deepcopy if we still want to keep model_fp unchanged after quantization since quantization apis change the input model
model_to_quantize = copy.deepcopy(model_fp)
model_to_quantize.eval()
qconfig_mapping = QConfigMapping().set_global(torch.ao.quantization.default_dynamic_qconfig)
# a tuple of one or more example inputs are needed to trace the model
example_inputs = (input_fp32)
# prepare
model_prepared = quantize_fx.prepare_fx(model_to_quantize, qconfig_mapping, example_inputs)
# no calibration needed when we only have dynamic/weight_only quantization
# quantize
model_quantized = quantize_fx.convert_fx(model_prepared)

#
# post training static quantization
#

model_to_quantize = copy.deepcopy(model_fp)
qconfig_mapping = get_default_qconfig_mapping("qnnpack")
model_to_quantize.eval()
# prepare
model_prepared = quantize_fx.prepare_fx(model_to_quantize, qconfig_mapping, example_inputs)
# calibrate (not shown)
# quantize
model_quantized = quantize_fx.convert_fx(model_prepared)

#
# quantization aware training for static quantization
#

model_to_quantize = copy.deepcopy(model_fp)
qconfig_mapping = get_default_qat_qconfig_mapping("qnnpack")
model_to_quantize.train()
# prepare
model_prepared = quantize_fx.prepare_qat_fx(model_to_quantize, qconfig_mapping, example_inputs)
# training loop (not shown)
# quantize
model_quantized = quantize_fx.convert_fx(model_prepared)

#
# fusion
#
model_to_quantize = copy.deepcopy(model_fp)
model_fused = quantize_fx.fuse_fx(model_to_quantize)

请按照以下教程详细了解 FX 图模式量化

(原型)PyTorch 2 导出量化

API 示例

import torch
from torch.ao.quantization.quantize_pt2e import prepare_pt2e
from torch._export import capture_pre_autograd_graph
from torch.ao.quantization.quantizer import (
    XNNPACKQuantizer,
    get_symmetric_quantization_config,
)

class M(torch.nn.Module):
    def __init__(self):
        super().__init__()
        self.linear = torch.nn.Linear(5, 10)

   def forward(self, x):
       return self.linear(x)

# initialize a floating point model
float_model = M().eval()

# define calibration function
def calibrate(model, data_loader):
    model.eval()
    with torch.no_grad():
        for image, target in data_loader:
            model(image)

# Step 1. program capture
# NOTE: this API will be updated to torch.export API in the future, but the captured
# result should mostly stay the same
m = capture_pre_autograd_graph(m, *example_inputs)
# we get a model with aten ops

# Step 2. quantization
# backend developer will write their own Quantizer and expose methods to allow
# users to express how they
# want the model to be quantized
quantizer = XNNPACKQuantizer().set_global(get_symmetric_quantization_config())
# or prepare_qat_pt2e for Quantization Aware Training
m = prepare_pt2e(m, quantizer)

# run calibration
# calibrate(m, sample_inference_data)
m = convert_pt2e(m)

# Step 3. lowering
# lower to target backend

请按照以下教程开始使用 PyTorch 2 导出量化

建模用户

后端开发人员(请查看所有建模用户文档)

量化堆栈

量化是将浮点模型转换为量化模型的过程。因此,在高层次上,量化堆栈可以分为两部分:1). 量化模型的构建块或抽象 2). 将浮点模型转换为量化模型的量化流程的构建块或抽象

量化模型

量化张量

为了在 PyTorch 中进行量化,我们需要能够在张量中表示量化数据。量化张量允许存储量化数据(表示为 int8/uint8/int32)以及量化参数,如比例和零点。量化张量允许进行许多有用的操作,使量化运算变得容易,此外还允许以量化格式序列化数据。

PyTorch 支持每张量和每通道对称和非对称量化。每张量表示张量中所有值使用相同的量化参数以相同的方式进行量化。每通道表示对于每个维度(通常是张量的通道维度),张量中的值使用不同的量化参数进行量化。这允许将张量转换为量化值时误差更小,因为异常值只会影响它所在的通道,而不是整个张量。

映射是通过使用以下方法转换浮点张量来执行的

_images/math-quantizer-equation.png

请注意,我们确保浮点中的零在量化后以无误差的方式表示,从而确保填充等操作不会导致额外的量化误差。

以下是量化张量的一些关键属性

  • QScheme(torch.qscheme):指定我们对张量进行量化方式的枚举

    • torch.per_tensor_affine

    • torch.per_tensor_symmetric

    • torch.per_channel_affine

    • torch.per_channel_symmetric

  • dtype(torch.dtype):量化张量的数据类型

    • torch.quint8

    • torch.qint8

    • torch.qint32

    • torch.float16

  • 量化参数(根据 QScheme 变化):所选量化方式的参数

    • torch.per_tensor_affine 将具有以下量化参数

      • scale(浮点数)

      • zero_point(整数)

    • torch.per_channel_affine 将具有以下量化参数

      • per_channel_scales(浮点数列表)

      • per_channel_zero_points(整数列表)

      • axis(整数)

量化和反量化

模型的输入和输出是浮点张量,但量化模型中的激活是量化的,因此我们需要操作符来在浮点张量和量化张量之间进行转换。

  • 量化(float -> 量化)

    • torch.quantize_per_tensor(x, scale, zero_point, dtype)

    • torch.quantize_per_channel(x, scales, zero_points, axis, dtype)

    • torch.quantize_per_tensor_dynamic(x, dtype, reduce_range)

    • to(torch.float16)

  • 反量化(量化 -> float)

    • quantized_tensor.dequantize() - 在 torch.float16 张量上调用 dequantize 将转换张量回 torch.float

    • torch.dequantize(x)

量化运算符/模块

  • 量化算子是将量化张量作为输入,并输出量化张量的算子。

  • 量化模块是执行量化操作的 PyTorch 模块。它们通常用于定义带权重的操作,例如线性运算和卷积运算。

量化引擎

当执行量化模型时,qengine(torch.backends.quantized.engine)指定要用于执行的后台。重要的是要确保 qengine 在量化激活和权重的值范围内与量化模型兼容。

量化流程

观察器和假量化

  • 观察器是用于

    • 收集张量统计信息,例如通过观察器的张量的最小值和最大值

    • 并根据收集的张量统计信息计算量化参数

  • 假量化是用于

    • 模拟网络中张量的量化(执行量化/反量化)

    • 它可以根据从观察器收集的统计信息计算量化参数,也可以学习量化参数

QConfig

  • QConfig 是观察器或假量化模块类的命名元组,可以用 qscheme、dtype 等进行配置。它用于配置如何观察算子。

    • 算子/模块的量化配置。

      • 不同类型的观察器/假量化。

      • dtype

      • qscheme

      • quant_min/quant_max:可用于模拟低精度张量。

    • 目前支持对激活和权重的配置。

    • 根据为给定算子或模块配置的 qconfig,我们将插入输入/权重/输出观察器。

通用量化流程

一般来说,流程如下

  • 准备

    • 根据用户指定的 qconfig 插入观察器/假量化模块。

  • 校准/训练(取决于训练后量化或量化感知训练)。

    • 允许观察器收集统计信息或假量化模块学习量化参数。

  • 转换

    • 将校准/训练后的模型转换为量化模型。

量化模式有多种,可以从两种方式进行分类。

在应用量化流程的位置方面,我们有

  1. 训练后量化(在训练后应用量化,量化参数根据样本校准数据计算得出)。

  2. 量化感知训练(在训练期间模拟量化,以便可以使用训练数据将量化参数与模型一起学习)。

在如何量化算子的方面,我们可以有

  • 仅权重量化(仅权重被静态量化)。

  • 动态量化(权重被静态量化,激活被动态量化)。

  • 静态量化(权重和激活都被静态量化)。

可以在同一个量化流程中混合不同的算子量化方式。例如,我们可以进行训练后量化,其中既有静态量化的算子,也有动态量化的算子。

量化支持矩阵

量化模式支持

量化模式

数据集要求

最适合

准确率

备注

训练后量化

动态/仅权重量化

激活动态量化(fp16、int8)或未量化,权重静态量化(fp16、int8、in4)。

LSTM、MLP、嵌入、Transformer。

良好

易于使用,当性能受权重计算或内存限制时,接近静态量化。

静态量化

激活和权重静态量化(int8)。

校准数据集

CNN

良好

提供最佳性能,可能会对准确性产生重大影响,适用于仅支持 int8 计算的硬件。

量化感知训练

动态量化

激活和权重被假量化。

微调数据集

MLP、嵌入。

最佳

目前支持有限。

静态量化

激活和权重被假量化。

微调数据集

CNN、MLP、嵌入。

最佳

通常在静态量化导致准确率下降时使用,用于缩小准确率差距。

请查看我们的 Pytorch 量化简介 博客文章,更全面地了解这些量化类型之间的权衡。

量化流程支持

PyTorch 提供两种量化模式:急切模式量化和 FX 图模式量化。

急切模式量化是一个测试功能。用户需要手动进行融合并指定量化和反量化发生的位置,而且它只支持模块,不支持函数。

FX 图模式量化是 PyTorch 中的自动化量化框架,目前它是一个原型功能。它通过添加对函数的支持和自动化量化过程来改进急切模式量化,尽管人们可能需要重构模型以使模型与 FX 图模式量化(可以使用 torch.fx 符号跟踪)兼容。请注意,FX 图模式量化不能在任意模型上使用,因为模型可能无法符号跟踪,我们将将其集成到像 torchvision 这样的领域库中,用户将能够使用 FX 图模式量化对类似于支持领域库中的模型进行量化。对于任意模型,我们将提供一般指南,但要使其正常工作,用户可能需要熟悉 torch.fx,尤其是在如何使模型可符号跟踪方面。

鼓励量化的新用户首先尝试 FX 图模式量化,如果它不工作,用户可以尝试遵循 使用 FX 图模式量化 的指南,或回退到急切模式量化。

下表比较了急切模式量化和 FX 图模式量化之间的差异。

急切模式量化

FX 图模式量化

发布状态

测试

原型

运算符融合

手动

自动

Quant/DeQuant 布置

手动

自动

量化模块

支持

支持

量化函数/Torch 运算

手动

自动

对自定义的支持

有限支持

完全支持

量化模式支持

训练后量化:静态、动态、仅权重

量化感知训练:静态

训练后量化:静态、动态、仅权重

量化感知训练:静态

输入/输出模型类型

torch.nn.Module

torch.nn.Module(可能需要一些重构才能使模型与 FX 图模式量化兼容)

后端/硬件支持

硬件

内核库

急切模式量化

FX 图模式量化

量化模式支持

服务器 CPU

fbgemm/onednn

支持

全部支持

移动 CPU

qnnpack/xnnpack

服务器 GPU

TensorRT(早期原型)

不支持,它需要一个图。

支持

静态量化

目前,PyTorch 支持以下后端来高效地运行量化算子。

  • 具有 AVX2 支持或更高版本的 x86 CPU(没有 AVX2,某些操作的实现效率低下),通过 x86fbgemmonednn 优化(参见 RFC 中的详细信息)。

  • ARM CPU(通常存在于移动/嵌入式设备中),通过 qnnpack.

  • (早期原型)通过 fx2trt(即将开源)对 NVidia GPU 的支持,通过 TensorRT.

关于原生 CPU 后端的说明

我们使用相同的原生 pytorch 量化算子公开了 x86qnnpack,因此我们需要额外的标志来区分它们。x86qnnpack 的相应实现是根据 PyTorch 构建模式自动选择的,尽管用户可以选择通过将 torch.backends.quantization.engine 设置为 x86qnnpack 来覆盖此选项。

在准备量化模型时,需要确保 qconfig 和用于量化计算的引擎与模型将要执行的后端匹配。qconfig 控制在量化过程中使用的观察器类型。qengine 控制在为线性函数和卷积函数以及模块打包权重时是否使用 x86qnnpack 特定的打包函数。例如

x86 的默认设置

# set the qconfig for PTQ
# Note: the old 'fbgemm' is still available but 'x86' is the recommended default on x86 CPUs
qconfig = torch.ao.quantization.get_default_qconfig('x86')
# or, set the qconfig for QAT
qconfig = torch.ao.quantization.get_default_qat_qconfig('x86')
# set the qengine to control weight packing
torch.backends.quantized.engine = 'x86'

qnnpack 的默认设置

# set the qconfig for PTQ
qconfig = torch.ao.quantization.get_default_qconfig('qnnpack')
# or, set the qconfig for QAT
qconfig = torch.ao.quantization.get_default_qat_qconfig('qnnpack')
# set the qengine to control weight packing
torch.backends.quantized.engine = 'qnnpack'

算子支持

算子覆盖范围在动态量化和静态量化之间有所不同,并体现在下表中。请注意,对于 FX 图模式量化,相应的函数也受支持。

静态量化

动态量化

nn.Linear
nn.Conv1d/2d/3d
Y
Y
Y
N
nn.LSTM
nn.GRU
N
N
Y
Y
nn.RNNCell
nn.GRUCell
nn.LSTMCell
N
N
N
Y
Y
Y

nn.EmbeddingBag

Y(激活以 fp32 为单位)

Y

nn.Embedding

Y

Y

nn.MultiheadAttention

不支持

不支持

激活

广泛支持

保持不变,计算保持在 fp32

注意:这将很快用从原生 backend_config_dict 生成的某些信息进行更新。

量化 API 参考

量化 API 参考 包含量化 API 的文档,例如量化过程、量化张量操作以及支持的量化模块和函数。

量化后端配置

量化后端配置 包含有关如何为各种后端配置量化工作流程的文档。

量化准确性调试

量化准确性调试 包含有关如何调试量化准确性的文档。

量化自定义

虽然提供了基于观察到的张量数据选择比例因子和偏差的观察器的默认实现,但开发人员可以提供自己的量化函数。量化可以有选择地应用于模型的不同部分,或者为模型的不同部分配置不同的量化。

我们还提供对 conv1d()conv2d()conv3d()linear() 的逐通道量化的支持。

量化工作流程通过在模型的模块层次结构中添加(例如,添加观察器作为 .observer 子模块)或替换(例如,将 nn.Conv2d 转换为 nn.quantized.Conv2d)子模块来工作。这意味着模型在整个过程中保持一个常规的 nn.Module 基于实例,因此可以与 PyTorch API 的其余部分一起使用。

量化自定义模块 API

急切模式和 FX 图模式量化 API 都提供了一个钩子,供用户以自定义方式指定模块量化,使用用户定义的逻辑进行观察和量化。用户需要指定

  1. 源 fp32 模块(存在于模型中)的 Python 类型。

  2. 观察模块(由用户提供)的 Python 类型。该模块需要定义一个 from_float 函数,该函数定义如何从原始 fp32 模块创建观察模块。

  3. 量化模块(由用户提供)的 Python 类型。该模块需要定义一个 from_observed 函数,该函数定义如何从观察模块创建量化模块。

  4. 描述上述 (1)、(2)、(3) 的配置,传递给量化 API。

然后,框架将执行以下操作

  1. prepare 模块交换期间,它将使用 (2) 中类的 from_float 函数将每个类型为 (1) 的模块转换为类型为 (2) 的模块。

  2. convert 模块交换期间,它将使用 (3) 中类的 from_observed 函数将每个类型为 (2) 的模块转换为类型为 (3) 的模块。

目前,有一个要求,即 ObservedCustomModule 将只有一个张量输出,框架(而不是用户)将在该输出上添加一个观察器。观察器将作为自定义模块实例的属性存储在 activation_post_process 键下。将来可能会放宽这些限制。

自定义 API 示例

import torch
import torch.ao.nn.quantized as nnq
from torch.ao.quantization import QConfigMapping
import torch.ao.quantization.quantize_fx

# original fp32 module to replace
class CustomModule(torch.nn.Module):
    def __init__(self):
        super().__init__()
        self.linear = torch.nn.Linear(3, 3)

    def forward(self, x):
        return self.linear(x)

# custom observed module, provided by user
class ObservedCustomModule(torch.nn.Module):
    def __init__(self, linear):
        super().__init__()
        self.linear = linear

    def forward(self, x):
        return self.linear(x)

    @classmethod
    def from_float(cls, float_module):
        assert hasattr(float_module, 'qconfig')
        observed = cls(float_module.linear)
        observed.qconfig = float_module.qconfig
        return observed

# custom quantized module, provided by user
class StaticQuantCustomModule(torch.nn.Module):
    def __init__(self, linear):
        super().__init__()
        self.linear = linear

    def forward(self, x):
        return self.linear(x)

    @classmethod
    def from_observed(cls, observed_module):
        assert hasattr(observed_module, 'qconfig')
        assert hasattr(observed_module, 'activation_post_process')
        observed_module.linear.activation_post_process = \
            observed_module.activation_post_process
        quantized = cls(nnq.Linear.from_float(observed_module.linear))
        return quantized

#
# example API call (Eager mode quantization)
#

m = torch.nn.Sequential(CustomModule()).eval()
prepare_custom_config_dict = {
    "float_to_observed_custom_module_class": {
        CustomModule: ObservedCustomModule
    }
}
convert_custom_config_dict = {
    "observed_to_quantized_custom_module_class": {
        ObservedCustomModule: StaticQuantCustomModule
    }
}
m.qconfig = torch.ao.quantization.default_qconfig
mp = torch.ao.quantization.prepare(
    m, prepare_custom_config_dict=prepare_custom_config_dict)
# calibration (not shown)
mq = torch.ao.quantization.convert(
    mp, convert_custom_config_dict=convert_custom_config_dict)
#
# example API call (FX graph mode quantization)
#
m = torch.nn.Sequential(CustomModule()).eval()
qconfig_mapping = QConfigMapping().set_global(torch.ao.quantization.default_qconfig)
prepare_custom_config_dict = {
    "float_to_observed_custom_module_class": {
        "static": {
            CustomModule: ObservedCustomModule,
        }
    }
}
convert_custom_config_dict = {
    "observed_to_quantized_custom_module_class": {
        "static": {
            ObservedCustomModule: StaticQuantCustomModule,
        }
    }
}
mp = torch.ao.quantization.quantize_fx.prepare_fx(
    m, qconfig_mapping, torch.randn(3,3), prepare_custom_config=prepare_custom_config_dict)
# calibration (not shown)
mq = torch.ao.quantization.quantize_fx.convert_fx(
    mp, convert_custom_config=convert_custom_config_dict)

最佳实践

1. 如果你使用的是 x86 后端,我们需要使用 7 位而不是 8 位。确保你缩减了 quant_minquant_max 的范围,例如,如果 dtypetorch.quint8,确保将自定义 quant_min 设置为 0,将 quant_max 设置为 127 (255 / 2);如果 dtypetorch.qint8,确保将自定义 quant_min 设置为 -64 (-128 / 2),将 quant_max 设置为 63 (127 / 2),如果你调用了 torch.ao.quantization.get_default_qconfig(backend)torch.ao.quantization.get_default_qat_qconfig(backend) 函数来获取 x86qnnpack 后端的默认 qconfig,我们已经正确地设置了这些值。

2. 如果选择了 onednn 后端,在默认的 qconfig 映射 torch.ao.quantization.get_default_qconfig_mapping('onednn') 和默认的 qconfig torch.ao.quantization.get_default_qconfig('onednn') 中,激活将使用 8 位。建议在支持矢量神经网络指令 (VNNI) 的 CPU 上使用它。否则,将激活的观察器的 reduce_range 设置为 True,以便在不支持 VNNI 的 CPU 上获得更好的精度。

常见问题解答

  1. 如何在 GPU 上进行量化推理?

    我们目前还没有官方的 GPU 支持,但这是一个积极开发的领域,你可以在这里找到更多信息 here

  2. 在哪里可以获得量化模型的 ONNX 支持?

    如果你在导出模型时遇到错误(使用 torch.onnx 下的 API),你可以在 PyTorch 仓库中创建一个问题。将问题标题前缀为 [ONNX] 并将问题标记为 module: onnx

    如果你在 ONNX Runtime 中遇到问题,请在 GitHub - microsoft/onnxruntime 中创建一个问题。

  3. 如何将量化与 LSTM 一起使用?

    LSTM 通过我们自定义模块 API 在急切模式和 FX 图模式量化中都得到支持。示例可以在以下位置找到:急切模式:pytorch/test_quantized_op.py TestQuantizedOps.test_custom_module_lstm FX 图模式:pytorch/test_quantize_fx.py TestQuantizeFx.test_static_lstm

常见错误

将非量化张量传递到量化内核

如果你看到类似于以下的错误:

RuntimeError: Could not run 'quantized::some_operator' with arguments from the 'CPU' backend...

这意味着你试图将非量化张量传递到量化内核。一个常见的解决方法是使用 torch.ao.quantization.QuantStub 对张量进行量化。这需要在急切模式量化中手动完成。一个完整的示例

class M(torch.nn.Module):
    def __init__(self):
        super().__init__()
        self.quant = torch.ao.quantization.QuantStub()
        self.conv = torch.nn.Conv2d(1, 1, 1)

    def forward(self, x):
        # during the convert step, this will be replaced with a
        # `quantize_per_tensor` call
        x = self.quant(x)
        x = self.conv(x)
        return x

将量化张量传递到非量化内核

如果你看到类似于以下的错误:

RuntimeError: Could not run 'aten::thnn_conv2d_forward' with arguments from the 'QuantizedCPU' backend.

这意味着你试图将量化张量传递到非量化内核。一个常见的解决方法是使用 torch.ao.quantization.DeQuantStub 对张量进行反量化。这需要在急切模式量化中手动完成。一个完整的示例

class M(torch.nn.Module):
    def __init__(self):
        super().__init__()
        self.quant = torch.ao.quantization.QuantStub()
        self.conv1 = torch.nn.Conv2d(1, 1, 1)
        # this module will not be quantized (see `qconfig = None` logic below)
        self.conv2 = torch.nn.Conv2d(1, 1, 1)
        self.dequant = torch.ao.quantization.DeQuantStub()

    def forward(self, x):
        # during the convert step, this will be replaced with a
        # `quantize_per_tensor` call
        x = self.quant(x)
        x = self.conv1(x)
        # during the convert step, this will be replaced with a
        # `dequantize` call
        x = self.dequant(x)
        x = self.conv2(x)
        return x

m = M()
m.qconfig = some_qconfig
# turn off quantization for conv2
m.conv2.qconfig = None

保存和加载量化模型

当在量化模型上调用 torch.load 时,如果你看到类似于以下的错误:

AttributeError: 'LinearPackedParams' object has no attribute '_modules'

这是因为直接使用 torch.savetorch.load 保存和加载量化模型是不支持的。要保存/加载量化模型,可以使用以下方法:

  1. 保存/加载量化模型的 state_dict

一个示例

class M(torch.nn.Module):
    def __init__(self):
        super().__init__()
        self.linear = nn.Linear(5, 5)
        self.relu = nn.ReLU()

    def forward(self, x):
        x = self.linear(x)
        x = self.relu(x)
        return x

m = M().eval()
prepare_orig = prepare_fx(m, {'' : default_qconfig})
prepare_orig(torch.rand(5, 5))
quantized_orig = convert_fx(prepare_orig)

# Save/load using state_dict
b = io.BytesIO()
torch.save(quantized_orig.state_dict(), b)

m2 = M().eval()
prepared = prepare_fx(m2, {'' : default_qconfig})
quantized = convert_fx(prepared)
b.seek(0)
quantized.load_state_dict(torch.load(b))
  1. 使用 torch.jit.savetorch.jit.load 保存/加载脚本化的量化模型

一个示例

# Note: using the same model M from previous example
m = M().eval()
prepare_orig = prepare_fx(m, {'' : default_qconfig})
prepare_orig(torch.rand(5, 5))
quantized_orig = convert_fx(prepare_orig)

# save/load using scripted model
scripted = torch.jit.script(quantized_orig)
b = io.BytesIO()
torch.jit.save(scripted, b)
b.seek(0)
scripted_quantized = torch.jit.load(b)

使用 FX 图模式量化时出现符号追踪错误

符号可追踪性是 (原型 - 维护模式) FX 图模式量化 的一个要求,因此如果你将一个不可符号追踪的 PyTorch 模型传递给 torch.ao.quantization.prepare_fxtorch.ao.quantization.prepare_qat_fx,我们可能会看到以下错误:

torch.fx.proxy.TraceError: symbolically traced variables cannot be used as inputs to control flow

请查看 符号追踪的限制 并使用 - 使用 FX 图模式量化的用户指南 来解决问题。

文档

访问 PyTorch 的全面开发人员文档

查看文档

教程

获取针对初学者和高级开发人员的深入教程

查看教程

资源

查找开发资源并获得问题的解答

查看资源