Initial import: grid-bot — grid trading bot for BTC-USDT on Cifra Markets
This commit is contained in:
@@ -0,0 +1,213 @@
|
||||
# mypy: allow-untyped-defs
|
||||
|
||||
import sys
|
||||
from collections.abc import Callable
|
||||
from typing_extensions import TypeAliasType
|
||||
|
||||
import torch
|
||||
from torch import Tensor
|
||||
|
||||
from .fake_quantize import * # noqa: F403
|
||||
from .fuse_modules import fuse_modules, fuse_modules_qat # noqa: F403
|
||||
from .fuser_method_mappings import * # noqa: F403
|
||||
from .observer import * # noqa: F403
|
||||
from .qconfig import * # noqa: F403
|
||||
from .qconfig_mapping import * # noqa: F403
|
||||
from .quant_type import * # noqa: F403
|
||||
from .quantization_mappings import * # noqa: F403 # type: ignore[no-redef]
|
||||
from .quantize import * # noqa: F403
|
||||
from .quantize_jit import * # noqa: F403
|
||||
from .stubs import * # noqa: F403
|
||||
|
||||
|
||||
# ensure __module__ is set correctly for public APIs
|
||||
ObserverOrFakeQuantize = TypeAliasType(
|
||||
"ObserverOrFakeQuantize", ObserverBase | FakeQuantizeBase
|
||||
)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"DeQuantStub",
|
||||
"FakeQuantize",
|
||||
"FakeQuantizeBase",
|
||||
"FixedQParamsFakeQuantize",
|
||||
"FixedQParamsObserver",
|
||||
"FusedMovingAvgObsFakeQuantize",
|
||||
"HistogramObserver",
|
||||
# pyrefly: ignore [bad-dunder-all]
|
||||
"MatchAllNode",
|
||||
"MinMaxObserver",
|
||||
"MovingAverageMinMaxObserver",
|
||||
"MovingAveragePerChannelMinMaxObserver",
|
||||
"NoopObserver",
|
||||
"ObserverBase",
|
||||
"ObserverOrFakeQuantize",
|
||||
# pyrefly: ignore [bad-dunder-all]
|
||||
"Pattern",
|
||||
"PerChannelMinMaxObserver",
|
||||
"PlaceholderObserver",
|
||||
"QConfig",
|
||||
"QConfigAny",
|
||||
"QConfigDynamic",
|
||||
"QConfigMapping",
|
||||
"QuantStub",
|
||||
"QuantType",
|
||||
"QuantWrapper",
|
||||
"RecordingObserver",
|
||||
"ReuseInputObserver",
|
||||
"UniformQuantizationObserverBase",
|
||||
"add_quant_dequant",
|
||||
"convert",
|
||||
"convert_dynamic_jit",
|
||||
"convert_jit",
|
||||
"default_affine_fixed_qparams_fake_quant",
|
||||
"default_affine_fixed_qparams_observer",
|
||||
"default_debug_observer",
|
||||
"default_dynamic_fake_quant",
|
||||
"default_dynamic_quant_observer",
|
||||
"default_embedding_fake_quant",
|
||||
"default_embedding_fake_quant_4bit",
|
||||
"default_eval_fn",
|
||||
"default_fake_quant",
|
||||
"default_fixed_qparams_range_0to1_fake_quant",
|
||||
"default_fixed_qparams_range_0to1_observer",
|
||||
"default_fixed_qparams_range_neg1to1_fake_quant",
|
||||
"default_fixed_qparams_range_neg1to1_observer",
|
||||
"default_float_qparams_observer",
|
||||
"default_float_qparams_observer_4bit",
|
||||
"default_fused_act_fake_quant",
|
||||
"default_fused_per_channel_wt_fake_quant",
|
||||
"default_fused_wt_fake_quant",
|
||||
"default_histogram_fake_quant",
|
||||
"default_histogram_observer",
|
||||
"default_observer",
|
||||
"default_per_channel_weight_fake_quant",
|
||||
"default_per_channel_weight_observer",
|
||||
"default_placeholder_observer",
|
||||
"default_reuse_input_observer",
|
||||
"default_symmetric_fixed_qparams_fake_quant",
|
||||
"default_symmetric_fixed_qparams_observer",
|
||||
"default_weight_fake_quant",
|
||||
"default_weight_observer",
|
||||
"disable_fake_quant",
|
||||
"disable_observer",
|
||||
"enable_fake_quant",
|
||||
"enable_observer",
|
||||
"fuse_conv_bn",
|
||||
"fuse_conv_bn_jit",
|
||||
"fuse_conv_bn_relu",
|
||||
"fuse_convtranspose_bn",
|
||||
"fuse_linear_bn",
|
||||
"fuse_modules",
|
||||
"fuse_modules_qat",
|
||||
"fused_per_channel_wt_fake_quant_range_neg_127_to_127",
|
||||
"fused_wt_fake_quant_range_neg_127_to_127",
|
||||
# pyrefly: ignore [bad-dunder-all]
|
||||
"get_combined_dict",
|
||||
"get_default_compare_output_module_list",
|
||||
"get_default_custom_config_dict",
|
||||
"get_default_dynamic_quant_module_mappings",
|
||||
"get_default_dynamic_sparse_quant_module_mappings",
|
||||
"get_default_float_to_quantized_operator_mappings",
|
||||
"get_default_qat_module_mappings",
|
||||
"get_default_qat_qconfig",
|
||||
"get_default_qat_qconfig_dict",
|
||||
"get_default_qat_qconfig_mapping",
|
||||
"get_default_qconfig",
|
||||
"get_default_qconfig_dict",
|
||||
"get_default_qconfig_mapping",
|
||||
"get_default_qconfig_propagation_list",
|
||||
"get_default_static_quant_module_mappings",
|
||||
"get_default_static_quant_reference_module_mappings",
|
||||
"get_default_static_sparse_quant_module_mappings",
|
||||
"get_dynamic_quant_module_class",
|
||||
"get_embedding_qat_module_mappings",
|
||||
"get_embedding_static_quant_module_mappings",
|
||||
"get_fuser_method",
|
||||
"get_fuser_method_new",
|
||||
"get_observer_state_dict",
|
||||
"get_quantized_operator",
|
||||
"get_static_quant_module_class",
|
||||
"load_observer_state_dict",
|
||||
"no_observer_set",
|
||||
"per_channel_weight_observer_range_neg_127_to_127",
|
||||
"prepare",
|
||||
"prepare_dynamic_jit",
|
||||
"prepare_jit",
|
||||
"prepare_qat",
|
||||
"propagate_qconfig_",
|
||||
"qconfig_equals",
|
||||
"quantize",
|
||||
"quantize_dynamic",
|
||||
"quantize_dynamic_jit",
|
||||
"quantize_jit",
|
||||
"quantize_qat",
|
||||
"script_qconfig",
|
||||
"script_qconfig_dict",
|
||||
"swap_module",
|
||||
"weight_observer_range_neg_127_to_127",
|
||||
# from torchao, should be merged with torchao
|
||||
# in the future
|
||||
"AffineQuantizedObserverBase",
|
||||
"Granularity",
|
||||
"MappingType",
|
||||
"PerAxis",
|
||||
"PerBlock",
|
||||
"PerGroup",
|
||||
"PerRow",
|
||||
"PerTensor",
|
||||
"PerToken",
|
||||
"TorchAODType",
|
||||
"ZeroPointDomain",
|
||||
"get_block_size",
|
||||
]
|
||||
|
||||
|
||||
def default_eval_fn(model, calib_data):
|
||||
r"""Define the default evaluation function.
|
||||
|
||||
Default evaluation function takes a torch.utils.data.Dataset or a list of
|
||||
input Tensors and run the model on the dataset
|
||||
"""
|
||||
for data, _target in calib_data:
|
||||
model(data)
|
||||
|
||||
|
||||
class _DerivedObserverOrFakeQuantize(ObserverBase):
|
||||
r"""This observer is used to describe an observer whose quantization parameters
|
||||
are derived from other observers
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
dtype: torch.dtype,
|
||||
obs_or_fqs: list[ObserverOrFakeQuantize],
|
||||
derive_qparams_fn: Callable[
|
||||
[list[ObserverOrFakeQuantize]], tuple[Tensor, Tensor]
|
||||
],
|
||||
quant_min: int | None = None,
|
||||
quant_max: int | None = None,
|
||||
qscheme: torch.qscheme | None = None,
|
||||
ch_axis: int | None = None,
|
||||
):
|
||||
super().__init__(dtype)
|
||||
self.obs_or_fqs = obs_or_fqs
|
||||
self.derive_qparams_fn = derive_qparams_fn
|
||||
self.quant_min = quant_min
|
||||
self.quant_max = quant_max
|
||||
self.qscheme = qscheme
|
||||
self.ch_axis = ch_axis
|
||||
|
||||
from .utils import is_per_channel
|
||||
|
||||
if is_per_channel(self.qscheme):
|
||||
if self.ch_axis is None:
|
||||
raise AssertionError(
|
||||
"Must provide a valid ch_axis if qscheme is per channel"
|
||||
)
|
||||
|
||||
def forward(self, x: Tensor) -> Tensor:
|
||||
return x
|
||||
|
||||
def calculate_qparams(self): # type:ignore[override]
|
||||
return self.derive_qparams_fn(self.obs_or_fqs)
|
||||
@@ -0,0 +1,156 @@
|
||||
# mypy: allow-untyped-defs
|
||||
import torch
|
||||
import torch.ao.nn.quantized as nnq
|
||||
import torch.ao.ns._numeric_suite as ns
|
||||
import torch.ao.quantization
|
||||
import torch.nn as nn
|
||||
|
||||
|
||||
__all__ = [
|
||||
"get_module",
|
||||
"parent_child_names",
|
||||
"get_param",
|
||||
"MeanShadowLogger",
|
||||
"bias_correction",
|
||||
]
|
||||
|
||||
_supported_modules = {nn.Linear, nn.Conv2d}
|
||||
_supported_modules_quantized = {nnq.Linear, nnq.Conv2d}
|
||||
|
||||
|
||||
def get_module(model, name):
|
||||
"""Given name of submodule, this function grabs the submodule from given model."""
|
||||
return dict(model.named_modules())[name]
|
||||
|
||||
|
||||
def parent_child_names(name):
|
||||
"""Split full name of submodule into parent submodule's full name and submodule's name."""
|
||||
split_name = name.rsplit(".", 1)
|
||||
if len(split_name) == 1:
|
||||
return "", split_name[0]
|
||||
else:
|
||||
return split_name[0], split_name[1]
|
||||
|
||||
|
||||
def get_param(module, attr):
|
||||
"""Get the parameter given a module and attribute.
|
||||
|
||||
Sometimes the weights/bias attribute gives you the raw tensor, but sometimes
|
||||
gives a function that will give you the raw tensor, this function takes care of that logic
|
||||
"""
|
||||
param = getattr(module, attr, None)
|
||||
if callable(param):
|
||||
return param()
|
||||
else:
|
||||
return param
|
||||
|
||||
|
||||
class MeanShadowLogger(ns.Logger):
|
||||
"""Mean Logger for a Shadow module.
|
||||
|
||||
A logger for a Shadow module whose purpose is to record the rolling mean
|
||||
of the data passed to the floating point and quantized models
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""Set up initial values for float and quantized stats, count, float sum, and quant sum."""
|
||||
super().__init__()
|
||||
self.stats["float"] = None
|
||||
self.stats["quantized"] = None
|
||||
self.count = 0
|
||||
self.float_sum = None
|
||||
self.quant_sum = None
|
||||
|
||||
def forward(self, x, y): # type: ignore[override]
|
||||
"""Compute the average of quantized and floating-point data from modules.
|
||||
|
||||
The inputs x,y are output data from the quantized and floating-point modules.
|
||||
x is for the quantized module, y is for the floating point module
|
||||
"""
|
||||
if x.is_quantized:
|
||||
x = x.dequantize()
|
||||
|
||||
self.count += 1
|
||||
if self.stats["quantized"] is None:
|
||||
self.stats["quantized"] = x
|
||||
self.quant_sum = x
|
||||
else:
|
||||
self.quant_sum += x
|
||||
self.stats["quantized"] = self.quant_sum / self.count
|
||||
|
||||
if self.stats["float"] is None:
|
||||
self.stats["float"] = y
|
||||
self.float_sum = y
|
||||
else:
|
||||
self.float_sum += y
|
||||
self.stats["float"] = self.float_sum / self.count
|
||||
|
||||
def clear(self):
|
||||
self.stats["float"] = None
|
||||
self.stats["quantized"] = None
|
||||
self.count = 0
|
||||
self.float_sum = None
|
||||
self.quant_sum = None
|
||||
|
||||
|
||||
def bias_correction(
|
||||
float_model,
|
||||
quantized_model,
|
||||
img_data,
|
||||
target_modules=_supported_modules_quantized,
|
||||
neval_batches=None,
|
||||
):
|
||||
"""Perform bias correction on a module.
|
||||
|
||||
Using numeric suite shadow module, the expected output of the floating point and quantized modules
|
||||
is recorded. Using that data the bias of supported modules is shifted to compensate for the drift caused
|
||||
by quantization
|
||||
Paper reference: https://arxiv.org/pdf/1906.04721.pdf (Section 4.2)
|
||||
|
||||
Args:
|
||||
float_model: a trained model that serves as a reference to what bias correction should aim for
|
||||
quantized_model: quantized form of float_model that bias correction is to applied to
|
||||
img_data: calibration data to estimate the expected output (used to find quantization error)
|
||||
target_modules: specifies what submodules in quantized_model need bias correction (can be extended to
|
||||
unquantized submodules)
|
||||
neval_batches: a cap to the number of batches you want to be used for estimating the expected output
|
||||
"""
|
||||
ns.prepare_model_with_stubs(
|
||||
float_model, quantized_model, _supported_modules, MeanShadowLogger
|
||||
)
|
||||
|
||||
uncorrected_modules = {
|
||||
name: submodule
|
||||
for name, submodule in quantized_model.named_modules()
|
||||
if type(submodule) in target_modules
|
||||
}
|
||||
|
||||
for uncorrected_module in uncorrected_modules:
|
||||
quantized_submodule = get_module(quantized_model, uncorrected_module)
|
||||
bias = get_param(quantized_submodule, "bias")
|
||||
if bias is not None:
|
||||
for count, data in enumerate(img_data, start=1):
|
||||
quantized_model(data[0])
|
||||
if count == neval_batches:
|
||||
break
|
||||
ob_dict = ns.get_logger_dict(quantized_model)
|
||||
parent_name, _ = parent_child_names(uncorrected_module)
|
||||
|
||||
float_data = ob_dict[parent_name + ".stats"]["float"]
|
||||
quant_data = ob_dict[parent_name + ".stats"]["quantized"]
|
||||
|
||||
# math for expected_error
|
||||
quantization_error = quant_data - float_data
|
||||
dims = list(range(quantization_error.dim()))
|
||||
# Note: we don't want to take the mean over the output channel dimension
|
||||
dims.remove(1)
|
||||
expected_error = torch.mean(quantization_error, dims)
|
||||
|
||||
updated_bias = bias.data - expected_error
|
||||
|
||||
bias.data = updated_bias
|
||||
|
||||
# Resets the data contained in the loggers
|
||||
for submodule in quantized_model.modules():
|
||||
if isinstance(submodule, MeanShadowLogger):
|
||||
submodule.clear()
|
||||
@@ -0,0 +1,279 @@
|
||||
# mypy: allow-untyped-defs
|
||||
import copy
|
||||
from itertools import chain
|
||||
from typing import Any
|
||||
|
||||
import torch
|
||||
|
||||
|
||||
__all__ = [
|
||||
"set_module_weight",
|
||||
"set_module_bias",
|
||||
"has_bias",
|
||||
"get_module_weight",
|
||||
"get_module_bias",
|
||||
"max_over_ndim",
|
||||
"min_over_ndim",
|
||||
"channel_range",
|
||||
"get_name_by_module",
|
||||
"cross_layer_equalization",
|
||||
"process_paired_modules_list_to_name",
|
||||
"expand_groups_in_paired_modules_list",
|
||||
"equalize",
|
||||
"converged",
|
||||
]
|
||||
|
||||
_supported_types = {torch.nn.Conv2d, torch.nn.Linear, torch.nn.Conv1d}
|
||||
_supported_intrinsic_types = {
|
||||
torch.ao.nn.intrinsic.ConvReLU2d,
|
||||
torch.ao.nn.intrinsic.LinearReLU,
|
||||
torch.ao.nn.intrinsic.ConvReLU1d,
|
||||
}
|
||||
_all_supported_types = _supported_types.union(_supported_intrinsic_types)
|
||||
|
||||
|
||||
def set_module_weight(module, weight) -> None:
|
||||
if type(module) in _supported_types:
|
||||
module.weight = torch.nn.Parameter(weight)
|
||||
else:
|
||||
module[0].weight = torch.nn.Parameter(weight)
|
||||
|
||||
|
||||
def set_module_bias(module, bias) -> None:
|
||||
if type(module) in _supported_types:
|
||||
module.bias = torch.nn.Parameter(bias)
|
||||
else:
|
||||
module[0].bias = torch.nn.Parameter(bias)
|
||||
|
||||
|
||||
def has_bias(module) -> bool:
|
||||
if type(module) in _supported_types:
|
||||
return module.bias is not None
|
||||
else:
|
||||
return module[0].bias is not None
|
||||
|
||||
|
||||
def get_module_weight(module):
|
||||
if type(module) in _supported_types:
|
||||
return module.weight
|
||||
else:
|
||||
return module[0].weight
|
||||
|
||||
|
||||
def get_module_bias(module):
|
||||
if type(module) in _supported_types:
|
||||
return module.bias
|
||||
else:
|
||||
return module[0].bias
|
||||
|
||||
|
||||
def max_over_ndim(input, axis_list, keepdim=False):
|
||||
"""Apply 'torch.max' over the given axes."""
|
||||
axis_list.sort(reverse=True)
|
||||
for axis in axis_list:
|
||||
input, _ = input.max(axis, keepdim)
|
||||
return input
|
||||
|
||||
|
||||
def min_over_ndim(input, axis_list, keepdim=False):
|
||||
"""Apply 'torch.min' over the given axes."""
|
||||
axis_list.sort(reverse=True)
|
||||
for axis in axis_list:
|
||||
input, _ = input.min(axis, keepdim)
|
||||
return input
|
||||
|
||||
|
||||
def channel_range(input, axis=0):
|
||||
"""Find the range of weights associated with a specific channel."""
|
||||
size_of_tensor_dim = input.ndim
|
||||
axis_list = list(range(size_of_tensor_dim))
|
||||
axis_list.remove(axis)
|
||||
|
||||
mins = min_over_ndim(input, axis_list)
|
||||
maxs = max_over_ndim(input, axis_list)
|
||||
|
||||
if mins.size(0) != input.size(axis):
|
||||
raise AssertionError(
|
||||
"Dimensions of resultant channel range does not match size of requested axis"
|
||||
)
|
||||
return maxs - mins
|
||||
|
||||
|
||||
def get_name_by_module(model, module):
|
||||
"""Get the name of a module within a model.
|
||||
|
||||
Args:
|
||||
model: a model (nn.module) that equalization is to be applied on
|
||||
module: a module within the model
|
||||
|
||||
Returns:
|
||||
name: the name of the module within the model
|
||||
"""
|
||||
for name, m in model.named_modules():
|
||||
if m is module:
|
||||
return name
|
||||
raise ValueError("module is not in the model")
|
||||
|
||||
|
||||
def cross_layer_equalization(module1, module2, output_axis=0, input_axis=1):
|
||||
"""Scale the range of Tensor1.output to equal Tensor2.input.
|
||||
|
||||
Given two adjacent tensors', the weights are scaled such that
|
||||
the ranges of the first tensors' output channel are equal to the
|
||||
ranges of the second tensors' input channel
|
||||
"""
|
||||
if (
|
||||
type(module1) not in _all_supported_types
|
||||
or type(module2) not in _all_supported_types
|
||||
):
|
||||
raise ValueError(
|
||||
"module type not supported:", type(module1), " ", type(module2)
|
||||
)
|
||||
|
||||
bias = get_module_bias(module1) if has_bias(module1) else None
|
||||
|
||||
weight1 = get_module_weight(module1)
|
||||
weight2 = get_module_weight(module2)
|
||||
|
||||
if weight1.size(output_axis) != weight2.size(input_axis):
|
||||
raise TypeError(
|
||||
"Number of output channels of first arg do not match \
|
||||
number input channels of second arg"
|
||||
)
|
||||
|
||||
weight1_range = channel_range(weight1, output_axis)
|
||||
weight2_range = channel_range(weight2, input_axis)
|
||||
|
||||
# producing scaling factors to applied
|
||||
weight2_range += 1e-9
|
||||
scaling_factors = torch.sqrt(weight1_range / weight2_range)
|
||||
inverse_scaling_factors = torch.reciprocal(scaling_factors)
|
||||
|
||||
if bias is not None:
|
||||
bias = bias * inverse_scaling_factors
|
||||
|
||||
# formatting the scaling (1D) tensors to be applied on the given argument tensors
|
||||
# pads axis to (1D) tensors to then be broadcasted
|
||||
size1 = [1] * weight1.ndim
|
||||
size1[output_axis] = weight1.size(output_axis)
|
||||
size2 = [1] * weight2.ndim
|
||||
size2[input_axis] = weight2.size(input_axis)
|
||||
|
||||
scaling_factors = torch.reshape(scaling_factors, size2)
|
||||
inverse_scaling_factors = torch.reshape(inverse_scaling_factors, size1)
|
||||
|
||||
weight1 = weight1 * inverse_scaling_factors
|
||||
weight2 = weight2 * scaling_factors
|
||||
|
||||
set_module_weight(module1, weight1)
|
||||
if bias is not None:
|
||||
set_module_bias(module1, bias)
|
||||
set_module_weight(module2, weight2)
|
||||
|
||||
|
||||
def process_paired_modules_list_to_name(model, paired_modules_list):
|
||||
"""Processes a list of paired modules to a list of names of paired modules."""
|
||||
|
||||
for group in paired_modules_list:
|
||||
for i, item in enumerate(group):
|
||||
if isinstance(item, torch.nn.Module):
|
||||
group[i] = get_name_by_module(model, item)
|
||||
elif not isinstance(item, str):
|
||||
raise TypeError("item must be a nn.Module or a string")
|
||||
return paired_modules_list
|
||||
|
||||
|
||||
def expand_groups_in_paired_modules_list(paired_modules_list):
|
||||
"""Expands module pair groups larger than two into groups of two modules."""
|
||||
new_list = []
|
||||
|
||||
for group in paired_modules_list:
|
||||
if len(group) == 1:
|
||||
raise ValueError("Group must have at least two modules")
|
||||
elif len(group) == 2:
|
||||
new_list.append(group)
|
||||
elif len(group) > 2:
|
||||
new_list.extend([group[i], group[i + 1]] for i in range(len(group) - 1))
|
||||
|
||||
return new_list
|
||||
|
||||
|
||||
def equalize(model, paired_modules_list, threshold=1e-4, inplace=True):
|
||||
"""Equalize modules until convergence is achieved.
|
||||
|
||||
Given a list of adjacent modules within a model, equalization will
|
||||
be applied between each pair, this will repeated until convergence is achieved
|
||||
|
||||
Keeps a copy of the changing modules from the previous iteration, if the copies
|
||||
are not that different than the current modules (determined by converged_test),
|
||||
then the modules have converged enough that further equalizing is not necessary
|
||||
|
||||
Reference is section 4.1 of this paper https://arxiv.org/pdf/1906.04721.pdf
|
||||
|
||||
Args:
|
||||
model: a model (nn.Module) that equalization is to be applied on
|
||||
paired_modules_list (List(List[nn.module || str])): a list of lists
|
||||
where each sublist is a pair of two submodules found in the model,
|
||||
for each pair the two modules have to be adjacent in the model,
|
||||
with only piece-wise-linear functions like a (P)ReLU or LeakyReLU in between
|
||||
to get expected results.
|
||||
The list can contain either modules, or names of modules in the model.
|
||||
If you pass multiple modules in the same list, they will all be equalized together.
|
||||
threshold (float): a number used by the converged function to determine what degree
|
||||
of similarity between models is necessary for them to be called equivalent
|
||||
inplace (bool): determines if function is inplace or not
|
||||
"""
|
||||
|
||||
paired_modules_list = process_paired_modules_list_to_name(
|
||||
model, paired_modules_list
|
||||
)
|
||||
|
||||
if not inplace:
|
||||
model = copy.deepcopy(model)
|
||||
|
||||
paired_modules_list = expand_groups_in_paired_modules_list(paired_modules_list)
|
||||
|
||||
name_to_module: dict[str, torch.nn.Module] = {}
|
||||
previous_name_to_module: dict[str, Any] = {}
|
||||
name_set = set(chain.from_iterable(paired_modules_list))
|
||||
|
||||
for name, module in model.named_modules():
|
||||
if name in name_set:
|
||||
name_to_module[name] = module
|
||||
previous_name_to_module[name] = None
|
||||
while not converged(name_to_module, previous_name_to_module, threshold):
|
||||
for pair in paired_modules_list:
|
||||
previous_name_to_module[pair[0]] = copy.deepcopy(name_to_module[pair[0]])
|
||||
previous_name_to_module[pair[1]] = copy.deepcopy(name_to_module[pair[1]])
|
||||
|
||||
cross_layer_equalization(name_to_module[pair[0]], name_to_module[pair[1]])
|
||||
|
||||
return model
|
||||
|
||||
|
||||
def converged(curr_modules, prev_modules, threshold=1e-4):
|
||||
"""Test whether modules are converged to a specified threshold.
|
||||
|
||||
Tests for the summed norm of the differences between each set of modules
|
||||
being less than the given threshold
|
||||
|
||||
Takes two dictionaries mapping names to modules, the set of names for each dictionary
|
||||
should be the same, looping over the set of names, for each name take the difference
|
||||
between the associated modules in each dictionary
|
||||
|
||||
"""
|
||||
if curr_modules.keys() != prev_modules.keys():
|
||||
raise ValueError(
|
||||
"The keys to the given mappings must have the same set of names of modules"
|
||||
)
|
||||
|
||||
summed_norms = torch.tensor(0.0)
|
||||
if None in prev_modules.values():
|
||||
return False
|
||||
for name in curr_modules:
|
||||
curr_weight = get_module_weight(curr_modules[name])
|
||||
prev_weight = get_module_weight(prev_modules[name])
|
||||
|
||||
difference = curr_weight.sub(prev_weight)
|
||||
summed_norms += torch.norm(difference)
|
||||
return bool(summed_norms < threshold)
|
||||
+199
@@ -0,0 +1,199 @@
|
||||
# mypy: allow-untyped-defs
|
||||
|
||||
import torch
|
||||
from torch.nn.parameter import Parameter
|
||||
|
||||
|
||||
__all__: list[str] = []
|
||||
|
||||
|
||||
class _LearnableFakeQuantize(torch.ao.quantization.FakeQuantizeBase):
|
||||
r"""Generalized extension of the FakeQuantize module in fake_quantize.py.
|
||||
|
||||
This is an extension of the FakeQuantize module in fake_quantize.py, which
|
||||
supports more generalized lower-bit quantization and supports learning of the scale
|
||||
and zero point parameters through backpropagation.
|
||||
|
||||
In addition to the attributes in the original FakeQuantize module, the _LearnableFakeQuantize
|
||||
module also includes the following attributes to support quantization parameter learning.
|
||||
|
||||
* :attr:`channel_len` defines the length of the channel when initializing scale and zero point
|
||||
for the per channel case.
|
||||
|
||||
* :attr:`use_grad_scaling` defines the flag for whether the gradients for scale and zero point are
|
||||
normalized by the constant, which is proportional to the square root of the number of
|
||||
elements in the tensor. The related literature justifying the use of this particular constant
|
||||
can be found here: https://openreview.net/pdf?id=rkgO66VKDS.
|
||||
|
||||
* :attr:`fake_quant_enabled` defines the flag for enabling fake quantization on the output.
|
||||
|
||||
* :attr:`static_enabled` defines the flag for using observer's static estimation for
|
||||
scale and zero point.
|
||||
|
||||
* :attr:`learning_enabled` defines the flag for enabling backpropagation for scale and zero point.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
observer,
|
||||
quant_min=0,
|
||||
quant_max=255,
|
||||
scale=1.0,
|
||||
zero_point=0.0,
|
||||
channel_len=-1,
|
||||
use_grad_scaling=False,
|
||||
**observer_kwargs,
|
||||
):
|
||||
super().__init__()
|
||||
if quant_min >= quant_max:
|
||||
raise AssertionError("quant_min must be strictly less than quant_max.")
|
||||
self.quant_min = quant_min
|
||||
self.quant_max = quant_max
|
||||
# also pass quant_min and quant_max to observer
|
||||
observer_kwargs["quant_min"] = quant_min
|
||||
observer_kwargs["quant_max"] = quant_max
|
||||
self.use_grad_scaling = use_grad_scaling
|
||||
if channel_len == -1:
|
||||
self.scale = Parameter(torch.tensor([scale]))
|
||||
self.zero_point = Parameter(torch.tensor([zero_point]))
|
||||
else:
|
||||
if not (isinstance(channel_len, int) and channel_len > 0):
|
||||
raise AssertionError("Channel size must be a positive integer.")
|
||||
self.scale = Parameter(torch.tensor([scale] * channel_len))
|
||||
self.zero_point = Parameter(torch.tensor([zero_point] * channel_len))
|
||||
|
||||
self.activation_post_process = observer(**observer_kwargs)
|
||||
if torch.iinfo(self.activation_post_process.dtype).min > quant_min:
|
||||
raise AssertionError("quant_min out of bound")
|
||||
if quant_max > torch.iinfo(self.activation_post_process.dtype).max:
|
||||
raise AssertionError("quant_max out of bound")
|
||||
self.dtype = self.activation_post_process.dtype
|
||||
self.qscheme = self.activation_post_process.qscheme
|
||||
self.ch_axis = (
|
||||
self.activation_post_process.ch_axis
|
||||
if hasattr(self.activation_post_process, "ch_axis")
|
||||
else -1
|
||||
)
|
||||
self.register_buffer("fake_quant_enabled", torch.tensor([1], dtype=torch.uint8))
|
||||
self.register_buffer("static_enabled", torch.tensor([1], dtype=torch.uint8))
|
||||
self.register_buffer("learning_enabled", torch.tensor([0], dtype=torch.uint8))
|
||||
|
||||
bitrange = torch.tensor(quant_max - quant_min + 1).double()
|
||||
self.bitwidth = int(torch.log2(bitrange).item())
|
||||
self.register_buffer("eps", torch.tensor([torch.finfo(torch.float32).eps]))
|
||||
|
||||
@torch.jit.export
|
||||
def enable_param_learning(self):
|
||||
r"""Enable parameter learning over static observer estimates.
|
||||
|
||||
Enables learning of quantization parameters and
|
||||
disables static observer estimates. Forward path returns fake quantized X.
|
||||
"""
|
||||
self.toggle_qparam_learning(enabled=True).toggle_fake_quant(
|
||||
enabled=True
|
||||
).toggle_observer_update(enabled=False)
|
||||
return self
|
||||
|
||||
@torch.jit.export
|
||||
def enable_static_estimate(self):
|
||||
"""Enable static estimates of quantization parameters.
|
||||
|
||||
Enables static observer estimates and disables learning of
|
||||
quantization parameters. Forward path returns fake quantized X.
|
||||
"""
|
||||
self.toggle_qparam_learning(enabled=False).toggle_fake_quant(
|
||||
enabled=True
|
||||
).toggle_observer_update(enabled=True)
|
||||
|
||||
@torch.jit.export
|
||||
def enable_static_observation(self):
|
||||
"""Enable accumulation of data without updating quantization parameters.
|
||||
|
||||
Enables static observer accumulating data from input but doesn't
|
||||
update the quantization parameters. Forward path returns the original X.
|
||||
"""
|
||||
self.toggle_qparam_learning(enabled=False).toggle_fake_quant(
|
||||
enabled=False
|
||||
).toggle_observer_update(enabled=True)
|
||||
|
||||
@torch.jit.export
|
||||
def toggle_observer_update(self, enabled=True):
|
||||
self.static_enabled[0] = int(enabled) # type: ignore[operator]
|
||||
return self
|
||||
|
||||
@torch.jit.export
|
||||
def enable_observer(self, enabled=True):
|
||||
self.toggle_observer_update(enabled)
|
||||
|
||||
@torch.jit.export
|
||||
def toggle_qparam_learning(self, enabled=True):
|
||||
self.learning_enabled[0] = int(enabled) # type: ignore[operator]
|
||||
self.scale.requires_grad = enabled
|
||||
self.zero_point.requires_grad = enabled
|
||||
return self
|
||||
|
||||
@torch.jit.export
|
||||
def toggle_fake_quant(self, enabled=True):
|
||||
self.fake_quant_enabled[0] = int(enabled)
|
||||
return self
|
||||
|
||||
@torch.jit.export
|
||||
def observe_quant_params(self):
|
||||
print(f"_LearnableFakeQuantize Scale: {self.scale.detach()}")
|
||||
print(f"_LearnableFakeQuantize Zero Point: {self.zero_point.detach()}")
|
||||
|
||||
@torch.jit.export
|
||||
def calculate_qparams(self): # type: ignore[override]
|
||||
self.scale.data.clamp_(min=self.eps.item()) # type: ignore[operator]
|
||||
scale = self.scale.detach()
|
||||
zero_point = (
|
||||
self.zero_point.detach()
|
||||
.round()
|
||||
.clamp(self.quant_min, self.quant_max)
|
||||
.long()
|
||||
)
|
||||
return scale, zero_point
|
||||
|
||||
def forward(self, X):
|
||||
if self.static_enabled[0] == 1: # type: ignore[index]
|
||||
self.activation_post_process(X.detach())
|
||||
_scale, _zero_point = self.activation_post_process.calculate_qparams()
|
||||
_scale = _scale.to(self.scale.device)
|
||||
_zero_point = _zero_point.to(self.zero_point.device)
|
||||
self.scale.data.copy_(_scale)
|
||||
self.zero_point.data.copy_(_zero_point)
|
||||
else:
|
||||
self.scale.data.clamp_(min=self.eps.item()) # type: ignore[operator]
|
||||
|
||||
if self.fake_quant_enabled[0] == 1:
|
||||
if self.qscheme in (
|
||||
torch.per_channel_symmetric,
|
||||
torch.per_tensor_symmetric,
|
||||
):
|
||||
self.zero_point.data.zero_()
|
||||
|
||||
if self.use_grad_scaling:
|
||||
grad_factor = 1.0 / (X.numel() * self.quant_max) ** 0.5
|
||||
else:
|
||||
grad_factor = 1.0
|
||||
if self.qscheme in (torch.per_channel_symmetric, torch.per_channel_affine):
|
||||
X = torch._fake_quantize_learnable_per_channel_affine(
|
||||
X,
|
||||
self.scale,
|
||||
self.zero_point,
|
||||
self.ch_axis,
|
||||
self.quant_min,
|
||||
self.quant_max,
|
||||
grad_factor,
|
||||
)
|
||||
else:
|
||||
X = torch._fake_quantize_learnable_per_tensor_affine(
|
||||
X,
|
||||
self.scale,
|
||||
self.zero_point,
|
||||
self.quant_min,
|
||||
self.quant_max,
|
||||
grad_factor,
|
||||
)
|
||||
|
||||
return X
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
from .backend_config import (
|
||||
BackendConfig,
|
||||
BackendPatternConfig,
|
||||
DTypeConfig,
|
||||
DTypeWithConstraints,
|
||||
ObservationType,
|
||||
)
|
||||
from .executorch import get_executorch_backend_config
|
||||
from .fbgemm import get_fbgemm_backend_config
|
||||
from .native import get_native_backend_config, get_native_backend_config_dict
|
||||
from .onednn import get_onednn_backend_config
|
||||
from .qnnpack import get_qnnpack_backend_config
|
||||
from .tensorrt import get_tensorrt_backend_config, get_tensorrt_backend_config_dict
|
||||
|
||||
|
||||
__all__ = [
|
||||
"get_fbgemm_backend_config",
|
||||
"get_native_backend_config",
|
||||
"get_native_backend_config_dict",
|
||||
"get_qnnpack_backend_config",
|
||||
"get_tensorrt_backend_config",
|
||||
"get_tensorrt_backend_config_dict",
|
||||
"get_executorch_backend_config",
|
||||
"BackendConfig",
|
||||
"BackendPatternConfig",
|
||||
"DTypeConfig",
|
||||
"DTypeWithConstraints",
|
||||
"ObservationType",
|
||||
"get_onednn_backend_config",
|
||||
]
|
||||
+782
@@ -0,0 +1,782 @@
|
||||
# mypy: allow-untyped-defs
|
||||
import copy
|
||||
import operator
|
||||
from collections import namedtuple
|
||||
from collections.abc import Callable
|
||||
|
||||
import torch
|
||||
import torch.ao.nn.intrinsic as nni
|
||||
import torch.ao.nn.intrinsic.qat as nniqat
|
||||
import torch.ao.nn.qat as nnqat
|
||||
import torch.ao.nn.quantized.reference as nnqr
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
from torch.ao.quantization.fuser_method_mappings import (
|
||||
_sequential_wrapper2,
|
||||
fuse_conv_bn,
|
||||
fuse_conv_bn_relu,
|
||||
fuse_convtranspose_bn,
|
||||
fuse_linear_bn,
|
||||
)
|
||||
|
||||
from .backend_config import (
|
||||
BackendPatternConfig,
|
||||
DTypeConfig,
|
||||
DTypeWithConstraints,
|
||||
ObservationType,
|
||||
)
|
||||
|
||||
|
||||
__all__: list[str] = []
|
||||
|
||||
# TODO: rename to be more explicit, e.g. qat_conv_relu
|
||||
_ConvMetadata = namedtuple(
|
||||
"_ConvMetadata",
|
||||
[
|
||||
"root",
|
||||
"transpose",
|
||||
"bn",
|
||||
"reference",
|
||||
"transpose_reference",
|
||||
"fused_conv_relu",
|
||||
"fused_conv_bn",
|
||||
"fused_conv_bn_relu",
|
||||
"qat",
|
||||
"relu_qat",
|
||||
"bn_qat",
|
||||
"bn_relu_qat",
|
||||
"func",
|
||||
"func_transpose",
|
||||
],
|
||||
)
|
||||
_Conv1dMetadata = _ConvMetadata(
|
||||
nn.Conv1d,
|
||||
nn.ConvTranspose1d,
|
||||
nn.BatchNorm1d,
|
||||
nnqr.Conv1d,
|
||||
nnqr.ConvTranspose1d,
|
||||
nni.ConvReLU1d,
|
||||
nni.ConvBn1d,
|
||||
nni.ConvBnReLU1d,
|
||||
nnqat.Conv1d,
|
||||
nniqat.ConvReLU1d,
|
||||
nniqat.ConvBn1d,
|
||||
nniqat.ConvBnReLU1d,
|
||||
F.conv1d,
|
||||
F.conv_transpose1d,
|
||||
)
|
||||
_Conv2dMetadata = _ConvMetadata(
|
||||
nn.Conv2d,
|
||||
nn.ConvTranspose2d,
|
||||
nn.BatchNorm2d,
|
||||
nnqr.Conv2d,
|
||||
nnqr.ConvTranspose2d,
|
||||
nni.ConvReLU2d,
|
||||
nni.ConvBn2d,
|
||||
nni.ConvBnReLU2d,
|
||||
nnqat.Conv2d,
|
||||
nniqat.ConvReLU2d,
|
||||
nniqat.ConvBn2d,
|
||||
nniqat.ConvBnReLU2d,
|
||||
F.conv2d,
|
||||
F.conv_transpose2d,
|
||||
)
|
||||
_Conv3dMetadata = _ConvMetadata(
|
||||
nn.Conv3d,
|
||||
nn.ConvTranspose3d,
|
||||
nn.BatchNorm3d,
|
||||
nnqr.Conv3d,
|
||||
nnqr.ConvTranspose3d,
|
||||
nni.ConvReLU3d,
|
||||
nni.ConvBn3d,
|
||||
nni.ConvBnReLU3d,
|
||||
nnqat.Conv3d,
|
||||
nniqat.ConvReLU3d,
|
||||
nniqat.ConvBn3d,
|
||||
nniqat.ConvBnReLU3d,
|
||||
F.conv3d,
|
||||
F.conv_transpose3d,
|
||||
)
|
||||
|
||||
# Add constraints for fixed qparams ops like sigmoid and tanh to ensure values
|
||||
# fall within the proper ranges, e.g. [0, 1] for sigmoid, [-1, 1] for tanh
|
||||
_FIXED_QPARAM_OP_0TO1_CONSTRAINTS = DTypeWithConstraints(
|
||||
dtype=torch.quint8,
|
||||
quant_min_lower_bound=0,
|
||||
quant_max_upper_bound=255,
|
||||
scale_exact_match=1.0 / 256.0,
|
||||
zero_point_exact_match=0,
|
||||
)
|
||||
_FIXED_QPARAM_OP_NEG1TO1_CONSTRAINTS = DTypeWithConstraints(
|
||||
dtype=torch.quint8,
|
||||
quant_min_lower_bound=0,
|
||||
quant_max_upper_bound=255,
|
||||
scale_exact_match=2.0 / 256.0,
|
||||
zero_point_exact_match=128,
|
||||
)
|
||||
_FIXED_QPARAMS_OP_TO_CONSTRAINTS: dict[Callable | str, DTypeWithConstraints] = {
|
||||
torch.nn.Hardsigmoid: _FIXED_QPARAM_OP_0TO1_CONSTRAINTS,
|
||||
torch.nn.functional.hardsigmoid: _FIXED_QPARAM_OP_0TO1_CONSTRAINTS,
|
||||
"hardsigmoid": _FIXED_QPARAM_OP_0TO1_CONSTRAINTS,
|
||||
"hardsigmoid_": _FIXED_QPARAM_OP_0TO1_CONSTRAINTS,
|
||||
torch.nn.Sigmoid: _FIXED_QPARAM_OP_0TO1_CONSTRAINTS,
|
||||
torch.sigmoid: _FIXED_QPARAM_OP_0TO1_CONSTRAINTS,
|
||||
"sigmoid": _FIXED_QPARAM_OP_0TO1_CONSTRAINTS,
|
||||
"sigmoid_": _FIXED_QPARAM_OP_0TO1_CONSTRAINTS,
|
||||
torch.nn.Softmax: _FIXED_QPARAM_OP_0TO1_CONSTRAINTS,
|
||||
torch.nn.Tanh: _FIXED_QPARAM_OP_NEG1TO1_CONSTRAINTS,
|
||||
torch.tanh: _FIXED_QPARAM_OP_NEG1TO1_CONSTRAINTS,
|
||||
"tanh": _FIXED_QPARAM_OP_NEG1TO1_CONSTRAINTS,
|
||||
"tanh_": _FIXED_QPARAM_OP_NEG1TO1_CONSTRAINTS,
|
||||
}
|
||||
|
||||
|
||||
def _get_binary_op_configs(
|
||||
dtype_configs: list[DTypeConfig],
|
||||
) -> list[BackendPatternConfig]:
|
||||
binary_op_configs: list[BackendPatternConfig] = []
|
||||
num_tensor_args_to_observation_type_mapping = {
|
||||
# TODO: this is not used right now since we have extra check in prepare
|
||||
# will need to change this to NO_OBSERVER later after we implemented
|
||||
# Tensor dtype inference properly
|
||||
0: ObservationType.OUTPUT_USE_DIFFERENT_OBSERVER_AS_INPUT,
|
||||
1: ObservationType.OUTPUT_SHARE_OBSERVER_WITH_INPUT,
|
||||
2: ObservationType.OUTPUT_USE_DIFFERENT_OBSERVER_AS_INPUT,
|
||||
}
|
||||
for op_with_quantized_bop_scalar_variant in [
|
||||
operator.add,
|
||||
torch.add,
|
||||
operator.mul,
|
||||
torch.mul,
|
||||
]:
|
||||
bop_patterns = [
|
||||
(op_with_quantized_bop_scalar_variant, nn.ReLU),
|
||||
(op_with_quantized_bop_scalar_variant, F.relu),
|
||||
(op_with_quantized_bop_scalar_variant, torch.relu),
|
||||
op_with_quantized_bop_scalar_variant,
|
||||
]
|
||||
binary_op_configs.extend(
|
||||
BackendPatternConfig(bop_pattern)
|
||||
.set_dtype_configs(dtype_configs) # noqa: E131
|
||||
._set_num_tensor_args_to_observation_type(
|
||||
num_tensor_args_to_observation_type_mapping
|
||||
)
|
||||
for bop_pattern in bop_patterns
|
||||
)
|
||||
# matmul
|
||||
binary_op_configs.append(
|
||||
BackendPatternConfig(torch.matmul).set_dtype_configs(dtype_configs) # noqa: E131
|
||||
)
|
||||
return binary_op_configs
|
||||
|
||||
|
||||
def _get_linear_configs(dtype_configs: list[DTypeConfig]) -> list[BackendPatternConfig]:
|
||||
"""
|
||||
Return all configs related to linear modules and ops.
|
||||
"""
|
||||
observation_type = ObservationType.OUTPUT_USE_DIFFERENT_OBSERVER_AS_INPUT
|
||||
linear_configs: list[BackendPatternConfig] = []
|
||||
|
||||
# (1) Single linear modules/functions
|
||||
# -------------------------------------
|
||||
# linear module
|
||||
linear_configs.append(
|
||||
BackendPatternConfig(torch.nn.Linear)
|
||||
.set_observation_type(observation_type) # noqa: E131
|
||||
.set_dtype_configs(dtype_configs)
|
||||
.set_root_module(torch.nn.Linear)
|
||||
.set_reference_quantized_module(nnqr.Linear)
|
||||
.set_qat_module(nnqat.Linear)
|
||||
)
|
||||
# linear qat module
|
||||
linear_configs.append(
|
||||
BackendPatternConfig(nnqat.Linear)
|
||||
.set_observation_type(observation_type) # noqa: E131
|
||||
.set_dtype_configs(dtype_configs)
|
||||
.set_root_module(torch.nn.Linear)
|
||||
.set_reference_quantized_module(nnqr.Linear)
|
||||
)
|
||||
# functional linear
|
||||
linear_configs.append(
|
||||
BackendPatternConfig(torch.nn.functional.linear)
|
||||
.set_observation_type(observation_type) # noqa: E131
|
||||
.set_dtype_configs(dtype_configs)
|
||||
._set_input_type_to_index({"weight": 1, "bias": 2})
|
||||
)
|
||||
|
||||
# (2) Linear + relu
|
||||
# -------------------
|
||||
# 2.1 linear module + relu fusion config
|
||||
# linear relu, linear module + relu module
|
||||
linear_configs.append(
|
||||
BackendPatternConfig((torch.nn.Linear, torch.nn.ReLU))
|
||||
.set_dtype_configs(dtype_configs) # noqa: E131
|
||||
.set_fuser_method(_sequential_wrapper2(nni.LinearReLU))
|
||||
.set_fused_module(nni.LinearReLU)
|
||||
)
|
||||
# linear relu, linear module + functional relu
|
||||
linear_configs.append(
|
||||
BackendPatternConfig((torch.nn.Linear, torch.nn.functional.relu))
|
||||
.set_dtype_configs(dtype_configs) # noqa: E131
|
||||
.set_fuser_method(_sequential_wrapper2(nni.LinearReLU))
|
||||
.set_fused_module(nni.LinearReLU)
|
||||
)
|
||||
|
||||
# 2.2 linear module + relu, fused module configs
|
||||
# linear relu, fused module
|
||||
linear_configs.append(
|
||||
BackendPatternConfig(nni.LinearReLU)
|
||||
.set_observation_type(observation_type) # noqa: E131
|
||||
.set_dtype_configs(dtype_configs)
|
||||
.set_root_module(torch.nn.Linear)
|
||||
.set_reference_quantized_module(nnqr.Linear)
|
||||
.set_qat_module(nniqat.LinearReLU)
|
||||
)
|
||||
# linear relu, qat fused module
|
||||
linear_configs.append(
|
||||
BackendPatternConfig(nniqat.LinearReLU)
|
||||
.set_observation_type(observation_type) # noqa: E131
|
||||
.set_dtype_configs(dtype_configs)
|
||||
.set_root_module(torch.nn.Linear)
|
||||
.set_reference_quantized_module(nnqr.Linear)
|
||||
)
|
||||
# 2.3 functional linear + relu configs
|
||||
# linear relu, functional linear + relu module
|
||||
linear_configs.append(
|
||||
BackendPatternConfig((F.linear, torch.nn.ReLU))
|
||||
.set_observation_type(observation_type) # noqa: E131
|
||||
.set_dtype_configs(dtype_configs)
|
||||
)
|
||||
# linear relu, functional linear + functional relu
|
||||
linear_configs.append(
|
||||
BackendPatternConfig((F.linear, F.relu))
|
||||
.set_observation_type(observation_type) # noqa: E131
|
||||
.set_dtype_configs(dtype_configs)
|
||||
)
|
||||
|
||||
# (3) Linear + batchnorm
|
||||
# ------------------------
|
||||
# 3.1 linear bn fusion
|
||||
linear_configs.append(
|
||||
BackendPatternConfig((nn.Linear, nn.BatchNorm1d))
|
||||
.set_dtype_configs(dtype_configs) # noqa: E131
|
||||
.set_fuser_method(fuse_linear_bn)
|
||||
.set_fused_module(nni.LinearBn1d)
|
||||
)
|
||||
|
||||
# 3.2 linear bn fused
|
||||
# linear bn, fused module
|
||||
linear_configs.append(
|
||||
BackendPatternConfig(nni.LinearBn1d)
|
||||
.set_observation_type(observation_type) # noqa: E131
|
||||
.set_dtype_configs(dtype_configs)
|
||||
.set_root_module(torch.nn.Linear)
|
||||
.set_reference_quantized_module(nnqr.Linear)
|
||||
.set_qat_module(nniqat.LinearBn1d)
|
||||
)
|
||||
# linear bn, qat fused module
|
||||
linear_configs.append(
|
||||
BackendPatternConfig(nniqat.LinearBn1d)
|
||||
.set_observation_type(observation_type) # noqa: E131
|
||||
.set_dtype_configs(dtype_configs)
|
||||
.set_root_module(torch.nn.Linear)
|
||||
.set_reference_quantized_module(nnqr.Linear)
|
||||
)
|
||||
return linear_configs
|
||||
|
||||
|
||||
def _get_conv_configs(dtype_configs):
|
||||
"""
|
||||
Return all configs related to conv modules and ops.
|
||||
"""
|
||||
conv_configs = []
|
||||
observation_type = ObservationType.OUTPUT_USE_DIFFERENT_OBSERVER_AS_INPUT
|
||||
for convs in [_Conv1dMetadata, _Conv2dMetadata, _Conv3dMetadata]:
|
||||
# (1) Single conv modules/functions
|
||||
# -----------------------------------
|
||||
# conv module
|
||||
conv_configs.append(
|
||||
BackendPatternConfig(convs.root)
|
||||
.set_observation_type(observation_type) # noqa: E131
|
||||
.set_dtype_configs(dtype_configs)
|
||||
.set_root_module(convs.root)
|
||||
.set_reference_quantized_module(convs.reference)
|
||||
.set_qat_module(convs.qat)
|
||||
)
|
||||
# conv qat module
|
||||
conv_configs.append(
|
||||
BackendPatternConfig(convs.qat)
|
||||
.set_observation_type(observation_type) # noqa: E131
|
||||
.set_dtype_configs(dtype_configs)
|
||||
.set_root_module(convs.root)
|
||||
.set_reference_quantized_module(convs.reference)
|
||||
)
|
||||
# functional conv
|
||||
conv_configs.append(
|
||||
BackendPatternConfig(convs.func)
|
||||
.set_observation_type(observation_type) # noqa: E131
|
||||
.set_dtype_configs(dtype_configs)
|
||||
._set_input_type_to_index({"weight": 1, "bias": 2})
|
||||
)
|
||||
|
||||
# (2) Conv + relu
|
||||
# -----------------
|
||||
# 2.1 conv module + relu fusion configs
|
||||
# conv relu fusion, conv module + relu module
|
||||
conv_configs.append(
|
||||
BackendPatternConfig((convs.root, torch.nn.ReLU))
|
||||
.set_dtype_configs(dtype_configs) # noqa: E131
|
||||
.set_fuser_method(_sequential_wrapper2(convs.fused_conv_relu))
|
||||
.set_fused_module(convs.fused_conv_relu)
|
||||
)
|
||||
# conv relu fusion, conv module + functional relu
|
||||
conv_configs.append(
|
||||
BackendPatternConfig((convs.root, F.relu))
|
||||
.set_dtype_configs(dtype_configs) # noqa: E131
|
||||
.set_fuser_method(_sequential_wrapper2(convs.fused_conv_relu))
|
||||
.set_fused_module(convs.fused_conv_relu)
|
||||
)
|
||||
# 2.2 conv module + relu fused module configs
|
||||
# conv relu, fused module
|
||||
conv_configs.append(
|
||||
BackendPatternConfig(convs.fused_conv_relu)
|
||||
.set_observation_type(observation_type) # noqa: E131
|
||||
.set_dtype_configs(dtype_configs)
|
||||
.set_root_module(convs.root)
|
||||
.set_reference_quantized_module(convs.reference)
|
||||
.set_qat_module(convs.relu_qat)
|
||||
)
|
||||
# conv relu, qat fused module
|
||||
conv_configs.append(
|
||||
BackendPatternConfig(convs.relu_qat)
|
||||
.set_observation_type(observation_type) # noqa: E131
|
||||
.set_dtype_configs(dtype_configs)
|
||||
.set_root_module(convs.root)
|
||||
.set_reference_quantized_module(convs.reference)
|
||||
)
|
||||
# 2.3 functional conv + relu configs
|
||||
# conv relu, functional conv + relu module
|
||||
conv_configs.append(
|
||||
BackendPatternConfig((convs.func, torch.nn.ReLU))
|
||||
.set_observation_type(observation_type) # noqa: E131
|
||||
.set_dtype_configs(dtype_configs)
|
||||
)
|
||||
# conv relu, functional conv + functional relu
|
||||
conv_configs.append(
|
||||
BackendPatternConfig((convs.func, F.relu))
|
||||
.set_observation_type(observation_type) # noqa: E131
|
||||
.set_dtype_configs(dtype_configs)
|
||||
)
|
||||
|
||||
# fused conv relu
|
||||
conv_configs.append(
|
||||
BackendPatternConfig(convs.fused_conv_relu)
|
||||
.set_dtype_configs(dtype_configs) # noqa: E131
|
||||
.set_qat_module(convs.relu_qat)
|
||||
)
|
||||
|
||||
conv_configs.append(
|
||||
BackendPatternConfig(convs.relu_qat)
|
||||
.set_dtype_configs(dtype_configs) # noqa: E131
|
||||
.set_root_module(convs.root)
|
||||
.set_reference_quantized_module(convs.reference)
|
||||
)
|
||||
|
||||
# (3) Conv + batchnorm (+ relu)
|
||||
# -------------------------------
|
||||
# 3.1 conv bn fusion configs
|
||||
# conv + bn fusion
|
||||
conv_configs.append(
|
||||
BackendPatternConfig((convs.root, convs.bn))
|
||||
.set_dtype_configs(dtype_configs) # noqa: E131
|
||||
.set_fuser_method(fuse_conv_bn)
|
||||
.set_fused_module(convs.fused_conv_bn)
|
||||
)
|
||||
# conv + bn + relu module fusion
|
||||
conv_configs.append(
|
||||
BackendPatternConfig((convs.root, convs.bn, nn.ReLU))
|
||||
.set_dtype_configs(dtype_configs) # noqa: E131
|
||||
.set_fuser_method(fuse_conv_bn_relu)
|
||||
.set_fused_module(convs.fused_conv_bn_relu)
|
||||
)
|
||||
# conv + bn + relu functional fusion
|
||||
conv_configs.append(
|
||||
BackendPatternConfig((convs.root, convs.bn, F.relu))
|
||||
.set_dtype_configs(dtype_configs) # noqa: E131
|
||||
.set_root_module(convs.root)
|
||||
.set_fuser_method(fuse_conv_bn_relu)
|
||||
.set_fused_module(convs.fused_conv_bn_relu)
|
||||
)
|
||||
# TODO: we can add fusion for torch.relu as well
|
||||
|
||||
# 3.2 conv + bn (+ relu) fused module configs
|
||||
# fused conv bn
|
||||
conv_configs.append(
|
||||
BackendPatternConfig(convs.fused_conv_bn)
|
||||
.set_dtype_configs(dtype_configs) # noqa: E131
|
||||
.set_qat_module(convs.bn_qat)
|
||||
)
|
||||
|
||||
# fused conv bn relu
|
||||
conv_configs.append(
|
||||
BackendPatternConfig(convs.fused_conv_bn_relu)
|
||||
.set_dtype_configs(dtype_configs) # noqa: E131
|
||||
.set_qat_module(convs.bn_relu_qat)
|
||||
)
|
||||
|
||||
# conv bn, qat fused module
|
||||
conv_configs.append(
|
||||
BackendPatternConfig(convs.bn_qat)
|
||||
.set_observation_type(observation_type) # noqa: E131
|
||||
.set_dtype_configs(dtype_configs)
|
||||
.set_root_module(convs.root)
|
||||
.set_reference_quantized_module(convs.reference)
|
||||
)
|
||||
# conv bn relu, qat fused module
|
||||
conv_configs.append(
|
||||
BackendPatternConfig(convs.bn_relu_qat)
|
||||
.set_observation_type(observation_type) # noqa: E131
|
||||
.set_dtype_configs(dtype_configs)
|
||||
.set_root_module(convs.root)
|
||||
.set_reference_quantized_module(convs.reference)
|
||||
)
|
||||
|
||||
# (4) conv transpose and its fusion
|
||||
# 4.1 conv transpose config
|
||||
conv_configs.append(
|
||||
BackendPatternConfig(convs.transpose)
|
||||
.set_dtype_configs(dtype_configs) # noqa: E131
|
||||
.set_root_module(convs.transpose)
|
||||
.set_reference_quantized_module(convs.transpose_reference)
|
||||
)
|
||||
|
||||
# 4.2 conv transpose + bn fusion
|
||||
conv_configs.append(
|
||||
BackendPatternConfig((convs.transpose, convs.bn))
|
||||
.set_dtype_configs(dtype_configs) # noqa: E131
|
||||
.set_fuser_method(fuse_convtranspose_bn)
|
||||
.set_root_module(convs.transpose)
|
||||
.set_reference_quantized_module(convs.transpose_reference)
|
||||
)
|
||||
|
||||
# 4.3 functional conv transpose
|
||||
conv_configs.append(
|
||||
BackendPatternConfig(convs.func_transpose)
|
||||
.set_dtype_configs(dtype_configs) # noqa: E131
|
||||
._set_input_type_to_index({"weight": 1, "bias": 2})
|
||||
)
|
||||
|
||||
return conv_configs
|
||||
|
||||
|
||||
def _get_cat_config(dtype_configs: list[DTypeConfig]) -> BackendPatternConfig:
|
||||
return (
|
||||
BackendPatternConfig(torch.cat)
|
||||
.set_observation_type(ObservationType.OUTPUT_SHARE_OBSERVER_WITH_INPUT)
|
||||
.set_dtype_configs(dtype_configs)
|
||||
)
|
||||
|
||||
|
||||
def _get_ln_configs(dtype_configs: list[DTypeConfig]) -> list[BackendPatternConfig]:
|
||||
ln_configs = []
|
||||
ln_configs.append(
|
||||
BackendPatternConfig(torch.nn.LayerNorm)
|
||||
.set_observation_type(ObservationType.OUTPUT_USE_DIFFERENT_OBSERVER_AS_INPUT) # noqa: E131
|
||||
.set_dtype_configs(dtype_configs)
|
||||
)
|
||||
ln_configs.append(
|
||||
BackendPatternConfig(torch.nn.functional.layer_norm)
|
||||
.set_observation_type(ObservationType.OUTPUT_USE_DIFFERENT_OBSERVER_AS_INPUT) # noqa: E131
|
||||
.set_dtype_configs(dtype_configs)
|
||||
._set_input_type_to_index({"weight": 2, "bias": 3})
|
||||
)
|
||||
return ln_configs
|
||||
|
||||
|
||||
def _get_default_op_configs(
|
||||
dtype_configs: list[DTypeConfig],
|
||||
) -> list[BackendPatternConfig]:
|
||||
default_ops = [
|
||||
torch.nn.ELU,
|
||||
torch.nn.LeakyReLU,
|
||||
torch.nn.Hardswish,
|
||||
torch.nn.InstanceNorm1d,
|
||||
torch.nn.InstanceNorm2d,
|
||||
torch.nn.InstanceNorm3d,
|
||||
torch.nn.Dropout,
|
||||
torch.nn.PReLU,
|
||||
torch.nn.functional.elu,
|
||||
torch.nn.functional.hardswish,
|
||||
torch.nn.functional.leaky_relu,
|
||||
torch.nn.functional.dropout,
|
||||
]
|
||||
configs = [
|
||||
BackendPatternConfig(op)
|
||||
.set_observation_type(ObservationType.OUTPUT_USE_DIFFERENT_OBSERVER_AS_INPUT) # noqa: E131
|
||||
.set_dtype_configs(dtype_configs)
|
||||
for op in default_ops
|
||||
]
|
||||
|
||||
configs.append(
|
||||
BackendPatternConfig(torch.nn.functional.group_norm)
|
||||
.set_observation_type(ObservationType.OUTPUT_USE_DIFFERENT_OBSERVER_AS_INPUT) # noqa: E131
|
||||
.set_dtype_configs(dtype_configs)
|
||||
._set_input_type_to_index({"weight": 2, "bias": 3})
|
||||
)
|
||||
|
||||
configs.append(
|
||||
BackendPatternConfig(torch.nn.functional.instance_norm)
|
||||
.set_observation_type(ObservationType.OUTPUT_USE_DIFFERENT_OBSERVER_AS_INPUT) # noqa: E131
|
||||
.set_dtype_configs(dtype_configs)
|
||||
._set_input_type_to_index({"weight": 3, "bias": 4})
|
||||
)
|
||||
return configs
|
||||
|
||||
|
||||
def _add_fixed_qparams_to_dtype_configs(
|
||||
dtype_configs: list[DTypeConfig],
|
||||
constraints: DTypeWithConstraints,
|
||||
) -> list[DTypeConfig]:
|
||||
"""
|
||||
Return a copy of the list of DTypeConfigs where activations are subject to the specified
|
||||
constraints required for fixed qparams ops.
|
||||
|
||||
If the data type doesn't match the one in the constraints, simply leave the corresponding
|
||||
DTypeConfig unchanged.
|
||||
|
||||
If `scale_min_lower_bound` or `scale_max_upper_bound` is specified in the activations,
|
||||
throw an exception since these settings are incompatible with fixed qparams ops.
|
||||
"""
|
||||
new_dtype_configs = []
|
||||
for dtype_config in dtype_configs:
|
||||
dc = copy.deepcopy(dtype_config)
|
||||
for orig_constraints in [
|
||||
dc.input_dtype_with_constraints,
|
||||
dc.output_dtype_with_constraints,
|
||||
]:
|
||||
if orig_constraints.dtype != constraints.dtype:
|
||||
continue
|
||||
if orig_constraints.scale_min_lower_bound is not None:
|
||||
raise ValueError(
|
||||
f"scale_min_lower_bound is invalid for fixed qparams ops: {dtype_config}"
|
||||
)
|
||||
if orig_constraints.scale_max_upper_bound is not None:
|
||||
raise ValueError(
|
||||
f"scale_max_upper_bound is invalid for fixed qparams ops: {dtype_config}"
|
||||
)
|
||||
orig_constraints.quant_min_lower_bound = constraints.quant_min_lower_bound
|
||||
orig_constraints.quant_max_upper_bound = constraints.quant_max_upper_bound
|
||||
orig_constraints.scale_exact_match = constraints.scale_exact_match
|
||||
orig_constraints.zero_point_exact_match = constraints.zero_point_exact_match
|
||||
new_dtype_configs.append(dc)
|
||||
return new_dtype_configs
|
||||
|
||||
|
||||
def _get_fixed_qparams_op_configs(
|
||||
dtype_configs: list[DTypeConfig],
|
||||
) -> list[BackendPatternConfig]:
|
||||
fixed_qparams_op_configs = []
|
||||
for fixed_qparam_op, constraints in _FIXED_QPARAMS_OP_TO_CONSTRAINTS.items():
|
||||
new_dtype_configs = _add_fixed_qparams_to_dtype_configs(
|
||||
dtype_configs, constraints
|
||||
)
|
||||
fixed_qparams_op_configs.append(
|
||||
BackendPatternConfig(fixed_qparam_op)
|
||||
.set_observation_type(
|
||||
ObservationType.OUTPUT_USE_DIFFERENT_OBSERVER_AS_INPUT
|
||||
) # noqa: E131
|
||||
.set_dtype_configs(new_dtype_configs)
|
||||
)
|
||||
return fixed_qparams_op_configs
|
||||
|
||||
|
||||
def _get_share_qparams_op_configs(dtype_configs):
|
||||
"""Get the operator config for the operators that works for both float and quantized input
|
||||
if input is quantized, the output Tensor shares the same quantization parameter
|
||||
with input.
|
||||
Example operator: avgpool2d, reshape, transpose, maxpool2d
|
||||
Example observed operator:
|
||||
observer_0 - avgpool2d - observer_0 (same observer instance as input)
|
||||
"""
|
||||
|
||||
def _get_share_qprams_op_backend_config(op):
|
||||
return (
|
||||
BackendPatternConfig(op)
|
||||
.set_observation_type(ObservationType.OUTPUT_SHARE_OBSERVER_WITH_INPUT)
|
||||
.set_dtype_configs(dtype_configs)
|
||||
)
|
||||
|
||||
share_qparams_ops = [
|
||||
torch.nn.AdaptiveAvgPool1d,
|
||||
torch.nn.AdaptiveAvgPool2d,
|
||||
torch.nn.AdaptiveAvgPool3d,
|
||||
torch.nn.AvgPool1d,
|
||||
torch.nn.AvgPool2d,
|
||||
torch.nn.AvgPool3d,
|
||||
torch.nn.Hardtanh,
|
||||
torch.nn.Identity,
|
||||
torch.nn.MaxPool1d,
|
||||
torch.nn.MaxPool2d,
|
||||
torch.nn.MaxPool3d,
|
||||
torch.nn.PixelShuffle,
|
||||
torch.nn.PixelUnshuffle,
|
||||
torch.nn.ReLU,
|
||||
torch.nn.ReLU6,
|
||||
torch.adaptive_avg_pool1d,
|
||||
torch.nn.functional.adaptive_avg_pool2d,
|
||||
torch.nn.functional.adaptive_avg_pool3d,
|
||||
torch.nn.functional.hardtanh,
|
||||
torch.nn.functional.hardtanh_,
|
||||
torch.nn.functional.interpolate,
|
||||
torch.nn.functional.max_pool1d,
|
||||
torch.nn.functional.max_pool2d,
|
||||
torch.nn.functional.max_pool3d,
|
||||
torch.nn.functional.pixel_shuffle,
|
||||
torch.nn.functional.pixel_unshuffle,
|
||||
torch.nn.functional.relu,
|
||||
torch.nn.functional.relu6,
|
||||
torch.avg_pool1d,
|
||||
torch._C._nn.avg_pool2d,
|
||||
torch._C._nn.avg_pool3d,
|
||||
torch.clamp,
|
||||
torch.flatten,
|
||||
torch.mean,
|
||||
torch.narrow,
|
||||
torch.repeat_interleave,
|
||||
torch.transpose,
|
||||
torch.squeeze,
|
||||
torch.stack,
|
||||
torch.unsqueeze,
|
||||
operator.floordiv,
|
||||
"contiguous",
|
||||
"clamp",
|
||||
"detach",
|
||||
"detach_",
|
||||
"mean",
|
||||
"permute",
|
||||
"repeat",
|
||||
"repeat_interleave",
|
||||
"reshape",
|
||||
"resize_",
|
||||
"relu",
|
||||
"relu_",
|
||||
"squeeze",
|
||||
"squeeze_",
|
||||
"transpose",
|
||||
"unsqueeze",
|
||||
"unsqueeze_",
|
||||
"view",
|
||||
]
|
||||
return [_get_share_qprams_op_backend_config(op) for op in share_qparams_ops]
|
||||
|
||||
|
||||
def _get_bn_configs(dtype_configs: list[DTypeConfig]) -> list[BackendPatternConfig]:
|
||||
"""Get configs related to batchnorm."""
|
||||
bn_configs = []
|
||||
bn_to_fused_bn = {
|
||||
torch.nn.BatchNorm2d: nni.BNReLU2d,
|
||||
torch.nn.BatchNorm3d: nni.BNReLU3d,
|
||||
}
|
||||
for bn in bn_to_fused_bn:
|
||||
fused_bn = bn_to_fused_bn[bn]
|
||||
# bn module + relu module fusion config
|
||||
bn_configs.append(
|
||||
BackendPatternConfig((bn, nn.ReLU))
|
||||
.set_dtype_configs(dtype_configs) # noqa: E131
|
||||
.set_fuser_method(_sequential_wrapper2(fused_bn))
|
||||
.set_fused_module(fused_bn)
|
||||
)
|
||||
# bn module + F.relu fusion config
|
||||
bn_configs.append(
|
||||
BackendPatternConfig((bn, F.relu))
|
||||
.set_dtype_configs(dtype_configs) # noqa: E131
|
||||
.set_fuser_method(_sequential_wrapper2(fused_bn))
|
||||
.set_fused_module(fused_bn)
|
||||
)
|
||||
bn_configs.append(
|
||||
BackendPatternConfig(bn)
|
||||
.set_observation_type(
|
||||
ObservationType.OUTPUT_USE_DIFFERENT_OBSERVER_AS_INPUT
|
||||
) # noqa: E131
|
||||
.set_dtype_configs(dtype_configs)
|
||||
)
|
||||
|
||||
# fused bn configs
|
||||
for fused_bn in bn_to_fused_bn.values():
|
||||
bn_configs.append(
|
||||
BackendPatternConfig(fused_bn)
|
||||
.set_observation_type(
|
||||
ObservationType.OUTPUT_USE_DIFFERENT_OBSERVER_AS_INPUT
|
||||
) # noqa: E131
|
||||
.set_dtype_configs(dtype_configs)
|
||||
)
|
||||
return bn_configs
|
||||
|
||||
|
||||
def _get_rnn_op_configs(dtype_configs: list[DTypeConfig]) -> list[BackendPatternConfig]:
|
||||
rnn_op_configs = []
|
||||
for rnn_op, ref_rnn_op in [
|
||||
(nn.GRUCell, nnqr.GRUCell),
|
||||
(nn.LSTMCell, nnqr.LSTMCell),
|
||||
(nn.RNNCell, nnqr.RNNCell),
|
||||
(nn.LSTM, nnqr.LSTM),
|
||||
(nn.GRU, nnqr.GRU),
|
||||
]:
|
||||
rnn_op_configs.append(
|
||||
BackendPatternConfig(rnn_op)
|
||||
.set_observation_type(
|
||||
ObservationType.OUTPUT_USE_DIFFERENT_OBSERVER_AS_INPUT
|
||||
) # noqa: E131
|
||||
.set_dtype_configs(dtype_configs)
|
||||
.set_root_module(rnn_op)
|
||||
.set_reference_quantized_module(ref_rnn_op)
|
||||
)
|
||||
return rnn_op_configs
|
||||
|
||||
|
||||
def _get_embedding_op_configs(
|
||||
dtype_configs: list[DTypeConfig],
|
||||
) -> list[BackendPatternConfig]:
|
||||
embedding_op_configs = []
|
||||
for embedding_op, qat_embedding_op, ref_embedding_op in [
|
||||
(nn.Embedding, nnqat.Embedding, nnqr.Embedding),
|
||||
(nn.EmbeddingBag, nnqat.EmbeddingBag, nnqr.EmbeddingBag),
|
||||
]:
|
||||
embedding_op_configs.append(
|
||||
BackendPatternConfig(embedding_op)
|
||||
.set_observation_type(
|
||||
ObservationType.OUTPUT_USE_DIFFERENT_OBSERVER_AS_INPUT
|
||||
) # noqa: E131
|
||||
.set_dtype_configs(dtype_configs)
|
||||
.set_qat_module(qat_embedding_op)
|
||||
.set_root_module(embedding_op)
|
||||
.set_reference_quantized_module(ref_embedding_op)
|
||||
)
|
||||
|
||||
# config for qat op
|
||||
embedding_op_configs.append(
|
||||
BackendPatternConfig(qat_embedding_op)
|
||||
.set_observation_type(
|
||||
ObservationType.OUTPUT_USE_DIFFERENT_OBSERVER_AS_INPUT
|
||||
) # noqa: E131
|
||||
.set_dtype_configs(dtype_configs)
|
||||
.set_root_module(embedding_op)
|
||||
.set_reference_quantized_module(ref_embedding_op)
|
||||
)
|
||||
return embedding_op_configs
|
||||
|
||||
|
||||
def _get_tensor_info_op_configs(dtype_configs):
|
||||
"""
|
||||
These ops work on tensors of different dtypes but return non-tensors
|
||||
containing information about the input tensor.
|
||||
"""
|
||||
|
||||
def _get_config(op):
|
||||
return (
|
||||
BackendPatternConfig(op)
|
||||
.set_observation_type(ObservationType.INPUT_OUTPUT_NOT_OBSERVED)
|
||||
.set_dtype_configs(dtype_configs)
|
||||
)
|
||||
|
||||
return [_get_config(op) for op in ("shape", "size")]
|
||||
+181
@@ -0,0 +1,181 @@
|
||||
# mypy: allow-untyped-defs
|
||||
import operator
|
||||
|
||||
import torch
|
||||
from torch.ao.quantization.backend_config import (
|
||||
BackendConfig,
|
||||
BackendPatternConfig,
|
||||
DTypeConfig,
|
||||
ObservationType,
|
||||
)
|
||||
|
||||
|
||||
weighted_op_quint8_dtype_config = DTypeConfig(
|
||||
input_dtype=torch.quint8,
|
||||
output_dtype=torch.quint8,
|
||||
weight_dtype=torch.qint8,
|
||||
bias_dtype=torch.float,
|
||||
)
|
||||
|
||||
|
||||
def get_linear_configs():
|
||||
linear_configs = []
|
||||
observation_type = ObservationType.OUTPUT_USE_DIFFERENT_OBSERVER_AS_INPUT
|
||||
dtype_configs = [weighted_op_quint8_dtype_config]
|
||||
|
||||
# TODO: need to fix the way we insert observers for this pattern
|
||||
# should be solved in the new fusion API
|
||||
# reason that this doesn't work: the pattern is a bit complicated and we don't
|
||||
# have a way to specify which input of the pattern we would like to observe
|
||||
# pattern:
|
||||
# bias input weight
|
||||
# \ | /
|
||||
# \ | t
|
||||
# \ | /
|
||||
# addmm
|
||||
# we want to observe "weight" as weight, but there is not way to convey this
|
||||
# information with current pattern language
|
||||
#
|
||||
# right now:
|
||||
# original:
|
||||
# weight - t \
|
||||
# input - addmm
|
||||
# observed (no hack):
|
||||
# weight - t - observer \
|
||||
# input - observer - addmm
|
||||
# target:
|
||||
# weight - observer - t \
|
||||
# input - observer - addmm
|
||||
|
||||
# def root_node_getter(node_pattern):
|
||||
# addmm, bias, act, weight = node_pattern
|
||||
# return addmm
|
||||
|
||||
# linear_configs.append(
|
||||
# BackendPatternConfig((torch.ops.aten.addmm.default, MatchAllNode, MatchAllNode, torch.ops.aten.t.default))
|
||||
# .set_observation_type(observation_type) # noqa: E131
|
||||
# .set_dtype_configs(dtype_configs)
|
||||
# ._set_root_node_getter(root_node_getter))
|
||||
|
||||
linear_configs.append(
|
||||
BackendPatternConfig(torch.ops.aten.addmm.default)
|
||||
.set_observation_type(observation_type) # noqa: E131
|
||||
.set_dtype_configs(dtype_configs)
|
||||
._set_input_type_to_index({"weight": 2, "bias": 0})
|
||||
)
|
||||
# linear is decomposed to `t - mm` if bias is not present
|
||||
linear_configs.append(
|
||||
BackendPatternConfig(torch.ops.aten.mm.default)
|
||||
.set_observation_type(observation_type) # noqa: E131
|
||||
.set_dtype_configs(dtype_configs)
|
||||
._set_input_type_to_index({"weight": 1})
|
||||
)
|
||||
return linear_configs
|
||||
|
||||
|
||||
def get_conv_configs():
|
||||
conv_configs = []
|
||||
observation_type = ObservationType.OUTPUT_USE_DIFFERENT_OBSERVER_AS_INPUT
|
||||
dtype_configs = [weighted_op_quint8_dtype_config]
|
||||
conv_configs.append(
|
||||
BackendPatternConfig(torch.ops.aten.convolution.default)
|
||||
.set_observation_type(observation_type) # noqa: E131
|
||||
.set_dtype_configs(dtype_configs)
|
||||
._set_input_type_to_index({"weight": 1, "bias": 2})
|
||||
)
|
||||
conv_configs.append(
|
||||
BackendPatternConfig(
|
||||
(torch.ops.aten.convolution.default, torch.ops.aten.relu.default)
|
||||
)
|
||||
.set_observation_type(observation_type) # noqa: E131
|
||||
.set_dtype_configs(dtype_configs)
|
||||
._set_input_type_to_index({"weight": 1, "bias": 2})
|
||||
)
|
||||
# TODO: remove when functionalization is supported in PT2 mode
|
||||
conv_configs.append(
|
||||
BackendPatternConfig(
|
||||
(torch.ops.aten.convolution.default, torch.ops.aten.relu_.default)
|
||||
)
|
||||
.set_observation_type(observation_type) # noqa: E131
|
||||
.set_dtype_configs(dtype_configs)
|
||||
._set_input_type_to_index({"weight": 1, "bias": 2})
|
||||
)
|
||||
return conv_configs
|
||||
|
||||
|
||||
def get_pooling_configs():
|
||||
backend_pattern_configs = []
|
||||
observation_type = ObservationType.OUTPUT_SHARE_OBSERVER_WITH_INPUT
|
||||
dtype_configs = [weighted_op_quint8_dtype_config]
|
||||
|
||||
def root_node_getter(node_pattern):
|
||||
_getitem, maxpool, _index = node_pattern
|
||||
return maxpool
|
||||
|
||||
backend_pattern_configs.append(
|
||||
BackendPatternConfig()
|
||||
._set_pattern_complex_format(
|
||||
(operator.getitem, torch.ops.aten.max_pool2d_with_indices.default, 0)
|
||||
)
|
||||
.set_observation_type(observation_type) # noqa: E131
|
||||
.set_dtype_configs(dtype_configs)
|
||||
._set_root_node_getter(root_node_getter)
|
||||
)
|
||||
|
||||
return backend_pattern_configs
|
||||
|
||||
|
||||
def get_relu_configs():
|
||||
backend_pattern_configs = []
|
||||
observation_type = ObservationType.OUTPUT_SHARE_OBSERVER_WITH_INPUT
|
||||
dtype_configs = [weighted_op_quint8_dtype_config]
|
||||
backend_pattern_configs.append(
|
||||
BackendPatternConfig(torch.ops.aten.relu.default)
|
||||
.set_observation_type(observation_type) # noqa: E131
|
||||
.set_dtype_configs(dtype_configs)
|
||||
)
|
||||
return backend_pattern_configs
|
||||
|
||||
|
||||
def get_binary_op_configs():
|
||||
binary_op_configs: list[BackendPatternConfig] = []
|
||||
dtype_configs = [weighted_op_quint8_dtype_config]
|
||||
num_tensor_args_to_observation_type_mapping = {
|
||||
# TODO: this is not used right now since we have extra check in prepare
|
||||
# will need to change this to NO_OBSERVER later after we implemented
|
||||
# Tensor dtype inference properly
|
||||
0: ObservationType.OUTPUT_USE_DIFFERENT_OBSERVER_AS_INPUT,
|
||||
1: ObservationType.OUTPUT_SHARE_OBSERVER_WITH_INPUT,
|
||||
2: ObservationType.OUTPUT_USE_DIFFERENT_OBSERVER_AS_INPUT,
|
||||
}
|
||||
for op_with_quantized_bop_scalar_variant in [
|
||||
torch.ops.aten.add.Tensor,
|
||||
torch.ops.aten.add_.Tensor,
|
||||
]:
|
||||
bop_patterns = [
|
||||
(op_with_quantized_bop_scalar_variant, torch.ops.aten.relu.default),
|
||||
op_with_quantized_bop_scalar_variant,
|
||||
# TODO: remove when functionalization is supported in pt2_mode
|
||||
(op_with_quantized_bop_scalar_variant, torch.ops.aten.relu_.default),
|
||||
]
|
||||
binary_op_configs.extend(
|
||||
BackendPatternConfig(bop_pattern)
|
||||
.set_dtype_configs(dtype_configs) # noqa: E131
|
||||
._set_num_tensor_args_to_observation_type(
|
||||
num_tensor_args_to_observation_type_mapping
|
||||
)
|
||||
for bop_pattern in bop_patterns
|
||||
)
|
||||
|
||||
return binary_op_configs
|
||||
|
||||
|
||||
def get_qnnpack_pt2e_backend_config():
|
||||
return (
|
||||
BackendConfig("qnnpack_pytorch_2.0_export")
|
||||
.set_backend_pattern_configs(get_linear_configs())
|
||||
.set_backend_pattern_configs(get_binary_op_configs())
|
||||
.set_backend_pattern_configs(get_conv_configs())
|
||||
.set_backend_pattern_configs(get_pooling_configs())
|
||||
.set_backend_pattern_configs(get_relu_configs())
|
||||
)
|
||||
+751
@@ -0,0 +1,751 @@
|
||||
# mypy: allow-untyped-defs
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from typing import Any, TYPE_CHECKING
|
||||
|
||||
import torch
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Callable
|
||||
|
||||
from torch.ao.quantization.utils import Pattern
|
||||
|
||||
|
||||
__all__ = [
|
||||
"BackendConfig",
|
||||
"BackendPatternConfig",
|
||||
"DTypeConfig",
|
||||
"DTypeWithConstraints",
|
||||
"ObservationType",
|
||||
]
|
||||
|
||||
|
||||
# DTypeConfig dict keys
|
||||
INPUT_DTYPE_DICT_KEY = "input_dtype"
|
||||
OUTPUT_DTYPE_DICT_KEY = "output_dtype"
|
||||
WEIGHT_DTYPE_DICT_KEY = "weight_dtype"
|
||||
BIAS_DTYPE_DICT_KEY = "bias_dtype"
|
||||
IS_DYNAMIC_DICT_KEY = "is_dynamic"
|
||||
|
||||
# BackendConfig dict keys
|
||||
NAME_DICT_KEY = "name"
|
||||
CONFIGS_DICT_KEY = "configs"
|
||||
|
||||
# BackendPatternConfig dict keys
|
||||
PATTERN_DICT_KEY = "pattern"
|
||||
PATTERN_COMPLEX_FORMAT_DICT_KEY = "pattern_complex_format"
|
||||
OBSERVATION_TYPE_DICT_KEY = "observation_type"
|
||||
DTYPE_CONFIGS_DICT_KEY = "dtype_configs"
|
||||
ROOT_MODULE_DICT_KEY = "root_module"
|
||||
QAT_MODULE_DICT_KEY = "qat_module"
|
||||
REFERENCE_QUANTIZED_MODULE_DICT_KEY = "reference_quantized_module_for_root"
|
||||
FUSED_MODULE_DICT_KEY = "fused_module"
|
||||
FUSER_METHOD_DICT_KEY = "fuser_method"
|
||||
ROOT_NODE_GETTER_DICT_KEY = "root_node_getter"
|
||||
EXTRA_INPUTS_GETTER_DICT_KEY = "extra_inputs_getter"
|
||||
NUM_TENSOR_ARGS_TO_OBSERVATION_TYPE_DICT_KEY = "num_tensor_args_to_observation_type"
|
||||
INPUT_TYPE_TO_INDEX_DICT_KEY = "input_type_to_index"
|
||||
|
||||
|
||||
# TODO: maybe rename this to something that's not related to observer
|
||||
# e.g. QParamsType
|
||||
class ObservationType(Enum):
|
||||
"""An enum that represents different ways of how an operator/operator pattern
|
||||
should be observed
|
||||
"""
|
||||
|
||||
OUTPUT_USE_DIFFERENT_OBSERVER_AS_INPUT = 0
|
||||
"""this means input and output are observed with different observers, based
|
||||
on qconfig.activation
|
||||
example: conv, linear, softmax
|
||||
"""
|
||||
|
||||
OUTPUT_SHARE_OBSERVER_WITH_INPUT = 1
|
||||
"""this means the output will use the same observer instance as input, based
|
||||
on qconfig.activation
|
||||
example: torch.cat, maxpool
|
||||
"""
|
||||
|
||||
INPUT_OUTPUT_NOT_OBSERVED = 2
|
||||
"""this means the input and output are never observed
|
||||
example: x.shape, x.size
|
||||
"""
|
||||
|
||||
|
||||
@dataclass
|
||||
class DTypeWithConstraints:
|
||||
"""
|
||||
Config for specifying additional constraints for a given dtype, such as quantization
|
||||
value ranges, scale value ranges, and fixed quantization params, to be used in
|
||||
:class:`~torch.ao.quantization.backend_config.DTypeConfig`.
|
||||
|
||||
The constraints currently supported are:
|
||||
|
||||
* `quant_min_lower_bound` and `quant_max_upper_bound`: Lower and upper
|
||||
bounds for the minimum and maximum quantized values respectively. If
|
||||
the QConfig's `quant_min` and `quant_max` fall outside this range,
|
||||
then the QConfig will be ignored.
|
||||
|
||||
* `scale_min_lower_bound` and `scale_max_upper_bound`: Lower and upper
|
||||
bounds for the minimum and maximum scale values respectively. If the
|
||||
QConfig's minimum scale value (currently exposed as `eps`) falls below
|
||||
the lower bound, then the QConfig will be ignored. Note that the upper
|
||||
bound is currently not enforced.
|
||||
|
||||
* `scale_exact_match` and `zero_point_exact_match`: Exact match requirements
|
||||
for scale and zero point, to be used for operators with fixed quantization
|
||||
parameters such as sigmoid and tanh. If the observer specified in the QConfig
|
||||
is neither `FixedQParamsObserver` nor `FixedQParamsFakeQuantize`, or if
|
||||
the quantization parameters don't match, then the QConfig will be ignored.
|
||||
"""
|
||||
|
||||
dtype: torch.dtype | None = None
|
||||
quant_min_lower_bound: int | float | None = None
|
||||
quant_max_upper_bound: int | float | None = None
|
||||
scale_min_lower_bound: int | float | None = None
|
||||
scale_max_upper_bound: int | float | None = None
|
||||
scale_exact_match: float | None = None
|
||||
zero_point_exact_match: int | None = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class DTypeConfig:
|
||||
"""
|
||||
Config object that specifies the supported data types passed as arguments to
|
||||
quantize ops in the reference model spec, for input and output activations,
|
||||
weights, and biases.
|
||||
|
||||
For example, consider the following reference model:
|
||||
|
||||
quant1 - [dequant1 - fp32_linear - quant2] - dequant2
|
||||
|
||||
The pattern in the square brackets refers to the reference pattern of
|
||||
statically quantized linear. Setting the input dtype as `torch.quint8`
|
||||
in the DTypeConfig means we pass in `torch.quint8` as the dtype argument
|
||||
to the first quantize op (quant1). Similarly, setting the output dtype as
|
||||
`torch.quint8` means we pass in `torch.quint8` as the dtype argument to
|
||||
the second quantize op (quant2).
|
||||
|
||||
Note that the dtype here does not refer to the interface dtypes of the
|
||||
op. For example, the "input dtype" here is not the dtype of the input
|
||||
tensor passed to the quantized linear op. Though it can still be the
|
||||
same as the interface dtype, this is not always the case, e.g. the
|
||||
interface dtype is fp32 in dynamic quantization but the "input dtype"
|
||||
specified in the DTypeConfig would still be quint8. The semantics of
|
||||
dtypes here are the same as the semantics of the dtypes specified in
|
||||
the observers.
|
||||
|
||||
These dtypes are matched against the ones specified in the user's
|
||||
QConfig. If there is a match, and the QConfig satisfies the constraints
|
||||
specified in the DTypeConfig (if any), then we will quantize the given
|
||||
pattern using this DTypeConfig. Otherwise, the QConfig is ignored and
|
||||
the pattern will not be quantized.
|
||||
|
||||
Example usage::
|
||||
|
||||
>>> # xdoctest: +SKIP(failing)
|
||||
>>> dtype_config1 = DTypeConfig(
|
||||
... input_dtype=torch.quint8,
|
||||
... output_dtype=torch.quint8,
|
||||
... weight_dtype=torch.qint8,
|
||||
... bias_dtype=torch.float)
|
||||
|
||||
>>> dtype_config2 = DTypeConfig(
|
||||
... input_dtype=DTypeWithConstraints(
|
||||
... dtype=torch.quint8,
|
||||
... quant_min_lower_bound=0,
|
||||
... quant_max_upper_bound=255,
|
||||
... ),
|
||||
... output_dtype=DTypeWithConstraints(
|
||||
... dtype=torch.quint8,
|
||||
... quant_min_lower_bound=0,
|
||||
... quant_max_upper_bound=255,
|
||||
... ),
|
||||
... weight_dtype=DTypeWithConstraints(
|
||||
... dtype=torch.qint8,
|
||||
... quant_min_lower_bound=-128,
|
||||
... quant_max_upper_bound=127,
|
||||
... ),
|
||||
... bias_dtype=torch.float)
|
||||
|
||||
>>> dtype_config1.input_dtype
|
||||
torch.quint8
|
||||
|
||||
>>> dtype_config2.input_dtype
|
||||
torch.quint8
|
||||
|
||||
>>> dtype_config2.input_dtype_with_constraints
|
||||
DTypeWithConstraints(dtype=torch.quint8, quant_min_lower_bound=0, quant_max_upper_bound=255, \
|
||||
scale_min_lower_bound=None, scale_max_upper_bound=None)
|
||||
"""
|
||||
|
||||
input_dtype_with_constraints: DTypeWithConstraints
|
||||
output_dtype_with_constraints: DTypeWithConstraints
|
||||
weight_dtype_with_constraints: DTypeWithConstraints
|
||||
bias_dtype: torch.dtype | None
|
||||
is_dynamic: bool | None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
input_dtype: torch.dtype | DTypeWithConstraints | None = None,
|
||||
output_dtype: torch.dtype | DTypeWithConstraints | None = None,
|
||||
weight_dtype: torch.dtype | DTypeWithConstraints | None = None,
|
||||
bias_dtype: torch.dtype | None = None,
|
||||
is_dynamic: bool | None = None,
|
||||
):
|
||||
if isinstance(input_dtype, DTypeWithConstraints):
|
||||
self.input_dtype_with_constraints = input_dtype
|
||||
else:
|
||||
self.input_dtype_with_constraints = DTypeWithConstraints(dtype=input_dtype)
|
||||
|
||||
if isinstance(output_dtype, DTypeWithConstraints):
|
||||
self.output_dtype_with_constraints = output_dtype
|
||||
else:
|
||||
self.output_dtype_with_constraints = DTypeWithConstraints(
|
||||
dtype=output_dtype
|
||||
)
|
||||
|
||||
if isinstance(weight_dtype, DTypeWithConstraints):
|
||||
self.weight_dtype_with_constraints = weight_dtype
|
||||
else:
|
||||
self.weight_dtype_with_constraints = DTypeWithConstraints(
|
||||
dtype=weight_dtype
|
||||
)
|
||||
|
||||
self.bias_dtype = bias_dtype
|
||||
self.is_dynamic = is_dynamic
|
||||
|
||||
@property
|
||||
def input_dtype(self) -> torch.dtype | None:
|
||||
return self.input_dtype_with_constraints.dtype
|
||||
|
||||
@property
|
||||
def output_dtype(self) -> torch.dtype | None:
|
||||
return self.output_dtype_with_constraints.dtype
|
||||
|
||||
@property
|
||||
def weight_dtype(self) -> torch.dtype | None:
|
||||
return self.weight_dtype_with_constraints.dtype
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, dtype_config_dict: dict[str, Any]) -> DTypeConfig:
|
||||
"""
|
||||
Create a ``DTypeConfig`` from a dictionary with the following items (all optional):
|
||||
"input_dtype": torch.dtype or ``DTypeWithConstraints``
|
||||
"output_dtype": torch.dtype or ``DTypeWithConstraints``
|
||||
"weight_dtype": torch.dtype or ``DTypeWithConstraints``
|
||||
"bias_type": torch.dtype
|
||||
"is_dynamic": bool
|
||||
"""
|
||||
input_dtype = dtype_config_dict.get(INPUT_DTYPE_DICT_KEY)
|
||||
if input_dtype is not None and not isinstance(
|
||||
input_dtype, (torch.dtype, DTypeWithConstraints)
|
||||
):
|
||||
raise ValueError(
|
||||
"Expected input_dtype to be a torch.dtype or DTypeWithConstraints"
|
||||
)
|
||||
output_dtype = dtype_config_dict.get(OUTPUT_DTYPE_DICT_KEY)
|
||||
if output_dtype is not None and not isinstance(
|
||||
output_dtype, (torch.dtype, DTypeWithConstraints)
|
||||
):
|
||||
raise ValueError(
|
||||
"Expected output_dtype to be a torch.dtype or DTypeWithConstraints"
|
||||
)
|
||||
weight_dtype = dtype_config_dict.get(WEIGHT_DTYPE_DICT_KEY)
|
||||
if weight_dtype is not None and not isinstance(
|
||||
weight_dtype, (torch.dtype, DTypeWithConstraints)
|
||||
):
|
||||
raise ValueError(
|
||||
"Expected weight_dtype to be a torch.dtype or DTypeWithConstraints"
|
||||
)
|
||||
bias_dtype = dtype_config_dict.get(BIAS_DTYPE_DICT_KEY)
|
||||
is_dynamic = dtype_config_dict.get(IS_DYNAMIC_DICT_KEY)
|
||||
return cls(input_dtype, output_dtype, weight_dtype, bias_dtype, is_dynamic)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
"""
|
||||
Convert this ``DTypeConfig`` to a dictionary with the items described in
|
||||
:func:`~torch.ao.quantization.backend_config.DTypeConfig.from_dict`.
|
||||
"""
|
||||
dtype_config_dict: dict[str, Any] = {}
|
||||
if self.input_dtype is not None:
|
||||
dtype_config_dict[INPUT_DTYPE_DICT_KEY] = self.input_dtype_with_constraints
|
||||
if self.output_dtype is not None:
|
||||
dtype_config_dict[OUTPUT_DTYPE_DICT_KEY] = (
|
||||
self.output_dtype_with_constraints
|
||||
)
|
||||
if self.weight_dtype is not None:
|
||||
dtype_config_dict[WEIGHT_DTYPE_DICT_KEY] = (
|
||||
self.weight_dtype_with_constraints
|
||||
)
|
||||
if self.bias_dtype is not None:
|
||||
dtype_config_dict[BIAS_DTYPE_DICT_KEY] = self.bias_dtype
|
||||
if self.is_dynamic is not None:
|
||||
dtype_config_dict[IS_DYNAMIC_DICT_KEY] = self.is_dynamic
|
||||
return dtype_config_dict
|
||||
|
||||
|
||||
class BackendConfig:
|
||||
# TODO: refer to NativeBackendConfig once that is implemented
|
||||
"""Config that defines the set of patterns that can be quantized on a given backend, and how reference
|
||||
quantized models can be produced from these patterns.
|
||||
|
||||
A pattern in this context refers to a module, a functional, an operator, or a directed acyclic graph
|
||||
of the above. Each pattern supported on the target backend can be individually configured through
|
||||
:class:`~torch.ao.quantization.backend_config.BackendPatternConfig` in terms of:
|
||||
|
||||
(1) The supported input/output activation, weight, and bias data types
|
||||
|
||||
(2) How observers and quant/dequant ops are inserted in order to construct the reference pattern, and
|
||||
|
||||
(3) (Optionally) Fusion, QAT, and reference module mappings.
|
||||
|
||||
The format of the patterns is described in:
|
||||
https://github.com/pytorch/pytorch/blob/master/torch/ao/quantization/backend_config/README.md
|
||||
|
||||
Example usage::
|
||||
|
||||
import torch
|
||||
from torch.ao.quantization.backend_config import (
|
||||
BackendConfig,
|
||||
BackendPatternConfig,
|
||||
DTypeConfig,
|
||||
ObservationType,
|
||||
)
|
||||
|
||||
weighted_int8_dtype_config = DTypeConfig(
|
||||
input_dtype=torch.quint8,
|
||||
output_dtype=torch.quint8,
|
||||
weight_dtype=torch.qint8,
|
||||
bias_dtype=torch.float)
|
||||
|
||||
def fuse_conv2d_relu(is_qat, conv, relu):
|
||||
return torch.ao.nn.intrinsic.ConvReLU2d(conv, relu)
|
||||
|
||||
# For quantizing Linear
|
||||
linear_config = BackendPatternConfig(torch.nn.Linear) \
|
||||
.set_observation_type(ObservationType.OUTPUT_USE_DIFFERENT_OBSERVER_AS_INPUT) \
|
||||
.add_dtype_config(weighted_int8_dtype_config) \
|
||||
.set_root_module(torch.nn.Linear) \
|
||||
.set_qat_module(torch.ao.nn.qat.Linear) \
|
||||
.set_reference_quantized_module(torch.ao.nn.quantized.reference.Linear)
|
||||
|
||||
# For fusing Conv2d + ReLU into ConvReLU2d
|
||||
conv_relu_config = BackendPatternConfig((torch.nn.Conv2d, torch.nn.ReLU)) \
|
||||
.set_observation_type(ObservationType.OUTPUT_USE_DIFFERENT_OBSERVER_AS_INPUT) \
|
||||
.add_dtype_config(weighted_int8_dtype_config) \
|
||||
.set_fused_module(torch.ao.nn.intrinsic.ConvReLU2d) \
|
||||
.set_fuser_method(fuse_conv2d_relu)
|
||||
|
||||
# For quantizing ConvReLU2d
|
||||
fused_conv_relu_config = BackendPatternConfig(torch.ao.nn.intrinsic.ConvReLU2d) \
|
||||
.set_observation_type(ObservationType.OUTPUT_USE_DIFFERENT_OBSERVER_AS_INPUT) \
|
||||
.add_dtype_config(weighted_int8_dtype_config) \
|
||||
.set_root_module(torch.nn.Conv2d) \
|
||||
.set_qat_module(torch.ao.nn.intrinsic.qat.ConvReLU2d) \
|
||||
.set_reference_quantized_module(torch.ao.nn.quantized.reference.Conv2d)
|
||||
|
||||
backend_config = BackendConfig("my_backend") \
|
||||
.set_backend_pattern_config(linear_config) \
|
||||
.set_backend_pattern_config(conv_relu_config) \
|
||||
.set_backend_pattern_config(fused_conv_relu_config)
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, name: str = ""):
|
||||
self.name = name
|
||||
# Store all BackendPatternConfigs in a map to handle duplicates
|
||||
# Note: the key in this map uses the complex reversed tuple format.
|
||||
# This is intended only for internal use; users who wish to access
|
||||
# the original patterns should go through `self.configs` instead.
|
||||
self._pattern_complex_format_to_config: dict[Pattern, BackendPatternConfig] = {}
|
||||
|
||||
def __repr__(self):
|
||||
return f"BackendConfig({self.__dict__})"
|
||||
|
||||
def set_name(self, name: str) -> BackendConfig:
|
||||
"""
|
||||
Set the name of the target backend.
|
||||
"""
|
||||
self.name = name
|
||||
return self
|
||||
|
||||
def set_backend_pattern_config(self, config: BackendPatternConfig) -> BackendConfig:
|
||||
"""
|
||||
Set the config for an pattern that can be run on the target backend.
|
||||
This overrides any existing config for the given pattern.
|
||||
"""
|
||||
# Avoid circular dependencies
|
||||
pattern_complex_format = torch.ao.quantization.backend_config.utils._get_pattern_in_reversed_nested_tuple_format(
|
||||
config
|
||||
) # type: ignore[attr-defined]
|
||||
self._pattern_complex_format_to_config[pattern_complex_format] = config
|
||||
return self
|
||||
|
||||
def set_backend_pattern_configs(
|
||||
self, configs: list[BackendPatternConfig]
|
||||
) -> BackendConfig:
|
||||
"""
|
||||
Set the configs for patterns that can be run on the target backend.
|
||||
This overrides any existing config for a given pattern if it was previously registered already.
|
||||
"""
|
||||
for conf in configs:
|
||||
self.set_backend_pattern_config(conf)
|
||||
return self
|
||||
|
||||
@property
|
||||
def configs(self) -> list[BackendPatternConfig]:
|
||||
"""
|
||||
Return a copy of the list of configs set in this `BackendConfig`.
|
||||
"""
|
||||
return list(self._pattern_complex_format_to_config.values())
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, backend_config_dict: dict[str, Any]) -> BackendConfig:
|
||||
"""
|
||||
Create a ``BackendConfig`` from a dictionary with the following items:
|
||||
|
||||
"name": the name of the target backend
|
||||
|
||||
"configs": a list of dictionaries that each represents a `BackendPatternConfig`
|
||||
|
||||
"""
|
||||
conf = cls(backend_config_dict.get(NAME_DICT_KEY, ""))
|
||||
for d in backend_config_dict.get(CONFIGS_DICT_KEY, []):
|
||||
if isinstance(d, BackendPatternConfig):
|
||||
conf.set_backend_pattern_config(d)
|
||||
elif isinstance(d, dict):
|
||||
conf.set_backend_pattern_config(BackendPatternConfig.from_dict(d))
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Expected backend_config_dict['{CONFIGS_DICT_KEY}'] to be a dictionary"
|
||||
)
|
||||
return conf
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
"""
|
||||
Convert this ``BackendConfig`` to a dictionary with the items described in
|
||||
:func:`~torch.ao.quantization.backend_config.BackendConfig.from_dict`.
|
||||
"""
|
||||
return {
|
||||
NAME_DICT_KEY: self.name,
|
||||
CONFIGS_DICT_KEY: [c.to_dict() for c in self.configs],
|
||||
}
|
||||
|
||||
|
||||
class BackendPatternConfig:
|
||||
"""
|
||||
Config object that specifies quantization behavior for a given operator pattern.
|
||||
For a detailed example usage, see :class:`~torch.ao.quantization.backend_config.BackendConfig`.
|
||||
"""
|
||||
|
||||
def __init__(self, pattern: Pattern | None = None):
|
||||
self.pattern: Pattern | None = pattern
|
||||
self.observation_type = ObservationType.OUTPUT_USE_DIFFERENT_OBSERVER_AS_INPUT
|
||||
self.dtype_configs: list[DTypeConfig] = []
|
||||
self.root_module: type[torch.nn.Module] | None = None
|
||||
self.qat_module: type[torch.nn.Module] | None = None
|
||||
self.reference_quantized_module: type[torch.nn.Module] | None = None
|
||||
self.fused_module: type[torch.nn.Module] | None = None
|
||||
self.fuser_method: Callable | None = None
|
||||
|
||||
# Temporary/internal configs
|
||||
self._root_node_getter: Callable | None = None
|
||||
self._extra_inputs_getter: Callable | None = None
|
||||
self._num_tensor_args_to_observation_type: dict[int, ObservationType] = {}
|
||||
self._input_type_to_index: dict[str, int] = {}
|
||||
self._pattern_complex_format: Pattern | None = None
|
||||
|
||||
def __repr__(self):
|
||||
dict_nonempty = {
|
||||
k: v
|
||||
for k, v in self.__dict__.items()
|
||||
if (
|
||||
(not isinstance(v, (list, dict)) and v is not None)
|
||||
or (isinstance(v, (list, dict)) and len(v) > 0)
|
||||
)
|
||||
}
|
||||
return f"BackendPatternConfig({dict_nonempty})"
|
||||
|
||||
def set_pattern(self, pattern: Pattern) -> BackendPatternConfig:
|
||||
"""
|
||||
Set the pattern to configure.
|
||||
|
||||
The pattern can be a float module, functional operator, pytorch operator, or a tuple
|
||||
combination of the above. Tuple patterns are treated as sequential patterns, and
|
||||
currently only tuples of 2 or 3 elements are supported.
|
||||
"""
|
||||
if self._pattern_complex_format is not None:
|
||||
raise ValueError(
|
||||
"Only one of 'pattern' or 'pattern_complex_format' can be set"
|
||||
)
|
||||
self.pattern = pattern
|
||||
return self
|
||||
|
||||
def set_observation_type(
|
||||
self, observation_type: ObservationType
|
||||
) -> BackendPatternConfig:
|
||||
"""
|
||||
Set how observers should be inserted in the graph for this pattern.
|
||||
|
||||
Observation type here refers to how observers (or quant-dequant ops) will be placed
|
||||
in the graph. This is used to produce the desired reference patterns understood by
|
||||
the backend. Weighted ops such as linear and conv require different observers
|
||||
(or quantization parameters passed to quantize ops in the reference model) for the
|
||||
input and the output.
|
||||
|
||||
There are two observation types:
|
||||
|
||||
`OUTPUT_USE_DIFFERENT_OBSERVER_AS_INPUT` (default): the output observer instance
|
||||
will be different from the input. This is the most common observation type.
|
||||
|
||||
`OUTPUT_SHARE_OBSERVER_WITH_INPUT`: the output observer instance will be the
|
||||
same as the input. This is useful for operators like `cat`.
|
||||
|
||||
Note: This will be renamed in the near future, since we will soon insert QuantDeQuantStubs
|
||||
with observers (and fake quantizes) attached instead of observers themselves.
|
||||
"""
|
||||
self.observation_type = observation_type
|
||||
return self
|
||||
|
||||
def add_dtype_config(self, dtype_config: DTypeConfig) -> BackendPatternConfig:
|
||||
"""
|
||||
Add a set of supported data types passed as arguments to quantize ops in the
|
||||
reference model spec.
|
||||
"""
|
||||
self.dtype_configs.append(dtype_config)
|
||||
return self
|
||||
|
||||
def set_dtype_configs(
|
||||
self, dtype_configs: list[DTypeConfig]
|
||||
) -> BackendPatternConfig:
|
||||
"""
|
||||
Set the supported data types passed as arguments to quantize ops in the
|
||||
reference model spec, overriding all previously registered data types.
|
||||
"""
|
||||
self.dtype_configs = dtype_configs
|
||||
return self
|
||||
|
||||
def set_root_module(
|
||||
self, root_module: type[torch.nn.Module]
|
||||
) -> BackendPatternConfig:
|
||||
"""
|
||||
Set the module that represents the root for this pattern.
|
||||
|
||||
When we construct the reference quantized model during the convert phase,
|
||||
the root modules (e.g. torch.nn.Linear for torch.ao.nn.intrinsic.LinearReLU)
|
||||
will be swapped to the corresponding reference quantized modules (e.g.
|
||||
torch.ao.nn.reference.quantized.Linear). This allows custom backends to
|
||||
specify custom reference quantized module implementations to match the
|
||||
numerics of their lowered operators. Since this is a one-to-one mapping,
|
||||
both the root module and the reference quantized module must be specified
|
||||
in the same BackendPatternConfig in order for the conversion to take place.
|
||||
"""
|
||||
self.root_module = root_module
|
||||
return self
|
||||
|
||||
def set_qat_module(self, qat_module: type[torch.nn.Module]) -> BackendPatternConfig:
|
||||
"""
|
||||
Set the module that represents the QAT implementation for this pattern.
|
||||
"""
|
||||
self.qat_module = qat_module
|
||||
return self
|
||||
|
||||
def set_reference_quantized_module(
|
||||
self, reference_quantized_module: type[torch.nn.Module]
|
||||
) -> BackendPatternConfig:
|
||||
"""
|
||||
Set the module that represents the reference quantized implementation for
|
||||
this pattern's root module.
|
||||
|
||||
For more detail, see :func:`~torch.ao.quantization.backend_config.BackendPatternConfig.set_root_module`.
|
||||
"""
|
||||
self.reference_quantized_module = reference_quantized_module
|
||||
return self
|
||||
|
||||
def set_fused_module(
|
||||
self, fused_module: type[torch.nn.Module]
|
||||
) -> BackendPatternConfig:
|
||||
"""
|
||||
Set the module that represents the fused implementation for this pattern.
|
||||
"""
|
||||
self.fused_module = fused_module
|
||||
return self
|
||||
|
||||
def set_fuser_method(self, fuser_method: Callable) -> BackendPatternConfig:
|
||||
"""
|
||||
Set the function that specifies how to fuse this BackendPatternConfig's pattern.
|
||||
|
||||
The first argument of this function should be `is_qat`, and the rest of the arguments
|
||||
should be the items in the tuple pattern. The return value of this function should be
|
||||
the resulting fused module.
|
||||
|
||||
For example, the fuser method for the pattern `(torch.nn.Linear, torch.nn.ReLU)` can be:
|
||||
|
||||
def fuse_linear_relu(is_qat, linear, relu):
|
||||
return torch.ao.nn.intrinsic.LinearReLU(linear, relu)
|
||||
|
||||
For a more complicated example, see https://gist.github.com/jerryzh168/8bea7180a8ba3c279f2c9b050f2a69a6.
|
||||
"""
|
||||
self.fuser_method = fuser_method
|
||||
return self
|
||||
|
||||
def _set_root_node_getter(self, root_node_getter: Callable) -> BackendPatternConfig:
|
||||
self._root_node_getter = root_node_getter
|
||||
return self
|
||||
|
||||
def _set_extra_inputs_getter(
|
||||
self, extra_inputs_getter: Callable
|
||||
) -> BackendPatternConfig:
|
||||
self._extra_inputs_getter = extra_inputs_getter
|
||||
return self
|
||||
|
||||
def _set_num_tensor_args_to_observation_type(
|
||||
self, num_tensor_args_to_observation_type: dict[int, ObservationType]
|
||||
) -> BackendPatternConfig:
|
||||
self._num_tensor_args_to_observation_type = num_tensor_args_to_observation_type
|
||||
return self
|
||||
|
||||
def _set_input_type_to_index(
|
||||
self, input_type_to_index: dict[str, int]
|
||||
) -> BackendPatternConfig:
|
||||
self._input_type_to_index = input_type_to_index
|
||||
return self
|
||||
|
||||
def _set_pattern_complex_format(self, pattern: Pattern) -> BackendPatternConfig:
|
||||
"""
|
||||
Set the pattern to configure, using the reversed nested tuple format.
|
||||
|
||||
See the BackendConfig README for more detail:
|
||||
https://github.com/pytorch/pytorch/blob/master/torch/ao/quantization/backend_config/README.md#advanced-pattern-specification
|
||||
"""
|
||||
if self.pattern is not None:
|
||||
raise ValueError(
|
||||
"Only one of 'pattern' or 'pattern_complex_format' can be set"
|
||||
)
|
||||
self._pattern_complex_format = pattern
|
||||
return self
|
||||
|
||||
@classmethod
|
||||
def from_dict(
|
||||
cls, backend_pattern_config_dict: dict[str, Any]
|
||||
) -> BackendPatternConfig:
|
||||
"""
|
||||
Create a ``BackendPatternConfig`` from a dictionary with the following items:
|
||||
|
||||
"pattern": the pattern being configured
|
||||
"observation_type": the :class:`~torch.ao.quantization.backend_config.ObservationType` that specifies how
|
||||
observers should be inserted for this pattern
|
||||
"dtype_configs": a list of dictionaries that represents :class:`~torch.ao.quantization.backend_config.DTypeConfig` s
|
||||
"root_module": a :class:`torch.nn.Module` that represents the root for this pattern
|
||||
"qat_module": a :class:`torch.nn.Module` that represents the QAT implementation for this pattern
|
||||
"reference_quantized_module": a :class:`torch.nn.Module` that represents the reference quantized
|
||||
implementation for this pattern's root module.
|
||||
"fused_module": a :class:`torch.nn.Module` that represents the fused implementation for this pattern
|
||||
"fuser_method": a function that specifies how to fuse the pattern for this pattern
|
||||
"pattern_complex_format": the pattern specified in the reversed nested tuple format (deprecated)
|
||||
|
||||
"""
|
||||
|
||||
def _get_dtype_config(obj: Any) -> DTypeConfig:
|
||||
"""
|
||||
Convert the given object into a ``DTypeConfig`` if possible, else throw an exception.
|
||||
"""
|
||||
if isinstance(obj, DTypeConfig):
|
||||
return obj
|
||||
if isinstance(obj, dict):
|
||||
return DTypeConfig.from_dict(obj)
|
||||
raise ValueError(
|
||||
f"Expected a list of DTypeConfigs in "
|
||||
f"backend_pattern_config_dict[\"{DTYPE_CONFIGS_DICT_KEY}\"], got '{type(obj)}'"
|
||||
)
|
||||
|
||||
conf = cls()
|
||||
if PATTERN_DICT_KEY in backend_pattern_config_dict:
|
||||
conf.set_pattern(backend_pattern_config_dict[PATTERN_DICT_KEY])
|
||||
if OBSERVATION_TYPE_DICT_KEY in backend_pattern_config_dict:
|
||||
conf.set_observation_type(
|
||||
backend_pattern_config_dict[OBSERVATION_TYPE_DICT_KEY]
|
||||
)
|
||||
for d in backend_pattern_config_dict.get(DTYPE_CONFIGS_DICT_KEY, []):
|
||||
conf.add_dtype_config(_get_dtype_config(d))
|
||||
conf.set_root_module(
|
||||
backend_pattern_config_dict.get(ROOT_MODULE_DICT_KEY) # type: ignore[arg-type]
|
||||
)
|
||||
conf.set_qat_module(backend_pattern_config_dict.get(QAT_MODULE_DICT_KEY)) # type: ignore[arg-type]
|
||||
conf.set_reference_quantized_module(
|
||||
backend_pattern_config_dict.get(REFERENCE_QUANTIZED_MODULE_DICT_KEY) # type: ignore[arg-type]
|
||||
)
|
||||
conf.set_fused_module(
|
||||
backend_pattern_config_dict.get(FUSED_MODULE_DICT_KEY) # type: ignore[arg-type]
|
||||
)
|
||||
conf.set_fuser_method(
|
||||
backend_pattern_config_dict.get(FUSER_METHOD_DICT_KEY) # type: ignore[arg-type]
|
||||
)
|
||||
conf._set_root_node_getter(
|
||||
backend_pattern_config_dict.get(ROOT_NODE_GETTER_DICT_KEY) # type: ignore[arg-type]
|
||||
)
|
||||
conf._set_extra_inputs_getter(
|
||||
backend_pattern_config_dict.get(EXTRA_INPUTS_GETTER_DICT_KEY) # type: ignore[arg-type]
|
||||
)
|
||||
conf._set_num_tensor_args_to_observation_type(
|
||||
backend_pattern_config_dict.get(
|
||||
NUM_TENSOR_ARGS_TO_OBSERVATION_TYPE_DICT_KEY, {}
|
||||
)
|
||||
)
|
||||
conf._set_input_type_to_index(
|
||||
backend_pattern_config_dict.get(INPUT_TYPE_TO_INDEX_DICT_KEY, {})
|
||||
)
|
||||
if PATTERN_COMPLEX_FORMAT_DICT_KEY in backend_pattern_config_dict:
|
||||
conf._set_pattern_complex_format(
|
||||
backend_pattern_config_dict[PATTERN_COMPLEX_FORMAT_DICT_KEY]
|
||||
)
|
||||
return conf
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
"""
|
||||
Convert this ``BackendPatternConfig`` to a dictionary with the items described in
|
||||
:func:`~torch.ao.quantization.backend_config.BackendPatternConfig.from_dict`.
|
||||
"""
|
||||
backend_pattern_config_dict: dict[str, Any] = {
|
||||
OBSERVATION_TYPE_DICT_KEY: self.observation_type,
|
||||
DTYPE_CONFIGS_DICT_KEY: [c.to_dict() for c in self.dtype_configs],
|
||||
}
|
||||
if self.pattern is not None:
|
||||
backend_pattern_config_dict[PATTERN_DICT_KEY] = self.pattern
|
||||
if self.root_module is not None:
|
||||
backend_pattern_config_dict[ROOT_MODULE_DICT_KEY] = self.root_module
|
||||
if self.qat_module is not None:
|
||||
backend_pattern_config_dict[QAT_MODULE_DICT_KEY] = self.qat_module
|
||||
if self.reference_quantized_module is not None:
|
||||
backend_pattern_config_dict[REFERENCE_QUANTIZED_MODULE_DICT_KEY] = (
|
||||
self.reference_quantized_module
|
||||
)
|
||||
if self.fused_module is not None:
|
||||
backend_pattern_config_dict[FUSED_MODULE_DICT_KEY] = self.fused_module
|
||||
if self.fuser_method is not None:
|
||||
backend_pattern_config_dict[FUSER_METHOD_DICT_KEY] = self.fuser_method
|
||||
if self._root_node_getter is not None:
|
||||
backend_pattern_config_dict[ROOT_NODE_GETTER_DICT_KEY] = (
|
||||
self._root_node_getter
|
||||
)
|
||||
if self._extra_inputs_getter is not None:
|
||||
backend_pattern_config_dict[EXTRA_INPUTS_GETTER_DICT_KEY] = (
|
||||
self._extra_inputs_getter
|
||||
)
|
||||
if len(self._num_tensor_args_to_observation_type) > 0:
|
||||
backend_pattern_config_dict[
|
||||
NUM_TENSOR_ARGS_TO_OBSERVATION_TYPE_DICT_KEY
|
||||
] = self._num_tensor_args_to_observation_type
|
||||
if len(self._input_type_to_index) > 0:
|
||||
backend_pattern_config_dict[INPUT_TYPE_TO_INDEX_DICT_KEY] = (
|
||||
self._input_type_to_index
|
||||
)
|
||||
if self._pattern_complex_format is not None:
|
||||
backend_pattern_config_dict[PATTERN_COMPLEX_FORMAT_DICT_KEY] = (
|
||||
self._pattern_complex_format
|
||||
)
|
||||
return backend_pattern_config_dict
|
||||
+498
@@ -0,0 +1,498 @@
|
||||
# TODO: rename executorch to qnnpack_executorch since executorch is a general runtime
|
||||
# not a specific backend
|
||||
|
||||
import operator
|
||||
|
||||
import torch
|
||||
import torch.ao.nn.qat as nnqat
|
||||
import torch.ao.nn.quantized.reference as nnqr
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
from torch.ao.quantization.fuser_method_mappings import (
|
||||
_sequential_wrapper2,
|
||||
fuse_conv_bn,
|
||||
fuse_conv_bn_relu,
|
||||
)
|
||||
|
||||
from ._common_operator_config_utils import _Conv2dMetadata
|
||||
from .backend_config import (
|
||||
BackendConfig,
|
||||
BackendPatternConfig,
|
||||
DTypeConfig,
|
||||
DTypeWithConstraints,
|
||||
ObservationType,
|
||||
)
|
||||
from .qnnpack import (
|
||||
qnnpack_default_op_qint8_symmetric_dtype_config,
|
||||
qnnpack_weighted_op_qint8_symmetric_dtype_config,
|
||||
)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"get_executorch_backend_config",
|
||||
]
|
||||
|
||||
|
||||
# ===================
|
||||
# | DTYPE CONFIGS |
|
||||
# ===================
|
||||
|
||||
executorch_weighted_op_int8_dtype_config = DTypeConfig(
|
||||
input_dtype=torch.quint8,
|
||||
output_dtype=torch.quint8,
|
||||
weight_dtype=torch.qint8,
|
||||
bias_dtype=torch.float,
|
||||
)
|
||||
|
||||
executorch_default_op_quint8_dtype_config = DTypeConfig(
|
||||
input_dtype=torch.quint8,
|
||||
output_dtype=torch.quint8,
|
||||
)
|
||||
|
||||
executorch_default_dynamic_quint8_dtype_config = DTypeConfig(
|
||||
input_dtype=torch.quint8,
|
||||
output_dtype=torch.float,
|
||||
weight_dtype=torch.qint8,
|
||||
bias_dtype=torch.float,
|
||||
is_dynamic=True,
|
||||
)
|
||||
|
||||
executorch_act_qint8_scale_min_2_neg_12 = DTypeWithConstraints(
|
||||
dtype=torch.qint8,
|
||||
scale_min_lower_bound=2**-12,
|
||||
)
|
||||
|
||||
executorch_weight_qint8_neg_127_to_127_scale_min_2_neg_12 = DTypeWithConstraints(
|
||||
dtype=torch.qint8,
|
||||
quant_min_lower_bound=-127,
|
||||
quant_max_upper_bound=127,
|
||||
scale_min_lower_bound=2**-12,
|
||||
)
|
||||
|
||||
executorch_default_dynamic_qint8_dtype_config = DTypeConfig(
|
||||
input_dtype=executorch_act_qint8_scale_min_2_neg_12,
|
||||
output_dtype=torch.float,
|
||||
weight_dtype=executorch_weight_qint8_neg_127_to_127_scale_min_2_neg_12,
|
||||
bias_dtype=torch.float,
|
||||
is_dynamic=True,
|
||||
)
|
||||
|
||||
executorch_default_dynamic_float16_dtype_config = DTypeConfig(
|
||||
input_dtype=torch.float16,
|
||||
output_dtype=torch.float,
|
||||
weight_dtype=torch.float16,
|
||||
bias_dtype=torch.float,
|
||||
is_dynamic=True,
|
||||
)
|
||||
|
||||
executorch_weight_only_quint8_dtype_config = DTypeConfig(
|
||||
input_dtype=torch.float,
|
||||
output_dtype=torch.float,
|
||||
weight_dtype=torch.quint8,
|
||||
)
|
||||
|
||||
|
||||
# =============================
|
||||
# | BACKEND PATTERN CONFIGS |
|
||||
# =============================
|
||||
|
||||
|
||||
def _get_linear_configs() -> list[BackendPatternConfig]:
|
||||
"""
|
||||
Return all configs related to linear modules and ops.
|
||||
"""
|
||||
observation_type = ObservationType.OUTPUT_USE_DIFFERENT_OBSERVER_AS_INPUT
|
||||
dtype_configs = [
|
||||
qnnpack_weighted_op_qint8_symmetric_dtype_config,
|
||||
executorch_weighted_op_int8_dtype_config,
|
||||
executorch_default_dynamic_quint8_dtype_config,
|
||||
executorch_default_dynamic_qint8_dtype_config,
|
||||
executorch_default_dynamic_float16_dtype_config,
|
||||
]
|
||||
linear_configs: list[BackendPatternConfig] = []
|
||||
# linear module
|
||||
linear_configs.append(
|
||||
BackendPatternConfig(torch.nn.Linear)
|
||||
.set_observation_type(observation_type) # noqa: E131
|
||||
.set_dtype_configs(dtype_configs)
|
||||
.set_root_module(torch.nn.Linear)
|
||||
.set_reference_quantized_module(nnqr.Linear)
|
||||
.set_qat_module(nnqat.Linear)
|
||||
)
|
||||
# linear qat module
|
||||
linear_configs.append(
|
||||
BackendPatternConfig(nnqat.Linear)
|
||||
.set_observation_type(observation_type) # noqa: E131
|
||||
.set_dtype_configs(dtype_configs)
|
||||
.set_root_module(torch.nn.Linear)
|
||||
.set_reference_quantized_module(nnqr.Linear)
|
||||
)
|
||||
# functional linear
|
||||
linear_configs.append(
|
||||
BackendPatternConfig(torch.nn.functional.linear)
|
||||
.set_observation_type(observation_type) # noqa: E131
|
||||
.set_dtype_configs(dtype_configs)
|
||||
._set_input_type_to_index({"weight": 1, "bias": 2})
|
||||
)
|
||||
return linear_configs
|
||||
|
||||
|
||||
def _get_conv_configs() -> list[BackendPatternConfig]:
|
||||
"""
|
||||
Return all configs related to conv modules and ops.
|
||||
"""
|
||||
observation_type = ObservationType.OUTPUT_USE_DIFFERENT_OBSERVER_AS_INPUT
|
||||
dtype_configs = [
|
||||
qnnpack_weighted_op_qint8_symmetric_dtype_config,
|
||||
executorch_weighted_op_int8_dtype_config,
|
||||
]
|
||||
conv_configs = []
|
||||
for convs in [_Conv2dMetadata]:
|
||||
# (1) Single conv modules/functions
|
||||
# -----------------------------------
|
||||
# conv module
|
||||
conv_configs.append(
|
||||
BackendPatternConfig(convs.root)
|
||||
.set_observation_type(observation_type) # noqa: E131
|
||||
.set_dtype_configs(dtype_configs)
|
||||
.set_root_module(convs.root)
|
||||
.set_reference_quantized_module(convs.reference)
|
||||
.set_qat_module(convs.qat)
|
||||
)
|
||||
# conv qat module
|
||||
conv_configs.append(
|
||||
BackendPatternConfig(convs.qat)
|
||||
.set_observation_type(observation_type) # noqa: E131
|
||||
.set_dtype_configs(dtype_configs)
|
||||
.set_root_module(convs.root)
|
||||
.set_reference_quantized_module(convs.reference)
|
||||
)
|
||||
# functional conv
|
||||
conv_configs.append(
|
||||
BackendPatternConfig(convs.func)
|
||||
.set_observation_type(observation_type) # noqa: E131
|
||||
.set_dtype_configs(dtype_configs)
|
||||
._set_input_type_to_index({"weight": 1, "bias": 2})
|
||||
)
|
||||
|
||||
# (2) Conv + relu
|
||||
# -----------------------------------
|
||||
# conv module + relu module
|
||||
conv_configs.append(
|
||||
BackendPatternConfig((convs.root, nn.ReLU))
|
||||
.set_dtype_configs(dtype_configs) # noqa: E131
|
||||
.set_fuser_method(_sequential_wrapper2(convs.fused_conv_relu))
|
||||
.set_fused_module(convs.fused_conv_relu)
|
||||
)
|
||||
# conv module + functional relu
|
||||
conv_configs.append(
|
||||
BackendPatternConfig((convs.root, F.relu))
|
||||
.set_dtype_configs(dtype_configs) # noqa: E131
|
||||
.set_fuser_method(_sequential_wrapper2(convs.fused_conv_relu))
|
||||
.set_fused_module(convs.fused_conv_relu)
|
||||
)
|
||||
# fused conv relu module
|
||||
conv_configs.append(
|
||||
BackendPatternConfig(convs.fused_conv_relu)
|
||||
.set_observation_type(observation_type) # noqa: E131
|
||||
.set_dtype_configs(dtype_configs)
|
||||
.set_root_module(convs.root)
|
||||
.set_reference_quantized_module(convs.reference)
|
||||
.set_qat_module(convs.relu_qat)
|
||||
)
|
||||
# conv relu, qat fused module
|
||||
conv_configs.append(
|
||||
BackendPatternConfig(convs.relu_qat)
|
||||
.set_observation_type(observation_type) # noqa: E131
|
||||
.set_dtype_configs(dtype_configs)
|
||||
.set_root_module(convs.root)
|
||||
.set_reference_quantized_module(convs.reference)
|
||||
)
|
||||
# functional conv + relu module
|
||||
conv_configs.append(
|
||||
BackendPatternConfig((convs.func, nn.ReLU))
|
||||
.set_observation_type(observation_type) # noqa: E131
|
||||
.set_dtype_configs(dtype_configs)
|
||||
)
|
||||
# functional conv + functional relu
|
||||
conv_configs.append(
|
||||
BackendPatternConfig((convs.func, F.relu))
|
||||
.set_observation_type(observation_type) # noqa: E131
|
||||
.set_dtype_configs(dtype_configs)
|
||||
)
|
||||
# fused conv relu
|
||||
conv_configs.append(
|
||||
BackendPatternConfig(convs.fused_conv_relu)
|
||||
.set_dtype_configs(dtype_configs) # noqa: E131
|
||||
.set_qat_module(convs.relu_qat)
|
||||
)
|
||||
|
||||
conv_configs.append(
|
||||
BackendPatternConfig(convs.relu_qat)
|
||||
.set_dtype_configs(dtype_configs) # noqa: E131
|
||||
.set_root_module(convs.root)
|
||||
.set_reference_quantized_module(convs.reference)
|
||||
)
|
||||
|
||||
# (3) Conv + batchnorm (+ relu)
|
||||
# -------------------------------
|
||||
# conv + batchnorm (+ relu)
|
||||
conv_configs.append(
|
||||
BackendPatternConfig((convs.root, convs.bn))
|
||||
.set_dtype_configs(dtype_configs) # noqa: E131
|
||||
.set_fuser_method(fuse_conv_bn)
|
||||
.set_fused_module(convs.fused_conv_bn)
|
||||
)
|
||||
# conv + bn + relu module fusion
|
||||
conv_configs.append(
|
||||
BackendPatternConfig((convs.root, convs.bn, nn.ReLU))
|
||||
.set_dtype_configs(dtype_configs) # noqa: E131
|
||||
.set_fuser_method(fuse_conv_bn_relu)
|
||||
.set_fused_module(convs.fused_conv_bn_relu)
|
||||
)
|
||||
# conv + bn + relu functional fusion
|
||||
conv_configs.append(
|
||||
BackendPatternConfig((convs.root, convs.bn, F.relu))
|
||||
.set_dtype_configs(dtype_configs) # noqa: E131
|
||||
.set_root_module(convs.root)
|
||||
.set_fuser_method(fuse_conv_bn_relu)
|
||||
.set_fused_module(convs.fused_conv_bn_relu)
|
||||
)
|
||||
# TODO: we can add fusion for torch.relu as well
|
||||
# 3.2 conv + bn (+ relu) fused module configs
|
||||
# fused conv bn
|
||||
conv_configs.append(
|
||||
BackendPatternConfig(convs.fused_conv_bn)
|
||||
.set_dtype_configs(dtype_configs) # noqa: E131
|
||||
.set_qat_module(convs.bn_qat)
|
||||
)
|
||||
|
||||
# fused conv bn relu
|
||||
conv_configs.append(
|
||||
BackendPatternConfig(convs.fused_conv_bn_relu)
|
||||
.set_dtype_configs(dtype_configs) # noqa: E131
|
||||
.set_qat_module(convs.bn_relu_qat)
|
||||
)
|
||||
|
||||
# conv bn, qat fused module
|
||||
conv_configs.append(
|
||||
BackendPatternConfig(convs.bn_qat)
|
||||
.set_observation_type(observation_type) # noqa: E131
|
||||
.set_dtype_configs(dtype_configs)
|
||||
.set_root_module(convs.root)
|
||||
.set_reference_quantized_module(convs.reference)
|
||||
)
|
||||
# conv bn relu, qat fused module
|
||||
conv_configs.append(
|
||||
BackendPatternConfig(convs.bn_relu_qat)
|
||||
.set_observation_type(observation_type) # noqa: E131
|
||||
.set_dtype_configs(dtype_configs)
|
||||
.set_root_module(convs.root)
|
||||
.set_reference_quantized_module(convs.reference)
|
||||
)
|
||||
return conv_configs
|
||||
|
||||
|
||||
def _get_binary_ops_configs() -> list[BackendPatternConfig]:
|
||||
"""
|
||||
Return all configs related to binary ops.
|
||||
"""
|
||||
dtype_configs = [
|
||||
qnnpack_default_op_qint8_symmetric_dtype_config,
|
||||
executorch_weighted_op_int8_dtype_config,
|
||||
]
|
||||
num_tensor_args_to_observation_type_mapping = {
|
||||
# TODO: this is not used right now since we have extra check in prepare
|
||||
# will need to change this to NO_OBSERVER later after we implemented
|
||||
# Tensor dtype inference properly
|
||||
0: ObservationType.OUTPUT_USE_DIFFERENT_OBSERVER_AS_INPUT,
|
||||
1: ObservationType.OUTPUT_SHARE_OBSERVER_WITH_INPUT,
|
||||
2: ObservationType.OUTPUT_USE_DIFFERENT_OBSERVER_AS_INPUT,
|
||||
}
|
||||
binary_op_configs: list[BackendPatternConfig] = []
|
||||
for op in [
|
||||
operator.add,
|
||||
torch.add,
|
||||
operator.sub,
|
||||
torch.sub,
|
||||
operator.mul,
|
||||
torch.mul,
|
||||
]:
|
||||
bop_patterns = [
|
||||
(op, torch.nn.ReLU),
|
||||
(op, torch.nn.functional.relu),
|
||||
(op, torch.relu),
|
||||
op,
|
||||
]
|
||||
binary_op_configs.extend(
|
||||
BackendPatternConfig(bop_pattern)
|
||||
.set_dtype_configs(dtype_configs) # noqa: E131
|
||||
._set_num_tensor_args_to_observation_type(
|
||||
num_tensor_args_to_observation_type_mapping
|
||||
)
|
||||
for bop_pattern in bop_patterns
|
||||
)
|
||||
return binary_op_configs
|
||||
|
||||
|
||||
def _get_share_qparams_ops_configs() -> list[BackendPatternConfig]:
|
||||
"""
|
||||
Return the operator configs for the operators that works for both float and quantized
|
||||
input if input is quantized, the output Tensor shares the same quantization parameter
|
||||
with input.
|
||||
|
||||
Example operator: avgpool2d, reshape, transpose, maxpool2d
|
||||
Example observed operator:
|
||||
observer_0 - avgpool2d - observer_0 (same observer instance as input)
|
||||
"""
|
||||
observation_type = ObservationType.OUTPUT_SHARE_OBSERVER_WITH_INPUT
|
||||
dtype_configs = [
|
||||
qnnpack_default_op_qint8_symmetric_dtype_config,
|
||||
executorch_default_op_quint8_dtype_config,
|
||||
]
|
||||
share_qparams_ops = [
|
||||
torch.nn.Flatten,
|
||||
F.adaptive_avg_pool2d,
|
||||
F.elu,
|
||||
F.hardtanh,
|
||||
F.max_pool2d,
|
||||
F.pad,
|
||||
F.relu,
|
||||
F.relu6,
|
||||
F.leaky_relu,
|
||||
F.leaky_relu_,
|
||||
torch.nn.AdaptiveAvgPool2d,
|
||||
torch.nn.ConstantPad2d,
|
||||
torch.nn.ELU,
|
||||
torch.nn.MaxPool2d,
|
||||
torch.nn.ReLU6,
|
||||
torch.nn.Hardtanh,
|
||||
torch.nn.LeakyReLU,
|
||||
torch.clamp,
|
||||
torch.flatten,
|
||||
torch.mean,
|
||||
torch.permute,
|
||||
torch.permute_copy,
|
||||
torch.squeeze,
|
||||
"clamp",
|
||||
"mean",
|
||||
"permute",
|
||||
"reshape",
|
||||
"relu",
|
||||
"relu_",
|
||||
"squeeze",
|
||||
"squeeze_",
|
||||
"leaky_relu",
|
||||
]
|
||||
share_qparams_op_configs: list[BackendPatternConfig] = [
|
||||
BackendPatternConfig(op)
|
||||
.set_observation_type(observation_type) # noqa: E131
|
||||
.set_dtype_configs(dtype_configs)
|
||||
for op in share_qparams_ops
|
||||
]
|
||||
return share_qparams_op_configs
|
||||
|
||||
|
||||
def _get_bn_configs() -> list[BackendPatternConfig]:
|
||||
"""
|
||||
Return all configs related to batchnorm.
|
||||
"""
|
||||
observation_type = ObservationType.OUTPUT_USE_DIFFERENT_OBSERVER_AS_INPUT
|
||||
dtype_configs = [
|
||||
qnnpack_default_op_qint8_symmetric_dtype_config,
|
||||
executorch_default_op_quint8_dtype_config,
|
||||
]
|
||||
bn_configs = []
|
||||
bn_configs.append(
|
||||
BackendPatternConfig(nn.BatchNorm2d)
|
||||
.set_observation_type(observation_type) # noqa: E131
|
||||
.set_dtype_configs(dtype_configs)
|
||||
)
|
||||
return bn_configs
|
||||
|
||||
|
||||
def _get_cat_configs() -> list[BackendPatternConfig]:
|
||||
dtype_configs = [
|
||||
qnnpack_default_op_qint8_symmetric_dtype_config,
|
||||
executorch_default_op_quint8_dtype_config,
|
||||
]
|
||||
cat_configs = []
|
||||
cat_configs.append(
|
||||
BackendPatternConfig(torch.cat)
|
||||
.set_observation_type(ObservationType.OUTPUT_SHARE_OBSERVER_WITH_INPUT)
|
||||
.set_dtype_configs(dtype_configs)
|
||||
)
|
||||
cat_configs.append(
|
||||
BackendPatternConfig(torch.concat)
|
||||
.set_observation_type(ObservationType.OUTPUT_SHARE_OBSERVER_WITH_INPUT)
|
||||
.set_dtype_configs(dtype_configs)
|
||||
)
|
||||
cat_configs.append(
|
||||
BackendPatternConfig(torch.concatenate)
|
||||
.set_observation_type(ObservationType.OUTPUT_SHARE_OBSERVER_WITH_INPUT)
|
||||
.set_dtype_configs(dtype_configs)
|
||||
)
|
||||
return cat_configs
|
||||
|
||||
|
||||
def _get_embedding_op_configs() -> list[BackendPatternConfig]:
|
||||
dtype_configs = [
|
||||
executorch_weight_only_quint8_dtype_config,
|
||||
]
|
||||
embedding_op_configs = []
|
||||
for embedding_op, qat_embedding_op, ref_embedding_op in [
|
||||
(nn.Embedding, nnqat.Embedding, nnqr.Embedding),
|
||||
(nn.EmbeddingBag, nnqat.EmbeddingBag, nnqr.EmbeddingBag),
|
||||
]:
|
||||
embedding_op_configs.append(
|
||||
BackendPatternConfig(embedding_op)
|
||||
.set_observation_type(
|
||||
ObservationType.OUTPUT_USE_DIFFERENT_OBSERVER_AS_INPUT
|
||||
) # noqa: E131
|
||||
.set_dtype_configs(dtype_configs)
|
||||
.set_qat_module(qat_embedding_op)
|
||||
.set_root_module(embedding_op)
|
||||
.set_reference_quantized_module(ref_embedding_op)
|
||||
)
|
||||
# config for qat op
|
||||
embedding_op_configs.append(
|
||||
BackendPatternConfig(qat_embedding_op)
|
||||
.set_observation_type(
|
||||
ObservationType.OUTPUT_USE_DIFFERENT_OBSERVER_AS_INPUT
|
||||
) # noqa: E131
|
||||
.set_dtype_configs(dtype_configs)
|
||||
.set_root_module(embedding_op)
|
||||
.set_reference_quantized_module(ref_embedding_op)
|
||||
)
|
||||
|
||||
# config for functional embedding
|
||||
embedding_op_configs.append(
|
||||
BackendPatternConfig(torch.nn.functional.embedding)
|
||||
.set_observation_type(
|
||||
ObservationType.OUTPUT_USE_DIFFERENT_OBSERVER_AS_INPUT
|
||||
) # noqa: E131
|
||||
.set_dtype_configs(dtype_configs)
|
||||
._set_input_type_to_index({"weight": 1})
|
||||
)
|
||||
return embedding_op_configs
|
||||
|
||||
|
||||
# =====================
|
||||
# | BACKEND CONFIGS |
|
||||
# =====================
|
||||
|
||||
|
||||
def get_executorch_backend_config() -> BackendConfig:
|
||||
"""
|
||||
Return the `BackendConfig` for backends PyTorch lowers to through the Executorch stack.
|
||||
"""
|
||||
return (
|
||||
BackendConfig("executorch")
|
||||
.set_backend_pattern_configs(_get_linear_configs())
|
||||
.set_backend_pattern_configs(_get_conv_configs())
|
||||
.set_backend_pattern_configs(_get_binary_ops_configs())
|
||||
.set_backend_pattern_configs(_get_share_qparams_ops_configs())
|
||||
.set_backend_pattern_configs(_get_bn_configs())
|
||||
.set_backend_pattern_configs(_get_cat_configs())
|
||||
.set_backend_pattern_configs(_get_embedding_op_configs())
|
||||
)
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
import torch
|
||||
|
||||
from ._common_operator_config_utils import (
|
||||
_get_binary_op_configs,
|
||||
_get_bn_configs,
|
||||
_get_cat_config,
|
||||
_get_conv_configs,
|
||||
_get_default_op_configs,
|
||||
_get_embedding_op_configs,
|
||||
_get_fixed_qparams_op_configs,
|
||||
_get_linear_configs,
|
||||
_get_rnn_op_configs,
|
||||
_get_share_qparams_op_configs,
|
||||
_get_tensor_info_op_configs,
|
||||
)
|
||||
from .backend_config import BackendConfig, DTypeConfig
|
||||
|
||||
|
||||
__all__ = [
|
||||
"get_fbgemm_backend_config",
|
||||
]
|
||||
|
||||
# ===================
|
||||
# | DTYPE CONFIGS |
|
||||
# ===================
|
||||
|
||||
# TODO: For now, these DTypeConfigs are identical to the ones defined in native.py
|
||||
# In the future, once we support specifying quant_min/quant_max and scale_min/scale_max,
|
||||
# these will diverge. In particular, for FBGEMM, we will restrict the activation quantized
|
||||
# values to within [0, 127].
|
||||
|
||||
fbgemm_weighted_op_quint8_dtype_config = DTypeConfig(
|
||||
input_dtype=torch.quint8,
|
||||
output_dtype=torch.quint8,
|
||||
weight_dtype=torch.qint8,
|
||||
bias_dtype=torch.float,
|
||||
)
|
||||
|
||||
fbgemm_default_op_quint8_dtype_config = DTypeConfig(
|
||||
input_dtype=torch.quint8,
|
||||
output_dtype=torch.quint8,
|
||||
)
|
||||
|
||||
fbgemm_default_op_fp16_dtype_config = DTypeConfig(
|
||||
input_dtype=torch.float16,
|
||||
output_dtype=torch.float16,
|
||||
weight_dtype=torch.float16,
|
||||
bias_dtype=torch.float16,
|
||||
)
|
||||
|
||||
fbgemm_default_dynamic_int8_dtype_config = DTypeConfig(
|
||||
input_dtype=torch.quint8,
|
||||
output_dtype=torch.float,
|
||||
weight_dtype=torch.qint8,
|
||||
bias_dtype=torch.float,
|
||||
is_dynamic=True,
|
||||
)
|
||||
|
||||
fbgemm_default_dynamic_float16_dtype_config = DTypeConfig(
|
||||
input_dtype=torch.float16,
|
||||
output_dtype=torch.float,
|
||||
weight_dtype=torch.float16,
|
||||
bias_dtype=torch.float,
|
||||
is_dynamic=True,
|
||||
)
|
||||
|
||||
fbgemm_weight_only_quint8_dtype_config = DTypeConfig(
|
||||
input_dtype=torch.float,
|
||||
output_dtype=torch.float,
|
||||
weight_dtype=torch.quint8,
|
||||
)
|
||||
|
||||
fbgemm_weight_only_quint4x2_dtype_config = DTypeConfig(
|
||||
input_dtype=torch.float,
|
||||
output_dtype=torch.float,
|
||||
weight_dtype=torch.quint4x2,
|
||||
)
|
||||
|
||||
|
||||
# =====================
|
||||
# | BACKEND CONFIGS |
|
||||
# =====================
|
||||
|
||||
|
||||
def get_fbgemm_backend_config() -> BackendConfig:
|
||||
"""
|
||||
Return the `BackendConfig` for PyTorch's native FBGEMM backend.
|
||||
"""
|
||||
conv_dtype_configs = [fbgemm_weighted_op_quint8_dtype_config]
|
||||
linear_dtype_configs = [
|
||||
fbgemm_weighted_op_quint8_dtype_config,
|
||||
fbgemm_default_dynamic_int8_dtype_config,
|
||||
fbgemm_default_dynamic_float16_dtype_config,
|
||||
]
|
||||
binary_op_dtype_configs = [fbgemm_default_op_quint8_dtype_config]
|
||||
default_op_dtype_configs = [fbgemm_default_op_quint8_dtype_config]
|
||||
fixed_qparams_op_dtype_configs = [fbgemm_default_op_quint8_dtype_config]
|
||||
share_qparams_op_dtype_configs = [fbgemm_default_op_quint8_dtype_config]
|
||||
tensor_info_op_dtype_configs = [fbgemm_default_op_quint8_dtype_config]
|
||||
rnn_op_dtype_configs = [
|
||||
fbgemm_default_dynamic_int8_dtype_config,
|
||||
fbgemm_default_dynamic_float16_dtype_config,
|
||||
]
|
||||
embedding_op_dtype_configs = [
|
||||
fbgemm_weight_only_quint8_dtype_config,
|
||||
fbgemm_weight_only_quint4x2_dtype_config,
|
||||
]
|
||||
return (
|
||||
BackendConfig("fbgemm")
|
||||
.set_backend_pattern_configs(_get_conv_configs(conv_dtype_configs))
|
||||
.set_backend_pattern_configs(_get_linear_configs(linear_dtype_configs))
|
||||
.set_backend_pattern_configs(_get_binary_op_configs(binary_op_dtype_configs))
|
||||
.set_backend_pattern_config(_get_cat_config(default_op_dtype_configs))
|
||||
.set_backend_pattern_configs(_get_default_op_configs(default_op_dtype_configs))
|
||||
.set_backend_pattern_configs(
|
||||
_get_fixed_qparams_op_configs(fixed_qparams_op_dtype_configs)
|
||||
)
|
||||
.set_backend_pattern_configs(
|
||||
_get_share_qparams_op_configs(share_qparams_op_dtype_configs)
|
||||
)
|
||||
.set_backend_pattern_configs(
|
||||
_get_tensor_info_op_configs(tensor_info_op_dtype_configs)
|
||||
)
|
||||
.set_backend_pattern_configs(_get_bn_configs(default_op_dtype_configs))
|
||||
.set_backend_pattern_configs(_get_rnn_op_configs(rnn_op_dtype_configs))
|
||||
.set_backend_pattern_configs(
|
||||
_get_embedding_op_configs(embedding_op_dtype_configs)
|
||||
)
|
||||
)
|
||||
+231
@@ -0,0 +1,231 @@
|
||||
# mypy: allow-untyped-defs
|
||||
import torch
|
||||
|
||||
from ._common_operator_config_utils import (
|
||||
_get_binary_op_configs,
|
||||
_get_bn_configs,
|
||||
_get_cat_config,
|
||||
_get_conv_configs,
|
||||
_get_default_op_configs,
|
||||
_get_embedding_op_configs,
|
||||
_get_fixed_qparams_op_configs,
|
||||
_get_linear_configs,
|
||||
_get_ln_configs,
|
||||
_get_rnn_op_configs,
|
||||
_get_share_qparams_op_configs,
|
||||
_get_tensor_info_op_configs,
|
||||
)
|
||||
from .backend_config import BackendConfig, DTypeConfig
|
||||
|
||||
|
||||
__all__ = [
|
||||
"get_test_only_legacy_native_backend_config",
|
||||
"default_op_quint8_dtype_config",
|
||||
"default_op_fp16_dtype_config",
|
||||
"default_dynamic_int8_dtype_config",
|
||||
"default_dynamic_float16_dtype_config",
|
||||
"input_output_only_quint8_dtype_config",
|
||||
"weight_only_quint8_dtype_config",
|
||||
"weight_only_quint4x2_dtype_config",
|
||||
"get_native_backend_config",
|
||||
"get_native_backend_config_dict",
|
||||
"get_test_only_legacy_native_backend_config_dict",
|
||||
]
|
||||
|
||||
# ===================
|
||||
# | DTYPE CONFIGS |
|
||||
# ===================
|
||||
|
||||
# weighted op int8 dtype config
|
||||
# this is config for ops that has quantized weights, like linear, conv
|
||||
weighted_op_quint8_dtype_config = DTypeConfig(
|
||||
input_dtype=torch.quint8,
|
||||
output_dtype=torch.quint8,
|
||||
weight_dtype=torch.qint8,
|
||||
bias_dtype=torch.float,
|
||||
)
|
||||
|
||||
default_op_quint8_dtype_config = DTypeConfig(
|
||||
input_dtype=torch.quint8,
|
||||
output_dtype=torch.quint8,
|
||||
)
|
||||
|
||||
default_op_fp16_dtype_config = DTypeConfig(
|
||||
input_dtype=torch.float16,
|
||||
output_dtype=torch.float16,
|
||||
weight_dtype=torch.float16,
|
||||
bias_dtype=torch.float16,
|
||||
)
|
||||
|
||||
default_dynamic_int8_dtype_config = DTypeConfig(
|
||||
input_dtype=torch.quint8,
|
||||
output_dtype=torch.float,
|
||||
weight_dtype=torch.qint8,
|
||||
bias_dtype=torch.float,
|
||||
# currently the dtype check is not yet enabled, so we provided the dtype_configs but
|
||||
# it is not really used yet,
|
||||
# we will enable it a bit later after we moved everything to backend_config_dict
|
||||
is_dynamic=True,
|
||||
)
|
||||
|
||||
default_dynamic_float16_dtype_config = DTypeConfig(
|
||||
input_dtype=torch.float16,
|
||||
output_dtype=torch.float,
|
||||
weight_dtype=torch.float16,
|
||||
bias_dtype=torch.float,
|
||||
# currently the dtype check is not yet enabled, so we provided the dtype_configs but
|
||||
# it is not really used yet,
|
||||
# we will enable it a bit later after we moved everything to backend_config_dict
|
||||
is_dynamic=True,
|
||||
)
|
||||
|
||||
# Needed for LayerNorm and f.layer_norm, since currently the kernel only supports float weights
|
||||
input_output_only_quint8_dtype_config = DTypeConfig(
|
||||
input_dtype=torch.quint8,
|
||||
output_dtype=torch.quint8,
|
||||
weight_dtype=torch.float,
|
||||
bias_dtype=torch.float,
|
||||
)
|
||||
|
||||
weight_only_quint8_dtype_config = DTypeConfig(
|
||||
input_dtype=torch.float,
|
||||
output_dtype=torch.float,
|
||||
weight_dtype=torch.quint8,
|
||||
)
|
||||
|
||||
weight_only_quint4x2_dtype_config = DTypeConfig(
|
||||
input_dtype=torch.float,
|
||||
output_dtype=torch.float,
|
||||
weight_dtype=torch.quint4x2,
|
||||
)
|
||||
|
||||
|
||||
# =====================
|
||||
# | BACKEND CONFIGS |
|
||||
# =====================
|
||||
|
||||
|
||||
def get_test_only_legacy_native_backend_config() -> BackendConfig:
|
||||
"""
|
||||
Return the `BackendConfig` for PyTorch Native backend (fbgemm/qnnpack) with various additional fp16 ops.
|
||||
"""
|
||||
conv_dtype_configs = [weighted_op_quint8_dtype_config]
|
||||
linear_dtype_configs = [
|
||||
weighted_op_quint8_dtype_config,
|
||||
default_dynamic_int8_dtype_config,
|
||||
default_dynamic_float16_dtype_config,
|
||||
default_op_fp16_dtype_config,
|
||||
]
|
||||
binary_op_dtype_configs = [
|
||||
default_op_quint8_dtype_config,
|
||||
default_op_fp16_dtype_config,
|
||||
]
|
||||
default_op_dtype_configs = [default_op_quint8_dtype_config]
|
||||
fixed_qparams_op_dtype_configs = [
|
||||
default_op_quint8_dtype_config,
|
||||
default_op_fp16_dtype_config,
|
||||
]
|
||||
share_qparams_op_dtype_configs = [
|
||||
default_op_quint8_dtype_config,
|
||||
default_op_fp16_dtype_config,
|
||||
]
|
||||
tensor_info_op_dtype_configs = [
|
||||
default_op_quint8_dtype_config,
|
||||
]
|
||||
rnn_op_dtype_configs = [
|
||||
default_dynamic_int8_dtype_config,
|
||||
default_dynamic_float16_dtype_config,
|
||||
]
|
||||
embedding_op_dtype_configs = [
|
||||
weight_only_quint8_dtype_config,
|
||||
weight_only_quint4x2_dtype_config,
|
||||
]
|
||||
layer_norm_op_dtype_configs = [input_output_only_quint8_dtype_config]
|
||||
return (
|
||||
BackendConfig("_native_and_fp16")
|
||||
.set_backend_pattern_configs(_get_conv_configs(conv_dtype_configs))
|
||||
.set_backend_pattern_configs(_get_linear_configs(linear_dtype_configs))
|
||||
.set_backend_pattern_configs(_get_binary_op_configs(binary_op_dtype_configs))
|
||||
.set_backend_pattern_config(_get_cat_config(default_op_dtype_configs))
|
||||
.set_backend_pattern_configs(_get_default_op_configs(default_op_dtype_configs))
|
||||
.set_backend_pattern_configs(
|
||||
_get_fixed_qparams_op_configs(fixed_qparams_op_dtype_configs)
|
||||
)
|
||||
.set_backend_pattern_configs(
|
||||
_get_share_qparams_op_configs(share_qparams_op_dtype_configs)
|
||||
)
|
||||
.set_backend_pattern_configs(
|
||||
_get_tensor_info_op_configs(tensor_info_op_dtype_configs)
|
||||
)
|
||||
.set_backend_pattern_configs(_get_bn_configs(default_op_dtype_configs))
|
||||
.set_backend_pattern_configs(_get_ln_configs(layer_norm_op_dtype_configs))
|
||||
.set_backend_pattern_configs(_get_rnn_op_configs(rnn_op_dtype_configs))
|
||||
.set_backend_pattern_configs(
|
||||
_get_embedding_op_configs(embedding_op_dtype_configs)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def get_native_backend_config() -> BackendConfig:
|
||||
"""
|
||||
Return the `BackendConfig` for PyTorch Native backend (fbgemm/qnnpack).
|
||||
"""
|
||||
# TODO: express this BackendConfig as a union of the FBGEMM and QNNPACK BackendConfigs
|
||||
conv_dtype_configs = [weighted_op_quint8_dtype_config]
|
||||
linear_dtype_configs = [
|
||||
weighted_op_quint8_dtype_config,
|
||||
default_dynamic_int8_dtype_config,
|
||||
default_dynamic_float16_dtype_config,
|
||||
]
|
||||
binary_op_dtype_configs = [default_op_quint8_dtype_config]
|
||||
default_op_dtype_configs = [default_op_quint8_dtype_config]
|
||||
fixed_qparams_op_dtype_configs = [default_op_quint8_dtype_config]
|
||||
share_qparams_op_dtype_configs = [default_op_quint8_dtype_config]
|
||||
tensor_info_op_dtype_configs = [default_op_quint8_dtype_config]
|
||||
rnn_op_dtype_configs = [
|
||||
default_dynamic_int8_dtype_config,
|
||||
default_dynamic_float16_dtype_config,
|
||||
]
|
||||
embedding_op_dtype_configs = [
|
||||
weight_only_quint8_dtype_config,
|
||||
weight_only_quint4x2_dtype_config,
|
||||
]
|
||||
layer_norm_op_dtype_configs = [input_output_only_quint8_dtype_config]
|
||||
return (
|
||||
BackendConfig("native")
|
||||
.set_backend_pattern_configs(_get_conv_configs(conv_dtype_configs))
|
||||
.set_backend_pattern_configs(_get_linear_configs(linear_dtype_configs))
|
||||
.set_backend_pattern_configs(_get_binary_op_configs(binary_op_dtype_configs))
|
||||
.set_backend_pattern_config(_get_cat_config(default_op_dtype_configs))
|
||||
.set_backend_pattern_configs(_get_default_op_configs(default_op_dtype_configs))
|
||||
.set_backend_pattern_configs(
|
||||
_get_fixed_qparams_op_configs(fixed_qparams_op_dtype_configs)
|
||||
)
|
||||
.set_backend_pattern_configs(
|
||||
_get_share_qparams_op_configs(share_qparams_op_dtype_configs)
|
||||
)
|
||||
.set_backend_pattern_configs(
|
||||
_get_tensor_info_op_configs(tensor_info_op_dtype_configs)
|
||||
)
|
||||
.set_backend_pattern_configs(_get_bn_configs(default_op_dtype_configs))
|
||||
.set_backend_pattern_configs(_get_ln_configs(layer_norm_op_dtype_configs))
|
||||
.set_backend_pattern_configs(_get_rnn_op_configs(rnn_op_dtype_configs))
|
||||
.set_backend_pattern_configs(
|
||||
_get_embedding_op_configs(embedding_op_dtype_configs)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def get_native_backend_config_dict():
|
||||
"""
|
||||
Return the `BackendConfig` for PyTorch Native backend (fbgemm/qnnpack) in dictionary form.
|
||||
"""
|
||||
return get_native_backend_config().to_dict()
|
||||
|
||||
|
||||
def get_test_only_legacy_native_backend_config_dict():
|
||||
"""
|
||||
Return the `BackendConfig` for PyTorch Native backend (fbgemm/qnnpack) with various additional
|
||||
fp16 ops in dictionary form.
|
||||
"""
|
||||
return get_test_only_legacy_native_backend_config().to_dict()
|
||||
+641
@@ -0,0 +1,641 @@
|
||||
# mypy: allow-untyped-defs
|
||||
import itertools
|
||||
import operator
|
||||
|
||||
import torch
|
||||
import torch.ao.nn.intrinsic as nni
|
||||
import torch.ao.nn.quantized.reference as nnqr
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
from torch.ao.quantization.fuser_method_mappings import _sequential_wrapper2
|
||||
from torch.ao.quantization.utils import MatchAllNode
|
||||
|
||||
from ._common_operator_config_utils import (
|
||||
_get_binary_op_configs,
|
||||
_get_bn_configs,
|
||||
_get_cat_config,
|
||||
_get_conv_configs,
|
||||
_get_default_op_configs,
|
||||
_get_embedding_op_configs,
|
||||
_get_fixed_qparams_op_configs,
|
||||
_get_linear_configs,
|
||||
_get_ln_configs,
|
||||
_get_rnn_op_configs,
|
||||
_get_share_qparams_op_configs,
|
||||
)
|
||||
from .backend_config import (
|
||||
BackendConfig,
|
||||
BackendPatternConfig,
|
||||
DTypeConfig,
|
||||
ObservationType,
|
||||
)
|
||||
|
||||
|
||||
# ===================
|
||||
# | DTYPE CONFIGS |
|
||||
# ===================
|
||||
|
||||
onednn_weighted_op_int8_dtype_config = DTypeConfig(
|
||||
input_dtype=torch.quint8,
|
||||
output_dtype=torch.quint8,
|
||||
weight_dtype=torch.qint8,
|
||||
bias_dtype=torch.float,
|
||||
)
|
||||
|
||||
onednn_op_quint8_dtype_config = DTypeConfig(
|
||||
input_dtype=torch.quint8,
|
||||
output_dtype=torch.quint8,
|
||||
)
|
||||
|
||||
onednn_dynamic_int8_dtype_config = DTypeConfig(
|
||||
input_dtype=torch.quint8,
|
||||
output_dtype=torch.float,
|
||||
weight_dtype=torch.qint8,
|
||||
bias_dtype=torch.float,
|
||||
is_dynamic=True,
|
||||
)
|
||||
|
||||
onednn_weight_only_qint8_dtype_config = DTypeConfig(
|
||||
input_dtype=torch.float,
|
||||
output_dtype=torch.float,
|
||||
weight_dtype=torch.qint8,
|
||||
)
|
||||
|
||||
onednn_input_output_only_quint8_dtype_config = DTypeConfig(
|
||||
input_dtype=torch.quint8,
|
||||
output_dtype=torch.quint8,
|
||||
weight_dtype=torch.float,
|
||||
bias_dtype=torch.float,
|
||||
)
|
||||
|
||||
# ===================
|
||||
# | FUSER METHODS |
|
||||
# ===================
|
||||
|
||||
|
||||
def _fuse_linear_bn_leaky_relu(is_qat, linear, bn, leaky_relu):
|
||||
r"""Given the linear, bn and leaky_relu modules, fuses them and returns the fused module
|
||||
Args:
|
||||
is_qat: a flag for whether we are using quantization aware training fusion
|
||||
or post training quantization fusion
|
||||
linear: Module instance of type Linear
|
||||
bn: BatchNorm1d instance that needs to be fused with the linear layer
|
||||
leaky_relu: LeakyReLU instance that needs to be fused with the linear layer
|
||||
Examples::
|
||||
>>> # xdoctest: +SKIP(failing)
|
||||
>>> m1 = nn.Linear(20, 10)
|
||||
>>> b1 = nn.BatchNorm1d(10)
|
||||
>>> lr = nn.LeakyReLU(0.01)
|
||||
>>> m2 = _fuse_linear_bn_leaky_relu(m1, b1, lr)
|
||||
"""
|
||||
if linear.training != bn.training or bn.training != leaky_relu.training:
|
||||
raise AssertionError(
|
||||
"Linear, BN and LeakyReLU all must be in the same mode (train or eval)."
|
||||
)
|
||||
|
||||
if is_qat:
|
||||
raise NotImplementedError(
|
||||
f"Cannot fuse train modules: {(linear, bn, leaky_relu)}"
|
||||
)
|
||||
else:
|
||||
map_to_fused_module_eval = {
|
||||
nn.Linear: nni.LinearLeakyReLU,
|
||||
}
|
||||
fused_module = map_to_fused_module_eval.get(type(linear))
|
||||
if fused_module is not None:
|
||||
fused_linear = nn.utils.fusion.fuse_linear_bn_eval(linear, bn)
|
||||
fm = fused_module(fused_linear, leaky_relu)
|
||||
return fm
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
f"Cannot fuse eval modules: {(linear, bn, leaky_relu)}"
|
||||
)
|
||||
|
||||
|
||||
# ======================
|
||||
# | CONFIGS FOR CONV |
|
||||
# ======================
|
||||
observation_type = ObservationType.OUTPUT_USE_DIFFERENT_OBSERVER_AS_INPUT
|
||||
|
||||
conv_dtype_configs = [onednn_weighted_op_int8_dtype_config]
|
||||
conv_configs = _get_conv_configs(conv_dtype_configs)
|
||||
|
||||
# (1) Conv2d + Add
|
||||
|
||||
# conv2d Y
|
||||
# \ /
|
||||
# add
|
||||
|
||||
# include:
|
||||
# conv2d conv2d
|
||||
# \ /
|
||||
# add
|
||||
|
||||
|
||||
def _fuse_conv_add_left(is_qat, add, conv, _):
|
||||
return nni.ConvAdd2d(conv, add)
|
||||
|
||||
|
||||
def _conv_add_root_node_getter_left(pattern):
|
||||
_, conv, _ = pattern
|
||||
return conv
|
||||
|
||||
|
||||
def _conv_add_extra_inputs_getter_left(pattern):
|
||||
"""get inputs pattern for extra inputs, inputs for root node
|
||||
are assumed to be copied over from root node to the fused node
|
||||
"""
|
||||
_, _conv, extra_input = pattern
|
||||
return [extra_input]
|
||||
|
||||
|
||||
# conv2d
|
||||
# \
|
||||
# bn Y
|
||||
# \ /
|
||||
# add
|
||||
|
||||
|
||||
def _fuse_conv_bn_add_left(is_qat, add, bn_conv, _):
|
||||
bn, conv = bn_conv
|
||||
if is_qat:
|
||||
raise NotImplementedError(f"Cannot fuse train modules: {(conv, bn, add)}")
|
||||
else:
|
||||
fused_conv = nn.utils.fusion.fuse_conv_bn_eval(conv, bn)
|
||||
return nni.ConvAdd2d(fused_conv, add)
|
||||
|
||||
|
||||
def _conv_bn_add_root_node_getter_left(add_pattern):
|
||||
_, bn_conv, _ = add_pattern
|
||||
_bn, conv = bn_conv
|
||||
return conv
|
||||
|
||||
|
||||
def _conv_bn_add_extra_inputs_getter_left(add_pattern):
|
||||
"""get inputs pattern for extra inputs, inputs for root node
|
||||
are assumed to be copied over from root node to the fused node
|
||||
"""
|
||||
_, _bn_conv, extra_input = add_pattern
|
||||
return [extra_input]
|
||||
|
||||
|
||||
conv_add_left_optioins = itertools.product(
|
||||
[True, False], # with_bn
|
||||
[torch.add, operator.add], # add_op
|
||||
)
|
||||
|
||||
for with_bn, add_op in conv_add_left_optioins:
|
||||
if with_bn:
|
||||
conv_configs.append(
|
||||
BackendPatternConfig()
|
||||
._set_pattern_complex_format(
|
||||
(add_op, (nn.BatchNorm2d, nn.Conv2d), MatchAllNode)
|
||||
) # noqa: E131
|
||||
.set_observation_type(observation_type)
|
||||
.set_dtype_configs(conv_dtype_configs)
|
||||
.set_fuser_method(_fuse_conv_bn_add_left)
|
||||
._set_root_node_getter(_conv_bn_add_root_node_getter_left)
|
||||
._set_extra_inputs_getter(_conv_bn_add_extra_inputs_getter_left)
|
||||
.set_fused_module(nni.ConvAdd2d)
|
||||
)
|
||||
else:
|
||||
conv_configs.append(
|
||||
BackendPatternConfig()
|
||||
._set_pattern_complex_format((add_op, nn.Conv2d, MatchAllNode)) # noqa: E131
|
||||
.set_observation_type(observation_type)
|
||||
.set_dtype_configs(conv_dtype_configs)
|
||||
.set_fuser_method(_fuse_conv_add_left)
|
||||
._set_root_node_getter(_conv_add_root_node_getter_left)
|
||||
._set_extra_inputs_getter(_conv_add_extra_inputs_getter_left)
|
||||
.set_fused_module(nni.ConvAdd2d)
|
||||
)
|
||||
|
||||
# Y conv2d
|
||||
# \ /
|
||||
# add
|
||||
|
||||
|
||||
def _fuse_conv_add_right(is_qat, add, _, conv):
|
||||
return nni.ConvAdd2d(conv, add)
|
||||
|
||||
|
||||
def _conv_add_root_node_getter_right(pattern):
|
||||
_add, _, conv = pattern
|
||||
return conv
|
||||
|
||||
|
||||
def _conv_add_extra_inputs_getter_right(pattern):
|
||||
"""get inputs pattern for extra inputs, inputs for root node
|
||||
are assumed to be copied over from root node to the fused node
|
||||
"""
|
||||
_, extra_input, _conv = pattern
|
||||
return [extra_input]
|
||||
|
||||
|
||||
# conv2d
|
||||
# /
|
||||
# Y bn
|
||||
# \ /
|
||||
# add
|
||||
|
||||
|
||||
def _fuse_conv_bn_add_right(is_qat, add, _, bn_conv):
|
||||
bn, conv = bn_conv
|
||||
if is_qat:
|
||||
raise NotImplementedError(f"Cannot fuse train modules: {(conv, bn, add)}")
|
||||
else:
|
||||
fused_conv = nn.utils.fusion.fuse_conv_bn_eval(conv, bn)
|
||||
return nni.ConvAdd2d(fused_conv, add)
|
||||
|
||||
|
||||
def _conv_bn_add_root_node_getter_right(pattern):
|
||||
_add, _, bn_conv = pattern
|
||||
_bn, conv = bn_conv
|
||||
return conv
|
||||
|
||||
|
||||
def _conv_bn_add_extra_inputs_getter_right(pattern):
|
||||
"""get inputs pattern for extra inputs, inputs for root node
|
||||
are assumed to be copied over from root node to the fused node
|
||||
"""
|
||||
_, extra_input, _bn_conv = pattern
|
||||
return [extra_input]
|
||||
|
||||
|
||||
conv_add_optioins = itertools.product(
|
||||
[True, False], # with_bn
|
||||
[torch.add, operator.add], # add_op
|
||||
)
|
||||
|
||||
for with_bn, add_op in conv_add_optioins:
|
||||
if with_bn:
|
||||
conv_configs.append(
|
||||
BackendPatternConfig()
|
||||
._set_pattern_complex_format(
|
||||
(add_op, MatchAllNode, (nn.BatchNorm2d, nn.Conv2d))
|
||||
) # noqa: E131
|
||||
.set_observation_type(observation_type)
|
||||
.set_dtype_configs(conv_dtype_configs)
|
||||
.set_fuser_method(_fuse_conv_bn_add_right)
|
||||
._set_root_node_getter(_conv_bn_add_root_node_getter_right)
|
||||
._set_extra_inputs_getter(_conv_bn_add_extra_inputs_getter_right)
|
||||
.set_fused_module(nni.ConvAdd2d)
|
||||
)
|
||||
else:
|
||||
conv_configs.append(
|
||||
BackendPatternConfig()
|
||||
._set_pattern_complex_format((add_op, MatchAllNode, nn.Conv2d)) # noqa: E131
|
||||
.set_observation_type(observation_type)
|
||||
.set_dtype_configs(conv_dtype_configs)
|
||||
.set_fuser_method(_fuse_conv_add_right)
|
||||
._set_root_node_getter(_conv_add_root_node_getter_right)
|
||||
._set_extra_inputs_getter(_conv_add_extra_inputs_getter_right)
|
||||
.set_fused_module(nni.ConvAdd2d)
|
||||
)
|
||||
|
||||
conv_configs.append(
|
||||
BackendPatternConfig(nni.ConvAdd2d)
|
||||
.set_observation_type(observation_type) # noqa: E131
|
||||
.set_dtype_configs(conv_dtype_configs)
|
||||
.set_root_module(nn.Conv2d)
|
||||
.set_reference_quantized_module(nnqr.Conv2d)
|
||||
)
|
||||
|
||||
# (2) Conv2d + Add + Relu
|
||||
|
||||
# conv2d Y
|
||||
# \ /
|
||||
# add
|
||||
# \
|
||||
# relu
|
||||
|
||||
|
||||
def _fuse_conv_add_relu_left(is_qat, relu, add_pattern):
|
||||
add, conv, _ = add_pattern
|
||||
return nni.ConvAddReLU2d(conv, add, relu)
|
||||
|
||||
|
||||
def _conv_add_relu_root_node_getter_left(pattern):
|
||||
_relu, add_pattern = pattern
|
||||
_, conv, _ = add_pattern
|
||||
return conv
|
||||
|
||||
|
||||
def _conv_add_relu_extra_inputs_getter_left(pattern):
|
||||
"""get inputs pattern for extra inputs, inputs for root node
|
||||
are assumed to be copied over from root node to the fused node
|
||||
"""
|
||||
_relu, add_pattern = pattern
|
||||
_, _conv, extra_input = add_pattern
|
||||
return [extra_input]
|
||||
|
||||
|
||||
# conv2d
|
||||
# \
|
||||
# bn Y
|
||||
# \ /
|
||||
# add
|
||||
# \
|
||||
# relu
|
||||
|
||||
|
||||
def _fuse_conv_bn_add_relu_left(is_qat, relu, add_pattern):
|
||||
add, bn_conv, _ = add_pattern
|
||||
bn, conv = bn_conv
|
||||
if is_qat:
|
||||
raise NotImplementedError(f"Cannot fuse train modules: {(conv, bn, add, relu)}")
|
||||
else:
|
||||
fused_conv = nn.utils.fusion.fuse_conv_bn_eval(conv, bn)
|
||||
return nni.ConvAddReLU2d(fused_conv, add, relu)
|
||||
|
||||
|
||||
def _conv_bn_add_relu_root_node_getter_left(pattern):
|
||||
_relu, add_pattern = pattern
|
||||
_, bn_conv, _ = add_pattern
|
||||
_bn, conv = bn_conv
|
||||
return conv
|
||||
|
||||
|
||||
def _conv_bn_add_relu_extra_inputs_getter_left(pattern):
|
||||
"""get inputs pattern for extra inputs, inputs for root node
|
||||
are assumed to be copied over from root node to the fused node
|
||||
"""
|
||||
_relu, add_pattern = pattern
|
||||
_, _bn_conv, extra_input = add_pattern
|
||||
return [extra_input]
|
||||
|
||||
|
||||
conv_add_relu_left_optioins = itertools.product(
|
||||
[True, False], # with_bn
|
||||
[torch.add, operator.add], # add_op
|
||||
)
|
||||
|
||||
for with_bn, add_op in conv_add_relu_left_optioins:
|
||||
if with_bn:
|
||||
conv_configs.append(
|
||||
BackendPatternConfig()
|
||||
._set_pattern_complex_format(
|
||||
(nn.ReLU, (add_op, (nn.BatchNorm2d, nn.Conv2d), MatchAllNode))
|
||||
) # noqa: E131
|
||||
.set_observation_type(observation_type)
|
||||
.set_dtype_configs(conv_dtype_configs)
|
||||
.set_fuser_method(_fuse_conv_bn_add_relu_left)
|
||||
._set_root_node_getter(_conv_bn_add_relu_root_node_getter_left)
|
||||
._set_extra_inputs_getter(_conv_bn_add_relu_extra_inputs_getter_left)
|
||||
.set_fused_module(nni.ConvAddReLU2d)
|
||||
)
|
||||
else:
|
||||
conv_configs.append(
|
||||
BackendPatternConfig()
|
||||
._set_pattern_complex_format((nn.ReLU, (add_op, nn.Conv2d, MatchAllNode))) # noqa: E131
|
||||
.set_observation_type(observation_type)
|
||||
.set_dtype_configs(conv_dtype_configs)
|
||||
.set_fuser_method(_fuse_conv_add_relu_left)
|
||||
._set_root_node_getter(_conv_add_relu_root_node_getter_left)
|
||||
._set_extra_inputs_getter(_conv_add_relu_extra_inputs_getter_left)
|
||||
.set_fused_module(nni.ConvAddReLU2d)
|
||||
)
|
||||
|
||||
# Y conv2d
|
||||
# \ /
|
||||
# add
|
||||
# \
|
||||
# relu
|
||||
|
||||
|
||||
def _fuse_conv_add_relu_right(is_qat, relu, add_pattern):
|
||||
add, _, conv = add_pattern
|
||||
return nni.ConvAddReLU2d(conv, add, relu)
|
||||
|
||||
|
||||
def _conv_add_relu_root_node_getter_right(pattern):
|
||||
_relu, add_pattern = pattern
|
||||
_, _extra_input, conv = add_pattern
|
||||
return conv
|
||||
|
||||
|
||||
def _conv_add_relu_extra_inputs_getter_right(pattern):
|
||||
"""get inputs pattern for extra inputs, inputs for root node
|
||||
are assumed to be copied over from root node to the fused node
|
||||
"""
|
||||
_relu, add_pattern = pattern
|
||||
_, extra_input, _conv = add_pattern
|
||||
return [extra_input]
|
||||
|
||||
|
||||
# conv2d
|
||||
# /
|
||||
# Y bn
|
||||
# \ /
|
||||
# add
|
||||
# \
|
||||
# relu
|
||||
|
||||
|
||||
def _fuse_conv_bn_add_relu_right(is_qat, relu, add_pattern):
|
||||
add, _, bn_conv = add_pattern
|
||||
bn, conv = bn_conv
|
||||
if is_qat:
|
||||
raise NotImplementedError(f"Cannot fuse train modules: {(conv, bn, add, relu)}")
|
||||
else:
|
||||
fused_conv = nn.utils.fusion.fuse_conv_bn_eval(conv, bn)
|
||||
return nni.ConvAddReLU2d(fused_conv, add, relu)
|
||||
|
||||
|
||||
def _conv_bn_add_relu_root_node_getter_right(pattern):
|
||||
_relu, add_pattern = pattern
|
||||
_, _, bn_conv = add_pattern
|
||||
_bn, conv = bn_conv
|
||||
return conv
|
||||
|
||||
|
||||
def _conv_bn_add_relu_extra_inputs_getter_right(pattern):
|
||||
"""get inputs pattern for extra inputs, inputs for root node
|
||||
are assumed to be copied over from root node to the fused node
|
||||
"""
|
||||
_relu, add_pattern = pattern
|
||||
_, extra_input, _bn_conv = add_pattern
|
||||
return [extra_input]
|
||||
|
||||
|
||||
conv_add_relu_left_optioins = itertools.product(
|
||||
[True, False], # with_bn
|
||||
[torch.add, operator.add], # add_op
|
||||
)
|
||||
|
||||
for with_bn, add_op in conv_add_relu_left_optioins:
|
||||
if with_bn:
|
||||
conv_configs.append(
|
||||
BackendPatternConfig()
|
||||
._set_pattern_complex_format(
|
||||
(nn.ReLU, (add_op, MatchAllNode, (nn.BatchNorm2d, nn.Conv2d)))
|
||||
) # noqa: E131
|
||||
.set_observation_type(observation_type)
|
||||
.set_dtype_configs(conv_dtype_configs)
|
||||
.set_fuser_method(_fuse_conv_bn_add_relu_right)
|
||||
._set_root_node_getter(_conv_bn_add_relu_root_node_getter_right)
|
||||
._set_extra_inputs_getter(_conv_bn_add_relu_extra_inputs_getter_right)
|
||||
.set_fused_module(nni.ConvAddReLU2d)
|
||||
)
|
||||
else:
|
||||
conv_configs.append(
|
||||
BackendPatternConfig()
|
||||
._set_pattern_complex_format((nn.ReLU, (add_op, MatchAllNode, nn.Conv2d))) # noqa: E131
|
||||
.set_observation_type(observation_type)
|
||||
.set_dtype_configs(conv_dtype_configs)
|
||||
.set_fuser_method(_fuse_conv_add_relu_right)
|
||||
._set_root_node_getter(_conv_add_relu_root_node_getter_right)
|
||||
._set_extra_inputs_getter(_conv_add_relu_extra_inputs_getter_right)
|
||||
.set_fused_module(nni.ConvAddReLU2d)
|
||||
)
|
||||
|
||||
conv_configs.append(
|
||||
BackendPatternConfig(nni.ConvAddReLU2d)
|
||||
.set_observation_type(observation_type) # noqa: E131
|
||||
.set_dtype_configs(conv_dtype_configs)
|
||||
.set_root_module(nn.Conv2d)
|
||||
.set_reference_quantized_module(nnqr.Conv2d)
|
||||
)
|
||||
|
||||
# ========================
|
||||
# | CONFIGS FOR LINEAR |
|
||||
# ========================
|
||||
|
||||
linear_dtype_configs = [
|
||||
onednn_weighted_op_int8_dtype_config,
|
||||
onednn_dynamic_int8_dtype_config,
|
||||
]
|
||||
linear_configs = _get_linear_configs(linear_dtype_configs)
|
||||
|
||||
|
||||
def _add_eltwise_fusion_configs(
|
||||
configs,
|
||||
root_module,
|
||||
root_op,
|
||||
post_module,
|
||||
post_op,
|
||||
dtype_configs,
|
||||
fuser_method,
|
||||
fused_module,
|
||||
observation_type,
|
||||
ref_quant_module,
|
||||
):
|
||||
# 1 base module + op module fusion config
|
||||
configs.append(
|
||||
BackendPatternConfig((root_module, post_module))
|
||||
.set_dtype_configs(dtype_configs) # noqa: E131
|
||||
.set_fuser_method(fuser_method)
|
||||
.set_fused_module(fused_module)
|
||||
)
|
||||
# base module + functional post op
|
||||
configs.append(
|
||||
BackendPatternConfig((root_module, post_op))
|
||||
.set_dtype_configs(dtype_configs) # noqa: E131
|
||||
.set_fuser_method(fuser_method)
|
||||
.set_fused_module(fused_module)
|
||||
)
|
||||
|
||||
# 2 fused module configs
|
||||
configs.append(
|
||||
BackendPatternConfig(fused_module)
|
||||
.set_observation_type(observation_type) # noqa: E131
|
||||
.set_dtype_configs(dtype_configs)
|
||||
.set_root_module(root_module)
|
||||
.set_reference_quantized_module(ref_quant_module)
|
||||
)
|
||||
|
||||
# 3 functional base op + post op configs
|
||||
configs.append(
|
||||
BackendPatternConfig((root_op, post_module))
|
||||
.set_observation_type(observation_type) # noqa: E131
|
||||
.set_dtype_configs(dtype_configs)
|
||||
)
|
||||
configs.append(
|
||||
BackendPatternConfig((root_op, post_op))
|
||||
.set_observation_type(observation_type) # noqa: E131
|
||||
.set_dtype_configs(dtype_configs)
|
||||
)
|
||||
|
||||
|
||||
# Configs for linear + leaky_relu fusion
|
||||
_add_eltwise_fusion_configs(
|
||||
linear_configs,
|
||||
nn.Linear,
|
||||
F.linear,
|
||||
nn.LeakyReLU,
|
||||
F.leaky_relu,
|
||||
linear_dtype_configs,
|
||||
_sequential_wrapper2(nni.LinearLeakyReLU),
|
||||
nni.LinearLeakyReLU,
|
||||
observation_type,
|
||||
nnqr.Linear,
|
||||
)
|
||||
|
||||
# Configs for linear module + batchnorm + leaky_relu
|
||||
linear_configs.append(
|
||||
BackendPatternConfig((nn.Linear, nn.BatchNorm1d, nn.LeakyReLU))
|
||||
.set_dtype_configs(linear_dtype_configs) # noqa: E131
|
||||
.set_fuser_method(_fuse_linear_bn_leaky_relu)
|
||||
.set_fused_module(nni.LinearLeakyReLU)
|
||||
)
|
||||
|
||||
# Configs for linear + tanh fusion
|
||||
_add_eltwise_fusion_configs(
|
||||
linear_configs,
|
||||
nn.Linear,
|
||||
F.linear,
|
||||
nn.Tanh,
|
||||
torch.tanh,
|
||||
linear_dtype_configs,
|
||||
_sequential_wrapper2(nni.LinearTanh),
|
||||
nni.LinearTanh,
|
||||
observation_type,
|
||||
nnqr.Linear,
|
||||
)
|
||||
|
||||
# ===========================
|
||||
# | CONFIGS FOR OTHER OPS |
|
||||
# ===========================
|
||||
|
||||
binary_op_dtype_configs = [onednn_op_quint8_dtype_config]
|
||||
default_op_dtype_configs = [onednn_op_quint8_dtype_config]
|
||||
fixed_qparams_op_dtype_configs = [onednn_op_quint8_dtype_config]
|
||||
share_qparams_op_dtype_configs = [onednn_op_quint8_dtype_config]
|
||||
rnn_op_dtype_configs = [onednn_dynamic_int8_dtype_config]
|
||||
embedding_op_dtype_configs = [onednn_weight_only_qint8_dtype_config]
|
||||
layer_norm_op_dtype_configs = [onednn_input_output_only_quint8_dtype_config]
|
||||
|
||||
# =====================
|
||||
# | BACKEND CONFIGS |
|
||||
# =====================
|
||||
|
||||
|
||||
def get_onednn_backend_config() -> BackendConfig:
|
||||
"""
|
||||
Return the `BackendConfig` for PyTorch's native ONEDNN backend.
|
||||
"""
|
||||
return (
|
||||
BackendConfig("onednn")
|
||||
.set_backend_pattern_configs(conv_configs)
|
||||
.set_backend_pattern_configs(linear_configs)
|
||||
.set_backend_pattern_configs(_get_binary_op_configs(binary_op_dtype_configs))
|
||||
.set_backend_pattern_config(_get_cat_config(default_op_dtype_configs))
|
||||
.set_backend_pattern_configs(_get_default_op_configs(default_op_dtype_configs))
|
||||
.set_backend_pattern_configs(
|
||||
_get_fixed_qparams_op_configs(fixed_qparams_op_dtype_configs)
|
||||
)
|
||||
.set_backend_pattern_configs(
|
||||
_get_share_qparams_op_configs(share_qparams_op_dtype_configs)
|
||||
)
|
||||
.set_backend_pattern_configs(_get_bn_configs(default_op_dtype_configs))
|
||||
.set_backend_pattern_configs(_get_ln_configs(layer_norm_op_dtype_configs))
|
||||
.set_backend_pattern_configs(_get_rnn_op_configs(rnn_op_dtype_configs))
|
||||
.set_backend_pattern_configs(
|
||||
_get_embedding_op_configs(embedding_op_dtype_configs)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"get_onednn_backend_config",
|
||||
]
|
||||
+171
@@ -0,0 +1,171 @@
|
||||
import torch
|
||||
|
||||
from ._common_operator_config_utils import (
|
||||
_get_binary_op_configs,
|
||||
_get_bn_configs,
|
||||
_get_cat_config,
|
||||
_get_conv_configs,
|
||||
_get_default_op_configs,
|
||||
_get_embedding_op_configs,
|
||||
_get_fixed_qparams_op_configs,
|
||||
_get_linear_configs,
|
||||
_get_rnn_op_configs,
|
||||
_get_share_qparams_op_configs,
|
||||
)
|
||||
from .backend_config import BackendConfig, DTypeConfig, DTypeWithConstraints
|
||||
|
||||
|
||||
__all__ = [
|
||||
"get_qnnpack_backend_config",
|
||||
]
|
||||
|
||||
# ===================
|
||||
# | DTYPE CONFIGS |
|
||||
# ===================
|
||||
|
||||
qnnpack_weighted_op_quint8_dtype_config = DTypeConfig(
|
||||
input_dtype=torch.quint8,
|
||||
output_dtype=torch.quint8,
|
||||
weight_dtype=torch.qint8,
|
||||
bias_dtype=torch.float,
|
||||
)
|
||||
|
||||
qnnpack_default_op_quint8_dtype_config = DTypeConfig(
|
||||
input_dtype=torch.quint8,
|
||||
output_dtype=torch.quint8,
|
||||
)
|
||||
|
||||
qnnpack_default_op_fp16_dtype_config = DTypeConfig(
|
||||
input_dtype=torch.float16,
|
||||
output_dtype=torch.float16,
|
||||
weight_dtype=torch.float16,
|
||||
bias_dtype=torch.float16,
|
||||
)
|
||||
|
||||
qnnpack_default_dynamic_int8_dtype_config = DTypeConfig(
|
||||
input_dtype=torch.quint8,
|
||||
output_dtype=torch.float,
|
||||
weight_dtype=torch.qint8,
|
||||
bias_dtype=torch.float,
|
||||
is_dynamic=True,
|
||||
)
|
||||
|
||||
qnnpack_default_dynamic_float16_dtype_config = DTypeConfig(
|
||||
input_dtype=torch.float16,
|
||||
output_dtype=torch.float,
|
||||
weight_dtype=torch.float16,
|
||||
bias_dtype=torch.float,
|
||||
is_dynamic=True,
|
||||
)
|
||||
|
||||
qnnpack_weight_only_quint8_dtype_config = DTypeConfig(
|
||||
input_dtype=torch.float,
|
||||
output_dtype=torch.float,
|
||||
weight_dtype=torch.quint8,
|
||||
)
|
||||
|
||||
qnnpack_weight_only_quint4x2_dtype_config = DTypeConfig(
|
||||
input_dtype=torch.float,
|
||||
output_dtype=torch.float,
|
||||
weight_dtype=torch.quint4x2,
|
||||
)
|
||||
|
||||
# xnnpack compatible dtype configs
|
||||
|
||||
# We restrict scale values to be 2 ** -12 to ensure the
|
||||
# requantization scale never falls below the xnnpack lower
|
||||
# threshold. Additionally, for qint8 weight, we restrict
|
||||
# the quantization values to [-127, +127], excluding -128.
|
||||
# For more detail, refer to the description of
|
||||
# `default_symmetric_qnnpack_qconfig`.
|
||||
|
||||
# TODO: add additional restriction on qscheme to ensure it
|
||||
# is either per_tensor_symmetric or per_channel_symmetric
|
||||
|
||||
qnnpack_act_qint8_scale_min_2_neg_12 = DTypeWithConstraints(
|
||||
dtype=torch.qint8,
|
||||
scale_min_lower_bound=2**-12,
|
||||
)
|
||||
|
||||
qnnpack_weight_qint8_neg_127_to_127_scale_min_2_neg_12 = DTypeWithConstraints(
|
||||
dtype=torch.qint8,
|
||||
quant_min_lower_bound=-127,
|
||||
quant_max_upper_bound=127,
|
||||
scale_min_lower_bound=2**-12,
|
||||
)
|
||||
|
||||
qnnpack_weighted_op_qint8_symmetric_dtype_config = DTypeConfig(
|
||||
input_dtype=qnnpack_act_qint8_scale_min_2_neg_12,
|
||||
output_dtype=qnnpack_act_qint8_scale_min_2_neg_12,
|
||||
weight_dtype=qnnpack_weight_qint8_neg_127_to_127_scale_min_2_neg_12,
|
||||
bias_dtype=torch.float,
|
||||
)
|
||||
|
||||
qnnpack_default_op_qint8_symmetric_dtype_config = DTypeConfig(
|
||||
input_dtype=qnnpack_act_qint8_scale_min_2_neg_12,
|
||||
output_dtype=qnnpack_act_qint8_scale_min_2_neg_12,
|
||||
)
|
||||
|
||||
|
||||
# =====================
|
||||
# | BACKEND CONFIGS |
|
||||
# =====================
|
||||
|
||||
|
||||
def get_qnnpack_backend_config() -> BackendConfig:
|
||||
"""
|
||||
Return the `BackendConfig` for PyTorch's native QNNPACK backend.
|
||||
"""
|
||||
conv_dtype_configs = [
|
||||
qnnpack_weighted_op_qint8_symmetric_dtype_config,
|
||||
qnnpack_weighted_op_quint8_dtype_config,
|
||||
]
|
||||
linear_dtype_configs = [
|
||||
qnnpack_weighted_op_qint8_symmetric_dtype_config,
|
||||
qnnpack_weighted_op_quint8_dtype_config,
|
||||
qnnpack_default_dynamic_int8_dtype_config,
|
||||
qnnpack_default_dynamic_float16_dtype_config,
|
||||
]
|
||||
binary_op_dtype_configs = [
|
||||
qnnpack_default_op_qint8_symmetric_dtype_config,
|
||||
qnnpack_default_op_quint8_dtype_config,
|
||||
]
|
||||
default_op_dtype_configs = [
|
||||
qnnpack_default_op_qint8_symmetric_dtype_config,
|
||||
qnnpack_default_op_quint8_dtype_config,
|
||||
]
|
||||
fixed_qparams_op_dtype_configs = [
|
||||
qnnpack_default_op_qint8_symmetric_dtype_config,
|
||||
qnnpack_default_op_quint8_dtype_config,
|
||||
]
|
||||
share_qparams_op_dtype_configs = [
|
||||
qnnpack_default_op_qint8_symmetric_dtype_config,
|
||||
qnnpack_default_op_quint8_dtype_config,
|
||||
]
|
||||
rnn_op_dtype_configs = [
|
||||
qnnpack_default_dynamic_int8_dtype_config,
|
||||
qnnpack_default_dynamic_float16_dtype_config,
|
||||
]
|
||||
embedding_op_dtype_configs = [
|
||||
qnnpack_weight_only_quint8_dtype_config,
|
||||
qnnpack_weight_only_quint4x2_dtype_config,
|
||||
]
|
||||
return (
|
||||
BackendConfig("qnnpack")
|
||||
.set_backend_pattern_configs(_get_conv_configs(conv_dtype_configs))
|
||||
.set_backend_pattern_configs(_get_linear_configs(linear_dtype_configs))
|
||||
.set_backend_pattern_configs(_get_binary_op_configs(binary_op_dtype_configs))
|
||||
.set_backend_pattern_config(_get_cat_config(default_op_dtype_configs))
|
||||
.set_backend_pattern_configs(_get_default_op_configs(default_op_dtype_configs))
|
||||
.set_backend_pattern_configs(
|
||||
_get_fixed_qparams_op_configs(fixed_qparams_op_dtype_configs)
|
||||
)
|
||||
.set_backend_pattern_configs(
|
||||
_get_share_qparams_op_configs(share_qparams_op_dtype_configs)
|
||||
)
|
||||
.set_backend_pattern_configs(_get_bn_configs(default_op_dtype_configs))
|
||||
.set_backend_pattern_configs(_get_rnn_op_configs(rnn_op_dtype_configs))
|
||||
.set_backend_pattern_configs(
|
||||
_get_embedding_op_configs(embedding_op_dtype_configs)
|
||||
)
|
||||
)
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
# mypy: allow-untyped-defs
|
||||
import torch
|
||||
|
||||
from ._common_operator_config_utils import (
|
||||
_get_binary_op_configs,
|
||||
_get_conv_configs,
|
||||
_get_linear_configs,
|
||||
_get_share_qparams_op_configs,
|
||||
_get_tensor_info_op_configs,
|
||||
)
|
||||
from .backend_config import (
|
||||
BackendConfig,
|
||||
BackendPatternConfig,
|
||||
DTypeConfig,
|
||||
ObservationType,
|
||||
)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"get_tensorrt_backend_config",
|
||||
"get_tensorrt_backend_config_dict",
|
||||
]
|
||||
|
||||
|
||||
def get_tensorrt_backend_config() -> BackendConfig:
|
||||
"""
|
||||
Return the `BackendConfig` for the TensorRT backend.
|
||||
NOTE: Current api will change in the future, it's just to unblock experimentation for
|
||||
new backends, please don't use it right now.
|
||||
TODO: add a README when it's more stable
|
||||
"""
|
||||
# dtype configs
|
||||
weighted_op_qint8_dtype_config = DTypeConfig(
|
||||
input_dtype=torch.qint8,
|
||||
output_dtype=torch.qint8,
|
||||
weight_dtype=torch.qint8,
|
||||
bias_dtype=torch.float,
|
||||
)
|
||||
non_weighted_op_qint8_dtype_config = DTypeConfig(
|
||||
input_dtype=torch.qint8,
|
||||
output_dtype=torch.qint8,
|
||||
)
|
||||
|
||||
addmm_config = (
|
||||
BackendPatternConfig(torch.addmm)
|
||||
.set_observation_type(ObservationType.OUTPUT_USE_DIFFERENT_OBSERVER_AS_INPUT)
|
||||
.add_dtype_config(weighted_op_qint8_dtype_config)
|
||||
._set_input_type_to_index(
|
||||
{
|
||||
"bias": 0,
|
||||
"input": 1,
|
||||
"weight": 2,
|
||||
}
|
||||
)
|
||||
)
|
||||
cat_config = (
|
||||
BackendPatternConfig(torch.cat)
|
||||
.set_observation_type(ObservationType.OUTPUT_SHARE_OBSERVER_WITH_INPUT)
|
||||
.add_dtype_config(non_weighted_op_qint8_dtype_config)
|
||||
)
|
||||
conv_dtype_configs = [
|
||||
weighted_op_qint8_dtype_config,
|
||||
]
|
||||
linear_dtype_configs = [
|
||||
weighted_op_qint8_dtype_config,
|
||||
]
|
||||
binary_op_dtype_configs = [
|
||||
weighted_op_qint8_dtype_config,
|
||||
]
|
||||
share_qparams_op_dtype_configs = [
|
||||
non_weighted_op_qint8_dtype_config,
|
||||
]
|
||||
tensor_info_op_dtype_configs = [
|
||||
non_weighted_op_qint8_dtype_config,
|
||||
]
|
||||
# there might be things not supported in fx2trt, but it will error out
|
||||
# during fx2trt conversion and can support them after that
|
||||
return (
|
||||
BackendConfig("tensorrt")
|
||||
.set_backend_pattern_configs(_get_conv_configs(conv_dtype_configs))
|
||||
.set_backend_pattern_config(addmm_config)
|
||||
.set_backend_pattern_config(cat_config)
|
||||
.set_backend_pattern_configs(_get_linear_configs(linear_dtype_configs))
|
||||
.set_backend_pattern_configs(_get_binary_op_configs(binary_op_dtype_configs))
|
||||
.set_backend_pattern_configs(
|
||||
_get_share_qparams_op_configs(share_qparams_op_dtype_configs)
|
||||
)
|
||||
.set_backend_pattern_configs(
|
||||
_get_tensor_info_op_configs(tensor_info_op_dtype_configs)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def get_tensorrt_backend_config_dict():
|
||||
"""
|
||||
Return the `BackendConfig` for the TensorRT backend in dictionary form.
|
||||
"""
|
||||
return get_tensorrt_backend_config().to_dict()
|
||||
+321
@@ -0,0 +1,321 @@
|
||||
# mypy: allow-untyped-defs
|
||||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
from torch.ao.quantization.fuser_method_mappings import _reverse2, _reverse3
|
||||
from torch.ao.quantization.utils import Pattern
|
||||
|
||||
from .backend_config import BackendConfig, BackendPatternConfig, DTypeConfig
|
||||
|
||||
|
||||
__all__ = [
|
||||
"get_pattern_to_dtype_configs",
|
||||
"get_qat_module_classes",
|
||||
"get_fused_module_classes",
|
||||
"get_pattern_to_input_type_to_index",
|
||||
"get_root_module_to_quantized_reference_module",
|
||||
"get_fuser_method_mapping",
|
||||
"get_module_to_qat_module",
|
||||
"get_fusion_pattern_to_root_node_getter",
|
||||
"get_fusion_pattern_to_extra_inputs_getter",
|
||||
"remove_boolean_dispatch_from_name",
|
||||
"pattern_to_human_readable",
|
||||
"entry_to_pretty_str",
|
||||
]
|
||||
|
||||
|
||||
def get_pattern_to_dtype_configs(
|
||||
backend_config: BackendConfig,
|
||||
) -> dict[Pattern, list[DTypeConfig]]:
|
||||
pattern_to_dtype_configs: dict[Pattern, list[DTypeConfig]] = {}
|
||||
for pattern, config in backend_config._pattern_complex_format_to_config.items():
|
||||
pattern_to_dtype_configs[pattern] = config.dtype_configs
|
||||
return pattern_to_dtype_configs
|
||||
|
||||
|
||||
def get_qat_module_classes(backend_config: BackendConfig) -> tuple[type, ...]:
|
||||
qat_module_classes = [
|
||||
config.qat_module
|
||||
for config in backend_config.configs
|
||||
if config.qat_module is not None
|
||||
]
|
||||
return tuple(set(qat_module_classes))
|
||||
|
||||
|
||||
def get_fused_module_classes(backend_config: BackendConfig) -> tuple[type, ...]:
|
||||
fused_module_classes = [
|
||||
config.fused_module
|
||||
for config in backend_config.configs
|
||||
if config.fused_module is not None
|
||||
]
|
||||
return tuple(set(fused_module_classes))
|
||||
|
||||
|
||||
def get_pattern_to_input_type_to_index(
|
||||
backend_config: BackendConfig,
|
||||
) -> dict[Pattern, dict[str, int]]:
|
||||
pattern_to_input_type_to_index: dict[Pattern, dict[str, int]] = {}
|
||||
for pattern, config in backend_config._pattern_complex_format_to_config.items():
|
||||
pattern_to_input_type_to_index[pattern] = config._input_type_to_index
|
||||
return pattern_to_input_type_to_index
|
||||
|
||||
|
||||
def get_root_module_to_quantized_reference_module(
|
||||
backend_config: BackendConfig,
|
||||
) -> dict[type[torch.nn.Module], type[torch.nn.Module]]:
|
||||
mapping: dict[type[torch.nn.Module], type[torch.nn.Module]] = {}
|
||||
for config in backend_config.configs:
|
||||
if (
|
||||
config.root_module is not None
|
||||
and config.reference_quantized_module is not None
|
||||
):
|
||||
mapping[config.root_module] = config.reference_quantized_module
|
||||
return mapping
|
||||
|
||||
|
||||
def get_fuser_method_mapping(
|
||||
backend_config: BackendConfig,
|
||||
) -> dict[Pattern, nn.Sequential | Callable]:
|
||||
fuser_method_mapping: dict[Pattern, nn.Sequential | Callable] = {}
|
||||
for pattern, config in backend_config._pattern_complex_format_to_config.items():
|
||||
if config.fuser_method is not None:
|
||||
# Note: both the fuser method and the pattern are specified in forward order in the
|
||||
# BackendConfig, but the internal pattern matching code uses the reversed nested tuple
|
||||
# format, so we need to convert both to the internal format
|
||||
fuser_method = _get_fuser_method_in_reversed_nested_tuple_format(config)
|
||||
fuser_method_mapping[pattern] = fuser_method
|
||||
return fuser_method_mapping
|
||||
|
||||
|
||||
def get_module_to_qat_module(
|
||||
backend_config: BackendConfig,
|
||||
) -> dict[Pattern, type[torch.nn.Module]]:
|
||||
module_to_qat_module: dict[Pattern, type[torch.nn.Module]] = {}
|
||||
for pattern, config in backend_config._pattern_complex_format_to_config.items():
|
||||
if config.qat_module is not None:
|
||||
module_to_qat_module[pattern] = config.qat_module
|
||||
return module_to_qat_module
|
||||
|
||||
|
||||
def get_fusion_pattern_to_root_node_getter(
|
||||
backend_config: BackendConfig,
|
||||
) -> dict[Pattern, Callable]:
|
||||
"""Get a map from fusion pattern to a function that returns the root node
|
||||
from the fusion pattern, e.g. the most common one is:
|
||||
def get_root_node(node_pattern):
|
||||
while not isinstance(node_pattern[-1], Node):
|
||||
node_pattern = node_pattern[-1]
|
||||
return node_pattern[-1]
|
||||
This can work for all patterns whose root node is the "last node" in the pattern,
|
||||
e.g. (torch.add, MatchAllNode, (torch.ReLU, torch.Conv2d))
|
||||
"""
|
||||
root_node_getter_mapping: dict[Pattern, Callable] = {}
|
||||
for pattern, config in backend_config._pattern_complex_format_to_config.items():
|
||||
if config._root_node_getter is not None:
|
||||
root_node_getter_mapping[pattern] = config._root_node_getter
|
||||
return root_node_getter_mapping
|
||||
|
||||
|
||||
def get_fusion_pattern_to_extra_inputs_getter(
|
||||
backend_config: BackendConfig,
|
||||
) -> dict[Pattern, Callable]:
|
||||
"""Get a map from fusion pattern to a function that returns extra input nodes
|
||||
from the fusion pattern, in the order required by the root node. This is optional,
|
||||
if not specified, we will not copy over any extra inputs for the root node.
|
||||
Example:
|
||||
# Let's say we have the pattern (torch.add, MatchAllNode, (torch.nn.BatchNorm2d, torch.nn.Conv2d))
|
||||
# and root node is torch.nn.Conv2d, and the node in MatchAllNode would be an extra
|
||||
# argument to the fused module, we can unpack the pattern and return the node at
|
||||
# MatchAllNode here
|
||||
# we can implement extra_inputs_getter as follows:
|
||||
def extra_inputs_getter(pattern) -> List[Any]:
|
||||
add, extra_input, conv_pattern = pattern
|
||||
return [extra_input]
|
||||
"""
|
||||
extra_inputs_getter_mapping: dict[Pattern, Callable] = {}
|
||||
for pattern, config in backend_config._pattern_complex_format_to_config.items():
|
||||
if config._extra_inputs_getter is not None:
|
||||
extra_inputs_getter_mapping[pattern] = config._extra_inputs_getter
|
||||
return extra_inputs_getter_mapping
|
||||
|
||||
|
||||
def remove_boolean_dispatch_from_name(p) -> Any:
|
||||
"""
|
||||
Some ops have a default string representation such as
|
||||
'<function boolean_dispatch.<locals>.fn at 0x7ff1106bf280>',
|
||||
this function replaces them with the hardcoded function names.
|
||||
"""
|
||||
if p is F.fractional_max_pool2d:
|
||||
return "torch.nn.functional.fractional_max_pool2d"
|
||||
elif p is F.fractional_max_pool3d:
|
||||
return "torch.nn.functional.fractional_max_pool3d"
|
||||
elif p is F.max_pool1d:
|
||||
return "torch.nn.functional.max_pool1d"
|
||||
elif p is F.max_pool2d:
|
||||
return "torch.nn.functional.max_pool2d"
|
||||
elif p is F.max_pool3d:
|
||||
return "torch.nn.functional.max_pool3d"
|
||||
elif p is F.adaptive_max_pool1d:
|
||||
return "torch.nn.functional.adaptive_max_pool1d"
|
||||
elif p is F.adaptive_max_pool2d:
|
||||
return "torch.nn.functional.adaptive_max_pool2d"
|
||||
elif p is F.adaptive_max_pool3d:
|
||||
return "torch.nn.functional.adaptive_max_pool3d"
|
||||
if "boolean_dispatch" in str(p):
|
||||
raise AssertionError(
|
||||
f"{p} does not have a human readable representation in "
|
||||
+ "quantization documentation"
|
||||
)
|
||||
return p
|
||||
|
||||
|
||||
def pattern_to_human_readable(p) -> Any:
|
||||
if isinstance(p, tuple):
|
||||
# nested patterns, recurse
|
||||
return tuple(pattern_to_human_readable(inner_p) for inner_p in p)
|
||||
elif isinstance(p, str):
|
||||
# method names are already human readable
|
||||
return p
|
||||
else:
|
||||
p = remove_boolean_dispatch_from_name(p)
|
||||
return p
|
||||
|
||||
|
||||
# TODO(future PR): move backend_config_dict to use dataclass and move this logic to
|
||||
# the corresponding __str__ function
|
||||
def entry_to_pretty_str(entry) -> str:
|
||||
"""
|
||||
Given a backend_config_dict entry, returns a string with the human readable
|
||||
representation of it.
|
||||
"""
|
||||
s = "{\n"
|
||||
|
||||
# always output the pattern first
|
||||
if "pattern" in entry:
|
||||
pattern_str = pattern_to_human_readable(entry["pattern"])
|
||||
|
||||
s += f" 'pattern': {pattern_str},\n"
|
||||
|
||||
# custom output for dtype_configs to make it look nice
|
||||
if "dtype_configs" in entry:
|
||||
s += " 'dtype_configs': [\n"
|
||||
for dtype_config in entry["dtype_configs"]:
|
||||
s += " {\n"
|
||||
for k, v in dtype_config.items():
|
||||
s += f" '{k}': {v},\n"
|
||||
s += " },\n"
|
||||
s += " ],\n"
|
||||
|
||||
# custom output for num_tensor_args_to_observation_type to make it look nice
|
||||
if "num_tensor_args_to_observation_type" in entry:
|
||||
s += " 'num_tensor_args_to_observation_type': {\n"
|
||||
for k, v in entry["num_tensor_args_to_observation_type"].items():
|
||||
s += f" {k}: {v},\n"
|
||||
s += " },\n"
|
||||
|
||||
# output all the other fields
|
||||
custom_handled_fields = [
|
||||
"pattern",
|
||||
"dtype_configs",
|
||||
"num_tensor_args_to_observation_type",
|
||||
]
|
||||
for field_name in entry:
|
||||
if field_name in custom_handled_fields:
|
||||
continue
|
||||
s += f" '{field_name}': {entry[field_name]},\n"
|
||||
|
||||
s += "}"
|
||||
return s
|
||||
|
||||
|
||||
def _get_pattern_in_reversed_nested_tuple_format(
|
||||
config: BackendPatternConfig,
|
||||
) -> Pattern:
|
||||
"""
|
||||
Return the pattern specified in the given config in the reversed nested tuple format
|
||||
used internally in the quantization pattern matching code.
|
||||
|
||||
If the pattern is not a tuple, or the pattern is already specified in the reversed
|
||||
nested tuple format, return the pattern as is. Otherwise:
|
||||
|
||||
For 2-tuples (a, b), return (b, a).
|
||||
For 3-tuples (a, b, c), return (c, (b, a)).
|
||||
|
||||
For example:
|
||||
* Given nn.Linear, return nn.Linear
|
||||
* Given (nn.Linear, nn.ReLU), return (nn.ReLU, nn.Linear)
|
||||
* Given (nn.Conv2d, nn.BatchNorm2d, nn.ReLU), return
|
||||
(nn.ReLU, (nn.BatchNorm2d, nn.Conv2d))
|
||||
|
||||
For context, the reason why this is needed is the user-facing BackendConfig
|
||||
API accepts the flat 2-or-3-tuple format in forward order. While this simple
|
||||
format handles the vast majority of use cases, it does not handle the more
|
||||
complex ones, and so the internal pattern matching code for quantization uses
|
||||
the following, more general reversed nested tuple format instead:
|
||||
|
||||
operator = module_type | functional | torch op | native op | MatchAllNode
|
||||
Pattern = (operator, Pattern, Pattern, ...) | operator
|
||||
|
||||
In the future, we expect to replace the above complex format with the one used
|
||||
by the subgraph rewriter in torch.fx, so we don't have to maintain our own
|
||||
complex pattern matching code. Then we won't need this helper function anymore.
|
||||
"""
|
||||
if config._pattern_complex_format is not None:
|
||||
return config._pattern_complex_format
|
||||
if config.pattern is None:
|
||||
raise ValueError(
|
||||
"Either 'pattern' or 'pattern_complex_format' must be specified"
|
||||
)
|
||||
if not isinstance(config.pattern, tuple):
|
||||
return config.pattern
|
||||
|
||||
# Pattern is specified in the simple tuple format, need to convert
|
||||
if len(config.pattern) == 2:
|
||||
(a, b) = config.pattern
|
||||
return (b, a)
|
||||
elif len(config.pattern) == 3:
|
||||
(a, b, c) = config.pattern
|
||||
return (c, (b, a))
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Expected a tuple with 2 or 3 elements, got: {config.pattern}"
|
||||
)
|
||||
|
||||
|
||||
def _get_fuser_method_in_reversed_nested_tuple_format(
|
||||
config: BackendPatternConfig,
|
||||
) -> Callable:
|
||||
"""
|
||||
Return the fuser method specified in the given config in the reversed nested
|
||||
tuple format used internally in the quantization pattern matching code.
|
||||
|
||||
If pattern is specified in the reversed nested tuple format, we assume the
|
||||
fuser method is also specified in this format and simply return it as is.
|
||||
Otherwise, we convert the fuser method as follows:
|
||||
|
||||
* Given f(is_qat, conv, relu), return f'(is_qat, relu, conv)
|
||||
* Given f(is_qat, conv, bn, relu), return f'(is_qat, relu, bn_conv),
|
||||
where bn_conv is a 2-tuple (bn, conv)
|
||||
|
||||
The first argument of a fuser method is always `is_qat` and is not affected
|
||||
in the conversion. We currently only support functions with 3 or 4 arguments.
|
||||
"""
|
||||
if config.fuser_method is None:
|
||||
raise AssertionError("config.fuser_method must be provided")
|
||||
if config._pattern_complex_format is not None:
|
||||
return config.fuser_method
|
||||
if not isinstance(config.pattern, tuple):
|
||||
raise ValueError(f"Expected pattern to be a tuple, got: {config.pattern}")
|
||||
|
||||
# Pattern is specified in the simple tuple format, need to convert
|
||||
if len(config.pattern) == 2:
|
||||
return _reverse2(config.fuser_method)
|
||||
elif len(config.pattern) == 3:
|
||||
return _reverse3(config.fuser_method)
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Expected a tuple with 2 or 3 elements, got: {config.pattern}"
|
||||
)
|
||||
@@ -0,0 +1,126 @@
|
||||
import torch
|
||||
|
||||
from ._common_operator_config_utils import (
|
||||
_get_binary_op_configs,
|
||||
_get_bn_configs,
|
||||
_get_cat_config,
|
||||
_get_conv_configs,
|
||||
_get_default_op_configs,
|
||||
_get_embedding_op_configs,
|
||||
_get_fixed_qparams_op_configs,
|
||||
_get_linear_configs,
|
||||
_get_rnn_op_configs,
|
||||
_get_share_qparams_op_configs,
|
||||
_get_tensor_info_op_configs,
|
||||
)
|
||||
from .backend_config import BackendConfig, DTypeConfig
|
||||
|
||||
|
||||
__all__ = [
|
||||
"get_x86_backend_config",
|
||||
]
|
||||
|
||||
# ===================
|
||||
# | DTYPE CONFIGS |
|
||||
# ===================
|
||||
|
||||
# X86 aligns with FBGEMM for now
|
||||
|
||||
x86_weighted_op_int8_dtype_config = DTypeConfig(
|
||||
input_dtype=torch.quint8,
|
||||
output_dtype=torch.quint8,
|
||||
weight_dtype=torch.qint8,
|
||||
bias_dtype=torch.float,
|
||||
)
|
||||
|
||||
x86_default_op_quint8_dtype_config = DTypeConfig(
|
||||
input_dtype=torch.quint8,
|
||||
output_dtype=torch.quint8,
|
||||
)
|
||||
|
||||
x86_default_op_fp16_dtype_config = DTypeConfig(
|
||||
input_dtype=torch.float16,
|
||||
output_dtype=torch.float16,
|
||||
weight_dtype=torch.float16,
|
||||
bias_dtype=torch.float16,
|
||||
)
|
||||
|
||||
x86_default_dynamic_int8_dtype_config = DTypeConfig(
|
||||
input_dtype=torch.quint8,
|
||||
output_dtype=torch.float,
|
||||
weight_dtype=torch.qint8,
|
||||
bias_dtype=torch.float,
|
||||
is_dynamic=True,
|
||||
)
|
||||
|
||||
x86_default_dynamic_float16_dtype_config = DTypeConfig(
|
||||
input_dtype=torch.float16,
|
||||
output_dtype=torch.float,
|
||||
weight_dtype=torch.float16,
|
||||
bias_dtype=torch.float,
|
||||
is_dynamic=True,
|
||||
)
|
||||
|
||||
x86_weight_only_quint8_dtype_config = DTypeConfig(
|
||||
input_dtype=torch.float,
|
||||
output_dtype=torch.float,
|
||||
weight_dtype=torch.quint8,
|
||||
)
|
||||
|
||||
x86_weight_only_quint4x2_dtype_config = DTypeConfig(
|
||||
input_dtype=torch.float,
|
||||
output_dtype=torch.float,
|
||||
weight_dtype=torch.quint4x2,
|
||||
)
|
||||
|
||||
|
||||
# =====================
|
||||
# | BACKEND CONFIGS |
|
||||
# =====================
|
||||
|
||||
|
||||
def get_x86_backend_config() -> BackendConfig:
|
||||
"""
|
||||
Return the `BackendConfig` for PyTorch's native x86 backend.
|
||||
"""
|
||||
conv_dtype_configs = [x86_weighted_op_int8_dtype_config]
|
||||
linear_dtype_configs = [
|
||||
x86_weighted_op_int8_dtype_config,
|
||||
x86_default_dynamic_int8_dtype_config,
|
||||
x86_default_dynamic_float16_dtype_config,
|
||||
]
|
||||
binary_op_dtype_configs = [x86_weighted_op_int8_dtype_config]
|
||||
default_op_dtype_configs = [x86_default_op_quint8_dtype_config]
|
||||
fixed_qparams_op_dtype_configs = [x86_weighted_op_int8_dtype_config]
|
||||
share_qparams_op_dtype_configs = [x86_default_op_quint8_dtype_config]
|
||||
tensor_info_op_dtype_configs = [x86_default_op_quint8_dtype_config]
|
||||
rnn_op_dtype_configs = [
|
||||
x86_default_dynamic_int8_dtype_config,
|
||||
x86_default_dynamic_float16_dtype_config,
|
||||
]
|
||||
embedding_op_dtype_configs = [
|
||||
x86_weight_only_quint8_dtype_config,
|
||||
x86_weight_only_quint4x2_dtype_config,
|
||||
]
|
||||
return (
|
||||
BackendConfig("x86")
|
||||
.set_backend_pattern_configs(_get_conv_configs(conv_dtype_configs))
|
||||
.set_backend_pattern_configs(_get_linear_configs(linear_dtype_configs))
|
||||
.set_backend_pattern_configs(_get_binary_op_configs(binary_op_dtype_configs))
|
||||
.set_backend_pattern_config(_get_cat_config(default_op_dtype_configs))
|
||||
.set_backend_pattern_configs(_get_default_op_configs(default_op_dtype_configs))
|
||||
.set_backend_pattern_configs(
|
||||
_get_fixed_qparams_op_configs(fixed_qparams_op_dtype_configs)
|
||||
)
|
||||
.set_backend_pattern_configs(
|
||||
_get_share_qparams_op_configs(share_qparams_op_dtype_configs)
|
||||
)
|
||||
.set_backend_pattern_configs(
|
||||
_get_tensor_info_op_configs(tensor_info_op_dtype_configs)
|
||||
)
|
||||
.set_backend_pattern_configs(_get_bn_configs(default_op_dtype_configs))
|
||||
.set_backend_pattern_configs(_get_rnn_op_configs(rnn_op_dtype_configs))
|
||||
.set_backend_pattern_configs(
|
||||
_get_embedding_op_configs(embedding_op_dtype_configs)
|
||||
)
|
||||
)
|
||||
@@ -0,0 +1,663 @@
|
||||
# mypy: allow-untyped-decorators
|
||||
# mypy: allow-untyped-defs
|
||||
"""Implements modules used to perform fake quantization."""
|
||||
|
||||
import re
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Any
|
||||
|
||||
import torch
|
||||
from torch.ao.quantization.observer import (
|
||||
_with_args,
|
||||
default_fixed_qparams_range_0to1_observer,
|
||||
default_fixed_qparams_range_neg1to1_observer,
|
||||
FixedQParamsObserver,
|
||||
HistogramObserver,
|
||||
MovingAverageMinMaxObserver,
|
||||
MovingAveragePerChannelMinMaxObserver,
|
||||
)
|
||||
from torch.nn import Module
|
||||
|
||||
|
||||
__all__ = [
|
||||
"FakeQuantizeBase",
|
||||
"FakeQuantize",
|
||||
"FixedQParamsFakeQuantize",
|
||||
"FusedMovingAvgObsFakeQuantize",
|
||||
"disable_fake_quant",
|
||||
"disable_observer",
|
||||
"enable_fake_quant",
|
||||
"enable_observer",
|
||||
"default_fake_quant",
|
||||
"default_weight_fake_quant",
|
||||
"default_dynamic_fake_quant",
|
||||
"default_fixed_qparams_range_neg1to1_fake_quant",
|
||||
"default_fixed_qparams_range_0to1_fake_quant",
|
||||
"default_symmetric_fixed_qparams_fake_quant",
|
||||
"default_affine_fixed_qparams_fake_quant",
|
||||
"default_per_channel_weight_fake_quant",
|
||||
"default_embedding_fake_quant",
|
||||
"default_embedding_fake_quant_4bit",
|
||||
"default_histogram_fake_quant",
|
||||
"default_fused_act_fake_quant",
|
||||
"default_fused_wt_fake_quant",
|
||||
"default_fused_per_channel_wt_fake_quant",
|
||||
"fused_wt_fake_quant_range_neg_127_to_127",
|
||||
"fused_per_channel_wt_fake_quant_range_neg_127_to_127",
|
||||
]
|
||||
|
||||
|
||||
def _is_per_channel(qscheme: "torch.qscheme") -> bool:
|
||||
return qscheme in [
|
||||
torch.per_channel_symmetric,
|
||||
torch.per_channel_affine,
|
||||
torch.per_channel_affine_float_qparams,
|
||||
]
|
||||
|
||||
|
||||
def _is_per_tensor(qscheme: "torch.qscheme") -> bool:
|
||||
return qscheme in [torch.per_tensor_symmetric, torch.per_tensor_affine]
|
||||
|
||||
|
||||
def _is_symmetric_quant(qscheme: "torch.qscheme") -> bool:
|
||||
return qscheme in [torch.per_tensor_symmetric, torch.per_channel_symmetric]
|
||||
|
||||
|
||||
def _is_float_qparams(qscheme: "torch.qscheme") -> bool:
|
||||
return qscheme == torch.per_channel_affine_float_qparams
|
||||
|
||||
|
||||
class FakeQuantizeBase(ABC, Module):
|
||||
r"""Base fake quantize module.
|
||||
|
||||
Base fake quantize module
|
||||
Any fake quantize implementation should derive from this class.
|
||||
|
||||
Concrete fake quantize module should follow the same API. In forward, they will update
|
||||
the statistics of the observed Tensor and fake quantize the input. They should also provide a
|
||||
`calculate_qparams` function that computes the quantization parameters given
|
||||
the collected statistics.
|
||||
|
||||
"""
|
||||
|
||||
fake_quant_enabled: torch.Tensor
|
||||
observer_enabled: torch.Tensor
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Set fake_quant_enabled and observer_enabled."""
|
||||
super().__init__()
|
||||
# fake_quant_enabled and observer_enabled are buffers to support their
|
||||
# replication in DDP. Data type is uint8 because NCCL does not support
|
||||
# bool tensors.
|
||||
self.register_buffer("fake_quant_enabled", torch.tensor([1], dtype=torch.uint8))
|
||||
self.register_buffer("observer_enabled", torch.tensor([1], dtype=torch.uint8))
|
||||
|
||||
@abstractmethod
|
||||
def forward(self, x):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def calculate_qparams(self, **kwargs):
|
||||
pass
|
||||
|
||||
@torch.jit.export
|
||||
def enable_fake_quant(self, enabled: bool = True) -> None:
|
||||
self.fake_quant_enabled[0] = 1 if enabled else 0
|
||||
|
||||
@torch.jit.export
|
||||
def disable_fake_quant(self):
|
||||
self.enable_fake_quant(False)
|
||||
|
||||
@torch.jit.export
|
||||
def enable_observer(self, enabled: bool = True) -> None:
|
||||
self.observer_enabled[0] = 1 if enabled else 0
|
||||
|
||||
@torch.jit.export
|
||||
def disable_observer(self):
|
||||
self.enable_observer(False)
|
||||
|
||||
@classmethod
|
||||
def with_args(cls, **kwargs):
|
||||
fake_quant_constructor = _with_args(cls, **kwargs)
|
||||
# need to assign the correct module to fake_quantize
|
||||
# constructors to satisfy public v private requirements
|
||||
fake_quant_constructor.__module__ = "torch.ao.quantization.fake_quantize"
|
||||
return fake_quant_constructor
|
||||
|
||||
|
||||
class FakeQuantize(FakeQuantizeBase):
|
||||
r"""Simulate the quantize and dequantize operations in training time.
|
||||
|
||||
The output of this module is given by::
|
||||
|
||||
x_out = (
|
||||
clamp(round(x / scale + zero_point), quant_min, quant_max) - zero_point
|
||||
) * scale
|
||||
|
||||
* :attr:`is_dynamic` indicates whether the fake quantie is a placeholder for dynamic quantization
|
||||
operators (choose_qparams -> q -> dq) or static quantization operators (q -> dq)
|
||||
|
||||
* :attr:`scale` defines the scale factor used for quantization.
|
||||
|
||||
* :attr:`zero_point` specifies the quantized value to which 0 in floating point maps to
|
||||
|
||||
* :attr:`fake_quant_enabled` controls the application of fake quantization on tensors, note that
|
||||
statistics can still be updated.
|
||||
|
||||
* :attr:`observer_enabled` controls statistics collection on tensors
|
||||
|
||||
* :attr:`dtype` specifies the quantized dtype that is being emulated with fake-quantization,
|
||||
allowable values are torch.qint8 and torch.quint8.
|
||||
|
||||
Args:
|
||||
|
||||
observer (module): Module for observing statistics on input tensors and calculating scale
|
||||
and zero-point.
|
||||
observer_kwargs (optional): Arguments for the observer module
|
||||
|
||||
Attributes:
|
||||
activation_post_process (Module): User provided module that collects statistics on the input tensor and
|
||||
provides a method to calculate scale and zero-point.
|
||||
|
||||
"""
|
||||
|
||||
scale: torch.Tensor
|
||||
zero_point: torch.Tensor
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
observer=MovingAverageMinMaxObserver,
|
||||
quant_min=None,
|
||||
quant_max=None,
|
||||
is_dynamic=False,
|
||||
**observer_kwargs,
|
||||
):
|
||||
super().__init__()
|
||||
# Populate quant_min/quant_max to observer_kwargs if valid
|
||||
if quant_min is not None and quant_max is not None:
|
||||
if quant_min > quant_max:
|
||||
raise AssertionError(
|
||||
"quant_min must be less than or equal to quant_max"
|
||||
)
|
||||
dtype = observer_kwargs.get("dtype", torch.quint8)
|
||||
if hasattr(observer, "p"):
|
||||
# In case observer is _PartialWrapper, dtype can be stored in
|
||||
# observer.p.keywords["dtype"]
|
||||
dtype = getattr(getattr(observer, "p", {}), "keywords", {}).get(
|
||||
"dtype", dtype
|
||||
)
|
||||
|
||||
if torch.iinfo(dtype).min > quant_min:
|
||||
raise AssertionError("quant_min out of bound")
|
||||
|
||||
if quant_max > torch.iinfo(dtype).max:
|
||||
raise AssertionError("quant_max out of bound")
|
||||
observer_kwargs.update({"quant_min": quant_min, "quant_max": quant_max})
|
||||
observer_kwargs["is_dynamic"] = is_dynamic
|
||||
self.activation_post_process = observer(**observer_kwargs)
|
||||
# TODO: keeping self.quant_min/max for BC; remove after a couple releases
|
||||
# Users should use self.activation_post_process.quant_min
|
||||
self.quant_min = self.activation_post_process.quant_min
|
||||
self.quant_max = self.activation_post_process.quant_max
|
||||
self.is_dynamic = self.activation_post_process.is_dynamic
|
||||
if _is_float_qparams(self.activation_post_process.qscheme):
|
||||
zero_point_dtype = torch.float
|
||||
else:
|
||||
zero_point_dtype = torch.int
|
||||
self.register_buffer("scale", torch.tensor([1.0], dtype=torch.float))
|
||||
self.register_buffer("zero_point", torch.tensor([0], dtype=zero_point_dtype))
|
||||
self.dtype = self.activation_post_process.dtype
|
||||
self.qscheme = self.activation_post_process.qscheme
|
||||
self.ch_axis = (
|
||||
self.activation_post_process.ch_axis
|
||||
if hasattr(self.activation_post_process, "ch_axis")
|
||||
else -1
|
||||
)
|
||||
if not (_is_per_channel(self.qscheme) or _is_per_tensor(self.qscheme)):
|
||||
raise AssertionError(
|
||||
"Only per channel and per tensor quantization are supported in fake quantize"
|
||||
+ " got qscheme: "
|
||||
+ str(self.qscheme)
|
||||
)
|
||||
self.is_per_channel = _is_per_channel(self.qscheme)
|
||||
|
||||
@torch.jit.export
|
||||
def calculate_qparams(self): # type: ignore[override]
|
||||
return self.activation_post_process.calculate_qparams()
|
||||
|
||||
def forward(self, X):
|
||||
if self.observer_enabled[0] == 1:
|
||||
self.activation_post_process(X.detach())
|
||||
_scale, _zero_point = self.calculate_qparams()
|
||||
_scale, _zero_point = (
|
||||
_scale.to(self.scale.device),
|
||||
_zero_point.to(self.zero_point.device),
|
||||
)
|
||||
if self.scale.shape != _scale.shape:
|
||||
self.scale.resize_(_scale.shape)
|
||||
self.zero_point.resize_(_zero_point.shape)
|
||||
self.scale.copy_(_scale)
|
||||
self.zero_point.copy_(_zero_point)
|
||||
|
||||
if self.fake_quant_enabled[0] == 1:
|
||||
if self.is_per_channel:
|
||||
X = torch.fake_quantize_per_channel_affine(
|
||||
X,
|
||||
self.scale,
|
||||
self.zero_point,
|
||||
self.ch_axis,
|
||||
self.activation_post_process.quant_min,
|
||||
self.activation_post_process.quant_max,
|
||||
)
|
||||
else:
|
||||
X = torch.fake_quantize_per_tensor_affine(
|
||||
X,
|
||||
self.scale,
|
||||
self.zero_point,
|
||||
self.activation_post_process.quant_min,
|
||||
self.activation_post_process.quant_max,
|
||||
)
|
||||
return X
|
||||
|
||||
@torch.jit.export
|
||||
def extra_repr(self):
|
||||
return (
|
||||
f"fake_quant_enabled={self.fake_quant_enabled}, observer_enabled={self.observer_enabled}, "
|
||||
f"quant_min={self.activation_post_process.quant_min}, quant_max={self.activation_post_process.quant_max}, "
|
||||
f"dtype={self.dtype}, qscheme={self.qscheme}, ch_axis={self.ch_axis}, "
|
||||
f"scale={self.scale}, zero_point={self.zero_point}"
|
||||
)
|
||||
|
||||
def _save_to_state_dict(self, destination, prefix, keep_vars):
|
||||
# We cannot currently register scalar values as buffers, so need to manually
|
||||
# specify serialization here.
|
||||
super()._save_to_state_dict(destination, prefix, keep_vars)
|
||||
destination[prefix + "scale"] = self.scale
|
||||
destination[prefix + "zero_point"] = self.zero_point
|
||||
|
||||
def _load_from_state_dict(
|
||||
self,
|
||||
state_dict,
|
||||
prefix,
|
||||
local_metadata,
|
||||
strict,
|
||||
missing_keys,
|
||||
unexpected_keys,
|
||||
error_msgs,
|
||||
):
|
||||
# Removing this function throws an error that the size of the loaded tensor does not match the original size
|
||||
# i.e., These buffers start out with numel 0 and become numel 1 once they have their first forward pass.
|
||||
local_state = ["scale", "zero_point"]
|
||||
for name in local_state:
|
||||
key = prefix + name
|
||||
if key in state_dict:
|
||||
val = state_dict[key]
|
||||
# Custom handling to allow loading scale and zero_point
|
||||
# of size N into uninitialized buffers of size 0. The
|
||||
# buffers are resized here, and the values are copied in
|
||||
# the default state_dict loading code of the parent.
|
||||
if name == "scale":
|
||||
self.scale.resize_(val.shape)
|
||||
else:
|
||||
if name != "zero_point":
|
||||
raise AssertionError(
|
||||
"Expected 'zero_point' but got different state key"
|
||||
)
|
||||
self.zero_point.resize_(val.shape)
|
||||
# For torchscript module we need to update the attributes here since we do not
|
||||
# call the `_load_from_state_dict` function defined module.py
|
||||
if torch.jit.is_scripting():
|
||||
if name == "scale":
|
||||
self.scale.copy_(val)
|
||||
else:
|
||||
if name != "zero_point":
|
||||
raise AssertionError(
|
||||
"Expected 'zero_point' but got different state key"
|
||||
)
|
||||
self.zero_point.copy_(val)
|
||||
elif strict:
|
||||
missing_keys.append(key)
|
||||
super()._load_from_state_dict(
|
||||
state_dict,
|
||||
prefix,
|
||||
local_metadata,
|
||||
strict,
|
||||
missing_keys,
|
||||
unexpected_keys,
|
||||
error_msgs,
|
||||
)
|
||||
|
||||
|
||||
class FixedQParamsFakeQuantize(FakeQuantize):
|
||||
"""Simulate quantize and dequantize in training time.
|
||||
|
||||
Simulate quantize and dequantize with fixed quantization
|
||||
parameters in training time. Only per tensor quantization
|
||||
is supported.
|
||||
"""
|
||||
|
||||
# TODO: rename observer to observer_ctr
|
||||
def __init__(self, observer):
|
||||
super().__init__(observer=observer)
|
||||
if type(self.activation_post_process) is not FixedQParamsObserver:
|
||||
raise AssertionError(
|
||||
f"{self.__class__.__name__}'s observer must be a {FixedQParamsObserver.__name__}"
|
||||
)
|
||||
self._observer_ctr = observer
|
||||
self.scale = self.activation_post_process.scale
|
||||
self.zero_point = self.activation_post_process.zero_point
|
||||
if not _is_per_tensor(self.qscheme):
|
||||
raise AssertionError(
|
||||
"Only per tensor quantization is supported"
|
||||
+ " FixedQParamsFakeQuantize module, got qscheme:"
|
||||
+ str(self.qscheme)
|
||||
)
|
||||
|
||||
@torch.jit.export
|
||||
def calculate_qparams(self): # type: ignore[override]
|
||||
return self.scale, self.zero_point
|
||||
|
||||
@torch.jit.export
|
||||
def extra_repr(self):
|
||||
"""Define a string representation of the object's attributes."""
|
||||
return (
|
||||
f"fake_quant_enabled={self.fake_quant_enabled}, observer_enabled={self.observer_enabled}, "
|
||||
f"scale={self.scale}, zero_point={self.zero_point}, "
|
||||
f"dtype={self.dtype}, quant_min={self.activation_post_process.quant_min}, "
|
||||
f"quant_max={self.activation_post_process.quant_max}, qscheme={self.qscheme}"
|
||||
)
|
||||
|
||||
|
||||
class FusedMovingAvgObsFakeQuantize(FakeQuantize):
|
||||
r"""Define a fused module to observe the tensor.
|
||||
|
||||
Fused module that is used to observe the input tensor (compute min/max), compute
|
||||
scale/zero_point and fake_quantize the tensor.
|
||||
This module uses calculation similar MovingAverageMinMaxObserver for the inputs,
|
||||
to compute the min/max values in order to compute the scale/zero_point.
|
||||
The qscheme input in the observer is used to differentiate between symmetric/affine
|
||||
quantization scheme.
|
||||
|
||||
The output of this module is given by
|
||||
x_out = (clamp(round(x/scale + zero_point), quant_min, quant_max)-zero_point)*scale
|
||||
|
||||
Similar to :class:`~torch.ao.quantization.FakeQuantize`, and accepts the same attributes as the
|
||||
base class.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
observer: Any = MovingAverageMinMaxObserver,
|
||||
quant_min: int = 0,
|
||||
quant_max: int = 255,
|
||||
**observer_kwargs: Any,
|
||||
) -> None:
|
||||
super().__init__(observer, quant_min, quant_max, **observer_kwargs)
|
||||
if not isinstance(
|
||||
self.activation_post_process,
|
||||
(MovingAverageMinMaxObserver, MovingAveragePerChannelMinMaxObserver),
|
||||
):
|
||||
raise AssertionError(
|
||||
"Fused observer+fake_quant module only works with MovingAverageMinMaxObserver"
|
||||
)
|
||||
self.register_buffer("fake_quant_enabled", torch.tensor([1], dtype=torch.long))
|
||||
self.register_buffer("observer_enabled", torch.tensor([1], dtype=torch.long))
|
||||
self.is_symmetric_quant = _is_symmetric_quant(
|
||||
self.activation_post_process.qscheme
|
||||
)
|
||||
|
||||
@torch.jit.export
|
||||
def calculate_qparams(self) -> tuple[torch.Tensor, torch.Tensor]: # type: ignore[override]
|
||||
return self.activation_post_process.calculate_qparams()
|
||||
|
||||
@torch.jit.export
|
||||
def extra_repr(self) -> str:
|
||||
return (
|
||||
f"fake_quant_enabled={self.fake_quant_enabled}, observer_enabled={self.observer_enabled}, "
|
||||
f"scale={self.scale}, zero_point={self.zero_point}, dtype={self.dtype}, "
|
||||
f"quant_min={self.activation_post_process.quant_min}, quant_max={self.activation_post_process.quant_max}, "
|
||||
f"qscheme={self.qscheme}, reduce_range={self.activation_post_process.reduce_range}"
|
||||
)
|
||||
|
||||
def forward(self, X: torch.Tensor) -> torch.Tensor:
|
||||
return torch.fused_moving_avg_obs_fake_quant(
|
||||
X,
|
||||
self.observer_enabled,
|
||||
self.fake_quant_enabled,
|
||||
self.activation_post_process.min_val,
|
||||
self.activation_post_process.max_val,
|
||||
self.scale,
|
||||
self.zero_point,
|
||||
self.activation_post_process.averaging_constant,
|
||||
self.activation_post_process.quant_min,
|
||||
self.activation_post_process.quant_max,
|
||||
self.ch_axis,
|
||||
self.is_per_channel,
|
||||
self.is_symmetric_quant,
|
||||
)
|
||||
|
||||
|
||||
default_fake_quant = FakeQuantize.with_args(
|
||||
observer=MovingAverageMinMaxObserver,
|
||||
quant_min=0,
|
||||
quant_max=255,
|
||||
dtype=torch.quint8,
|
||||
qscheme=torch.per_tensor_affine,
|
||||
reduce_range=True,
|
||||
)
|
||||
"""
|
||||
Default fake_quant for activations.
|
||||
"""
|
||||
|
||||
default_weight_fake_quant = FakeQuantize.with_args(
|
||||
observer=MovingAverageMinMaxObserver,
|
||||
quant_min=-128,
|
||||
quant_max=127,
|
||||
dtype=torch.qint8,
|
||||
qscheme=torch.per_tensor_symmetric,
|
||||
reduce_range=False,
|
||||
)
|
||||
"""
|
||||
Default fake_quant for weights.
|
||||
Observer is memoryless since averaging_constant is 1.
|
||||
"""
|
||||
|
||||
default_dynamic_fake_quant = FakeQuantize.with_args(
|
||||
observer=MovingAverageMinMaxObserver,
|
||||
quant_min=0,
|
||||
quant_max=255,
|
||||
is_dynamic=True,
|
||||
dtype=torch.quint8,
|
||||
averaging_constant=1,
|
||||
)
|
||||
"""
|
||||
Default dynamic fake_quant for activations.
|
||||
"""
|
||||
|
||||
default_fixed_qparams_range_neg1to1_fake_quant = FixedQParamsFakeQuantize.with_args(
|
||||
observer=default_fixed_qparams_range_neg1to1_observer
|
||||
)
|
||||
default_fixed_qparams_range_0to1_fake_quant = FixedQParamsFakeQuantize.with_args(
|
||||
observer=default_fixed_qparams_range_0to1_observer
|
||||
)
|
||||
# TODO: the following 2 variables are kept for backwards compatibility; remove after a few releases
|
||||
default_symmetric_fixed_qparams_fake_quant = (
|
||||
default_fixed_qparams_range_neg1to1_fake_quant
|
||||
)
|
||||
default_affine_fixed_qparams_fake_quant = default_fixed_qparams_range_0to1_fake_quant
|
||||
|
||||
default_per_channel_weight_fake_quant = FakeQuantize.with_args(
|
||||
observer=MovingAveragePerChannelMinMaxObserver,
|
||||
quant_min=-128,
|
||||
quant_max=127,
|
||||
dtype=torch.qint8,
|
||||
qscheme=torch.per_channel_symmetric,
|
||||
reduce_range=False,
|
||||
ch_axis=0,
|
||||
)
|
||||
"""
|
||||
Default fake_quant for per-channel weights.
|
||||
Observer is memoryless since averaging_constant is 1.
|
||||
"""
|
||||
default_embedding_fake_quant = FakeQuantize.with_args(
|
||||
observer=MovingAveragePerChannelMinMaxObserver,
|
||||
qscheme=torch.per_channel_affine_float_qparams,
|
||||
dtype=torch.quint8,
|
||||
quant_min=0,
|
||||
quant_max=255,
|
||||
ch_axis=0,
|
||||
averaging_constant=1,
|
||||
)
|
||||
"""
|
||||
Default fake_quant for embeddings.
|
||||
Observer is memoryless since averaging_constant is 1.
|
||||
"""
|
||||
|
||||
default_embedding_fake_quant_4bit = FakeQuantize.with_args(
|
||||
observer=MovingAveragePerChannelMinMaxObserver,
|
||||
qscheme=torch.per_channel_affine_float_qparams,
|
||||
ch_axis=0,
|
||||
dtype=torch.quint4x2,
|
||||
averaging_constant=1,
|
||||
)
|
||||
|
||||
default_histogram_fake_quant = FakeQuantize.with_args(
|
||||
observer=HistogramObserver,
|
||||
quant_min=0,
|
||||
quant_max=255,
|
||||
dtype=torch.quint8,
|
||||
qscheme=torch.per_tensor_affine,
|
||||
reduce_range=True,
|
||||
)
|
||||
"""
|
||||
Fake_quant for activations using a histogram..
|
||||
"""
|
||||
|
||||
|
||||
default_fused_act_fake_quant = FusedMovingAvgObsFakeQuantize.with_args(
|
||||
observer=MovingAverageMinMaxObserver,
|
||||
quant_min=0,
|
||||
quant_max=255,
|
||||
dtype=torch.quint8,
|
||||
)
|
||||
|
||||
"""
|
||||
Fused version of `default_fake_quant`, with improved performance.
|
||||
"""
|
||||
|
||||
|
||||
default_fused_wt_fake_quant = FusedMovingAvgObsFakeQuantize.with_args(
|
||||
observer=MovingAverageMinMaxObserver,
|
||||
quant_min=-128,
|
||||
quant_max=127,
|
||||
dtype=torch.qint8,
|
||||
qscheme=torch.per_tensor_symmetric,
|
||||
)
|
||||
"""
|
||||
Fused version of `default_weight_fake_quant`, with improved performance.
|
||||
"""
|
||||
|
||||
default_fused_per_channel_wt_fake_quant = FusedMovingAvgObsFakeQuantize.with_args(
|
||||
observer=MovingAveragePerChannelMinMaxObserver,
|
||||
quant_min=-128,
|
||||
quant_max=127,
|
||||
dtype=torch.qint8,
|
||||
qscheme=torch.per_channel_symmetric,
|
||||
)
|
||||
"""
|
||||
Fused version of `default_per_channel_weight_fake_quant`, with improved performance.
|
||||
"""
|
||||
|
||||
fused_wt_fake_quant_range_neg_127_to_127 = FusedMovingAvgObsFakeQuantize.with_args(
|
||||
observer=MovingAverageMinMaxObserver,
|
||||
quant_min=-127,
|
||||
quant_max=127,
|
||||
dtype=torch.qint8,
|
||||
qscheme=torch.per_tensor_symmetric,
|
||||
eps=2**-12,
|
||||
)
|
||||
"""
|
||||
Fused version of `default_weight_fake_quant`, with the 8-bit values restricted to [-127, +127], excluding -128.
|
||||
"""
|
||||
|
||||
fused_per_channel_wt_fake_quant_range_neg_127_to_127 = (
|
||||
FusedMovingAvgObsFakeQuantize.with_args(
|
||||
observer=MovingAveragePerChannelMinMaxObserver,
|
||||
quant_min=-127,
|
||||
quant_max=127,
|
||||
dtype=torch.qint8,
|
||||
qscheme=torch.per_channel_symmetric,
|
||||
eps=2**-12,
|
||||
)
|
||||
)
|
||||
|
||||
"""
|
||||
Fused version of `default_per_channel_weight_fake_quant`, with the 8-bit values restricted to [-127, +127], excluding -128.
|
||||
"""
|
||||
|
||||
|
||||
def _is_fake_quant_script_module(mod):
|
||||
"""Return true if given mod is an instance of FakeQuantize script module."""
|
||||
if isinstance(mod, torch.jit.RecursiveScriptModule):
|
||||
# qualified name looks like '__torch__.torch.ao.quantization.fake_quantize.___torch_mangle_2.FakeQuantize'
|
||||
suffix = mod._c.qualified_name.split(".", 1)[1]
|
||||
name = re.sub(r"\.___torch_mangle_\d+", "", suffix)
|
||||
return (
|
||||
name == "torch.ao.quantization.fake_quantize.FakeQuantize"
|
||||
or name
|
||||
== "torch.ao.quantization.fake_quantize.FusedMovingAvgObsFakeQuantize"
|
||||
)
|
||||
return False
|
||||
|
||||
|
||||
def disable_fake_quant(mod):
|
||||
"""Disable fake quantization for the module.
|
||||
|
||||
Disable fake quantization for this module, if applicable. Example usage::
|
||||
|
||||
# model is any PyTorch model
|
||||
model.apply(torch.ao.quantization.disable_fake_quant)
|
||||
|
||||
"""
|
||||
if isinstance(mod, FakeQuantizeBase) or _is_fake_quant_script_module(mod):
|
||||
mod.disable_fake_quant()
|
||||
|
||||
|
||||
def enable_fake_quant(mod):
|
||||
"""Enable fake quantization for the module.
|
||||
|
||||
Enable fake quantization for this module, if applicable. Example usage::
|
||||
|
||||
# model is any PyTorch model
|
||||
model.apply(torch.ao.quantization.enable_fake_quant)
|
||||
|
||||
"""
|
||||
if isinstance(mod, FakeQuantizeBase) or _is_fake_quant_script_module(mod):
|
||||
mod.enable_fake_quant()
|
||||
|
||||
|
||||
def disable_observer(mod):
|
||||
"""Disable observation for this module.
|
||||
|
||||
Disable observation for this module, if applicable. Example usage::
|
||||
|
||||
# model is any PyTorch model
|
||||
model.apply(torch.ao.quantization.disable_observer)
|
||||
|
||||
"""
|
||||
if isinstance(mod, FakeQuantizeBase) or _is_fake_quant_script_module(mod):
|
||||
mod.disable_observer()
|
||||
|
||||
|
||||
def enable_observer(mod):
|
||||
"""Enable observation for this module.
|
||||
|
||||
Enable observation for this module, if applicable. Example usage::
|
||||
|
||||
# model is any PyTorch model
|
||||
model.apply(torch.ao.quantization.enable_observer)
|
||||
|
||||
"""
|
||||
if isinstance(mod, FakeQuantizeBase) or _is_fake_quant_script_module(mod):
|
||||
mod.enable_observer()
|
||||
@@ -0,0 +1,215 @@
|
||||
# mypy: allow-untyped-defs
|
||||
import copy
|
||||
|
||||
import torch.nn as nn
|
||||
|
||||
# for backward compatibility
|
||||
from torch.ao.quantization.fuser_method_mappings import ( # noqa: F401 # noqa: F401
|
||||
fuse_conv_bn,
|
||||
fuse_conv_bn_relu,
|
||||
get_fuser_method,
|
||||
)
|
||||
from torch.nn.utils.parametrize import type_before_parametrizations
|
||||
|
||||
|
||||
__all__ = [
|
||||
"fuse_known_modules",
|
||||
"fuse_modules",
|
||||
"fuse_modules_qat",
|
||||
]
|
||||
|
||||
|
||||
# Generalization of getattr
|
||||
def _get_module(model, submodule_key):
|
||||
tokens = submodule_key.split(".")
|
||||
cur_mod = model
|
||||
for s in tokens:
|
||||
cur_mod = getattr(cur_mod, s)
|
||||
return cur_mod
|
||||
|
||||
|
||||
# Generalization of setattr
|
||||
def _set_module(model, submodule_key, module):
|
||||
tokens = submodule_key.split(".")
|
||||
sub_tokens = tokens[:-1]
|
||||
cur_mod = model
|
||||
for s in sub_tokens:
|
||||
cur_mod = getattr(cur_mod, s)
|
||||
|
||||
setattr(cur_mod, tokens[-1], module)
|
||||
|
||||
|
||||
def fuse_known_modules(mod_list, is_qat, additional_fuser_method_mapping=None):
|
||||
r"""Return a list of known fuse modules.
|
||||
|
||||
Returns a list of modules that fuses the operations specified
|
||||
in the input module list.
|
||||
|
||||
Fuses only the following sequence of modules:
|
||||
conv, bn
|
||||
conv, bn, relu
|
||||
conv, relu
|
||||
linear, bn
|
||||
linear, relu
|
||||
For these sequences, the first element in the output module list performs
|
||||
the fused operation. The rest of the elements are set to nn.Identity()
|
||||
"""
|
||||
types = tuple(type_before_parametrizations(m) for m in mod_list)
|
||||
fuser_method = get_fuser_method(types, additional_fuser_method_mapping)
|
||||
if fuser_method is None:
|
||||
raise NotImplementedError(f"Cannot fuse modules: {types}")
|
||||
new_mod: list[nn.Module | None] = [None] * len(mod_list)
|
||||
fused = fuser_method(is_qat, *mod_list)
|
||||
# NOTE: forward hooks not processed in the two following for loops will be lost after the fusion
|
||||
# Move pre forward hooks of the base module to resulting fused module
|
||||
for pre_hook_fn in mod_list[0]._forward_pre_hooks.values():
|
||||
fused.register_forward_pre_hook(pre_hook_fn)
|
||||
mod_list[0]._forward_pre_hooks.clear()
|
||||
# Move post forward hooks of the last module to resulting fused module
|
||||
for hook_fn in mod_list[-1]._forward_hooks.values():
|
||||
fused.register_forward_hook(hook_fn)
|
||||
mod_list[-1]._forward_hooks.clear()
|
||||
new_mod[0] = fused
|
||||
|
||||
for i in range(1, len(mod_list)):
|
||||
identity = nn.Identity()
|
||||
identity.training = mod_list[0].training
|
||||
new_mod[i] = identity
|
||||
|
||||
return new_mod
|
||||
|
||||
|
||||
def _fuse_modules_helper(
|
||||
model,
|
||||
modules_to_fuse,
|
||||
is_qat,
|
||||
fuser_func=fuse_known_modules,
|
||||
fuse_custom_config_dict=None,
|
||||
):
|
||||
if fuse_custom_config_dict is None:
|
||||
fuse_custom_config_dict = {}
|
||||
additional_fuser_method_mapping = fuse_custom_config_dict.get(
|
||||
"additional_fuser_method_mapping", {}
|
||||
)
|
||||
mod_list = [_get_module(model, item) for item in modules_to_fuse]
|
||||
|
||||
# Fuse list of modules
|
||||
new_mod_list = fuser_func(mod_list, is_qat, additional_fuser_method_mapping)
|
||||
|
||||
# Replace original module list with fused module list
|
||||
for i, item in enumerate(modules_to_fuse):
|
||||
_set_module(model, item, new_mod_list[i])
|
||||
|
||||
|
||||
def _fuse_modules(
|
||||
model,
|
||||
modules_to_fuse,
|
||||
is_qat,
|
||||
inplace=False,
|
||||
fuser_func=fuse_known_modules,
|
||||
fuse_custom_config_dict=None,
|
||||
):
|
||||
if not inplace:
|
||||
model = copy.deepcopy(model)
|
||||
|
||||
if all(isinstance(module_element, str) for module_element in modules_to_fuse):
|
||||
# Handle case of modules_to_fuse being a list
|
||||
_fuse_modules_helper(
|
||||
model, modules_to_fuse, is_qat, fuser_func, fuse_custom_config_dict
|
||||
)
|
||||
else:
|
||||
# Handle case of modules_to_fuse being a list of lists
|
||||
for module_list in modules_to_fuse:
|
||||
_fuse_modules_helper(
|
||||
model, module_list, is_qat, fuser_func, fuse_custom_config_dict
|
||||
)
|
||||
return model
|
||||
|
||||
|
||||
def fuse_modules(
|
||||
model,
|
||||
modules_to_fuse,
|
||||
inplace=False,
|
||||
fuser_func=fuse_known_modules,
|
||||
fuse_custom_config_dict=None,
|
||||
):
|
||||
r"""Fuse a list of modules into a single module.
|
||||
|
||||
Fuses only the following sequence of modules:
|
||||
conv, bn
|
||||
conv, bn, relu
|
||||
conv, relu
|
||||
linear, relu
|
||||
bn, relu
|
||||
All other sequences are left unchanged.
|
||||
For these sequences, replaces the first item in the list
|
||||
with the fused module, replacing the rest of the modules
|
||||
with identity.
|
||||
|
||||
Args:
|
||||
model: Model containing the modules to be fused
|
||||
modules_to_fuse: list of list of module names to fuse. Can also be a list
|
||||
of strings if there is only a single list of modules to fuse.
|
||||
inplace: bool specifying if fusion happens in place on the model, by default
|
||||
a new model is returned
|
||||
fuser_func: Function that takes in a list of modules and outputs a list of fused modules
|
||||
of the same length. For example,
|
||||
fuser_func([convModule, BNModule]) returns the list [ConvBNModule, nn.Identity()]
|
||||
Defaults to torch.ao.quantization.fuse_known_modules
|
||||
`fuse_custom_config_dict`: custom configuration for fusion
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Example of fuse_custom_config_dict
|
||||
fuse_custom_config_dict = {
|
||||
# Additional fuser_method mapping
|
||||
"additional_fuser_method_mapping": {
|
||||
(torch.nn.Conv2d, torch.nn.BatchNorm2d): fuse_conv_bn
|
||||
},
|
||||
}
|
||||
|
||||
Returns:
|
||||
model with fused modules. A new copy is created if inplace=True.
|
||||
|
||||
Examples::
|
||||
|
||||
>>> # xdoctest: +SKIP
|
||||
>>> m = M().eval()
|
||||
>>> # m is a module containing the sub-modules below
|
||||
>>> modules_to_fuse = [ ['conv1', 'bn1', 'relu1'], ['submodule.conv', 'submodule.relu']]
|
||||
>>> fused_m = torch.ao.quantization.fuse_modules(m, modules_to_fuse)
|
||||
>>> output = fused_m(input)
|
||||
|
||||
>>> m = M().eval()
|
||||
>>> # Alternately provide a single list of modules to fuse
|
||||
>>> modules_to_fuse = ['conv1', 'bn1', 'relu1']
|
||||
>>> fused_m = torch.ao.quantization.fuse_modules(m, modules_to_fuse)
|
||||
>>> output = fused_m(input)
|
||||
|
||||
"""
|
||||
return _fuse_modules(
|
||||
model,
|
||||
modules_to_fuse,
|
||||
is_qat=False,
|
||||
inplace=inplace,
|
||||
fuser_func=fuser_func,
|
||||
fuse_custom_config_dict=fuse_custom_config_dict,
|
||||
)
|
||||
|
||||
|
||||
def fuse_modules_qat(
|
||||
model,
|
||||
modules_to_fuse,
|
||||
inplace=False,
|
||||
fuser_func=fuse_known_modules,
|
||||
fuse_custom_config_dict=None,
|
||||
):
|
||||
"""QAT version for `fuse_modules`."""
|
||||
return _fuse_modules(
|
||||
model,
|
||||
modules_to_fuse,
|
||||
is_qat=True,
|
||||
inplace=inplace,
|
||||
fuser_func=fuser_func,
|
||||
fuse_custom_config_dict=fuse_custom_config_dict,
|
||||
)
|
||||
+314
@@ -0,0 +1,314 @@
|
||||
# mypy: allow-untyped-defs
|
||||
import itertools
|
||||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
|
||||
import torch.ao.nn.intrinsic as nni
|
||||
import torch.nn as nn
|
||||
from torch.ao.quantization.utils import get_combined_dict, MatchAllNode, Pattern
|
||||
|
||||
|
||||
__all__ = [
|
||||
"fuse_conv_bn",
|
||||
"fuse_conv_bn_relu",
|
||||
"fuse_linear_bn",
|
||||
"fuse_convtranspose_bn",
|
||||
"get_fuser_method",
|
||||
"get_fuser_method_new",
|
||||
]
|
||||
|
||||
|
||||
def fuse_conv_bn(is_qat, conv, bn):
|
||||
r"""Return the fused the conv and bn modules.
|
||||
Given the conv and bn modules, fuses them and returns the fused module
|
||||
|
||||
Args:
|
||||
is_qat: a flag for whether we are using quantization aware training fusion
|
||||
or post training quantization fusion
|
||||
conv: Module instance of type conv2d/conv3d
|
||||
bn: Spatial BN instance that needs to be fused with the conv
|
||||
|
||||
Examples::
|
||||
|
||||
>>> m1 = nn.Conv2d(10, 20, 3)
|
||||
>>> b1 = nn.BatchNorm2d(20)
|
||||
>>> # xdoctest: +SKIP
|
||||
>>> m2 = fuse_conv_bn(m1, b1)
|
||||
"""
|
||||
if conv.training != bn.training:
|
||||
raise AssertionError(
|
||||
"Conv and BN both must be in the same mode (train or eval)."
|
||||
)
|
||||
|
||||
fused_module_class_map = {
|
||||
nn.Conv1d: nni.ConvBn1d,
|
||||
nn.Conv2d: nni.ConvBn2d,
|
||||
nn.Conv3d: nni.ConvBn3d,
|
||||
}
|
||||
|
||||
if is_qat:
|
||||
if bn.num_features != conv.out_channels:
|
||||
raise AssertionError(
|
||||
"Output channel of Conv2d must match num_features of BatchNorm2d."
|
||||
)
|
||||
if not bn.affine:
|
||||
raise AssertionError(
|
||||
"Only support fusing BatchNorm2d with affine set to True"
|
||||
)
|
||||
if not bn.track_running_stats:
|
||||
raise AssertionError(
|
||||
"Only support fusing BatchNorm2d with tracking_running_stats set to True"
|
||||
)
|
||||
fused_module_class = fused_module_class_map.get(type(conv))
|
||||
if fused_module_class is not None:
|
||||
return fused_module_class(conv, bn)
|
||||
else:
|
||||
raise NotImplementedError(f"Cannot fuse train modules: {(conv, bn)}")
|
||||
else:
|
||||
return nn.utils.fuse_conv_bn_eval(conv, bn)
|
||||
|
||||
|
||||
def fuse_conv_bn_relu(is_qat, conv, bn, relu):
|
||||
r"""Return the fused conv and bv modules.
|
||||
|
||||
Given the conv and bn modules, fuses them and returns the fused module
|
||||
|
||||
Args:
|
||||
is_qat: a flag for whether we are using quantization aware training fusion
|
||||
or post training quantization fusion
|
||||
conv: Module instance of type conv2d/conv3d
|
||||
bn: Spatial BN instance that needs to be fused with the conv
|
||||
|
||||
Examples::
|
||||
|
||||
>>> m1 = nn.Conv2d(10, 20, 3)
|
||||
>>> b1 = nn.BatchNorm2d(20)
|
||||
>>> r1 = nn.ReLU(inplace=False)
|
||||
>>> # xdoctest: +SKIP
|
||||
>>> m2 = fuse_conv_bn_relu(m1, b1, r1)
|
||||
"""
|
||||
if not (conv.training == bn.training == relu.training):
|
||||
raise AssertionError(
|
||||
"Conv and BN both must be in the same mode (train or eval)."
|
||||
)
|
||||
fused_module: type[nn.Sequential] | None = None
|
||||
if is_qat:
|
||||
map_to_fused_module_train = {
|
||||
nn.Conv1d: nni.ConvBnReLU1d,
|
||||
nn.Conv2d: nni.ConvBnReLU2d,
|
||||
nn.Conv3d: nni.ConvBnReLU3d,
|
||||
}
|
||||
if bn.num_features != conv.out_channels:
|
||||
raise AssertionError(
|
||||
"Output channel of Conv2d must match num_features of BatchNorm2d"
|
||||
)
|
||||
if not bn.affine:
|
||||
raise AssertionError(
|
||||
"Only support fusing BatchNorm2d with affine set to True"
|
||||
)
|
||||
if not bn.track_running_stats:
|
||||
raise AssertionError(
|
||||
"Only support fusing BatchNorm2d with tracking_running_stats set to True"
|
||||
)
|
||||
fused_module = map_to_fused_module_train.get(type(conv))
|
||||
if fused_module is not None:
|
||||
return fused_module(conv, bn, relu)
|
||||
else:
|
||||
raise NotImplementedError(f"Cannot fuse train modules: {(conv, bn, relu)}")
|
||||
else:
|
||||
map_to_fused_module_eval = {
|
||||
nn.Conv1d: nni.ConvReLU1d,
|
||||
nn.Conv2d: nni.ConvReLU2d,
|
||||
nn.Conv3d: nni.ConvReLU3d,
|
||||
}
|
||||
fused_module = map_to_fused_module_eval.get(type(conv))
|
||||
if fused_module is not None:
|
||||
fused_conv = nn.utils.fusion.fuse_conv_bn_eval(conv, bn)
|
||||
return fused_module(fused_conv, relu)
|
||||
else:
|
||||
raise NotImplementedError(f"Cannot fuse eval modules: {(conv, bn, relu)}")
|
||||
|
||||
|
||||
def fuse_linear_bn(is_qat, linear, bn):
|
||||
r"""Return the fused linear and bn modules.
|
||||
Given the linear and bn modules, fuses them and returns the fused module
|
||||
|
||||
Args:
|
||||
is_qat: a flag for whether we are using quantization aware training fusion
|
||||
or post training quantization fusion
|
||||
linear: Module instance of type Linear
|
||||
bn: BatchNorm1d instance that needs to be fused with the linear layer
|
||||
|
||||
Examples::
|
||||
|
||||
>>> m1 = nn.Linear(20, 10)
|
||||
>>> b1 = nn.BatchNorm1d(10)
|
||||
>>> # xdoctest: +SKIP
|
||||
>>> m2 = fuse_linear_bn(m1, b1)
|
||||
"""
|
||||
if linear.training != bn.training:
|
||||
raise AssertionError(
|
||||
"Linear and BN both must be in the same mode (train or eval)."
|
||||
)
|
||||
|
||||
if is_qat:
|
||||
if bn.num_features != linear.out_features:
|
||||
raise AssertionError(
|
||||
"Output features of Linear must match num_features of BatchNorm1d"
|
||||
)
|
||||
if not bn.affine:
|
||||
raise AssertionError(
|
||||
"Only support fusing BatchNorm1d with affine set to True"
|
||||
)
|
||||
if not bn.track_running_stats:
|
||||
raise AssertionError(
|
||||
"Only support fusing BatchNorm1d with tracking_running_stats set to True"
|
||||
)
|
||||
return nni.LinearBn1d(linear, bn)
|
||||
else:
|
||||
return nn.utils.fusion.fuse_linear_bn_eval(linear, bn)
|
||||
|
||||
|
||||
def fuse_convtranspose_bn(is_qat, convt, bn):
|
||||
r"""Return the fused ConvTranspose and bn modules.
|
||||
Given ConvTranspose and bn modules, fuses them and returns the fused module
|
||||
|
||||
Args:
|
||||
convt: Module instance of type ConvTransposeNd
|
||||
bn: BatchNormNd instance that needs to be fused with the linear layer.
|
||||
batch norm N should match the ConvTranspose N
|
||||
|
||||
Examples::
|
||||
|
||||
>>> m1 = nn.ConvTranspose2d(10, 20, 3)
|
||||
>>> b1 = nn.BatchNorm2d(20)
|
||||
>>> # xdoctest: +SKIP
|
||||
>>> m2 = fuse_convtranspose_bn(m1, b1)
|
||||
"""
|
||||
if convt.training != bn.training:
|
||||
raise AssertionError(
|
||||
"ConvTranspose and BN both must be in the same mode (train or eval)."
|
||||
)
|
||||
|
||||
if is_qat:
|
||||
raise Exception( # noqa: TRY002
|
||||
"Fusing ConvTranspose+BatchNorm not yet supported in QAT."
|
||||
)
|
||||
else:
|
||||
return nn.utils.fusion.fuse_conv_bn_eval(convt, bn, transpose=True)
|
||||
|
||||
|
||||
def _sequential_wrapper2(sequential):
|
||||
"""Return a sequential wrapped that for is_qat and two modules.
|
||||
Given a sequential class for two modules, return a function that takes
|
||||
is_qat, and then two modules as argument, that ignores the is_qat flag
|
||||
and always returns the sequential that combines the two input modules
|
||||
"""
|
||||
|
||||
def fuser_method(is_qat, m1, m2):
|
||||
return sequential(m1, m2)
|
||||
|
||||
return fuser_method
|
||||
|
||||
|
||||
_DEFAULT_OP_LIST_TO_FUSER_METHOD: dict[tuple, nn.Sequential | Callable] = {
|
||||
(nn.Conv1d, nn.BatchNorm1d): fuse_conv_bn,
|
||||
(nn.Conv1d, nn.BatchNorm1d, nn.ReLU): fuse_conv_bn_relu,
|
||||
(nn.Conv2d, nn.BatchNorm2d): fuse_conv_bn,
|
||||
(nn.Conv2d, nn.BatchNorm2d, nn.ReLU): fuse_conv_bn_relu,
|
||||
(nn.Conv3d, nn.BatchNorm3d): fuse_conv_bn,
|
||||
(nn.Conv3d, nn.BatchNorm3d, nn.ReLU): fuse_conv_bn_relu,
|
||||
(nn.Conv1d, nn.ReLU): _sequential_wrapper2(nni.ConvReLU1d),
|
||||
(nn.Conv2d, nn.ReLU): _sequential_wrapper2(nni.ConvReLU2d),
|
||||
(nn.Conv3d, nn.ReLU): _sequential_wrapper2(nni.ConvReLU3d),
|
||||
(nn.Linear, nn.BatchNorm1d): fuse_linear_bn,
|
||||
(nn.Linear, nn.ReLU): _sequential_wrapper2(nni.LinearReLU),
|
||||
(nn.BatchNorm2d, nn.ReLU): _sequential_wrapper2(nni.BNReLU2d),
|
||||
(nn.BatchNorm3d, nn.ReLU): _sequential_wrapper2(nni.BNReLU3d),
|
||||
(nn.ConvTranspose1d, nn.BatchNorm1d): fuse_convtranspose_bn,
|
||||
(nn.ConvTranspose2d, nn.BatchNorm2d): fuse_convtranspose_bn,
|
||||
(nn.ConvTranspose3d, nn.BatchNorm3d): fuse_convtranspose_bn,
|
||||
}
|
||||
|
||||
|
||||
def get_fuser_method(op_list, additional_fuser_method_mapping=None):
|
||||
"""Get fuser method for the given list of module types.
|
||||
|
||||
Get fuser method for the given list of module types,
|
||||
return None if fuser method does not exist
|
||||
"""
|
||||
if additional_fuser_method_mapping is None:
|
||||
additional_fuser_method_mapping = {}
|
||||
all_mappings = get_combined_dict(
|
||||
_DEFAULT_OP_LIST_TO_FUSER_METHOD, additional_fuser_method_mapping
|
||||
)
|
||||
fuser_method = all_mappings.get(op_list, None)
|
||||
if fuser_method is None:
|
||||
raise AssertionError(f"did not find fuser method for: {op_list} ")
|
||||
return fuser_method
|
||||
|
||||
|
||||
def _reverse2(f):
|
||||
def reversed(is_qat, x, y):
|
||||
return f(is_qat, y, x)
|
||||
|
||||
return reversed
|
||||
|
||||
|
||||
def _reverse3(f):
|
||||
def reversed(is_qat, x, w):
|
||||
y, z = w
|
||||
return f(is_qat, z, y, x)
|
||||
|
||||
return reversed
|
||||
|
||||
|
||||
def _get_valid_patterns(op_pattern):
|
||||
"""Return a list of valid patterns generated from the op_pattern.
|
||||
|
||||
Returns a list of valid patterns generated from the op_pattern,
|
||||
since MatchAllNode can match all types of nodes,
|
||||
e.g. pattern (torch.nn.Conv2d, torch.add) should also be able to match keys like
|
||||
(MatchAllNode, torch.add) and (torch.nn.Conv2d, MatchAllNode)
|
||||
|
||||
Example Input:
|
||||
(torch.add, (torch.nn.ReLU, torch.nn.Conv2d))
|
||||
|
||||
Example Output:
|
||||
[(torch.add, (torch.nn.ReLU, torch.nn.Conv2d)),
|
||||
(torch.add, (torch.nn.ReLU, MatchAllNode)),
|
||||
(torch.add, (MatchAllNode, torch.nn.Conv2d)),
|
||||
(torch.add, (MatchAllNode, MatchAllNode)),
|
||||
(MatchAllNode, (torch.nn.ReLU, torch.nn.Conv2d)),
|
||||
(MatchAllNode, (torch.nn.ReLU, MatchAllNode)),
|
||||
(MatchAllNode, (MatchAllNode, torch.nn.Conv2d)),
|
||||
(MatchAllNode, (MatchAllNode, MatchAllNode)),
|
||||
]
|
||||
"""
|
||||
result: list[Any]
|
||||
if isinstance(op_pattern, (tuple, list)):
|
||||
sub_combs = [_get_valid_patterns(sub_pattern) for sub_pattern in op_pattern]
|
||||
result = list(itertools.product(*sub_combs))
|
||||
else:
|
||||
result = [op_pattern, MatchAllNode]
|
||||
return result
|
||||
|
||||
|
||||
def get_fuser_method_new(
|
||||
op_pattern: Pattern,
|
||||
fuser_method_mapping: dict[Pattern, nn.Sequential | Callable],
|
||||
):
|
||||
"""Get fuser method.
|
||||
|
||||
This will be made default after we deprecate the get_fuser_method
|
||||
Would like to implement this first and have a separate PR for deprecation
|
||||
"""
|
||||
op_patterns = _get_valid_patterns(op_pattern)
|
||||
fuser_method = None
|
||||
for op_pattern in op_patterns:
|
||||
fuser_method = fuser_method_mapping.get(op_pattern)
|
||||
if fuser_method is not None:
|
||||
break
|
||||
if fuser_method is None:
|
||||
raise AssertionError(f"did not find fuser method for: {op_pattern} ")
|
||||
return fuser_method
|
||||
@@ -0,0 +1,3 @@
|
||||
from .convert import convert
|
||||
from .fuse import fuse
|
||||
from .prepare import prepare
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+1413
File diff suppressed because it is too large
Load Diff
+1743
File diff suppressed because it is too large
Load Diff
+666
@@ -0,0 +1,666 @@
|
||||
# mypy: allow-untyped-defs
|
||||
from collections import OrderedDict
|
||||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
|
||||
import torch
|
||||
from torch.ao.quantization.fx._equalize import EqualizationQConfig
|
||||
from torch.ao.quantization.fx._model_report.detector import (
|
||||
DETECTOR_IS_POST_OBS_KEY,
|
||||
DETECTOR_OBS_ARGS_KEY,
|
||||
DETECTOR_OBS_TO_INSERT_KEY,
|
||||
DETECTOR_TARGET_NODE_KEY,
|
||||
DetectorBase,
|
||||
DetectorQConfigInfo,
|
||||
)
|
||||
from torch.ao.quantization.fx._model_report.model_report_visualizer import (
|
||||
ModelReportVisualizer,
|
||||
)
|
||||
from torch.ao.quantization.fx.graph_module import GraphModule
|
||||
from torch.ao.quantization.observer import ObserverBase
|
||||
from torch.ao.quantization.qconfig_mapping import QConfig, QConfigMapping
|
||||
|
||||
|
||||
class ModelReport:
|
||||
r"""
|
||||
The ModelReport class aims to provide users an easy way to diagnose issues that they run into
|
||||
with their models. The class works with all traceable GraphModules to help diagnose issues,
|
||||
though the requirements on the type of model more-so depends on the specific report the user
|
||||
is trying to generate. With respect to the reports, the ModelReport class is initialized with
|
||||
a set of Detector classes, each of which generate reports on quantization configuration
|
||||
issues a use might have.
|
||||
|
||||
Currently supports generating reports on:
|
||||
- Suggestions for per-channel vs. per-tensor quantization (nn.Module)
|
||||
- Suggestions for dynamic vs static quantization for linear layers (Graph Modules)
|
||||
- Suggestions for input-weight equalization for linear and conv layers (Graph Modules)
|
||||
- Suggestions for outlier detection for all layers (Graph Modules)
|
||||
|
||||
The ModelReport class has the primary functionality of inserting observers (primarily the ModelReportObserver)
|
||||
where needed for each detector to gather the information it needs, and then after calibration, the ModelReport
|
||||
class compiles the report generated by each Detector class into a single report to return to the user. It also
|
||||
has the capability to remove all the observers it inserted as well.
|
||||
|
||||
* :attr:`_model` The model we wish to generate the report for. Must be a traceable GraphModule
|
||||
|
||||
* :attr:`_desired_report_detectors` The set of Detectors representing desired reports from the ModelReport class
|
||||
Make sure that these are all unique types of detectors [do not have more than 1 of the same class]
|
||||
|
||||
* :attr:`_desired_detector_names` The set of detector names of the _desired_report_detectors.
|
||||
This set is generated by calling the get_detector_name() of each detector
|
||||
|
||||
* :attr:`_detector_name_to_observer_fqns` The mapping from each detector to fqns of observers of interest
|
||||
The purpose of this is to keep track of what observers were inserted for each detector, so that they
|
||||
can be removed at the end if desired
|
||||
|
||||
* :attr:`_prepared_flag` A boolean flag that keeps track of whether we have prepared the model or not
|
||||
This is to ensure we only insert observers once with the ModelReport instance
|
||||
|
||||
* :attr:`_removed_observers` A boolean to track if we have removed observers already
|
||||
The purpose is to ensure we don't attempt to remove observers twice with the same ModelReport
|
||||
instance. This also allows the functionality where we can generate the report multiple times
|
||||
as long as we haven't removed the observers yet.
|
||||
|
||||
Note:
|
||||
This class was initially designed to work with the Fx Graph Mode workflow in mind. However,
|
||||
full functionality is available as long as there is a traceable GraphModule that is being used.
|
||||
One method to get a traceable GraphModule without going through the Fx workflow is to use
|
||||
the QuantizationTracer class.
|
||||
|
||||
General Flow for Fx workflow:
|
||||
1.) Initialize ModelReport object with reports of interest by passing in initialized detector objects and model
|
||||
2.) Prepare your model with prepare_fx
|
||||
3.) Call model_report.prepare_detailed_calibration to add relevant observers
|
||||
4.) Calibrate your model with data
|
||||
5.) Call model_report.generate_report on your model to generate report and optionally remove added observers
|
||||
Optional
|
||||
6.) Call model_report.generate_visualizer to get a ModelReportVisualizer instance
|
||||
7.) To help in parsing report information and debugging, view report info as a:
|
||||
- Table
|
||||
- Histogram
|
||||
- Line plot
|
||||
8.) Call model_report.generate_qconfigs to generate the qconfigs based on the report suggestions
|
||||
|
||||
Example (with QuantizationTracer):
|
||||
>>> # xdoctest: +SKIP
|
||||
>>> # get the necessary qconfig
|
||||
>>> config = PrepareCustomConfig()
|
||||
>>> skipped_module_names, skipped_module_classes = (
|
||||
... get_skipped_module_name_and_classes(config, False)
|
||||
... )
|
||||
|
||||
>>> # initialize our model and get GraphModule
|
||||
>>> model = SomeModel()
|
||||
>>> tracer = QuantizationTracer(skipped_module_names, skipped_module_classes)
|
||||
>>> graph_module = GraphModule(model, tracer.trace(model))
|
||||
|
||||
>>> # get our set of detectors and ModelReport instance
|
||||
>>> detector_set = set(
|
||||
... [
|
||||
... DynamicStaticDetector(tolerance=0.5),
|
||||
... InputWeightEqualizationDetector(ratio_threshold=0.7),
|
||||
... ]
|
||||
... )
|
||||
>>> tracer_reporter = ModelReport(graph_module, tracer_detector_set)
|
||||
|
||||
>>> # now we insert the observers and calibrate the model
|
||||
>>> tracer_model_with_observers = tracer_reporter.prepare_detailed_calibration()
|
||||
>>> for i in range(num_callibration_batches):
|
||||
>>> example_input = get_callibration_input()
|
||||
>>> tracer_model_with_observers(example_input)
|
||||
|
||||
>>> # finally we generate the reports and optionally remove the observers we inserted
|
||||
>>> reports = tracer_reporter.generate_model_report(
|
||||
... remove_inserted_observers=True
|
||||
... )
|
||||
|
||||
>>> # Optional: we can generate the qconfig mapping based on the suggestions
|
||||
>>> qconfigs = model_report.generate_qconfig_mapping()
|
||||
|
||||
>>> # Optional: we can generate the equalization mapping based on the suggestions
|
||||
>>> qconfigs = model_report.generate_equalization_mapping()
|
||||
|
||||
>>> # Optional: we get a ModelReportVisualizer instance to do any visualizations desired
|
||||
>>> model_report_visualizer = tracer_reporter.generate_visualizer()
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, model: GraphModule, desired_report_detectors: set[DetectorBase]):
|
||||
if len(desired_report_detectors) == 0:
|
||||
raise ValueError("Should include at least 1 desired report")
|
||||
|
||||
# keep track of the model we wish to generate report for
|
||||
self._model: GraphModule = model
|
||||
|
||||
# keep the reports private so they can't be modified
|
||||
self._desired_report_detectors = desired_report_detectors
|
||||
self._desired_detector_names = {
|
||||
detector.get_detector_name() for detector in desired_report_detectors
|
||||
}
|
||||
|
||||
# keep a mapping of desired reports to observers of interest
|
||||
# this is to get the readings, and to remove them, can create a large set
|
||||
# this set can then be used to traverse the graph and remove added observers
|
||||
self._detector_name_to_observer_fqns: dict[str, set[str]] = {}
|
||||
|
||||
# initialize each report to have empty set of observers of interest
|
||||
for desired_report in self._desired_detector_names:
|
||||
self._detector_name_to_observer_fqns[desired_report] = set()
|
||||
|
||||
# flags to ensure that we can only prepare and remove observers once
|
||||
self._prepared_flag = False
|
||||
self._removed_observers = False
|
||||
|
||||
# store the reports that we generated for visualization purposes
|
||||
# initially empty since no reports generated
|
||||
self._generated_reports: dict[str, dict] = {}
|
||||
|
||||
def get_desired_reports_names(self) -> set[str]:
|
||||
"""Returns a copy of the desired reports for viewing"""
|
||||
return self._desired_detector_names.copy()
|
||||
|
||||
def get_observers_of_interest(self) -> dict[str, set[str]]:
|
||||
"""Returns a copy of the observers of interest for viewing"""
|
||||
return self._detector_name_to_observer_fqns.copy()
|
||||
|
||||
def prepare_detailed_calibration(self) -> GraphModule:
|
||||
r"""
|
||||
Takes in a graph model and inserts the following observers:
|
||||
- ModelReportObserver
|
||||
|
||||
Each observer is inserted based on the desired_reports into the relevant locations
|
||||
|
||||
Right now, each report in self._desired_detector_names has independent insertions
|
||||
However, if a module already has a Observer of the same type, the insertion will not occur
|
||||
This is because all of the same type of Observer collect same information, so redundant
|
||||
|
||||
Returns the same GraphModule with the observers inserted
|
||||
"""
|
||||
|
||||
# if already prepared once, cannot prepare again
|
||||
if self._prepared_flag:
|
||||
raise ValueError(
|
||||
"Already ran preparing detailed calibration. Run the report generation next after calibration."
|
||||
)
|
||||
|
||||
# loop through each detector, find where placements should be, and keep track
|
||||
insert_observers_fqns: dict[str, Any] = {}
|
||||
|
||||
for detector in self._desired_report_detectors:
|
||||
# determine observer points for each detector
|
||||
obs_fqn_to_info = detector.determine_observer_insert_points(self._model)
|
||||
# map each insert point to the observer to use
|
||||
insert_observers_fqns.update(obs_fqn_to_info)
|
||||
# update the set of observers this report cares about
|
||||
self._detector_name_to_observer_fqns[detector.get_detector_name()] = set(
|
||||
obs_fqn_to_info.keys()
|
||||
)
|
||||
|
||||
# now insert all the observers at their desired locations
|
||||
for observer_fqn in insert_observers_fqns:
|
||||
target_node = insert_observers_fqns[observer_fqn][DETECTOR_TARGET_NODE_KEY]
|
||||
insert_obs = insert_observers_fqns[observer_fqn][DETECTOR_OBS_TO_INSERT_KEY]
|
||||
insert_post = insert_observers_fqns[observer_fqn][DETECTOR_IS_POST_OBS_KEY]
|
||||
observer_args = insert_observers_fqns[observer_fqn][DETECTOR_OBS_ARGS_KEY]
|
||||
self._insert_observer_around_module(
|
||||
observer_fqn, target_node, insert_obs, observer_args, insert_post
|
||||
)
|
||||
|
||||
self._prepared_flag = True
|
||||
|
||||
return self._model
|
||||
|
||||
def _insert_observer_around_module(
|
||||
self,
|
||||
obs_fqn: str,
|
||||
target_node: torch.fx.node.Node,
|
||||
obs_to_insert: ObserverBase,
|
||||
observer_args: tuple,
|
||||
insert_post: bool,
|
||||
):
|
||||
r"""
|
||||
Helper function that inserts the observer into both the graph structure and the module of the model
|
||||
|
||||
Args
|
||||
node_fqn (str): The fully qualified name of the observer we want to insert
|
||||
target_node (torch.fx.node.Node): The node in model we are inserting observers around
|
||||
obs_to_insert (ObserverBase): The observer we are inserting around target_node
|
||||
observer_args (Tuple): The arguments we want to pass into the observer
|
||||
insert_post (bool): whether this is meant to be a post observer for this node
|
||||
"""
|
||||
# if we are inserting post, then our target node is the next node
|
||||
if insert_post:
|
||||
target_node = target_node.next
|
||||
|
||||
with self._model.graph.inserting_before(target_node):
|
||||
self._model.add_submodule(obs_fqn, obs_to_insert)
|
||||
self._model.graph.create_node(
|
||||
op="call_module", target=obs_fqn, args=observer_args
|
||||
)
|
||||
|
||||
# recompile model after inserts are made
|
||||
self._model.recompile()
|
||||
|
||||
def _get_node_from_fqn(self, node_fqn: str) -> torch.fx.node.Node:
|
||||
r"""
|
||||
Takes in a node fqn and returns the node based on the fqn
|
||||
|
||||
Args
|
||||
node_fqn (str): The fully qualified name of the node we want to find in model
|
||||
|
||||
Returns the Node object of the given node_fqn otherwise returns None
|
||||
"""
|
||||
node_to_return = None
|
||||
for node in self._model.graph.nodes:
|
||||
# if the target matches the fqn, it's the node we are looking for
|
||||
if node.target == node_fqn:
|
||||
node_to_return = node
|
||||
break
|
||||
|
||||
if node_to_return is None:
|
||||
raise ValueError("The node_fqn is was not found within the module.")
|
||||
|
||||
# assert for MyPy
|
||||
if not isinstance(node_to_return, torch.fx.node.Node):
|
||||
raise AssertionError("node_to_return must be a torch.fx.node.Node")
|
||||
|
||||
return node_to_return
|
||||
|
||||
def generate_model_report(
|
||||
self, remove_inserted_observers: bool
|
||||
) -> dict[str, tuple[str, dict]]:
|
||||
r"""
|
||||
Generates all the requested reports.
|
||||
|
||||
Note:
|
||||
You should have calibrated the model with relevant data before calling this
|
||||
|
||||
The reports generated are specified by the desired_reports specified in desired_reports
|
||||
|
||||
Can optionally remove all the observers inserted by the ModelReport instance
|
||||
|
||||
Args:
|
||||
remove_inserted_observers (bool): True to remove the observers inserted by this ModelReport instance
|
||||
|
||||
Returns a mapping of each desired report name to a tuple with:
|
||||
The textual summary of that report information
|
||||
A dictionary containing relevant statistics or information for that report
|
||||
|
||||
Note:
|
||||
Throws exception if we try to generate report on model we already removed observers from
|
||||
Throws exception if we try to generate report without preparing for calibration
|
||||
"""
|
||||
# if we haven't prepped model for calibration, then we shouldn't generate report yet
|
||||
if not self._prepared_flag:
|
||||
raise Exception( # noqa: TRY002
|
||||
"Cannot generate report without preparing model for calibration"
|
||||
)
|
||||
|
||||
# if we already removed the observers, we cannot generate report
|
||||
if self._removed_observers:
|
||||
raise Exception( # noqa: TRY002
|
||||
"Cannot generate report on model you already removed observers from"
|
||||
)
|
||||
|
||||
# keep track of all the reports of interest and their outputs
|
||||
reports_of_interest = {}
|
||||
|
||||
for detector in self._desired_report_detectors:
|
||||
# generate the individual report for the detector
|
||||
report_output = detector.generate_detector_report(self._model)
|
||||
reports_of_interest[detector.get_detector_name()] = report_output
|
||||
|
||||
# if user wishes to remove inserted observers, go ahead and remove
|
||||
if remove_inserted_observers:
|
||||
self._removed_observers = True
|
||||
# get the set of all Observers inserted by this instance of ModelReport
|
||||
all_observers_of_interest: set[str] = set()
|
||||
for desired_report in self._detector_name_to_observer_fqns:
|
||||
observers_of_interest = self._detector_name_to_observer_fqns[
|
||||
desired_report
|
||||
]
|
||||
all_observers_of_interest.update(observers_of_interest)
|
||||
|
||||
# go through all_observers_of_interest and remove them from the graph and model
|
||||
for observer_fqn in all_observers_of_interest:
|
||||
# remove the observer from the model
|
||||
self._model.delete_submodule(observer_fqn)
|
||||
|
||||
# remove the observer from the graph structure
|
||||
node_obj = self._get_node_from_fqn(observer_fqn)
|
||||
|
||||
if node_obj:
|
||||
self._model.graph.erase_node(node_obj)
|
||||
else:
|
||||
raise ValueError("Node no longer exists in GraphModule structure")
|
||||
|
||||
# remember to recompile the model
|
||||
self._model.recompile()
|
||||
|
||||
# save the generated reports for visualization purposes
|
||||
saved_reports: dict[str, dict] = {
|
||||
report_name: report_tuple[1]
|
||||
for report_name, report_tuple in reports_of_interest.items()
|
||||
}
|
||||
|
||||
self._generated_reports = saved_reports
|
||||
|
||||
# return the reports of interest
|
||||
return reports_of_interest
|
||||
|
||||
def _is_same_info_for_same_key(self, info_dict_a: dict, info_dict_b: dict) -> bool:
|
||||
r"""
|
||||
Takes in two dictionaries and ensures that any common keys between the two have the same
|
||||
values.
|
||||
|
||||
Args:
|
||||
info_dict_a (Dict): First dictionary we wish to compare
|
||||
info_dict_b (Dict): Second dictionary we wish to compare
|
||||
|
||||
Returns True if all shared keys have same values, false otherwise
|
||||
"""
|
||||
# get the set of keys for both
|
||||
dict_a_keys: set = set(info_dict_a.keys())
|
||||
dict_b_keys: set = set(info_dict_b.keys())
|
||||
|
||||
# get the insersection keys and check if same value for both dicts
|
||||
intersecting_keys: set = dict_a_keys.intersection(dict_b_keys)
|
||||
|
||||
for key in intersecting_keys:
|
||||
dict_a_val = info_dict_a[key]
|
||||
dict_b_val = info_dict_b[key]
|
||||
|
||||
# if it's a tensor we have to handle separately
|
||||
if type(dict_a_val) is torch.Tensor:
|
||||
# if dict_b_val not tensor, automatically false
|
||||
if (
|
||||
type(dict_b_val) is not torch.Tensor
|
||||
or sum(dict_a_val != dict_b_val) != 0
|
||||
):
|
||||
return False
|
||||
else:
|
||||
# for non-tensor vals
|
||||
if dict_a_val != dict_b_val:
|
||||
return False
|
||||
|
||||
# if no non matching shared keys found, return true
|
||||
return True
|
||||
|
||||
def _reformat_reports_for_visualizer(self) -> OrderedDict:
|
||||
r"""
|
||||
Takes the generated reports and reformats them into the format that is desired by the
|
||||
ModelReportVisualizer
|
||||
|
||||
Returns an OrderedDict mapping module_fqns to their features
|
||||
"""
|
||||
# we want to reorder and reformat the information so it is ordered in terms of order
|
||||
# found in the model
|
||||
|
||||
# first create new dict with all modules as keys and features under respective module
|
||||
module_fqns_to_features: dict[str, dict] = {}
|
||||
|
||||
for report_name in self._generated_reports:
|
||||
# get mod -> feature dict and go through
|
||||
module_info = self._generated_reports[report_name]
|
||||
|
||||
for module_fqn in module_info:
|
||||
# check if already in our accumulation dict
|
||||
if module_fqn in module_fqns_to_features:
|
||||
# we merge all the features together
|
||||
new_info: dict = module_info[module_fqn]
|
||||
present_info: dict = module_fqns_to_features[module_fqn]
|
||||
|
||||
# merge them together into the new unioned dict
|
||||
# same features keys -> same info, so okay if override
|
||||
|
||||
# do safety check to make sure shared keys have same info
|
||||
if self._is_same_info_for_same_key(new_info, present_info):
|
||||
module_fqns_to_features[module_fqn] = {
|
||||
**new_info,
|
||||
**present_info,
|
||||
}
|
||||
else:
|
||||
error_str = "You have the same key with different values across detectors. "
|
||||
error_str += "Someone incorrectly implemented a detector with conflicting keys to existing detectors."
|
||||
raise ValueError(error_str)
|
||||
else:
|
||||
# we just set it
|
||||
module_fqns_to_features[module_fqn] = module_info[module_fqn]
|
||||
|
||||
# our ordered dict so that modules can be ordered in order of how they appear in model
|
||||
features_by_module: OrderedDict[str, dict] = OrderedDict()
|
||||
|
||||
# we loop through modules in graph in order
|
||||
for fqn, _module in self._model.named_modules():
|
||||
# find that fqn in fqns_to_features
|
||||
if fqn in module_fqns_to_features:
|
||||
# add it to our ordered dict
|
||||
features_by_module[fqn] = module_fqns_to_features[fqn]
|
||||
|
||||
# return the ordered dict of info we created
|
||||
return features_by_module
|
||||
|
||||
def generate_visualizer(self) -> ModelReportVisualizer:
|
||||
r"""
|
||||
Generates a ModelReportVisualizer instance using the reports generated
|
||||
by the generate_model_report() method.
|
||||
|
||||
Returns the generated ModelReportVisualizer instance initialized
|
||||
|
||||
Note:
|
||||
Throws exception if attempt to get visualizers without generating report
|
||||
"""
|
||||
# check if user has generated reports at least once
|
||||
if len(self._generated_reports) == 0:
|
||||
raise Exception( # noqa: TRY002
|
||||
"Unable to generate visualizers without first generating reports"
|
||||
)
|
||||
|
||||
# get the ordered dict mapping modules to their full set of collected features / stats
|
||||
module_fqns_to_features: OrderedDict = self._reformat_reports_for_visualizer()
|
||||
|
||||
# create and return ModelReportVisualizer instance
|
||||
visualizer: ModelReportVisualizer = ModelReportVisualizer(
|
||||
module_fqns_to_features
|
||||
)
|
||||
|
||||
return visualizer
|
||||
|
||||
def _generate_qconfig_mapping_helper(
|
||||
self,
|
||||
detector_qconfig_info_combined: dict[str, DetectorQConfigInfo],
|
||||
generation_function: Callable,
|
||||
) -> QConfigMapping:
|
||||
r"""
|
||||
This helper takes in the compiled detector qconfig info that
|
||||
has been compiled together and merges it into a QConfigMapping
|
||||
"""
|
||||
# keep track of the qconfigmapping
|
||||
qconfig_mapping = QConfigMapping()
|
||||
|
||||
# loop through each module / fqn and attempt to create QConfigMapping
|
||||
for fqn, module in self._model.named_modules():
|
||||
# if we have a qconfig info for this module
|
||||
if fqn in detector_qconfig_info_combined:
|
||||
qconfig_info_compiled = detector_qconfig_info_combined[fqn]
|
||||
|
||||
# now generate the qconfig and add it to the mapping
|
||||
generated_qconfig = generation_function(qconfig_info_compiled, module)
|
||||
|
||||
# add to our config
|
||||
qconfig_mapping.set_module_name(fqn, generated_qconfig)
|
||||
|
||||
# return compiled mapping
|
||||
return qconfig_mapping
|
||||
|
||||
def _update_detector_quantizaiton_qconfig_info(
|
||||
self, combined_info: DetectorQConfigInfo, new_info: DetectorQConfigInfo
|
||||
):
|
||||
r"""
|
||||
Takes in the old and new information and updates the combined information.
|
||||
|
||||
Args:
|
||||
combined_info (DetectorQConfigInfo): The DetectorQConfigInfo we are compiling all of the information in
|
||||
new_info (DetectorQConfigInfo): The DetectorQConfigInfo with the information we are trying to merge the new info
|
||||
into it
|
||||
"""
|
||||
combined_info.is_activation_dynamic = (
|
||||
combined_info.is_activation_dynamic or new_info.is_activation_dynamic
|
||||
)
|
||||
combined_info.is_weight_per_channel = (
|
||||
combined_info.is_weight_per_channel or new_info.is_weight_per_channel
|
||||
)
|
||||
|
||||
def _update_detector_equalization_qconfig_info(
|
||||
self, combined_info: DetectorQConfigInfo, new_info: DetectorQConfigInfo
|
||||
):
|
||||
r"""
|
||||
Takes in the old and new information and updates the combined information.
|
||||
|
||||
Args:
|
||||
combined_info (DetectorQConfigInfo): The DetectorQConfigInfo we are compiling all of the information in
|
||||
new_info (DetectorQConfigInfo): The DetectorQConfigInfo with the information we are trying to merge the new info
|
||||
into it
|
||||
"""
|
||||
is_equalization_recommended = (
|
||||
combined_info.is_equalization_recommended
|
||||
or new_info.is_equalization_recommended
|
||||
)
|
||||
combined_info.is_equalization_recommended = is_equalization_recommended
|
||||
|
||||
def _generate_module_fqn_to_detector_info_mapping(
|
||||
self, update_qconfig_info_function: Callable
|
||||
) -> dict[str, DetectorQConfigInfo]:
|
||||
r"""
|
||||
Generates a QConfigMapping based on the suggestions of the
|
||||
ModelReport API. The generated mapping encompasses all the
|
||||
different types of feedback from the different detectors
|
||||
all into one place.
|
||||
|
||||
These configs are based on the suggestions provided by the ModelReport API
|
||||
and can only be generated once the reports have been generated.
|
||||
|
||||
Args:
|
||||
update_qconfig_info_function (Callable) takes in a function that takes in two DetectorQConfigInfo
|
||||
and updates the one that is being compiled
|
||||
|
||||
Returns a Dict mapping module_fqns to DetectorQConfigInfo objects
|
||||
|
||||
Note:
|
||||
Throws exception if we try to generate mapping on model we already removed observers from
|
||||
Throws exception if we try to generate mapping without preparing for calibration
|
||||
"""
|
||||
# if we haven't prepped model for calibration, then we shouldn't generate mapping yet
|
||||
if not self._prepared_flag:
|
||||
raise Exception( # noqa: TRY002
|
||||
"Cannot generate report without preparing model for calibration"
|
||||
)
|
||||
|
||||
# if we already removed the observers, we cannot mapping
|
||||
if self._removed_observers:
|
||||
raise Exception( # noqa: TRY002
|
||||
"Cannot generate report on model you already removed observers from"
|
||||
)
|
||||
|
||||
# keep track of qconfig info for each module across detectors
|
||||
detector_qconfig_info_combined: dict[str, DetectorQConfigInfo] = {}
|
||||
|
||||
for detector in self._desired_report_detectors:
|
||||
# get the info from the detector
|
||||
detector_info: dict[str, DetectorQConfigInfo] = detector.get_qconfig_info(
|
||||
self._model
|
||||
)
|
||||
|
||||
# we go through the modules
|
||||
for module_fqn in detector_info:
|
||||
# see if we already have info on it
|
||||
if module_fqn in detector_qconfig_info_combined:
|
||||
# we combine the current options with what is there
|
||||
current_options = detector_qconfig_info_combined[module_fqn]
|
||||
detector_options = detector_info[module_fqn]
|
||||
|
||||
update_qconfig_info_function(current_options, detector_options)
|
||||
else:
|
||||
# we just use this for now
|
||||
detector_qconfig_info_combined[module_fqn] = detector_info[
|
||||
module_fqn
|
||||
]
|
||||
|
||||
return detector_qconfig_info_combined
|
||||
|
||||
def generate_qconfig_mapping(self) -> QConfigMapping:
|
||||
r"""
|
||||
Generates a QConfigMapping based on the suggestions of the
|
||||
ModelReport API. The generated mapping encompasses all the
|
||||
different types of feedback from the different detectors
|
||||
all into one place.
|
||||
|
||||
These configs are based on the suggestions provided by the ModelReport API
|
||||
and can only be generated once the reports have been generated.
|
||||
|
||||
Returns a QConfigMapping for the quantization configuration
|
||||
|
||||
Note:
|
||||
Throws exception if we try to generate mapping on model we already removed observers from
|
||||
Throws exception if we try to generate mapping without preparing for calibration
|
||||
"""
|
||||
# get the mapping info
|
||||
detector_qconfig_info_combined = (
|
||||
self._generate_module_fqn_to_detector_info_mapping(
|
||||
self._update_detector_quantizaiton_qconfig_info
|
||||
)
|
||||
)
|
||||
|
||||
# we will do a bit of processing and remove fqns that don't have input weight recommended
|
||||
|
||||
# now we generate the QConfig for each of the options
|
||||
mapping: QConfigMapping = self._generate_qconfig_mapping_helper(
|
||||
detector_qconfig_info_combined, self._quantization_config_generator
|
||||
)
|
||||
|
||||
# return the generated mapping
|
||||
return mapping
|
||||
|
||||
def _quantization_config_generator(
|
||||
self, detector_qconfig_info: DetectorQConfigInfo, module: torch.nn.Module
|
||||
) -> QConfig:
|
||||
r"""
|
||||
Returns the quantization configuration generated by the DetectorQConfigInfo object
|
||||
"""
|
||||
return detector_qconfig_info.generate_quantization_qconfig(module)
|
||||
|
||||
def _equalization_config_generator(
|
||||
self, detector_qconfig_info: DetectorQConfigInfo, module: torch.nn.Module
|
||||
) -> EqualizationQConfig:
|
||||
r"""
|
||||
We ignore the module argument here, and only focus on thedetector_qconfig_info
|
||||
|
||||
Returns the equalization configuration generated by the DetectorQConfigInfo object
|
||||
"""
|
||||
return detector_qconfig_info.generate_equalization_qconfig()
|
||||
|
||||
def generate_equalization_mapping(self) -> QConfigMapping:
|
||||
r"""
|
||||
Generates a QConfigMapping based on the suggestions of the
|
||||
ModelReport API for equalization. The generated mapping encompasses all the
|
||||
different types of feedback from the input-weight equalization detector.
|
||||
|
||||
These configs are based on the suggestions provided by the ModelReport API
|
||||
and can only be generated once the reports have been generated.
|
||||
|
||||
Returns a QConfigMapping for the equalization configuration
|
||||
"""
|
||||
# get the mapping info
|
||||
detector_qconfig_info_combined = (
|
||||
self._generate_module_fqn_to_detector_info_mapping(
|
||||
self._update_detector_equalization_qconfig_info
|
||||
)
|
||||
)
|
||||
|
||||
# now we generate the QConfig for each of the options
|
||||
mapping: QConfigMapping = self._generate_qconfig_mapping_helper(
|
||||
detector_qconfig_info_combined, self._equalization_config_generator
|
||||
)
|
||||
|
||||
# return the generated mapping
|
||||
return mapping
|
||||
+285
@@ -0,0 +1,285 @@
|
||||
# mypy: allow-untyped-defs
|
||||
import torch
|
||||
from torch.ao.quantization.observer import ObserverBase
|
||||
|
||||
|
||||
class ModelReportObserver(ObserverBase):
|
||||
r"""This observer is used to record additional information regarding keeping track
|
||||
of S = average_batch_activation_range/epoch_activation_range.
|
||||
|
||||
The purpose of this information is to prepare a report to present to users on whether
|
||||
Dynamic or Static Quantization is more appropriate for their model given the general
|
||||
distributions of their data.
|
||||
|
||||
Args:
|
||||
ch_axis (int, optional): The channel axis for which the range and outlier stats are computed
|
||||
Default: 1
|
||||
comp_percentile (float, optional): The percentile to compare against 100 percentile to find outliers
|
||||
Should be between 0 and 1 exclusive
|
||||
Default: 0.9
|
||||
|
||||
* :attr:`num_batches_tracked` specifies number of batches passed through the observer
|
||||
|
||||
* :attr:`average_batch_activation_range` defines average across the ranges of each batch passed through
|
||||
|
||||
* :attr:`epoch_activation_min` defines the minimum value passed through the observer
|
||||
|
||||
* :attr:`epoch_activation_max` defines the maximum value passed through the observer
|
||||
|
||||
* :attr:`ch_axis` defines the channel being used to compute per channel min max stats
|
||||
|
||||
* :attr:`min_val` defines the per channel minimum values passed through
|
||||
|
||||
* :attr:`max_val` defines the per channel maximum values passed through
|
||||
|
||||
* :attr:`comp_percentile` defines comparison percentile to find outliers
|
||||
|
||||
* :attr:`average_percentile_ratio` defines the per channel average percentile ratios
|
||||
|
||||
* :attr:`percentile_batches_tracked` defines the number of percentile batches tracked for each channel
|
||||
|
||||
* :attr:`constant_channels` defines the number of batches that aren't constant channels per channel
|
||||
|
||||
Note: this tool is meant for FX Graph Mode Quantization
|
||||
"""
|
||||
|
||||
epoch_activation_min: torch.Tensor
|
||||
epoch_activation_max: torch.Tensor
|
||||
min_val: torch.Tensor
|
||||
max_val: torch.Tensor
|
||||
comp_percentile: torch.Tensor
|
||||
average_percentile_ratio: torch.Tensor
|
||||
percentile_batches_tracked: torch.Tensor
|
||||
constant_channels: torch.Tensor
|
||||
|
||||
def __init__(self, ch_axis: int = 1, comp_percentile: float = 0.9):
|
||||
super().__init__(torch.qint8)
|
||||
self.num_batches_tracked = 0
|
||||
|
||||
# keep track of the min and mix of the range for average batch and epoch as a whole
|
||||
self.average_batch_activation_range: torch.Tensor = torch.tensor(float(0))
|
||||
self.register_buffer("epoch_activation_min", torch.tensor(float("inf")))
|
||||
self.register_buffer("epoch_activation_max", torch.tensor(float("-inf")))
|
||||
|
||||
# keep track of per channel min max information using the given channel
|
||||
self.ch_axis: int = ch_axis
|
||||
self.register_buffer("min_val", torch.tensor([]))
|
||||
self.register_buffer("max_val", torch.tensor([]))
|
||||
|
||||
# keep track of percentile ratio information per channel
|
||||
self.register_buffer("comp_percentile", torch.tensor([comp_percentile]))
|
||||
self.register_buffer("average_percentile_ratio", torch.tensor([]))
|
||||
self.register_buffer("percentile_batches_tracked", torch.tensor([]))
|
||||
self.register_buffer("constant_channels", torch.tensor([]))
|
||||
|
||||
def forward(self, x):
|
||||
x_copy = x.detach() # avoid keeping autograd tape
|
||||
x_copy = x_copy.to(self.epoch_activation_min.dtype)
|
||||
|
||||
x_copy = self._calculate_range_stats(x_copy)
|
||||
x_copy = self._calculate_min_max_stats(x_copy)
|
||||
x_copy = self._calculate_percentile_stats(x_copy)
|
||||
|
||||
# return the passed in the value
|
||||
return x
|
||||
|
||||
def _calculate_range_stats(self, x_copy):
|
||||
r"""Calculates and stores range stats with forward values.
|
||||
|
||||
Args
|
||||
x_copy: A copy of the forward data
|
||||
|
||||
Returns the passed in x_copy
|
||||
"""
|
||||
# get the min, max values of the data
|
||||
min_val_cur, max_val_cur = torch.aminmax(x_copy)
|
||||
|
||||
# calculate new epoch range values
|
||||
epoch_min_val = torch.min(self.epoch_activation_min, min_val_cur)
|
||||
epoch_max_val = torch.max(self.epoch_activation_max, max_val_cur)
|
||||
|
||||
self.epoch_activation_min.copy_(epoch_min_val)
|
||||
self.epoch_activation_max.copy_(epoch_max_val)
|
||||
|
||||
# calculate the average batch activation range
|
||||
current_batch_range = max_val_cur - min_val_cur
|
||||
new_range = (
|
||||
self.average_batch_activation_range * self.num_batches_tracked
|
||||
+ current_batch_range
|
||||
) / (self.num_batches_tracked + 1)
|
||||
|
||||
self.average_batch_activation_range = new_range
|
||||
self.num_batches_tracked += 1 # new batch was processed
|
||||
|
||||
return x_copy
|
||||
|
||||
def _calculate_min_max_stats(self, x_copy):
|
||||
r"""Calculates and stores the per_channel min, max stats with forward values.
|
||||
Does calculation based on channel axis: self.ch_axis
|
||||
|
||||
Args
|
||||
x_copy: A copy of the forward data
|
||||
|
||||
Returns the passed in x_copy
|
||||
"""
|
||||
# get the current min and max vals
|
||||
min_val = self.min_val
|
||||
max_val = self.max_val
|
||||
x_dim = x_copy.size()
|
||||
|
||||
new_axis_list = [i for i in range(len(x_dim))] # noqa: C416
|
||||
new_axis_list[self.ch_axis] = 0
|
||||
new_axis_list[0] = self.ch_axis
|
||||
y = x_copy.permute(new_axis_list)
|
||||
# Need to match dtype of min/max because the updates to buffers
|
||||
# are done in place and types need to match for comparisons
|
||||
y = y.to(self.min_val.dtype)
|
||||
y = torch.flatten(y, start_dim=1)
|
||||
if min_val.numel() == 0 or max_val.numel() == 0:
|
||||
min_val, max_val = torch.aminmax(y, dim=1)
|
||||
else:
|
||||
min_val_cur, max_val_cur = torch.aminmax(y, dim=1)
|
||||
min_val = torch.min(min_val_cur, min_val)
|
||||
max_val = torch.max(max_val_cur, max_val)
|
||||
|
||||
self.min_val.resize_(min_val.shape)
|
||||
self.max_val.resize_(max_val.shape)
|
||||
self.min_val.copy_(min_val)
|
||||
self.max_val.copy_(max_val)
|
||||
|
||||
return x_copy
|
||||
|
||||
def _calculate_percentile_stats(self, x_copy):
|
||||
r"""Calculates and stores the per_channel percentile stats with forward values.
|
||||
Does calculation based on channel axis: self.ch_axis
|
||||
|
||||
Args
|
||||
x_copy: A copy of the forward data
|
||||
|
||||
Returns the passed in x_copy
|
||||
"""
|
||||
# get the dimension of the copy
|
||||
x_dim = x_copy.size()
|
||||
|
||||
new_axis_list = [i for i in range(len(x_dim))] # noqa: C416
|
||||
new_axis_list[self.ch_axis] = 0
|
||||
new_axis_list[0] = self.ch_axis
|
||||
y = x_copy.permute(new_axis_list)
|
||||
# Need to match dtype of min/max because the updates to buffers
|
||||
# are done in place and types need to match for comparisons
|
||||
y = y.to(self.min_val.dtype)
|
||||
y = torch.flatten(y, start_dim=1)
|
||||
y = y.to(dtype=self.min_val.dtype, device="cpu")
|
||||
|
||||
# find the percentile values along the axis
|
||||
# we want both 100th percentile and comp_percentile
|
||||
# we also want to find 0th quartile to see if we have constant channel
|
||||
quantiles_list = [0, self.comp_percentile, 1.00]
|
||||
quantiles_to_find = torch.tensor(quantiles_list, dtype=self.min_val.dtype)
|
||||
|
||||
# find the quantiles
|
||||
desired_quantiles = torch.quantile(
|
||||
y, quantiles_to_find, dim=self.ch_axis, interpolation="lower"
|
||||
)
|
||||
zero_quantile = desired_quantiles[0]
|
||||
comp_quantile = desired_quantiles[1]
|
||||
hundreth_quartile = desired_quantiles[2]
|
||||
|
||||
# if any of the channels have 0s, we ignore that channel for this calculation
|
||||
any_non_zero_quantile_value: torch.Tensor = (
|
||||
comp_quantile != torch.tensor([0])
|
||||
) | (hundreth_quartile != torch.tensor([0]))
|
||||
any_non_zero_quantile_value = (
|
||||
any_non_zero_quantile_value.int()
|
||||
) # transform boolean values to int values
|
||||
|
||||
# we also check if we have a constant channel
|
||||
any_constant_channels: torch.Tensor = (
|
||||
hundreth_quartile - zero_quantile
|
||||
) == torch.tensor([0])
|
||||
any_constant_channels = (
|
||||
any_constant_channels.int()
|
||||
) # transform boolean values to int values
|
||||
|
||||
# possibilities to get nan as an answer
|
||||
# will ignore any of these three cases with 0s and just not deal with them for now
|
||||
# case (1) 0 in numerator: issue if 0 is largest, all negative, and rest are really negative
|
||||
# case (2) 0 in denominator: is possible unless case 3, we just ignore
|
||||
# case (3) 0 in both: not outlier, channel just kinda useless, ignore
|
||||
|
||||
# get the ratio and get rid of nan values
|
||||
quantile_ratios = hundreth_quartile / comp_quantile
|
||||
quantile_ratios = torch.nan_to_num(quantile_ratios)
|
||||
# update averages, remembering to only update if didn't have zeros
|
||||
ratio_if_not_zero = any_non_zero_quantile_value * quantile_ratios
|
||||
|
||||
# if num_batches and average_ratio are not initialized, we want to initialize them
|
||||
if (
|
||||
self.percentile_batches_tracked.shape[0] == 0
|
||||
or self.average_percentile_ratio.shape[0] == 0
|
||||
):
|
||||
self.percentile_batches_tracked = torch.zeros_like(
|
||||
any_non_zero_quantile_value
|
||||
)
|
||||
self.average_percentile_ratio = torch.zeros_like(ratio_if_not_zero)
|
||||
|
||||
# also initialize the constant channel var if that is not initialized separately
|
||||
if self.constant_channels.shape[0] == 0:
|
||||
self.constant_channels = torch.zeros_like(any_constant_channels)
|
||||
|
||||
# get current num batches and average ratio
|
||||
num_batches = self.percentile_batches_tracked
|
||||
average_ratio = self.average_percentile_ratio
|
||||
|
||||
# calculate new_number of batches, new_ratios, and get rid of nans because of 0 size batches
|
||||
new_number_of_batches: torch.Tensor = num_batches + any_non_zero_quantile_value
|
||||
new_ratios: torch.Tensor = (
|
||||
(average_ratio * num_batches) + ratio_if_not_zero
|
||||
) / new_number_of_batches
|
||||
new_ratios = torch.nan_to_num(new_ratios)
|
||||
|
||||
# update the number of non-constant channels
|
||||
new_constant_count: torch.Tensor = (
|
||||
self.constant_channels + any_constant_channels
|
||||
)
|
||||
|
||||
# update the values locally
|
||||
self.percentile_batches_tracked.copy_(new_number_of_batches)
|
||||
self.average_percentile_ratio.copy_(new_ratios)
|
||||
self.constant_channels.copy_(new_constant_count)
|
||||
|
||||
return x_copy
|
||||
|
||||
@torch.jit.export
|
||||
def get_batch_to_epoch_ratio(self):
|
||||
epoch_activation_range = self.epoch_activation_max - self.epoch_activation_min
|
||||
|
||||
if epoch_activation_range == torch.tensor(float(0)):
|
||||
raise ValueError("Range for Epoch is 0")
|
||||
elif epoch_activation_range == torch.tensor(float("inf")):
|
||||
raise ValueError(
|
||||
"No data has been run through observer or infinity value present"
|
||||
)
|
||||
else:
|
||||
return self.average_batch_activation_range / epoch_activation_range
|
||||
|
||||
@torch.jit.export
|
||||
def reset_batch_and_epoch_values(self):
|
||||
# set all the values back to their original defaults for a new epoch
|
||||
# keep device
|
||||
device = self.max_val.device
|
||||
self.num_batches_tracked = 0
|
||||
self.average_batch_activation_range = torch.tensor(float(0), device=device)
|
||||
self.epoch_activation_min = torch.tensor(float("inf"), device=device)
|
||||
self.epoch_activation_max = torch.tensor(float("-inf"), device=device)
|
||||
self.min_val = torch.tensor([], device=device)
|
||||
self.max_val = torch.tensor([], device=device)
|
||||
self.average_percentile_ratio = torch.tensor([], device=device)
|
||||
self.percentile_batches_tracked = torch.tensor([], device=device)
|
||||
self.constant_channels = torch.tensor([], device=device)
|
||||
|
||||
@torch.jit.export
|
||||
def calculate_qparams(self): # type: ignore[override]
|
||||
raise Exception( # noqa: TRY002
|
||||
"calculate_qparams should not be called for ModelReportObserver"
|
||||
)
|
||||
+712
@@ -0,0 +1,712 @@
|
||||
# mypy: allow-untyped-defs
|
||||
from collections import OrderedDict, OrderedDict as OrdDict
|
||||
from typing import Any
|
||||
|
||||
import torch
|
||||
|
||||
|
||||
# try to import tablate
|
||||
got_tabulate = True
|
||||
try:
|
||||
from tabulate import tabulate
|
||||
except ImportError:
|
||||
got_tabulate = False
|
||||
|
||||
|
||||
# var to see if we could import matplotlib
|
||||
got_matplotlib = True
|
||||
try:
|
||||
import matplotlib.pyplot as plt
|
||||
except ImportError:
|
||||
got_matplotlib = False
|
||||
|
||||
|
||||
class ModelReportVisualizer:
|
||||
r"""
|
||||
The ModelReportVisualizer class aims to provide users a way to visualize some of the statistics
|
||||
that were generated by the ModelReport API. However, at a higher level, the class aims to provide
|
||||
some level of visualization of statistics to PyTorch in order to make it easier to parse data and
|
||||
diagnose any potential issues with data or a specific model. With respect to the visualizations,
|
||||
the ModelReportVisualizer class currently supports several methods of visualizing data.
|
||||
|
||||
Supported Visualization Methods Include:
|
||||
- Table format
|
||||
- Plot format (line graph)
|
||||
- Histogram format
|
||||
|
||||
For all of the existing visualization methods, there is the option to filter data based on:
|
||||
- A module fqn prefix
|
||||
- Feature [required for the plot and histogram]
|
||||
|
||||
* :attr:`generated_reports` The reports generated by the ModelReport class in the structure below
|
||||
Ensure sure that features that are the same across different report contain the same name
|
||||
Ensure that objects representing the same features are the same type / dimension (where applicable)
|
||||
|
||||
Note:
|
||||
Currently, the ModelReportVisualizer class supports visualization of data generated by the
|
||||
ModelReport class. However, this structure is extensible and should allow the visualization of
|
||||
other information as long as the information is structured in the following general format:
|
||||
|
||||
Report Structure
|
||||
-- module_fqn [module with attached detectors]
|
||||
|
|
||||
-- feature keys [not every detector extracts same information]
|
||||
[same collected info has same keys, unless can be specific to detector]
|
||||
|
||||
|
||||
The goal behind the class is that the generated visualizations can be used in conjunction with the generated
|
||||
report for people to get a better understanding of issues and what the fix might be. It is also just to provide
|
||||
a good visualization platform, since it might be hard to parse through the ModelReport returned dictionary as
|
||||
that grows in size.
|
||||
|
||||
General Use Flow Expected
|
||||
1.) Initialize ModelReport object with reports of interest by passing in initialized detector objects
|
||||
2.) Prepare your model with prepare_fx
|
||||
3.) Call model_report.prepare_detailed_calibration on your model to add relevant observers
|
||||
4.) Calibrate your model with data
|
||||
5.) Call model_report.generate_report on your model to generate report and optionally remove added observers
|
||||
6.) Use output of model_report.generate_report to initialize ModelReportVisualizer instance
|
||||
7.) Use instance to view different views of data as desired, applying filters as needed
|
||||
8.) Either see the super detailed information or just the actual printed or shown table / plot / histogram
|
||||
|
||||
"""
|
||||
|
||||
# keys for table dict
|
||||
TABLE_TENSOR_KEY = "tensor_level_info"
|
||||
TABLE_CHANNEL_KEY = "channel_level_info"
|
||||
|
||||
# Constants for header vals
|
||||
NUM_NON_FEATURE_TENSOR_HEADERS = 2
|
||||
NUM_NON_FEATURE_CHANNEL_HEADERS = 3
|
||||
|
||||
# Constants for row index in header
|
||||
CHANNEL_NUM_INDEX = 2
|
||||
|
||||
def __init__(self, generated_reports: OrderedDict[str, Any]):
|
||||
r"""
|
||||
Initializes the ModelReportVisualizer instance with the necessary reports.
|
||||
|
||||
Args:
|
||||
generated_reports (Dict[str, Any]): The reports generated by the ModelReport class
|
||||
can also be a dictionary generated in another manner, as long as format is same
|
||||
"""
|
||||
self.generated_reports = generated_reports
|
||||
|
||||
def get_all_unique_module_fqns(self) -> set[str]:
|
||||
r"""
|
||||
The purpose of this method is to provide a user the set of all module_fqns so that if
|
||||
they wish to use some of the filtering capabilities of the ModelReportVisualizer class,
|
||||
they don't need to manually parse the generated_reports dictionary to get this information.
|
||||
|
||||
Returns all the unique module fqns present in the reports the ModelReportVisualizer
|
||||
instance was initialized with.
|
||||
"""
|
||||
# returns the keys of the ordered dict
|
||||
return set(self.generated_reports.keys())
|
||||
|
||||
def get_all_unique_feature_names(
|
||||
self, plottable_features_only: bool = True
|
||||
) -> set[str]:
|
||||
r"""
|
||||
The purpose of this method is to provide a user the set of all feature names so that if
|
||||
they wish to use the filtering capabilities of the generate_table_view(), or use either of
|
||||
the generate_plot_view() or generate_histogram_view(), they don't need to manually parse
|
||||
the generated_reports dictionary to get this information.
|
||||
|
||||
Args:
|
||||
plottable_features_only (bool): True if the user is only looking for plottable features,
|
||||
False otherwise
|
||||
plottable features are those that are tensor values
|
||||
Default: True (only return those feature names that are plottable)
|
||||
|
||||
Returns all the unique module fqns present in the reports the ModelReportVisualizer
|
||||
instance was initialized with.
|
||||
"""
|
||||
unique_feature_names = set()
|
||||
for module_fqn in self.generated_reports:
|
||||
# get dict of the features
|
||||
feature_dict: dict[str, Any] = self.generated_reports[module_fqn]
|
||||
|
||||
# loop through features
|
||||
for feature_name in feature_dict:
|
||||
# if we need plottable, ensure type of val is tensor
|
||||
if (
|
||||
not plottable_features_only
|
||||
or type(feature_dict[feature_name]) is torch.Tensor
|
||||
):
|
||||
unique_feature_names.add(feature_name)
|
||||
|
||||
# return our compiled set of unique feature names
|
||||
return unique_feature_names
|
||||
|
||||
def _get_filtered_data(
|
||||
self, feature_filter: str, module_fqn_filter: str
|
||||
) -> OrderedDict[str, Any]:
|
||||
r"""
|
||||
Filters the data and returns it in the same ordered dictionary format so the relevant views can be displayed.
|
||||
|
||||
Args:
|
||||
feature_filter (str): The feature filter, if we want to filter the set of data to only include
|
||||
a certain set of features that include feature_filter
|
||||
If feature = "", then we do not filter based on any features
|
||||
module_fqn_filter (str): The filter on prefix for the module fqn. All modules that have fqn with
|
||||
this prefix will be included
|
||||
If module_fqn_filter = "" we do not filter based on module fqn, and include all modules
|
||||
|
||||
First, the data is filtered based on module_fqn, and then filtered based on feature
|
||||
Returns an OrderedDict (sorted in order of model) mapping:
|
||||
module_fqns -> feature_names -> values
|
||||
"""
|
||||
# create return dict
|
||||
filtered_dict: OrderedDict[str, Any] = OrdDict()
|
||||
|
||||
for module_fqn in self.generated_reports:
|
||||
# first filter based on module
|
||||
if module_fqn_filter == "" or module_fqn_filter in module_fqn:
|
||||
# create entry for module and loop through features
|
||||
filtered_dict[module_fqn] = {}
|
||||
module_reports = self.generated_reports[module_fqn]
|
||||
for feature_name in module_reports:
|
||||
# check if filtering on features and do so if desired
|
||||
if feature_filter == "" or feature_filter in feature_name:
|
||||
filtered_dict[module_fqn][feature_name] = module_reports[
|
||||
feature_name
|
||||
]
|
||||
|
||||
# we have populated the filtered dict, and must return it
|
||||
|
||||
return filtered_dict
|
||||
|
||||
def _generate_tensor_table(
|
||||
self,
|
||||
filtered_data: OrderedDict[str, dict[str, Any]],
|
||||
tensor_features: list[str],
|
||||
) -> tuple[list, list]:
|
||||
r"""
|
||||
Takes in the filtered data and features list and generates the tensor headers and table
|
||||
|
||||
Currently meant to generate the headers and table for both the tensor information.
|
||||
|
||||
Args:
|
||||
filtered_data (OrderedDict[str, Dict[str, Any]]): An OrderedDict (sorted in order of model) mapping:
|
||||
module_fqns -> feature_names -> values
|
||||
tensor_features (List[str]): A list of the tensor level features
|
||||
|
||||
Returns a tuple with:
|
||||
A list of the headers of the tensor table
|
||||
A list of lists containing the table information row by row
|
||||
The 0th index row will contain the headers of the columns
|
||||
The rest of the rows will contain data
|
||||
"""
|
||||
# now we compose the tensor information table
|
||||
tensor_table: list[list[Any]] = []
|
||||
tensor_headers: list[str] = []
|
||||
|
||||
# append the table row to the table only if we have features
|
||||
if len(tensor_features) > 0:
|
||||
# now we add all the data
|
||||
for index, module_fqn in enumerate(filtered_data):
|
||||
# we make a new row for the tensor table
|
||||
tensor_table_row = [index, module_fqn]
|
||||
for feature in tensor_features:
|
||||
# we iterate in same order of added features
|
||||
|
||||
if feature in filtered_data[module_fqn]:
|
||||
# add value if applicable to module
|
||||
feature_val = filtered_data[module_fqn][feature]
|
||||
else:
|
||||
# add that it is not applicable
|
||||
feature_val = "Not Applicable"
|
||||
|
||||
# if it's a tensor we want to extract val
|
||||
if isinstance(feature_val, torch.Tensor):
|
||||
feature_val = feature_val.item()
|
||||
|
||||
# we add to our list of values
|
||||
# pyrefly: ignore [bad-argument-type]
|
||||
tensor_table_row.append(feature_val)
|
||||
|
||||
tensor_table.append(tensor_table_row)
|
||||
|
||||
# add row of headers of we actually have something, otherwise just empty
|
||||
if len(tensor_table) != 0:
|
||||
tensor_headers = ["idx", "layer_fqn"] + tensor_features
|
||||
|
||||
return (tensor_headers, tensor_table)
|
||||
|
||||
def _generate_channels_table(
|
||||
self,
|
||||
filtered_data: OrderedDict[str, Any],
|
||||
channel_features: list[str],
|
||||
num_channels: int,
|
||||
) -> tuple[list, list]:
|
||||
r"""
|
||||
Takes in the filtered data and features list and generates the channels headers and table
|
||||
|
||||
Currently meant to generate the headers and table for both the channels information.
|
||||
|
||||
Args:
|
||||
filtered_data (OrderedDict[str, Any]): An OrderedDict (sorted in order of model) mapping:
|
||||
module_fqns -> feature_names -> values
|
||||
channel_features (List[str]): A list of the channel level features
|
||||
num_channels (int): Number of channels in the channel data
|
||||
|
||||
Returns a tuple with:
|
||||
A list of the headers of the channel table
|
||||
A list of lists containing the table information row by row
|
||||
The 0th index row will contain the headers of the columns
|
||||
The rest of the rows will contain data
|
||||
"""
|
||||
# now we compose the table for the channel information table
|
||||
channel_table: list[list[Any]] = []
|
||||
channel_headers: list[str] = []
|
||||
|
||||
# counter to keep track of number of entries in
|
||||
channel_table_entry_counter: int = 0
|
||||
|
||||
if len(channel_features) > 0:
|
||||
# now we add all channel data
|
||||
for module_fqn in filtered_data:
|
||||
# we iterate over all channels
|
||||
for channel in range(num_channels):
|
||||
# we make a new row for the channel
|
||||
new_channel_row = [channel_table_entry_counter, module_fqn, channel]
|
||||
for feature in channel_features:
|
||||
if feature in filtered_data[module_fqn]:
|
||||
# add value if applicable to module
|
||||
feature_val = filtered_data[module_fqn][feature][channel]
|
||||
else:
|
||||
# add that it is not applicable
|
||||
feature_val = "Not Applicable"
|
||||
|
||||
# if it's a tensor we want to extract val
|
||||
if type(feature_val) is torch.Tensor:
|
||||
feature_val = feature_val.item()
|
||||
|
||||
# add value to channel specific row
|
||||
# pyrefly: ignore [bad-argument-type]
|
||||
new_channel_row.append(feature_val)
|
||||
|
||||
# add to table and increment row index counter
|
||||
channel_table.append(new_channel_row)
|
||||
channel_table_entry_counter += 1
|
||||
|
||||
# add row of headers of we actually have something, otherwise just empty
|
||||
if len(channel_table) != 0:
|
||||
channel_headers = ["idx", "layer_fqn", "channel"] + channel_features
|
||||
|
||||
return (channel_headers, channel_table)
|
||||
|
||||
def generate_filtered_tables(
|
||||
self, feature_filter: str = "", module_fqn_filter: str = ""
|
||||
) -> dict[str, tuple[list, list]]:
|
||||
r"""
|
||||
Takes in optional filter values and generates two tables with desired information.
|
||||
|
||||
The generated tables are presented in both a list-of-lists format
|
||||
|
||||
The reason for the two tables are that they handle different things:
|
||||
1.) the first table handles all tensor level information
|
||||
2.) the second table handles and displays all channel based information
|
||||
|
||||
The reasoning for this is that having all the info in one table can make it ambiguous which collected
|
||||
statistics are global, and which are actually per-channel, so it's better to split it up into two
|
||||
tables. This also makes the information much easier to digest given the plethora of statistics collected
|
||||
|
||||
Tensor table columns:
|
||||
idx layer_fqn feature_1 feature_2 feature_3 .... feature_n
|
||||
---- --------- --------- --------- --------- ---------
|
||||
|
||||
Per-Channel table columns:
|
||||
idx layer_fqn channel feature_1 feature_2 feature_3 .... feature_n
|
||||
---- --------- ------- --------- --------- --------- ---------
|
||||
|
||||
Args:
|
||||
feature_filter (str, optional): Filters the features presented to only those that
|
||||
contain this filter substring
|
||||
Default = "", results in all the features being printed
|
||||
module_fqn_filter (str, optional): Only includes modules that contains this string
|
||||
Default = "", results in all the modules in the reports to be visible in the table
|
||||
|
||||
Returns a dictionary with two keys:
|
||||
(Dict[str, Tuple[List, List]]) A dict containing two keys:
|
||||
"tensor_level_info", "channel_level_info"
|
||||
Each key maps to a tuple with:
|
||||
A list of the headers of each table
|
||||
A list of lists containing the table information row by row
|
||||
The 0th index row will contain the headers of the columns
|
||||
The rest of the rows will contain data
|
||||
|
||||
Example Use:
|
||||
>>> # xdoctest: +SKIP("undefined variables")
|
||||
>>> mod_report_visualizer.generate_filtered_tables(
|
||||
... feature_filter="per_channel_min", module_fqn_filter="block1"
|
||||
... ) # generates table with per_channel_min info for all modules in block 1 of the model
|
||||
"""
|
||||
# first get the filtered data
|
||||
filtered_data: OrderedDict[str, Any] = self._get_filtered_data(
|
||||
feature_filter, module_fqn_filter
|
||||
)
|
||||
|
||||
# now we split into tensor and per-channel data
|
||||
tensor_features: set[str] = set()
|
||||
channel_features: set[str] = set()
|
||||
|
||||
# keep track of the number of channels we have
|
||||
num_channels: int = 0
|
||||
|
||||
for module_fqn in filtered_data:
|
||||
for feature_name in filtered_data[module_fqn]:
|
||||
# get the data for that specific feature
|
||||
feature_data = filtered_data[module_fqn][feature_name]
|
||||
|
||||
# check if not zero dim tensor
|
||||
is_tensor: bool = isinstance(feature_data, torch.Tensor)
|
||||
is_not_zero_dim: bool = is_tensor and len(feature_data.shape) != 0
|
||||
|
||||
if is_not_zero_dim or isinstance(feature_data, list):
|
||||
# works means per channel
|
||||
channel_features.add(feature_name)
|
||||
num_channels = len(feature_data)
|
||||
else:
|
||||
# means is per-tensor
|
||||
tensor_features.add(feature_name)
|
||||
|
||||
# we make them lists for iteration purposes
|
||||
tensor_features_list: list[str] = sorted(tensor_features)
|
||||
channel_features_list: list[str] = sorted(channel_features)
|
||||
|
||||
# get the tensor info
|
||||
tensor_headers, tensor_table = self._generate_tensor_table(
|
||||
filtered_data, tensor_features_list
|
||||
)
|
||||
|
||||
# get the channel info
|
||||
channel_headers, channel_table = self._generate_channels_table(
|
||||
filtered_data, channel_features_list, num_channels
|
||||
)
|
||||
|
||||
# let's now create the dictionary to return
|
||||
table_dict = {
|
||||
self.TABLE_TENSOR_KEY: (tensor_headers, tensor_table),
|
||||
self.TABLE_CHANNEL_KEY: (channel_headers, channel_table),
|
||||
}
|
||||
|
||||
# return the two tables
|
||||
return table_dict
|
||||
|
||||
def generate_table_visualization(
|
||||
self, feature_filter: str = "", module_fqn_filter: str = ""
|
||||
):
|
||||
r"""
|
||||
Takes in optional filter values and prints out formatted tables of the information.
|
||||
|
||||
The reason for the two tables printed out instead of one large one are that they handle different things:
|
||||
1.) the first table handles all tensor level information
|
||||
2.) the second table handles and displays all channel based information
|
||||
|
||||
The reasoning for this is that having all the info in one table can make it ambiguous which collected
|
||||
statistics are global, and which are actually per-channel, so it's better to split it up into two
|
||||
tables. This also makes the information much easier to digest given the plethora of statistics collected
|
||||
|
||||
Tensor table columns:
|
||||
idx layer_fqn feature_1 feature_2 feature_3 .... feature_n
|
||||
---- --------- --------- --------- --------- ---------
|
||||
|
||||
Per-Channel table columns:
|
||||
|
||||
idx layer_fqn channel feature_1 feature_2 feature_3 .... feature_n
|
||||
---- --------- ------- --------- --------- --------- ---------
|
||||
|
||||
Args:
|
||||
feature_filter (str, optional): Filters the features presented to only those that
|
||||
contain this filter substring
|
||||
Default = "", results in all the features being printed
|
||||
module_fqn_filter (str, optional): Only includes modules that contains this string
|
||||
Default = "", results in all the modules in the reports to be visible in the table
|
||||
|
||||
Example Use:
|
||||
>>> # xdoctest: +SKIP("undefined variables")
|
||||
>>> mod_report_visualizer.generate_table_visualization(
|
||||
... feature_filter="per_channel_min", module_fqn_filter="block1"
|
||||
... )
|
||||
>>> # prints out neatly formatted table with per_channel_min info
|
||||
>>> # for all modules in block 1 of the model
|
||||
"""
|
||||
# see if we got tabulate
|
||||
if not got_tabulate:
|
||||
print("Make sure to install tabulate and try again.")
|
||||
return None
|
||||
|
||||
# get the table dict and the specific tables of interest
|
||||
table_dict = self.generate_filtered_tables(feature_filter, module_fqn_filter)
|
||||
tensor_headers, tensor_table = table_dict[self.TABLE_TENSOR_KEY]
|
||||
channel_headers, channel_table = table_dict[self.TABLE_CHANNEL_KEY]
|
||||
|
||||
# get the table string and print it out
|
||||
# now we have populated the tables for each one
|
||||
# let's create the strings to be returned
|
||||
table_str = ""
|
||||
# the tables will have some headers columns that are non-feature
|
||||
# ex. table index, module name, channel index, etc.
|
||||
# we want to look at header columns for features, that come after those headers
|
||||
if len(tensor_headers) > self.NUM_NON_FEATURE_TENSOR_HEADERS:
|
||||
# if we have at least one tensor level feature to be added we add tensor table
|
||||
table_str += "Tensor Level Information \n"
|
||||
table_str += tabulate(tensor_table, headers=tensor_headers)
|
||||
if len(channel_headers) > self.NUM_NON_FEATURE_CHANNEL_HEADERS:
|
||||
# if we have at least one channel level feature to be added we add tensor table
|
||||
table_str += "\n\n Channel Level Information \n"
|
||||
table_str += tabulate(channel_table, headers=channel_headers)
|
||||
|
||||
# if no features at all, let user know
|
||||
if table_str == "":
|
||||
table_str = "No data points to generate table with."
|
||||
|
||||
print(table_str)
|
||||
|
||||
def _get_plottable_data(
|
||||
self, feature_filter: str, module_fqn_filter: str
|
||||
) -> tuple[list, list[list], bool]:
|
||||
r"""
|
||||
Takes in the feature filters and module filters and outputs the x and y data for plotting
|
||||
|
||||
Args:
|
||||
feature_filter (str): Filters the features presented to only those that
|
||||
contain this filter substring
|
||||
module_fqn_filter (str): Only includes modules that contains this string
|
||||
|
||||
Returns a tuple of three elements
|
||||
The first is a list containing relevant x-axis data
|
||||
The second is a list containing the corresponding y-axis data
|
||||
If the data is per channel
|
||||
"""
|
||||
# get the table dict and the specific tables of interest
|
||||
table_dict = self.generate_filtered_tables(feature_filter, module_fqn_filter)
|
||||
tensor_headers, tensor_table = table_dict[self.TABLE_TENSOR_KEY]
|
||||
channel_headers, channel_table = table_dict[self.TABLE_CHANNEL_KEY]
|
||||
|
||||
# make sure it is only 1 feature that is being plotted
|
||||
# get the number of features in each of these
|
||||
tensor_info_features_count = (
|
||||
len(tensor_headers) - ModelReportVisualizer.NUM_NON_FEATURE_TENSOR_HEADERS
|
||||
)
|
||||
channel_info_features_count = (
|
||||
len(channel_headers) - ModelReportVisualizer.NUM_NON_FEATURE_CHANNEL_HEADERS
|
||||
)
|
||||
|
||||
# see if valid tensor or channel plot
|
||||
is_valid_per_tensor_plot: bool = tensor_info_features_count == 1
|
||||
is_valid_per_channel_plot: bool = channel_info_features_count == 1
|
||||
|
||||
# offset should either be one of tensor or channel table or neither
|
||||
feature_column_offset = ModelReportVisualizer.NUM_NON_FEATURE_TENSOR_HEADERS
|
||||
table = tensor_table
|
||||
|
||||
# if a per_channel plot, we have different offset and table
|
||||
if is_valid_per_channel_plot:
|
||||
feature_column_offset = (
|
||||
ModelReportVisualizer.NUM_NON_FEATURE_CHANNEL_HEADERS
|
||||
)
|
||||
table = channel_table
|
||||
|
||||
x_data: list = []
|
||||
y_data: list[list] = []
|
||||
# the feature will either be a tensor feature or channel feature
|
||||
if is_valid_per_tensor_plot:
|
||||
for table_row_num, row in enumerate(table):
|
||||
# get x_value to append
|
||||
x_val_to_append = table_row_num
|
||||
# the index of the feature will the 0 + num non feature columns
|
||||
tensor_feature_index = feature_column_offset
|
||||
row_value = row[tensor_feature_index]
|
||||
if type(row_value) is not str:
|
||||
x_data.append(x_val_to_append)
|
||||
y_data.append(row_value)
|
||||
elif is_valid_per_channel_plot:
|
||||
# gather the x_data and multiple y_data
|
||||
# calculate the number of channels
|
||||
num_channels: int = max(row[self.CHANNEL_NUM_INDEX] for row in table) + 1
|
||||
|
||||
# separate data list per channel
|
||||
y_data.extend([] for _ in range(num_channels))
|
||||
|
||||
for table_row_num, row in enumerate(table):
|
||||
# get x_value to append
|
||||
x_val_to_append = table_row_num
|
||||
current_channel = row[
|
||||
self.CHANNEL_NUM_INDEX
|
||||
] # initially chose current channel
|
||||
new_module_index: int = table_row_num // num_channels
|
||||
x_val_to_append = new_module_index
|
||||
|
||||
# the index of the feature will the 0 + num non feature columns
|
||||
tensor_feature_index = feature_column_offset
|
||||
row_value = row[tensor_feature_index]
|
||||
if type(row_value) is not str:
|
||||
# only append if new index we are appending
|
||||
if len(x_data) == 0 or x_data[-1] != x_val_to_append:
|
||||
x_data.append(x_val_to_append)
|
||||
|
||||
# append value for that channel
|
||||
y_data[current_channel].append(row_value)
|
||||
else:
|
||||
# more than one feature was chosen
|
||||
error_str = "Make sure to pick only a single feature with your filter to plot a graph."
|
||||
error_str += " We recommend calling get_all_unique_feature_names() to find unique feature names."
|
||||
error_str += " Pick one of those features to plot."
|
||||
raise ValueError(error_str)
|
||||
|
||||
# return x, y values, and if data is per-channel
|
||||
return (x_data, y_data, is_valid_per_channel_plot)
|
||||
|
||||
def generate_plot_visualization(
|
||||
self, feature_filter: str, module_fqn_filter: str = ""
|
||||
):
|
||||
r"""
|
||||
Takes in a feature and optional module_filter and plots of the desired data.
|
||||
|
||||
For per channel features, it averages the value across the channels and plots a point
|
||||
per module. The reason for this is that for models with hundreds of channels, it can
|
||||
be hard to differentiate one channel line from another, and so the point of generating
|
||||
a single average point per module is to give a sense of general trends that encourage
|
||||
further deep dives.
|
||||
|
||||
Note:
|
||||
Only features in the report that have tensor value data are plottable by this class
|
||||
When the tensor information is plotted, it will plot:
|
||||
idx as the x val, feature value as the y_val
|
||||
When the channel information is plotted, it will plot:
|
||||
the first idx of each module as the x val, feature value as the y_val [for each channel]
|
||||
The reason for this is that we want to be able to compare values across the
|
||||
channels for same layer, and it will be hard if values are staggered by idx
|
||||
This means each module is represented by only 1 x value
|
||||
Args:
|
||||
feature_filter (str): Filters the features presented to only those that
|
||||
contain this filter substring
|
||||
module_fqn_filter (str, optional): Only includes modules that contains this string
|
||||
Default = "", results in all the modules in the reports to be visible in the table
|
||||
|
||||
Example Use:
|
||||
>>> # xdoctest: +SKIP("undefined variables")
|
||||
>>> mod_report_visualizer.generate_plot_visualization(
|
||||
... feature_filter="per_channel_min", module_fqn_filter="block1"
|
||||
... )
|
||||
>>> # outputs line plot of per_channel_min information for all
|
||||
>>> # modules in block1 of model each channel gets it's own line,
|
||||
>>> # and it's plotted across the in-order modules on the x-axis
|
||||
"""
|
||||
# checks if we have matplotlib and let's user know to install it if don't
|
||||
if not got_matplotlib:
|
||||
print("make sure to install matplotlib and try again.")
|
||||
return None
|
||||
|
||||
# get the x and y data and if per channel
|
||||
x_data, y_data, data_per_channel = self._get_plottable_data(
|
||||
feature_filter, module_fqn_filter
|
||||
)
|
||||
|
||||
# plot based on whether data is per channel or not
|
||||
ax = plt.subplot()
|
||||
ax.set_ylabel(feature_filter)
|
||||
ax.set_title(feature_filter + " Plot")
|
||||
plt.xticks(x_data) # only show ticks for actual points
|
||||
|
||||
if data_per_channel:
|
||||
ax.set_xlabel("First idx of module")
|
||||
# set the legend as well
|
||||
# plot a single line that is average of the channel values
|
||||
num_modules = len(
|
||||
y_data[0]
|
||||
) # all y_data have same length, so get num modules
|
||||
num_channels = len(
|
||||
y_data
|
||||
) # we want num channels to be able to calculate average later
|
||||
|
||||
avg_vals = [
|
||||
sum(y_data[:][index]) / num_channels for index in range(num_modules)
|
||||
]
|
||||
|
||||
# plot the three things we measured
|
||||
ax.plot(
|
||||
x_data, avg_vals, label=f"Average Value Across {num_channels} Channels"
|
||||
)
|
||||
ax.legend(loc="upper right")
|
||||
else:
|
||||
ax.set_xlabel("idx")
|
||||
ax.plot(x_data, y_data)
|
||||
|
||||
# actually show the plot
|
||||
plt.show()
|
||||
|
||||
def generate_histogram_visualization(
|
||||
self, feature_filter: str, module_fqn_filter: str = "", num_bins: int = 10
|
||||
):
|
||||
r"""
|
||||
Takes in a feature and optional module_filter and plots the histogram of desired data.
|
||||
|
||||
Note:
|
||||
Only features in the report that have tensor value data can be viewed as a histogram
|
||||
If you want to plot a histogram from all the channel values of a specific feature for
|
||||
a specific model, make sure to specify both the model and the feature properly
|
||||
in the filters and you should be able to see a distribution of the channel data
|
||||
|
||||
Args:
|
||||
feature_filter (str, optional): Filters the features presented to only those that
|
||||
contain this filter substring
|
||||
Default = "", results in all the features being printed
|
||||
module_fqn_filter (str, optional): Only includes modules that contains this string
|
||||
Default = "", results in all the modules in the reports to be visible in the table
|
||||
num_bins (int, optional): The number of bins to create the histogram with
|
||||
Default = 10, the values will be split into 10 equal sized bins
|
||||
|
||||
Example Use:
|
||||
>>> # xdoctest: +SKIP
|
||||
>>> mod_report_visualizer.generategenerate_histogram_visualization_plot_visualization(
|
||||
... feature_filter="per_channel_min", module_fqn_filter="block1"
|
||||
... )
|
||||
# outputs histogram of per_channel_min information for all modules in block1 of model
|
||||
information is gathered across all channels for all modules in block 1 for the
|
||||
per_channel_min and is displayed in a histogram of equally sized bins
|
||||
"""
|
||||
# checks if we have matplotlib and let's user know to install it if don't
|
||||
if not got_matplotlib:
|
||||
print("make sure to install matplotlib and try again.")
|
||||
return None
|
||||
|
||||
# get the x and y data and if per channel
|
||||
_x_data, y_data, data_per_channel = self._get_plottable_data(
|
||||
feature_filter, module_fqn_filter
|
||||
)
|
||||
|
||||
# for histogram, we just care about plotting the y data
|
||||
# plot based on whether data is per channel or not
|
||||
ax = plt.subplot()
|
||||
ax.set_xlabel(feature_filter)
|
||||
ax.set_ylabel("Frequency")
|
||||
ax.set_title(feature_filter + " Histogram")
|
||||
|
||||
if data_per_channel:
|
||||
# set the legend as well
|
||||
# combine all the data
|
||||
all_data = []
|
||||
for channel_info in y_data:
|
||||
all_data.extend(channel_info)
|
||||
|
||||
_val, bins, _ = plt.hist(
|
||||
all_data,
|
||||
bins=num_bins,
|
||||
stacked=True,
|
||||
rwidth=0.8,
|
||||
)
|
||||
plt.xticks(bins)
|
||||
else:
|
||||
_val, bins, _ = plt.hist(
|
||||
y_data,
|
||||
bins=num_bins,
|
||||
stacked=False,
|
||||
rwidth=0.8,
|
||||
)
|
||||
plt.xticks(bins)
|
||||
|
||||
plt.show()
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,521 @@
|
||||
# mypy: allow-untyped-defs
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from torch.ao.quantization import QConfigMapping
|
||||
from torch.ao.quantization.backend_config import BackendConfig
|
||||
from torch.ao.quantization.quant_type import (
|
||||
_get_quant_type_to_str,
|
||||
_quant_type_from_str,
|
||||
QuantType,
|
||||
)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"ConvertCustomConfig",
|
||||
"FuseCustomConfig",
|
||||
"PrepareCustomConfig",
|
||||
"StandaloneModuleConfigEntry",
|
||||
]
|
||||
|
||||
|
||||
# TODO: replace all usages with these constants
|
||||
STANDALONE_MODULE_NAME_DICT_KEY = "standalone_module_name"
|
||||
STANDALONE_MODULE_CLASS_DICT_KEY = "standalone_module_class"
|
||||
FLOAT_TO_OBSERVED_DICT_KEY = "float_to_observed_custom_module_class"
|
||||
OBSERVED_TO_QUANTIZED_DICT_KEY = "observed_to_quantized_custom_module_class"
|
||||
NON_TRACEABLE_MODULE_NAME_DICT_KEY = "non_traceable_module_name"
|
||||
NON_TRACEABLE_MODULE_CLASS_DICT_KEY = "non_traceable_module_class"
|
||||
INPUT_QUANTIZED_INDEXES_DICT_KEY = "input_quantized_idxs"
|
||||
OUTPUT_QUANTIZED_INDEXES_DICT_KEY = "output_quantized_idxs"
|
||||
PRESERVED_ATTRIBUTES_DICT_KEY = "preserved_attributes"
|
||||
|
||||
|
||||
@dataclass
|
||||
class StandaloneModuleConfigEntry:
|
||||
# qconfig_mapping for the prepare function called in the submodule,
|
||||
# None means use qconfig from parent qconfig_mapping
|
||||
qconfig_mapping: QConfigMapping | None
|
||||
example_inputs: tuple[Any, ...]
|
||||
prepare_custom_config: PrepareCustomConfig | None
|
||||
backend_config: BackendConfig | None
|
||||
|
||||
|
||||
class PrepareCustomConfig:
|
||||
"""
|
||||
Custom configuration for :func:`~torch.ao.quantization.quantize_fx.prepare_fx` and
|
||||
:func:`~torch.ao.quantization.quantize_fx.prepare_qat_fx`.
|
||||
|
||||
Example usage::
|
||||
|
||||
prepare_custom_config = PrepareCustomConfig() \
|
||||
.set_standalone_module_name("module1", qconfig_mapping, example_inputs, \
|
||||
child_prepare_custom_config, backend_config) \
|
||||
.set_standalone_module_class(MyStandaloneModule, qconfig_mapping, example_inputs, \
|
||||
child_prepare_custom_config, backend_config) \
|
||||
.set_float_to_observed_mapping(FloatCustomModule, ObservedCustomModule) \
|
||||
.set_non_traceable_module_names(["module2", "module3"]) \
|
||||
.set_non_traceable_module_classes([NonTraceableModule1, NonTraceableModule2]) \
|
||||
.set_input_quantized_indexes([0]) \
|
||||
.set_output_quantized_indexes([0]) \
|
||||
.set_preserved_attributes(["attr1", "attr2"])
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.standalone_module_names: dict[str, StandaloneModuleConfigEntry] = {}
|
||||
self.standalone_module_classes: dict[type, StandaloneModuleConfigEntry] = {}
|
||||
self.float_to_observed_mapping: dict[QuantType, dict[type, type]] = {}
|
||||
self.non_traceable_module_names: list[str] = []
|
||||
self.non_traceable_module_classes: list[type] = []
|
||||
self.input_quantized_indexes: list[int] = []
|
||||
self.output_quantized_indexes: list[int] = []
|
||||
self.preserved_attributes: list[str] = []
|
||||
|
||||
def __repr__(self):
|
||||
dict_nonempty = {k: v for k, v in self.__dict__.items() if len(v) > 0}
|
||||
return f"PrepareCustomConfig({dict_nonempty})"
|
||||
|
||||
def set_standalone_module_name(
|
||||
self,
|
||||
module_name: str,
|
||||
qconfig_mapping: QConfigMapping | None,
|
||||
example_inputs: tuple[Any, ...],
|
||||
prepare_custom_config: PrepareCustomConfig | None,
|
||||
backend_config: BackendConfig | None,
|
||||
) -> PrepareCustomConfig:
|
||||
"""
|
||||
Set the configuration for running a standalone module identified by ``module_name``.
|
||||
|
||||
If ``qconfig_mapping`` is None, the parent ``qconfig_mapping`` will be used instead.
|
||||
If ``prepare_custom_config`` is None, an empty ``PrepareCustomConfig`` will be used.
|
||||
If ``backend_config`` is None, the parent ``backend_config`` will be used instead.
|
||||
"""
|
||||
self.standalone_module_names[module_name] = StandaloneModuleConfigEntry(
|
||||
qconfig_mapping, example_inputs, prepare_custom_config, backend_config
|
||||
)
|
||||
return self
|
||||
|
||||
def set_standalone_module_class(
|
||||
self,
|
||||
module_class: type,
|
||||
qconfig_mapping: QConfigMapping | None,
|
||||
example_inputs: tuple[Any, ...],
|
||||
prepare_custom_config: PrepareCustomConfig | None,
|
||||
backend_config: BackendConfig | None,
|
||||
) -> PrepareCustomConfig:
|
||||
"""
|
||||
Set the configuration for running a standalone module identified by ``module_class``.
|
||||
|
||||
If ``qconfig_mapping`` is None, the parent ``qconfig_mapping`` will be used instead.
|
||||
If ``prepare_custom_config`` is None, an empty ``PrepareCustomConfig`` will be used.
|
||||
If ``backend_config`` is None, the parent ``backend_config`` will be used instead.
|
||||
"""
|
||||
self.standalone_module_classes[module_class] = StandaloneModuleConfigEntry(
|
||||
qconfig_mapping, example_inputs, prepare_custom_config, backend_config
|
||||
)
|
||||
return self
|
||||
|
||||
def set_float_to_observed_mapping(
|
||||
self,
|
||||
float_class: type,
|
||||
observed_class: type,
|
||||
quant_type: QuantType = QuantType.STATIC,
|
||||
) -> PrepareCustomConfig:
|
||||
"""
|
||||
Set the mapping from a custom float module class to a custom observed module class.
|
||||
|
||||
The observed module class must have a ``from_float`` class method that converts the float module class
|
||||
to the observed module class. This is currently only supported for static quantization.
|
||||
"""
|
||||
if quant_type != QuantType.STATIC:
|
||||
raise ValueError(
|
||||
"set_float_to_observed_mapping is currently only supported for static quantization"
|
||||
)
|
||||
if quant_type not in self.float_to_observed_mapping:
|
||||
self.float_to_observed_mapping[quant_type] = {}
|
||||
self.float_to_observed_mapping[quant_type][float_class] = observed_class
|
||||
return self
|
||||
|
||||
def set_non_traceable_module_names(
|
||||
self, module_names: list[str]
|
||||
) -> PrepareCustomConfig:
|
||||
"""
|
||||
Set the modules that are not symbolically traceable, identified by name.
|
||||
"""
|
||||
self.non_traceable_module_names = module_names
|
||||
return self
|
||||
|
||||
def set_non_traceable_module_classes(
|
||||
self, module_classes: list[type]
|
||||
) -> PrepareCustomConfig:
|
||||
"""
|
||||
Set the modules that are not symbolically traceable, identified by class.
|
||||
"""
|
||||
self.non_traceable_module_classes = module_classes
|
||||
return self
|
||||
|
||||
def set_input_quantized_indexes(self, indexes: list[int]) -> PrepareCustomConfig:
|
||||
"""
|
||||
Set the indexes of the inputs of the graph that should be quantized.
|
||||
Inputs are otherwise assumed to be in fp32 by default instead.
|
||||
"""
|
||||
self.input_quantized_indexes = indexes
|
||||
return self
|
||||
|
||||
def set_output_quantized_indexes(self, indexes: list[int]) -> PrepareCustomConfig:
|
||||
"""
|
||||
Set the indexes of the outputs of the graph that should be quantized.
|
||||
Outputs are otherwise assumed to be in fp32 by default instead.
|
||||
"""
|
||||
self.output_quantized_indexes = indexes
|
||||
return self
|
||||
|
||||
def set_preserved_attributes(self, attributes: list[str]) -> PrepareCustomConfig:
|
||||
"""
|
||||
Set the names of the attributes that will persist in the graph module even if they are not used in
|
||||
the model's ``forward`` method.
|
||||
"""
|
||||
self.preserved_attributes = attributes
|
||||
return self
|
||||
|
||||
# TODO: remove this
|
||||
@classmethod
|
||||
def from_dict(
|
||||
cls, prepare_custom_config_dict: dict[str, Any]
|
||||
) -> PrepareCustomConfig:
|
||||
"""
|
||||
Create a ``PrepareCustomConfig`` from a dictionary with the following items:
|
||||
|
||||
"standalone_module_name": a list of (module_name, qconfig_mapping, example_inputs,
|
||||
child_prepare_custom_config, backend_config) tuples
|
||||
|
||||
"standalone_module_class" a list of (module_class, qconfig_mapping, example_inputs,
|
||||
child_prepare_custom_config, backend_config) tuples
|
||||
|
||||
"float_to_observed_custom_module_class": a nested dictionary mapping from quantization
|
||||
mode to an inner mapping from float module classes to observed module classes, e.g.
|
||||
{"static": {FloatCustomModule: ObservedCustomModule}}
|
||||
|
||||
"non_traceable_module_name": a list of modules names that are not symbolically traceable
|
||||
"non_traceable_module_class": a list of module classes that are not symbolically traceable
|
||||
"input_quantized_idxs": a list of indexes of graph inputs that should be quantized
|
||||
"output_quantized_idxs": a list of indexes of graph outputs that should be quantized
|
||||
"preserved_attributes": a list of attributes that persist even if they are not used in ``forward``
|
||||
|
||||
This function is primarily for backward compatibility and may be removed in the future.
|
||||
"""
|
||||
|
||||
def _get_qconfig_mapping(obj: Any, dict_key: str) -> QConfigMapping | None:
|
||||
"""
|
||||
Convert the given object into a QConfigMapping if possible, else throw an exception.
|
||||
"""
|
||||
if isinstance(obj, QConfigMapping) or obj is None:
|
||||
return obj
|
||||
if isinstance(obj, dict):
|
||||
return QConfigMapping.from_dict(obj)
|
||||
raise ValueError(
|
||||
f"Expected QConfigMapping in prepare_custom_config_dict[\"{dict_key}\"], got '{type(obj)}'"
|
||||
)
|
||||
|
||||
def _get_prepare_custom_config(
|
||||
obj: Any, dict_key: str
|
||||
) -> PrepareCustomConfig | None:
|
||||
"""
|
||||
Convert the given object into a PrepareCustomConfig if possible, else throw an exception.
|
||||
"""
|
||||
if isinstance(obj, PrepareCustomConfig) or obj is None:
|
||||
return obj
|
||||
if isinstance(obj, dict):
|
||||
return PrepareCustomConfig.from_dict(obj)
|
||||
raise ValueError(
|
||||
f"Expected PrepareCustomConfig in prepare_custom_config_dict[\"{dict_key}\"], got '{type(obj)}'"
|
||||
)
|
||||
|
||||
def _get_backend_config(obj: Any, dict_key: str) -> BackendConfig | None:
|
||||
"""
|
||||
Convert the given object into a BackendConfig if possible, else throw an exception.
|
||||
"""
|
||||
if isinstance(obj, BackendConfig) or obj is None:
|
||||
return obj
|
||||
if isinstance(obj, dict):
|
||||
return BackendConfig.from_dict(obj)
|
||||
raise ValueError(
|
||||
f"Expected BackendConfig in prepare_custom_config_dict[\"{dict_key}\"], got '{type(obj)}'"
|
||||
)
|
||||
|
||||
conf = cls()
|
||||
for (
|
||||
module_name,
|
||||
qconfig_dict,
|
||||
example_inputs,
|
||||
_prepare_custom_config_dict,
|
||||
backend_config_dict,
|
||||
) in prepare_custom_config_dict.get(STANDALONE_MODULE_NAME_DICT_KEY, []):
|
||||
qconfig_mapping = _get_qconfig_mapping(
|
||||
qconfig_dict, STANDALONE_MODULE_NAME_DICT_KEY
|
||||
)
|
||||
prepare_custom_config = _get_prepare_custom_config(
|
||||
_prepare_custom_config_dict, STANDALONE_MODULE_NAME_DICT_KEY
|
||||
)
|
||||
backend_config = _get_backend_config(
|
||||
backend_config_dict, STANDALONE_MODULE_NAME_DICT_KEY
|
||||
)
|
||||
conf.set_standalone_module_name(
|
||||
module_name,
|
||||
qconfig_mapping,
|
||||
example_inputs,
|
||||
prepare_custom_config,
|
||||
backend_config,
|
||||
)
|
||||
for (
|
||||
module_class,
|
||||
qconfig_dict,
|
||||
example_inputs,
|
||||
_prepare_custom_config_dict,
|
||||
backend_config_dict,
|
||||
) in prepare_custom_config_dict.get(STANDALONE_MODULE_CLASS_DICT_KEY, []):
|
||||
qconfig_mapping = _get_qconfig_mapping(
|
||||
qconfig_dict, STANDALONE_MODULE_CLASS_DICT_KEY
|
||||
)
|
||||
prepare_custom_config = _get_prepare_custom_config(
|
||||
_prepare_custom_config_dict, STANDALONE_MODULE_CLASS_DICT_KEY
|
||||
)
|
||||
backend_config = _get_backend_config(
|
||||
backend_config_dict, STANDALONE_MODULE_CLASS_DICT_KEY
|
||||
)
|
||||
conf.set_standalone_module_class(
|
||||
module_class,
|
||||
qconfig_mapping,
|
||||
example_inputs,
|
||||
prepare_custom_config,
|
||||
backend_config,
|
||||
)
|
||||
for quant_type_name, custom_module_mapping in prepare_custom_config_dict.get(
|
||||
FLOAT_TO_OBSERVED_DICT_KEY, {}
|
||||
).items():
|
||||
quant_type = _quant_type_from_str(quant_type_name)
|
||||
for float_class, observed_class in custom_module_mapping.items():
|
||||
conf.set_float_to_observed_mapping(
|
||||
float_class, observed_class, quant_type
|
||||
)
|
||||
conf.set_non_traceable_module_names(
|
||||
prepare_custom_config_dict.get(NON_TRACEABLE_MODULE_NAME_DICT_KEY, [])
|
||||
)
|
||||
conf.set_non_traceable_module_classes(
|
||||
prepare_custom_config_dict.get(NON_TRACEABLE_MODULE_CLASS_DICT_KEY, [])
|
||||
)
|
||||
conf.set_input_quantized_indexes(
|
||||
prepare_custom_config_dict.get(INPUT_QUANTIZED_INDEXES_DICT_KEY, [])
|
||||
)
|
||||
conf.set_output_quantized_indexes(
|
||||
prepare_custom_config_dict.get(OUTPUT_QUANTIZED_INDEXES_DICT_KEY, [])
|
||||
)
|
||||
conf.set_preserved_attributes(
|
||||
prepare_custom_config_dict.get(PRESERVED_ATTRIBUTES_DICT_KEY, [])
|
||||
)
|
||||
return conf
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
"""
|
||||
Convert this ``PrepareCustomConfig`` to a dictionary with the items described in
|
||||
:func:`~torch.ao.quantization.fx.custom_config.PrepareCustomConfig.from_dict`.
|
||||
"""
|
||||
|
||||
def _make_tuple(key: Any, e: StandaloneModuleConfigEntry):
|
||||
qconfig_dict = e.qconfig_mapping.to_dict() if e.qconfig_mapping else None
|
||||
prepare_custom_config_dict = (
|
||||
e.prepare_custom_config.to_dict() if e.prepare_custom_config else None
|
||||
)
|
||||
return (
|
||||
key,
|
||||
qconfig_dict,
|
||||
e.example_inputs,
|
||||
prepare_custom_config_dict,
|
||||
e.backend_config,
|
||||
)
|
||||
|
||||
d: dict[str, Any] = {}
|
||||
for module_name, sm_config_entry in self.standalone_module_names.items():
|
||||
if STANDALONE_MODULE_NAME_DICT_KEY not in d:
|
||||
d[STANDALONE_MODULE_NAME_DICT_KEY] = []
|
||||
d[STANDALONE_MODULE_NAME_DICT_KEY].append(
|
||||
_make_tuple(module_name, sm_config_entry)
|
||||
)
|
||||
for module_class, sm_config_entry in self.standalone_module_classes.items():
|
||||
if STANDALONE_MODULE_CLASS_DICT_KEY not in d:
|
||||
d[STANDALONE_MODULE_CLASS_DICT_KEY] = []
|
||||
d[STANDALONE_MODULE_CLASS_DICT_KEY].append(
|
||||
_make_tuple(module_class, sm_config_entry)
|
||||
)
|
||||
for (
|
||||
quant_type,
|
||||
float_to_observed_mapping,
|
||||
) in self.float_to_observed_mapping.items():
|
||||
if FLOAT_TO_OBSERVED_DICT_KEY not in d:
|
||||
d[FLOAT_TO_OBSERVED_DICT_KEY] = {}
|
||||
d[FLOAT_TO_OBSERVED_DICT_KEY][_get_quant_type_to_str(quant_type)] = (
|
||||
float_to_observed_mapping
|
||||
)
|
||||
if len(self.non_traceable_module_names) > 0:
|
||||
d[NON_TRACEABLE_MODULE_NAME_DICT_KEY] = self.non_traceable_module_names
|
||||
if len(self.non_traceable_module_classes) > 0:
|
||||
d[NON_TRACEABLE_MODULE_CLASS_DICT_KEY] = self.non_traceable_module_classes
|
||||
if len(self.input_quantized_indexes) > 0:
|
||||
d[INPUT_QUANTIZED_INDEXES_DICT_KEY] = self.input_quantized_indexes
|
||||
if len(self.output_quantized_indexes) > 0:
|
||||
d[OUTPUT_QUANTIZED_INDEXES_DICT_KEY] = self.output_quantized_indexes
|
||||
if len(self.preserved_attributes) > 0:
|
||||
d[PRESERVED_ATTRIBUTES_DICT_KEY] = self.preserved_attributes
|
||||
return d
|
||||
|
||||
|
||||
class ConvertCustomConfig:
|
||||
"""
|
||||
Custom configuration for :func:`~torch.ao.quantization.quantize_fx.convert_fx`.
|
||||
|
||||
Example usage::
|
||||
|
||||
convert_custom_config = ConvertCustomConfig() \
|
||||
.set_observed_to_quantized_mapping(ObservedCustomModule, QuantizedCustomModule) \
|
||||
.set_preserved_attributes(["attr1", "attr2"])
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.observed_to_quantized_mapping: dict[QuantType, dict[type, type]] = {}
|
||||
self.preserved_attributes: list[str] = []
|
||||
|
||||
def __repr__(self):
|
||||
dict_nonempty = {k: v for k, v in self.__dict__.items() if len(v) > 0}
|
||||
return f"ConvertCustomConfig({dict_nonempty})"
|
||||
|
||||
def set_observed_to_quantized_mapping(
|
||||
self,
|
||||
observed_class: type,
|
||||
quantized_class: type,
|
||||
quant_type: QuantType = QuantType.STATIC,
|
||||
) -> ConvertCustomConfig:
|
||||
"""
|
||||
Set the mapping from a custom observed module class to a custom quantized module class.
|
||||
|
||||
The quantized module class must have a ``from_observed`` class method that converts the observed module class
|
||||
to the quantized module class.
|
||||
"""
|
||||
if quant_type not in self.observed_to_quantized_mapping:
|
||||
self.observed_to_quantized_mapping[quant_type] = {}
|
||||
self.observed_to_quantized_mapping[quant_type][observed_class] = quantized_class
|
||||
return self
|
||||
|
||||
def set_preserved_attributes(self, attributes: list[str]) -> ConvertCustomConfig:
|
||||
"""
|
||||
Set the names of the attributes that will persist in the graph module even if they are not used in
|
||||
the model's ``forward`` method.
|
||||
"""
|
||||
self.preserved_attributes = attributes
|
||||
return self
|
||||
|
||||
# TODO: remove this
|
||||
@classmethod
|
||||
def from_dict(
|
||||
cls, convert_custom_config_dict: dict[str, Any]
|
||||
) -> ConvertCustomConfig:
|
||||
"""
|
||||
Create a ``ConvertCustomConfig`` from a dictionary with the following items:
|
||||
|
||||
"observed_to_quantized_custom_module_class": a nested dictionary mapping from quantization
|
||||
mode to an inner mapping from observed module classes to quantized module classes, e.g.::
|
||||
{
|
||||
"static": {FloatCustomModule: ObservedCustomModule},
|
||||
"dynamic": {FloatCustomModule: ObservedCustomModule},
|
||||
"weight_only": {FloatCustomModule: ObservedCustomModule}
|
||||
}
|
||||
"preserved_attributes": a list of attributes that persist even if they are not used in ``forward``
|
||||
|
||||
This function is primarily for backward compatibility and may be removed in the future.
|
||||
"""
|
||||
conf = cls()
|
||||
for quant_type_name, custom_module_mapping in convert_custom_config_dict.get(
|
||||
OBSERVED_TO_QUANTIZED_DICT_KEY, {}
|
||||
).items():
|
||||
quant_type = _quant_type_from_str(quant_type_name)
|
||||
for observed_class, quantized_class in custom_module_mapping.items():
|
||||
conf.set_observed_to_quantized_mapping(
|
||||
observed_class, quantized_class, quant_type
|
||||
)
|
||||
conf.set_preserved_attributes(
|
||||
convert_custom_config_dict.get(PRESERVED_ATTRIBUTES_DICT_KEY, [])
|
||||
)
|
||||
return conf
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
"""
|
||||
Convert this ``ConvertCustomConfig`` to a dictionary with the items described in
|
||||
:func:`~torch.ao.quantization.fx.custom_config.ConvertCustomConfig.from_dict`.
|
||||
"""
|
||||
d: dict[str, Any] = {}
|
||||
for (
|
||||
quant_type,
|
||||
observed_to_quantized_mapping,
|
||||
) in self.observed_to_quantized_mapping.items():
|
||||
if OBSERVED_TO_QUANTIZED_DICT_KEY not in d:
|
||||
d[OBSERVED_TO_QUANTIZED_DICT_KEY] = {}
|
||||
d[OBSERVED_TO_QUANTIZED_DICT_KEY][_get_quant_type_to_str(quant_type)] = (
|
||||
observed_to_quantized_mapping
|
||||
)
|
||||
if len(self.preserved_attributes) > 0:
|
||||
d[PRESERVED_ATTRIBUTES_DICT_KEY] = self.preserved_attributes
|
||||
return d
|
||||
|
||||
|
||||
class FuseCustomConfig:
|
||||
"""
|
||||
Custom configuration for :func:`~torch.ao.quantization.quantize_fx.fuse_fx`.
|
||||
|
||||
Example usage::
|
||||
|
||||
fuse_custom_config = FuseCustomConfig().set_preserved_attributes(
|
||||
["attr1", "attr2"]
|
||||
)
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.preserved_attributes: list[str] = []
|
||||
|
||||
def __repr__(self):
|
||||
dict_nonempty = {k: v for k, v in self.__dict__.items() if len(v) > 0}
|
||||
return f"FuseCustomConfig({dict_nonempty})"
|
||||
|
||||
def set_preserved_attributes(self, attributes: list[str]) -> FuseCustomConfig:
|
||||
"""
|
||||
Set the names of the attributes that will persist in the graph module even if they are not used in
|
||||
the model's ``forward`` method.
|
||||
"""
|
||||
self.preserved_attributes = attributes
|
||||
return self
|
||||
|
||||
# TODO: remove this
|
||||
@classmethod
|
||||
def from_dict(cls, fuse_custom_config_dict: dict[str, Any]) -> FuseCustomConfig:
|
||||
"""
|
||||
Create a ``ConvertCustomConfig`` from a dictionary with the following items:
|
||||
|
||||
"preserved_attributes": a list of attributes that persist even if they are not used in ``forward``
|
||||
|
||||
This function is primarily for backward compatibility and may be removed in the future.
|
||||
"""
|
||||
conf = cls()
|
||||
conf.set_preserved_attributes(
|
||||
fuse_custom_config_dict.get(PRESERVED_ATTRIBUTES_DICT_KEY, [])
|
||||
)
|
||||
return conf
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
"""
|
||||
Convert this ``FuseCustomConfig`` to a dictionary with the items described in
|
||||
:func:`~torch.ao.quantization.fx.custom_config.ConvertCustomConfig.from_dict`.
|
||||
"""
|
||||
d: dict[str, Any] = {}
|
||||
if len(self.preserved_attributes) > 0:
|
||||
d[PRESERVED_ATTRIBUTES_DICT_KEY] = self.preserved_attributes
|
||||
return d
|
||||
@@ -0,0 +1,195 @@
|
||||
# mypy: allow-untyped-defs
|
||||
import warnings
|
||||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
|
||||
from torch.ao.quantization.backend_config import (
|
||||
BackendConfig,
|
||||
get_native_backend_config,
|
||||
)
|
||||
from torch.ao.quantization.backend_config.utils import (
|
||||
get_fuser_method_mapping,
|
||||
get_fusion_pattern_to_extra_inputs_getter,
|
||||
get_fusion_pattern_to_root_node_getter,
|
||||
)
|
||||
from torch.ao.quantization.utils import NodePattern, Pattern
|
||||
from torch.fx import GraphModule, map_arg, Node
|
||||
from torch.fx.graph import Graph
|
||||
|
||||
from .custom_config import FuseCustomConfig
|
||||
from .fuse_handler import _get_fusion_pattern_to_fuse_handler_cls, FuseHandler
|
||||
from .match_utils import _is_match, MatchAllNode
|
||||
from .pattern_utils import _sorted_patterns_dict
|
||||
|
||||
|
||||
__all__ = [
|
||||
"fuse",
|
||||
# TODO: We should make this private in the future
|
||||
# This is currently needed for test_public_bindings for some reason
|
||||
"FuseHandler",
|
||||
]
|
||||
|
||||
|
||||
def fuse(
|
||||
model: GraphModule,
|
||||
is_qat: bool,
|
||||
fuse_custom_config: FuseCustomConfig | dict[str, Any] | None = None,
|
||||
backend_config: BackendConfig | dict[str, Any] | None = None,
|
||||
) -> GraphModule:
|
||||
if fuse_custom_config is None:
|
||||
fuse_custom_config = FuseCustomConfig()
|
||||
|
||||
if isinstance(fuse_custom_config, dict):
|
||||
warnings.warn(
|
||||
"Passing a fuse_custom_config_dict to fuse is deprecated and will not be supported "
|
||||
"in a future version. Please pass in a FuseCustomConfig instead.",
|
||||
FutureWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
fuse_custom_config = FuseCustomConfig.from_dict(fuse_custom_config)
|
||||
|
||||
if isinstance(backend_config, dict):
|
||||
warnings.warn(
|
||||
"Passing a backend_config_dict to prepare is deprecated and will not be supported "
|
||||
"in a future version. Please pass in a BackendConfig instead.",
|
||||
FutureWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
backend_config = BackendConfig.from_dict(backend_config)
|
||||
|
||||
named_modules = dict(model.named_modules())
|
||||
|
||||
if backend_config is None:
|
||||
backend_config = get_native_backend_config()
|
||||
|
||||
fusion_pattern_to_fuse_handler_cls = _sorted_patterns_dict(
|
||||
_get_fusion_pattern_to_fuse_handler_cls(backend_config)
|
||||
)
|
||||
fuser_method_mapping = get_fuser_method_mapping(backend_config)
|
||||
fusion_pattern_to_root_node_getter = get_fusion_pattern_to_root_node_getter(
|
||||
backend_config
|
||||
)
|
||||
fusion_pattern_to_extra_inputs_getter = get_fusion_pattern_to_extra_inputs_getter(
|
||||
backend_config
|
||||
)
|
||||
|
||||
# find fusion
|
||||
fusion_pairs = _find_matches(model, model.graph, fusion_pattern_to_fuse_handler_cls)
|
||||
# TODO: change this to inplace changes to graph, since we no longer construct
|
||||
# new GraphModule anymore
|
||||
fused_graph = Graph()
|
||||
env: dict[Any, Any] = {}
|
||||
|
||||
def load_arg(a):
|
||||
return map_arg(a, lambda node: env[node.name])
|
||||
|
||||
def default_root_node_getter(node_pattern):
|
||||
while not isinstance(node_pattern[-1], Node):
|
||||
node_pattern = node_pattern[-1]
|
||||
return node_pattern[-1]
|
||||
|
||||
for node in model.graph.nodes:
|
||||
(
|
||||
maybe_last_node,
|
||||
pattern,
|
||||
matched_node_pattern,
|
||||
obj,
|
||||
node_to_subpattern,
|
||||
) = fusion_pairs.get(node.name, (None, None, None, None, None))
|
||||
# get the corresponding subpattern for the current node
|
||||
if node_to_subpattern is not None:
|
||||
node_subpattern = node_to_subpattern.get(node, None)
|
||||
else:
|
||||
node_subpattern = None
|
||||
if maybe_last_node is node:
|
||||
if obj is None:
|
||||
raise AssertionError(
|
||||
"fuse handler object must not be None for matched root node"
|
||||
)
|
||||
root_node_getter = fusion_pattern_to_root_node_getter.get(
|
||||
pattern, default_root_node_getter
|
||||
)
|
||||
root_node = root_node_getter(matched_node_pattern) # type: ignore[index]
|
||||
extra_inputs_getter = fusion_pattern_to_extra_inputs_getter.get(
|
||||
pattern, None
|
||||
)
|
||||
extra_inputs = []
|
||||
if extra_inputs_getter is not None:
|
||||
extra_inputs = extra_inputs_getter(matched_node_pattern)
|
||||
# TODO: add validation that root_node is a module and has the same type
|
||||
# as the root_module in the configuration
|
||||
env[node.name] = obj.fuse(
|
||||
load_arg,
|
||||
named_modules,
|
||||
fused_graph,
|
||||
root_node,
|
||||
extra_inputs,
|
||||
matched_node_pattern, # type: ignore[arg-type]
|
||||
fuse_custom_config,
|
||||
fuser_method_mapping,
|
||||
is_qat,
|
||||
)
|
||||
elif maybe_last_node is None or node_subpattern is MatchAllNode:
|
||||
env[node.name] = fused_graph.node_copy(node, load_arg)
|
||||
# node matched in patterns and is not root is removed here
|
||||
|
||||
model = GraphModule(model, fused_graph)
|
||||
return model
|
||||
|
||||
|
||||
def _find_matches(
|
||||
root: GraphModule,
|
||||
graph: Graph,
|
||||
pattern_to_fuse_handler_cls: dict[Pattern, Callable],
|
||||
) -> dict[str, tuple[Node, Pattern, NodePattern, FuseHandler, dict[Node, Any]]]:
|
||||
modules = dict(root.named_modules())
|
||||
# node name -> (root_node, match_value)
|
||||
match_map: dict[
|
||||
str, tuple[Node, Pattern, NodePattern, FuseHandler, dict[Node, Any]]
|
||||
] = {}
|
||||
# a map from node to the matched subpattern
|
||||
node_to_subpattern: dict[Node, Any] = {}
|
||||
|
||||
# TODO: dedup with quantization matching function in match_utils.py
|
||||
def apply_match(pattern, node, match, matched_node_pattern, node_to_subpattern):
|
||||
if isinstance(pattern, tuple):
|
||||
s, *args = pattern
|
||||
current_node_pattern: list[Node] = []
|
||||
apply_match(s, node, match, current_node_pattern, node_to_subpattern)
|
||||
for subpattern, arg in zip(args, node.args):
|
||||
apply_match(
|
||||
subpattern, arg, match, current_node_pattern, node_to_subpattern
|
||||
)
|
||||
matched_node_pattern.append(tuple(current_node_pattern))
|
||||
else:
|
||||
# the first pattern matches will take precedence
|
||||
if node.name not in match_map:
|
||||
matched_node_pattern.append(node)
|
||||
# MatchAllNode here is actually MatchAllInputNode which should not
|
||||
# be added to match_map
|
||||
if pattern is not MatchAllNode:
|
||||
node_to_subpattern[node] = pattern
|
||||
root_node, pattern, handler = match
|
||||
match_map[node.name] = (
|
||||
root_node,
|
||||
pattern,
|
||||
matched_node_pattern,
|
||||
handler,
|
||||
node_to_subpattern,
|
||||
)
|
||||
|
||||
for node in reversed(graph.nodes):
|
||||
if node.name not in match_map:
|
||||
for pattern, fuse_handler_cls in pattern_to_fuse_handler_cls.items():
|
||||
matched_node_pattern: list[Node] = []
|
||||
if _is_match(modules, node, pattern):
|
||||
apply_match(
|
||||
pattern,
|
||||
node,
|
||||
(node, pattern, fuse_handler_cls(node)),
|
||||
matched_node_pattern,
|
||||
node_to_subpattern,
|
||||
)
|
||||
break
|
||||
|
||||
return match_map
|
||||
@@ -0,0 +1,129 @@
|
||||
# mypy: allow-untyped-defs
|
||||
from abc import ABC, abstractmethod
|
||||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
|
||||
import torch
|
||||
from torch.ao.quantization.backend_config import BackendConfig
|
||||
from torch.ao.quantization.fuser_method_mappings import get_fuser_method_new
|
||||
from torch.ao.quantization.utils import _parent_name, NodePattern, Pattern
|
||||
from torch.fx.graph import Graph, Node
|
||||
from torch.nn.utils.parametrize import type_before_parametrizations
|
||||
|
||||
from .custom_config import FuseCustomConfig
|
||||
from .match_utils import MatchAllNode
|
||||
|
||||
|
||||
__all__ = [
|
||||
"DefaultFuseHandler",
|
||||
"FuseHandler",
|
||||
]
|
||||
|
||||
|
||||
# ----------------------------
|
||||
# Fusion Pattern Registrations
|
||||
# ----------------------------
|
||||
|
||||
|
||||
# Base Pattern Handler
|
||||
class FuseHandler(ABC):
|
||||
"""Base handler class for the fusion patterns"""
|
||||
|
||||
@abstractmethod
|
||||
def __init__(self, node: Node):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def fuse(
|
||||
self,
|
||||
load_arg: Callable,
|
||||
named_modules: dict[str, torch.nn.Module],
|
||||
fused_graph: Graph,
|
||||
root_node: Node,
|
||||
extra_inputs: list[Any],
|
||||
matched_node_pattern: NodePattern,
|
||||
fuse_custom_config: FuseCustomConfig,
|
||||
fuser_method_mapping: dict[Pattern, torch.nn.Sequential | Callable],
|
||||
is_qat: bool,
|
||||
) -> Node:
|
||||
pass
|
||||
|
||||
|
||||
class DefaultFuseHandler(FuseHandler):
|
||||
def __init__(self, node: Node): # pylint: disable=useless-parent-delegation
|
||||
super().__init__(node) # type:ignore[safe-super]
|
||||
|
||||
def fuse(
|
||||
self,
|
||||
load_arg: Callable,
|
||||
named_modules: dict[str, torch.nn.Module],
|
||||
fused_graph: Graph,
|
||||
root_node: Node,
|
||||
extra_inputs: list[Any],
|
||||
matched_node_pattern: NodePattern,
|
||||
fuse_custom_config: FuseCustomConfig,
|
||||
fuser_method_mapping: dict[Pattern, torch.nn.Sequential | Callable],
|
||||
is_qat: bool,
|
||||
) -> Node:
|
||||
if root_node.op != "call_module":
|
||||
raise AssertionError("Expecting module node to be a call_module Node")
|
||||
root_module = named_modules[str(root_node.target)]
|
||||
|
||||
def get_modules(pattern):
|
||||
"""Given a node pattern, extract the corresponding modules
|
||||
e.g. input: (relu_node, (bn_node, conv_node))
|
||||
output: (relu_module, (bn_module, conv_module))
|
||||
"""
|
||||
if isinstance(pattern, (tuple, list)):
|
||||
n, *args = pattern
|
||||
modules: list[torch.nn.Module] = []
|
||||
modules.append(get_modules(n))
|
||||
modules.extend(get_modules(a) for a in args)
|
||||
return tuple(modules)
|
||||
else:
|
||||
n = pattern
|
||||
if n.op == "call_module":
|
||||
return named_modules[n.target]
|
||||
elif n.op == "call_function" and n.target is torch.nn.functional.relu:
|
||||
relu = torch.nn.ReLU()
|
||||
relu.training = root_module.training
|
||||
return relu
|
||||
elif n.op == "call_function" or n.op == "call_method":
|
||||
return n.target
|
||||
else:
|
||||
return MatchAllNode
|
||||
|
||||
# since relu can be used multiple times, we'll need to create a relu module for each match
|
||||
matched_modules = get_modules(matched_node_pattern)
|
||||
|
||||
def get_matched_types(m):
|
||||
if isinstance(m, tuple):
|
||||
return tuple(map(get_matched_types, m))
|
||||
if isinstance(m, torch.nn.Module):
|
||||
return type_before_parametrizations(m)
|
||||
return m
|
||||
|
||||
matched_module_types = get_matched_types(matched_modules)
|
||||
module_parent_name, module_name = _parent_name(root_node.target)
|
||||
fuser_method = get_fuser_method_new(matched_module_types, fuser_method_mapping)
|
||||
# TODO: change the signature for fuser_method to take matched module patterns
|
||||
# as input
|
||||
fused_module = fuser_method(is_qat, *matched_modules)
|
||||
setattr(named_modules[module_parent_name], module_name, fused_module)
|
||||
extra_args = [load_arg(input) for input in extra_inputs]
|
||||
node = fused_graph.node_copy(root_node, load_arg)
|
||||
args = list(node.args)
|
||||
args.extend(extra_args)
|
||||
node.args = tuple(args)
|
||||
return node
|
||||
|
||||
|
||||
def _get_fusion_pattern_to_fuse_handler_cls(
|
||||
backend_config: BackendConfig,
|
||||
) -> dict[Pattern, Callable]:
|
||||
fusion_pattern_to_fuse_handlers: dict[Pattern, Callable] = {}
|
||||
for pattern, config in backend_config._pattern_complex_format_to_config.items():
|
||||
if config.fuser_method is not None:
|
||||
# TODO: is this logic right?
|
||||
fusion_pattern_to_fuse_handlers[pattern] = DefaultFuseHandler
|
||||
return fusion_pattern_to_fuse_handlers
|
||||
@@ -0,0 +1,205 @@
|
||||
# mypy: allow-untyped-defs
|
||||
import copy
|
||||
from typing import Any
|
||||
|
||||
import torch
|
||||
from torch.fx import GraphModule
|
||||
from torch.fx.graph import Graph
|
||||
|
||||
|
||||
__all__ = [
|
||||
"FusedGraphModule",
|
||||
"ObservedGraphModule",
|
||||
"ObservedStandaloneGraphModule",
|
||||
"QuantizedGraphModule",
|
||||
]
|
||||
|
||||
|
||||
class FusedGraphModule(GraphModule):
|
||||
def __init__(
|
||||
self,
|
||||
root: torch.nn.Module | dict[str, Any],
|
||||
graph: Graph,
|
||||
preserved_attr_names: set[str],
|
||||
):
|
||||
self.preserved_attr_names = preserved_attr_names
|
||||
preserved_attrs = {
|
||||
attr: getattr(root, attr)
|
||||
for attr in self.preserved_attr_names
|
||||
if hasattr(root, attr)
|
||||
}
|
||||
super().__init__(root, graph)
|
||||
for attr in preserved_attrs:
|
||||
setattr(self, attr, preserved_attrs[attr])
|
||||
|
||||
# GraphModule does not copy attributes which are not in the __dict__
|
||||
# of vanilla nn.Module. So, we override __deepcopy__ in order
|
||||
# to copy the quantization specific attributes correctly.
|
||||
def __deepcopy__(self, memo):
|
||||
fake_mod = torch.nn.Module()
|
||||
fake_mod.__dict__ = copy.deepcopy(self.__dict__)
|
||||
return FusedGraphModule(
|
||||
fake_mod,
|
||||
copy.deepcopy(self.graph),
|
||||
copy.deepcopy(self.preserved_attr_names),
|
||||
)
|
||||
|
||||
|
||||
class ObservedGraphModule(GraphModule):
|
||||
def __init__(
|
||||
self,
|
||||
root: torch.nn.Module | dict[str, Any],
|
||||
graph: Graph,
|
||||
preserved_attr_names: set[str],
|
||||
):
|
||||
self.preserved_attr_names = {
|
||||
"_activation_post_process_map",
|
||||
"_activation_post_process_indexes",
|
||||
"_patterns",
|
||||
"_node_name_to_qconfig",
|
||||
"_prepare_custom_config",
|
||||
"_equalization_node_name_to_qconfig",
|
||||
"_node_name_to_scope",
|
||||
"_qconfig_mapping",
|
||||
"_is_qat",
|
||||
"_observed_node_names",
|
||||
}.union(preserved_attr_names)
|
||||
preserved_attrs = {
|
||||
attr: getattr(root, attr)
|
||||
for attr in self.preserved_attr_names
|
||||
if hasattr(root, attr)
|
||||
}
|
||||
super().__init__(root, graph)
|
||||
for attr in preserved_attrs:
|
||||
setattr(self, attr, preserved_attrs[attr])
|
||||
|
||||
# GraphModule does not copy attributes which are not in the __dict__
|
||||
# of vanilla nn.Module. So, we override __deepcopy__ in order
|
||||
# to copy the quantization specific attributes correctly.
|
||||
def __deepcopy__(self, memo):
|
||||
fake_mod = torch.nn.Module()
|
||||
fake_mod.__dict__ = copy.deepcopy(self.__dict__)
|
||||
return ObservedGraphModule(
|
||||
fake_mod,
|
||||
copy.deepcopy(self.graph),
|
||||
copy.deepcopy(self.preserved_attr_names),
|
||||
)
|
||||
|
||||
|
||||
def _is_observed_module(module: Any) -> bool:
|
||||
return hasattr(module, "meta") and "_observed_graph_module_attrs" in module.meta
|
||||
|
||||
|
||||
def _get_observed_graph_module_attr(
|
||||
model: torch.nn.Module | GraphModule, attr_name: str
|
||||
) -> Any:
|
||||
if hasattr(model, "meta") and "_observed_graph_module_attrs" in model.meta: # type: ignore[operator, index]
|
||||
return getattr(model.meta["_observed_graph_module_attrs"], attr_name) # type: ignore[index]
|
||||
return None
|
||||
|
||||
|
||||
class ObservedStandaloneGraphModule(ObservedGraphModule):
|
||||
def __init__(
|
||||
self,
|
||||
root: torch.nn.Module | dict[str, Any],
|
||||
graph: Graph,
|
||||
preserved_attr_names: set[str],
|
||||
):
|
||||
preserved_attr_names = preserved_attr_names.union(
|
||||
{
|
||||
"_standalone_module_input_quantized_idxs",
|
||||
"_standalone_module_output_quantized_idxs",
|
||||
}
|
||||
)
|
||||
super().__init__(root, graph, preserved_attr_names)
|
||||
|
||||
def __deepcopy__(self, memo):
|
||||
fake_mod = torch.nn.Module()
|
||||
fake_mod.__dict__ = copy.deepcopy(self.__dict__)
|
||||
return ObservedStandaloneGraphModule(
|
||||
fake_mod,
|
||||
copy.deepcopy(self.graph),
|
||||
copy.deepcopy(self.preserved_attr_names),
|
||||
)
|
||||
|
||||
|
||||
def _is_observed_standalone_module(module: Any) -> bool:
|
||||
return (
|
||||
_is_observed_module(module)
|
||||
and module.meta["_observed_graph_module_attrs"].is_observed_standalone_module
|
||||
)
|
||||
|
||||
|
||||
def _save_packed_weight(self, destination, prefix, keep_vars):
|
||||
for attr_name in dir(self):
|
||||
if "_packed_weight" in attr_name and isinstance(
|
||||
getattr(self, attr_name), torch._C.ScriptObject
|
||||
): # type: ignore[attr-defined]
|
||||
packed_weight = getattr(self, attr_name)
|
||||
destination[prefix + attr_name] = packed_weight
|
||||
|
||||
|
||||
class QuantizedGraphModule(GraphModule):
|
||||
"""This class is created to make sure PackedParams
|
||||
(e.g. LinearPackedParams, Conv2dPackedParams) to appear in state_dict
|
||||
so that we can serialize and deserialize quantized graph module with
|
||||
torch.save(m.state_dict()) and m.load_state_dict(state_dict)
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
root: torch.nn.Module | dict[str, Any],
|
||||
graph: Graph,
|
||||
preserved_attr_names: set[str],
|
||||
):
|
||||
self.preserved_attr_names = preserved_attr_names
|
||||
preserved_attrs = {
|
||||
attr: getattr(root, attr)
|
||||
for attr in self.preserved_attr_names
|
||||
if hasattr(root, attr)
|
||||
}
|
||||
super().__init__(root, graph)
|
||||
for attr in preserved_attrs:
|
||||
setattr(self, attr, preserved_attrs[attr])
|
||||
self._register_state_dict_hook(_save_packed_weight)
|
||||
|
||||
def _load_from_state_dict(
|
||||
self,
|
||||
state_dict,
|
||||
prefix,
|
||||
local_metadata,
|
||||
strict,
|
||||
missing_keys,
|
||||
unexpected_keys,
|
||||
error_msgs,
|
||||
):
|
||||
attrs_to_pop = []
|
||||
for attr_name in state_dict:
|
||||
if attr_name.startswith("_packed_weight") and isinstance(
|
||||
state_dict[attr_name], torch._C.ScriptObject
|
||||
): # type: ignore[attr-defined] # noqa: B950
|
||||
setattr(self, attr_name, state_dict[attr_name])
|
||||
attrs_to_pop.append(attr_name)
|
||||
|
||||
# pop the packed param attributesn
|
||||
for attr_name in attrs_to_pop:
|
||||
state_dict.pop(attr_name)
|
||||
|
||||
super()._load_from_state_dict(
|
||||
state_dict,
|
||||
prefix,
|
||||
local_metadata,
|
||||
strict,
|
||||
missing_keys,
|
||||
unexpected_keys,
|
||||
error_msgs,
|
||||
)
|
||||
|
||||
def __deepcopy__(self, memo):
|
||||
fake_mod = torch.nn.Module()
|
||||
fake_mod.__dict__ = copy.deepcopy(self.__dict__)
|
||||
return QuantizedGraphModule(
|
||||
fake_mod,
|
||||
copy.deepcopy(self.graph),
|
||||
copy.deepcopy(self.preserved_attr_names),
|
||||
)
|
||||
@@ -0,0 +1,21 @@
|
||||
from torch.ao.quantization.qconfig import QConfigAny
|
||||
from torch.fx import GraphModule
|
||||
|
||||
from ._lower_to_native_backend import _lower_to_native_backend
|
||||
|
||||
|
||||
__all__ = ["lower_to_fbgemm"]
|
||||
|
||||
|
||||
def lower_to_fbgemm(
|
||||
model: GraphModule,
|
||||
qconfig_map: dict[str, QConfigAny],
|
||||
node_name_to_scope: dict[str, tuple[str, type]],
|
||||
keep_original_weights: bool = False,
|
||||
) -> GraphModule:
|
||||
"""Lower a quantized reference model (with reference quantized operator patterns)
|
||||
to fbgemm
|
||||
"""
|
||||
return _lower_to_native_backend(
|
||||
model, qconfig_map, node_name_to_scope, keep_original_weights
|
||||
)
|
||||
@@ -0,0 +1,18 @@
|
||||
from torch.ao.quantization.qconfig import QConfigAny
|
||||
from torch.fx import GraphModule
|
||||
|
||||
from ._lower_to_native_backend import _lower_to_native_backend
|
||||
|
||||
|
||||
__all__ = ["lower_to_qnnpack"]
|
||||
|
||||
|
||||
def lower_to_qnnpack(
|
||||
model: GraphModule,
|
||||
qconfig_map: dict[str, QConfigAny],
|
||||
node_name_to_scope: dict[str, tuple[str, type]],
|
||||
) -> GraphModule:
|
||||
"""Lower a quantized reference model (with reference quantized operator patterns)
|
||||
to qnnpack
|
||||
"""
|
||||
return _lower_to_native_backend(model, qconfig_map, node_name_to_scope)
|
||||
@@ -0,0 +1,228 @@
|
||||
import copy
|
||||
import operator
|
||||
from typing import Any, TYPE_CHECKING
|
||||
|
||||
import torch
|
||||
from torch.ao.quantization import (
|
||||
default_weight_fake_quant,
|
||||
default_weight_observer,
|
||||
FakeQuantizeBase,
|
||||
QConfig,
|
||||
QConfigMapping,
|
||||
)
|
||||
from torch.ao.quantization.backend_config import BackendConfig
|
||||
from torch.ao.quantization.observer import _PartialWrapper
|
||||
from torch.ao.quantization.quantize_fx import convert_to_reference_fx, prepare_fx
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Callable
|
||||
|
||||
|
||||
# TODO: move all LSTM util functions from fx/utils.py to this file
|
||||
def _get_lstm_with_individually_observed_parts(
|
||||
float_lstm: torch.nn.LSTM,
|
||||
example_inputs: tuple[Any, ...],
|
||||
backend_config: BackendConfig | None = None,
|
||||
linear_output_obs_ctr: _PartialWrapper | None = None,
|
||||
sigmoid_obs_ctr: _PartialWrapper | None = None,
|
||||
tanh_obs_ctr: _PartialWrapper | None = None,
|
||||
cell_state_obs_ctr: _PartialWrapper | None = None,
|
||||
hidden_state_obs_ctr: _PartialWrapper | None = None,
|
||||
split_gates: bool = False,
|
||||
) -> torch.ao.nn.quantizable.LSTM:
|
||||
"""
|
||||
Return an observed `torch.ao.nn.quantizable.LSTM` created from a `torch.nn.LSTM`
|
||||
with specific observers or fake quantizes assigned to the inner ops or submodules.
|
||||
|
||||
In both eager and FX graph mode quantization, `torch.ao.nn.quantizable.LSTM` is
|
||||
used as an observed custom module, which is responsible for inserting its own
|
||||
observers. By default, all inner ops inherit the parent custom module's QConfig.
|
||||
Users who wish to override this behavior may extend `torch.ao.nn.quantizable.LSTM`
|
||||
and use this helper function to customize the observer insertion logic.
|
||||
|
||||
This is meant to be used to convert a float module to an observed module in the
|
||||
custom module flow.
|
||||
|
||||
Args:
|
||||
`float_lstm`: The float LSTM module
|
||||
`example_inputs`: example inputs for the forward function of the LSTM module
|
||||
`backend_config`: BackendConfig to use to observe the LSTM module
|
||||
`linear_output_obs_ctr`: observer or fake quantize for linear outputs Wx + b,
|
||||
where W is the weight matrix, b is the bias, and x is either the inputs
|
||||
or the hidden state from the previous layer (if any)
|
||||
`sigmoid_obs_ctr`: observer or fake quantize for sigmoid activations
|
||||
`tanh_obs_ctr`: observer or fake quantize for tanh activations
|
||||
`cell_state_obs_ctr`: observer or fake quantize for the cell state
|
||||
`hidden_state_obs_ctr`: observer or fake quantize for the hidden state and
|
||||
the output
|
||||
|
||||
Return:
|
||||
A `torch.ao.nn.quantizable.LSTM` with the specified observers or fake quantizes
|
||||
assigned to the inner ops.
|
||||
"""
|
||||
|
||||
def make_qconfig(obs_ctr: _PartialWrapper) -> QConfig:
|
||||
"""
|
||||
Make a QConfig with fixed qparams observers or fake quantizes.
|
||||
"""
|
||||
if isinstance(obs_ctr(), FakeQuantizeBase):
|
||||
weight = default_weight_fake_quant
|
||||
else:
|
||||
weight = default_weight_observer
|
||||
return QConfig(activation=obs_ctr, weight=weight)
|
||||
|
||||
quantizable_lstm = torch.ao.nn.quantizable.LSTM(
|
||||
float_lstm.input_size,
|
||||
float_lstm.hidden_size,
|
||||
float_lstm.num_layers,
|
||||
float_lstm.bias,
|
||||
float_lstm.batch_first,
|
||||
float_lstm.dropout,
|
||||
float_lstm.bidirectional,
|
||||
split_gates=split_gates,
|
||||
)
|
||||
quantizable_lstm.qconfig = float_lstm.qconfig
|
||||
|
||||
for idx in range(float_lstm.num_layers):
|
||||
quantizable_lstm.layers[idx] = (
|
||||
torch.ao.nn.quantizable.modules.rnn._LSTMLayer.from_float(
|
||||
float_lstm,
|
||||
idx,
|
||||
float_lstm.qconfig,
|
||||
batch_first=False,
|
||||
split_gates=split_gates,
|
||||
)
|
||||
)
|
||||
|
||||
# Build QConfigMapping for the LSTM cell
|
||||
# Note: FloatFunctional qconfigs will be configured separately below
|
||||
cell_qm = QConfigMapping().set_global(float_lstm.qconfig) # type: ignore[arg-type]
|
||||
if sigmoid_obs_ctr is not None:
|
||||
cell_qm.set_module_name("input_gate", make_qconfig(sigmoid_obs_ctr))
|
||||
cell_qm.set_module_name("forget_gate", make_qconfig(sigmoid_obs_ctr))
|
||||
cell_qm.set_module_name("output_gate", make_qconfig(sigmoid_obs_ctr))
|
||||
if tanh_obs_ctr is not None:
|
||||
cell_qm.set_module_name("cell_gate", make_qconfig(tanh_obs_ctr))
|
||||
|
||||
# Insert observers into each LSTM cell
|
||||
# TODO: maybe make this work for layer_bw as well
|
||||
for layer in quantizable_lstm.layers:
|
||||
cell = layer.layer_fw.cell # type: ignore[union-attr]
|
||||
if not isinstance(cell, torch.nn.Module):
|
||||
raise AssertionError("cell should be a nn.Module")
|
||||
cell = prepare_fx(cell, cell_qm, example_inputs, backend_config=backend_config)
|
||||
# HACK: Manually replace the activation_post_process following these ops.
|
||||
# This is needed for FloatFunctional ops because there is currently no way
|
||||
# to configure these ops in FX graph mode quantization today. This is because
|
||||
# the FloatFunctional modules simply disappear from the graph after tracing.
|
||||
# In the future, we should rewrite quantizable LSTM without FloatFunctionals.
|
||||
if not split_gates:
|
||||
op_index_to_activation_post_process_ctr = {
|
||||
(torch.add, 0): linear_output_obs_ctr, # gates.add
|
||||
(torch.mul, 0): cell_state_obs_ctr, # fgate_cx.mul
|
||||
(torch.mul, 1): cell_state_obs_ctr, # igate_cgate.mul
|
||||
(torch.add, 1): cell_state_obs_ctr, # fgate_cx_igate_cgate.add
|
||||
(torch.mul, 2): hidden_state_obs_ctr, # ogate_cy.mul
|
||||
}
|
||||
else:
|
||||
op_index_to_activation_post_process_ctr = {
|
||||
(torch.add, 0): linear_output_obs_ctr, # gates.add (input)
|
||||
(torch.add, 1): linear_output_obs_ctr, # gates.add (forget)
|
||||
(torch.add, 2): linear_output_obs_ctr, # gates.add (cell)
|
||||
(torch.add, 3): linear_output_obs_ctr, # gates.add (output)
|
||||
(torch.mul, 0): cell_state_obs_ctr, # fgate_cx.mul
|
||||
(torch.mul, 1): cell_state_obs_ctr, # igate_cgate.mul
|
||||
(torch.add, 4): cell_state_obs_ctr, # fgate_cx_igate_cgate.add
|
||||
(torch.mul, 2): hidden_state_obs_ctr, # ogate_cy.mul
|
||||
}
|
||||
add_count = 0
|
||||
mul_count = 0
|
||||
for node in cell.graph.nodes:
|
||||
op_index: tuple[Callable, int] | None = None # e.g. (torch.add, 1)
|
||||
if node.target is torch.add:
|
||||
op_index = (torch.add, add_count)
|
||||
add_count += 1
|
||||
elif node.target is torch.mul:
|
||||
op_index = (torch.mul, mul_count)
|
||||
mul_count += 1
|
||||
else:
|
||||
# Neither torch.add nor torch.mul
|
||||
continue
|
||||
if op_index not in op_index_to_activation_post_process_ctr:
|
||||
continue
|
||||
if len(node.users) != 1:
|
||||
raise AssertionError("expected exactly one user for the node")
|
||||
activation_post_process_name = next(iter(node.users.keys())).name
|
||||
activation_post_process_ctr = op_index_to_activation_post_process_ctr[
|
||||
op_index
|
||||
]
|
||||
if activation_post_process_ctr is not None:
|
||||
setattr(
|
||||
cell, activation_post_process_name, activation_post_process_ctr()
|
||||
)
|
||||
layer.layer_fw.cell = cell # type: ignore[union-attr]
|
||||
return quantizable_lstm
|
||||
|
||||
|
||||
def _get_reference_quantized_lstm_module(
|
||||
observed_lstm: torch.ao.nn.quantizable.LSTM,
|
||||
backend_config: BackendConfig | None = None,
|
||||
) -> torch.ao.nn.quantized.LSTM:
|
||||
"""
|
||||
Return a `torch.ao.nn.quantized.LSTM` created from a `torch.ao.nn.quantizable.LSTM`
|
||||
with observers or fake quantizes inserted through `prepare_fx`, e.g. from
|
||||
`_get_lstm_with_individually_observed_parts`.
|
||||
|
||||
This is meant to be used to convert an observed module to a quantized module in the
|
||||
custom module flow.
|
||||
|
||||
Args:
|
||||
`observed_lstm`: a `torch.ao.nn.quantizable.LSTM` observed through `prepare_fx`
|
||||
`backend_config`: BackendConfig to use to produce the reference quantized model
|
||||
|
||||
Return:
|
||||
A reference `torch.ao.nn.quantized.LSTM` module.
|
||||
"""
|
||||
quantized_lstm = torch.ao.nn.quantized.LSTM(
|
||||
observed_lstm.input_size,
|
||||
observed_lstm.hidden_size,
|
||||
observed_lstm.num_layers,
|
||||
observed_lstm.bias,
|
||||
observed_lstm.batch_first,
|
||||
observed_lstm.dropout,
|
||||
observed_lstm.bidirectional,
|
||||
)
|
||||
|
||||
for i, layer in enumerate(quantized_lstm.layers):
|
||||
cell = copy.deepcopy(observed_lstm.layers.get_submodule(str(i)).layer_fw.cell) # type: ignore[union-attr]
|
||||
cell = convert_to_reference_fx(cell, backend_config=backend_config) # type: ignore[arg-type]
|
||||
if not isinstance(cell, torch.fx.GraphModule):
|
||||
raise AssertionError("cell must be converted to a torch.fx.GraphModule")
|
||||
# HACK: Manually remove input quantize nodes and output dequantize nodes,
|
||||
# since custom modules expect quint8 inputs and outputs for now. Note that
|
||||
# this functionality is supposedly handled through PrepareCustomConfig's
|
||||
# `set_input_quantized_indexes` and `set_output_quantized_indexes`, but that
|
||||
# API doesn't currently handle tuple inputs and outputs, so we have to do
|
||||
# this manually for now. In the future we should (1) relax the restriction
|
||||
# on custom module input/output dtypes, and (2) expand support for complex
|
||||
# input/output structures.
|
||||
for node in cell.graph.nodes:
|
||||
if node.target is torch.quantize_per_tensor:
|
||||
arg = node.args[0]
|
||||
# Remove quantize(x), quantize(hidden[0]), and quantize(hidden[1])
|
||||
if arg.target == "x" or (
|
||||
arg.target is operator.getitem and arg.args[0].target == "hidden"
|
||||
):
|
||||
with cell.graph.inserting_before(node):
|
||||
node.replace_all_uses_with(arg)
|
||||
cell.graph.erase_node(node)
|
||||
if node.target == "output":
|
||||
# Remove all dequantize nodes in the output tuple
|
||||
for arg in node.args[0]:
|
||||
with cell.graph.inserting_before(node):
|
||||
node.replace_input_with(arg, arg.args[0])
|
||||
cell.graph.eliminate_dead_code()
|
||||
cell.recompile()
|
||||
layer.layer_fw.cell = cell # type: ignore[union-attr]
|
||||
return quantized_lstm
|
||||
@@ -0,0 +1,231 @@
|
||||
# mypy: allow-untyped-defs
|
||||
import sys
|
||||
from collections.abc import Callable, Iterable
|
||||
from typing import Any
|
||||
|
||||
import torch
|
||||
from torch.ao.quantization.qconfig import QConfigAny
|
||||
from torch.ao.quantization.utils import MatchAllNode, Pattern
|
||||
from torch.fx.graph import Graph, Node
|
||||
from torch.nn.utils.parametrize import type_before_parametrizations
|
||||
|
||||
from .graph_module import _is_observed_standalone_module
|
||||
from .quantize_handler import QuantizeHandler
|
||||
|
||||
|
||||
__all__: list[str] = []
|
||||
|
||||
# TODO(future PR): the 1st argument is typed as `List[Node]`, but a better type
|
||||
# would be a recursive `List[Union[Node, Tuple[Union[Node, ...]]]]`
|
||||
_MatchResult = tuple[Node, list[Node], Pattern | None, QuantizeHandler]
|
||||
|
||||
_MatchResultWithQConfig = tuple[
|
||||
Node, list[Node], Pattern | None, QuantizeHandler, QConfigAny
|
||||
]
|
||||
|
||||
|
||||
# Note: The order of patterns is important! match function will take whatever is matched first, so we'll
|
||||
# need to put the fusion patterns before single patterns. For example, add_relu should be registered come before relu.
|
||||
# decorators are applied in the reverse order we see. Also when we match the nodes in the graph with these patterns,
|
||||
# we'll start from the last node of the graph and traverse back.
|
||||
def _is_match(modules, node, pattern, max_uses=sys.maxsize):
|
||||
"""Matches a node in fx against a pattern"""
|
||||
if isinstance(pattern, tuple):
|
||||
self_match, *arg_matches = pattern
|
||||
if self_match is getattr:
|
||||
if len(pattern) != 2:
|
||||
raise AssertionError("Expecting getattr pattern to have two elements")
|
||||
arg_matches = []
|
||||
else:
|
||||
self_match = pattern
|
||||
arg_matches = []
|
||||
|
||||
if isinstance(self_match, type) and issubclass(self_match, MatchAllNode):
|
||||
return True
|
||||
|
||||
if node == pattern:
|
||||
return True
|
||||
|
||||
if not isinstance(node, Node) or len(node.users) > max_uses:
|
||||
return False
|
||||
|
||||
if isinstance(self_match, type) and issubclass(self_match, torch.nn.Module):
|
||||
if node.op != "call_module":
|
||||
return False
|
||||
if type_before_parametrizations(modules[node.target]) != self_match:
|
||||
return False
|
||||
elif callable(self_match):
|
||||
if node.op != "call_function" or node.target is not self_match:
|
||||
return False
|
||||
elif node.target is getattr:
|
||||
if node.args[1] != pattern[1]:
|
||||
return False
|
||||
elif isinstance(self_match, str):
|
||||
if node.op != "call_method" or node.target != self_match:
|
||||
return False
|
||||
elif node.target != self_match:
|
||||
return False
|
||||
|
||||
if not arg_matches:
|
||||
return True
|
||||
|
||||
if len(arg_matches) != len(node.args):
|
||||
return False
|
||||
|
||||
return all(
|
||||
_is_match(modules, node, arg_match, max_uses=1)
|
||||
for node, arg_match in zip(node.args, arg_matches)
|
||||
)
|
||||
|
||||
|
||||
def _find_matches(
|
||||
graph: Graph,
|
||||
modules: dict[str, torch.nn.Module],
|
||||
patterns: dict[Pattern, QuantizeHandler],
|
||||
root_node_getter_mapping: dict[Pattern, Callable],
|
||||
standalone_module_names: list[str] | None = None,
|
||||
standalone_module_classes: list[type] | None = None,
|
||||
custom_module_classes: list[Any] | None = None,
|
||||
) -> dict[str, _MatchResult]:
|
||||
"""
|
||||
Matches the nodes in the input graph to quantization patterns, and
|
||||
outputs the information needed to quantize them in future steps.
|
||||
|
||||
Inputs:
|
||||
- graph: an fx.Graph object
|
||||
- modules: a mapping of fully qualified module name to instance,
|
||||
for example, {'foo': ModuleFoo, ...}
|
||||
- patterns: a mapping from a tuple of nodes in reverse order to
|
||||
uninitialized QuantizeHandler subclass.
|
||||
|
||||
Outputs a map of
|
||||
node_name ->
|
||||
(node, matched_values, matched_pattern, QuantizeHandler instance,
|
||||
qconfig)
|
||||
|
||||
For example, {
|
||||
'relu_1': (relu_1, [relu_1], torch.nn.functional.relu,
|
||||
<CopyNodeQuantizeHandler instance>, QConfig(...)),
|
||||
...
|
||||
}
|
||||
"""
|
||||
if custom_module_classes is None:
|
||||
custom_module_classes = []
|
||||
|
||||
if standalone_module_classes is None:
|
||||
standalone_module_classes = []
|
||||
|
||||
if standalone_module_names is None:
|
||||
standalone_module_names = []
|
||||
|
||||
match_map: dict[str, _MatchResult] = {}
|
||||
all_matched: set[str] = set()
|
||||
|
||||
def _recursive_record_node_in_match_map(
|
||||
last_node, match_map, node_pattern, matched_node_pattern, pattern, match_value
|
||||
):
|
||||
if isinstance(node_pattern, Node):
|
||||
match_map[node_pattern.name] = (
|
||||
last_node,
|
||||
matched_node_pattern,
|
||||
pattern,
|
||||
match_value,
|
||||
)
|
||||
elif not isinstance(node_pattern, Iterable):
|
||||
return
|
||||
else:
|
||||
for n in node_pattern:
|
||||
_recursive_record_node_in_match_map(
|
||||
last_node, match_map, n, matched_node_pattern, pattern, match_value
|
||||
)
|
||||
|
||||
# TODO: 1. merge with fuse matcher 2. document the code
|
||||
def record_match(pattern, node, last_node, matched_node_pattern, match_map):
|
||||
if isinstance(pattern, tuple):
|
||||
s, *args = pattern
|
||||
is_single_arg = len(args) == 1
|
||||
current_node_pattern: list[Node] = []
|
||||
record_match(s, node, last_node, matched_node_pattern, match_map)
|
||||
if pattern[0] is not getattr:
|
||||
for subpattern, arg in zip(args, node.args):
|
||||
record_match(subpattern, arg, node, current_node_pattern, match_map)
|
||||
if len(current_node_pattern) > 1:
|
||||
# current_node_pattern is the node pattern we get from matching
|
||||
# the subpattern with arguments of the node
|
||||
# we use is_single_arg to recover the original structure of the pattern
|
||||
# if the original pattern has a single argument, we will have
|
||||
# (original_op, (original_arg, ...))
|
||||
# otherwise, we'll have a list of arguments
|
||||
# (original_op, arg0, arg1, arg2, ...)
|
||||
if is_single_arg:
|
||||
matched_node_pattern.append(tuple(current_node_pattern))
|
||||
else:
|
||||
matched_node_pattern.extend(list(current_node_pattern))
|
||||
else:
|
||||
matched_node_pattern.append(current_node_pattern[0])
|
||||
else:
|
||||
matched_node_pattern.append(node)
|
||||
|
||||
for node in reversed(graph.nodes):
|
||||
if node.name not in match_map and node.name not in all_matched:
|
||||
for pattern, quantize_handler_cls in patterns.items():
|
||||
root_node_getter = root_node_getter_mapping.get(pattern)
|
||||
if _is_match(modules, node, pattern) and node.name not in match_map:
|
||||
matched_node_pattern: list[Node] = []
|
||||
record_match(pattern, node, node, matched_node_pattern, match_map)
|
||||
quantize_handler = quantize_handler_cls( # type: ignore[operator]
|
||||
matched_node_pattern, modules, root_node_getter
|
||||
)
|
||||
last_node = node
|
||||
# record the match for all nodes in the pattern
|
||||
_recursive_record_node_in_match_map(
|
||||
last_node,
|
||||
match_map,
|
||||
# we need to record all nodes in the matched pattern in the match_map
|
||||
matched_node_pattern,
|
||||
# this is a part of the value corresponding to the node
|
||||
matched_node_pattern,
|
||||
pattern,
|
||||
quantize_handler,
|
||||
)
|
||||
break
|
||||
|
||||
# add custom module instances to the match result
|
||||
if modules is None:
|
||||
raise AssertionError("modules must not be None")
|
||||
for node in graph.nodes:
|
||||
if (
|
||||
node.op == "call_module"
|
||||
and type(modules[node.target]) in custom_module_classes
|
||||
):
|
||||
match_map[node.name] = (
|
||||
node,
|
||||
node,
|
||||
None,
|
||||
QuantizeHandler(node, modules, is_custom_module=True),
|
||||
)
|
||||
|
||||
def is_standalone_module(node_target: str, modules: dict[str, torch.nn.Module]):
|
||||
if modules is None:
|
||||
raise AssertionError("modules must not be None")
|
||||
return (
|
||||
node_target in standalone_module_names
|
||||
or type(modules[node_target]) # type: ignore[operator]
|
||||
in standalone_module_classes # type: ignore[operator]
|
||||
)
|
||||
|
||||
# add standalone modules to the match
|
||||
for node in graph.nodes:
|
||||
if node.op == "call_module" and (
|
||||
is_standalone_module(node.target, modules)
|
||||
or _is_observed_standalone_module(modules[node.target])
|
||||
):
|
||||
# add node to matched nodes
|
||||
match_map[node.name] = (
|
||||
node,
|
||||
node,
|
||||
None,
|
||||
QuantizeHandler(node, modules, is_standalone_module=True),
|
||||
)
|
||||
|
||||
return match_map
|
||||
@@ -0,0 +1,112 @@
|
||||
# mypy: allow-untyped-defs
|
||||
import copy
|
||||
from collections import OrderedDict
|
||||
from typing import Any
|
||||
|
||||
from torch.ao.quantization.fake_quantize import FixedQParamsFakeQuantize
|
||||
from torch.ao.quantization.observer import ObserverBase
|
||||
from torch.ao.quantization.utils import Pattern
|
||||
|
||||
|
||||
__all__ = [
|
||||
"get_default_fusion_patterns",
|
||||
"get_default_quant_patterns",
|
||||
"get_default_output_activation_post_process_map",
|
||||
]
|
||||
|
||||
# TODO(future PR): fix the typing on QuantizeHandler (currently a circular dependency)
|
||||
QuantizeHandler = Any
|
||||
|
||||
# pattern for conv bn fusion
|
||||
_DEFAULT_FUSION_PATTERNS: dict[Pattern, QuantizeHandler] = OrderedDict()
|
||||
|
||||
|
||||
def _register_fusion_pattern(pattern):
|
||||
def insert(fn):
|
||||
_DEFAULT_FUSION_PATTERNS[pattern] = fn
|
||||
return fn
|
||||
|
||||
return insert
|
||||
|
||||
|
||||
def get_default_fusion_patterns() -> dict[Pattern, QuantizeHandler]:
|
||||
return copy.copy(_DEFAULT_FUSION_PATTERNS)
|
||||
|
||||
|
||||
_DEFAULT_QUANTIZATION_PATTERNS: dict[Pattern, QuantizeHandler] = OrderedDict()
|
||||
|
||||
# Mapping from pattern to activation_post_process(observer/fake_quant) constructor for output activation
|
||||
# e.g. pattern: torch.sigmoid,
|
||||
# output_activation_post_process: default_fixed_qparams_range_0to1_fake_quant
|
||||
_DEFAULT_OUTPUT_FAKE_QUANTIZE_MAP: dict[Pattern, QuantizeHandler] = {}
|
||||
_DEFAULT_OUTPUT_OBSERVER_MAP: dict[Pattern, QuantizeHandler] = {}
|
||||
|
||||
|
||||
# Register pattern for both static quantization and qat
|
||||
def _register_quant_pattern(pattern, fixed_qparams_observer=None):
|
||||
def insert(fn):
|
||||
_DEFAULT_QUANTIZATION_PATTERNS[pattern] = fn
|
||||
if fixed_qparams_observer is not None:
|
||||
_DEFAULT_OUTPUT_FAKE_QUANTIZE_MAP[pattern] = (
|
||||
FixedQParamsFakeQuantize.with_args(observer=fixed_qparams_observer)
|
||||
)
|
||||
_DEFAULT_OUTPUT_OBSERVER_MAP[pattern] = fixed_qparams_observer
|
||||
return fn
|
||||
|
||||
return insert
|
||||
|
||||
|
||||
# Get patterns for both static quantization and qat
|
||||
def get_default_quant_patterns() -> dict[Pattern, QuantizeHandler]:
|
||||
return copy.copy(_DEFAULT_QUANTIZATION_PATTERNS)
|
||||
|
||||
|
||||
# a map from pattern to output activation post process constructor
|
||||
# e.g. torch.sigmoid -> default_affine_fixed_qparam_fake_quant
|
||||
def get_default_output_activation_post_process_map(
|
||||
is_training,
|
||||
) -> dict[Pattern, ObserverBase]:
|
||||
if is_training:
|
||||
return copy.copy(_DEFAULT_OUTPUT_FAKE_QUANTIZE_MAP)
|
||||
else:
|
||||
return copy.copy(_DEFAULT_OUTPUT_OBSERVER_MAP)
|
||||
|
||||
|
||||
# Example use of register pattern function:
|
||||
# @_register_fusion_pattern(torch.nn.ReLU, (torch.nn.BatchNorm2d, torch.nn.Conv2d)))
|
||||
# class ConvOrLinearBNReLUFusion():
|
||||
# def __init__(...):
|
||||
# ...
|
||||
#
|
||||
|
||||
|
||||
def _sorted_patterns_dict(
|
||||
patterns_dict: dict[Pattern, QuantizeHandler],
|
||||
) -> dict[Pattern, QuantizeHandler]:
|
||||
"""
|
||||
Return a sorted version of the patterns dictionary such that longer patterns are matched first,
|
||||
e.g. match (F.relu, F.linear) before F.relu.
|
||||
This works for current use cases, but we may need to have a more clever way to sort
|
||||
things to address more complex patterns
|
||||
"""
|
||||
|
||||
def get_len(pattern):
|
||||
"""this will calculate the length of the pattern by counting all the entries
|
||||
in the pattern.
|
||||
this will make sure (nn.ReLU, (nn.BatchNorm, nn.Conv2d)) comes before
|
||||
(nn.BatchNorm, nn.Conv2d) so that we can match the former first
|
||||
"""
|
||||
len = 0
|
||||
if isinstance(pattern, tuple):
|
||||
for item in pattern:
|
||||
len += get_len(item)
|
||||
else:
|
||||
len += 1
|
||||
return len
|
||||
|
||||
return OrderedDict(
|
||||
sorted(
|
||||
patterns_dict.items(),
|
||||
key=lambda kv: -get_len(kv[0]) if isinstance(kv[0], tuple) else 1,
|
||||
)
|
||||
)
|
||||
File diff suppressed because it is too large
Load Diff
+401
@@ -0,0 +1,401 @@
|
||||
# mypy: allow-untyped-defs
|
||||
import re
|
||||
from collections import defaultdict, OrderedDict
|
||||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
|
||||
import torch
|
||||
from torch.ao.nn.intrinsic import _FusedModule
|
||||
from torch.ao.quantization import QConfig
|
||||
from torch.ao.quantization.backend_config import BackendConfig, DTypeConfig
|
||||
from torch.ao.quantization.backend_config.utils import get_module_to_qat_module
|
||||
from torch.ao.quantization.observer import _is_activation_post_process
|
||||
from torch.ao.quantization.qconfig import (
|
||||
_add_module_to_qconfig_obs_ctr,
|
||||
qconfig_equals,
|
||||
QConfigAny,
|
||||
)
|
||||
from torch.ao.quantization.qconfig_mapping import (
|
||||
_MODULE_NAME_DICT_KEY,
|
||||
_MODULE_NAME_REGEX_DICT_KEY,
|
||||
_OBJECT_TYPE_DICT_KEY,
|
||||
QConfigMapping,
|
||||
)
|
||||
from torch.ao.quantization.utils import _parent_name, get_qconfig_dtypes
|
||||
from torch.fx import GraphModule
|
||||
from torch.fx.graph import Graph
|
||||
|
||||
|
||||
__all__: list[str] = []
|
||||
|
||||
|
||||
def _maybe_adjust_qconfig_for_module_name_object_type_order(
|
||||
qconfig_mapping: QConfigMapping,
|
||||
cur_module_path: str,
|
||||
cur_object_type: Callable,
|
||||
cur_object_type_idx: int,
|
||||
fallback_qconfig: QConfigAny,
|
||||
) -> QConfigAny:
|
||||
for (
|
||||
module_name,
|
||||
object_type,
|
||||
index,
|
||||
), qconfig in qconfig_mapping.module_name_object_type_order_qconfigs.items():
|
||||
if (
|
||||
(module_name == cur_module_path)
|
||||
and (object_type == cur_object_type)
|
||||
and (index == cur_object_type_idx)
|
||||
):
|
||||
return qconfig
|
||||
return fallback_qconfig
|
||||
|
||||
|
||||
def _update_qconfig_for_fusion(model: GraphModule, qconfig_mapping: QConfigMapping):
|
||||
"""
|
||||
Update the QConfigMapping to account for fused modules such as LinearReLU.
|
||||
This assumes the QConfigMapping's attributes have already been converted to OrderedDicts.
|
||||
"""
|
||||
object_type_dict = qconfig_mapping.object_type_qconfigs
|
||||
if len(object_type_dict) == 0:
|
||||
return qconfig_mapping
|
||||
|
||||
modules = dict(model.named_modules())
|
||||
|
||||
for node in model.graph.nodes:
|
||||
if node.op == "call_module" and node.target in modules:
|
||||
maybe_fused_module = modules[str(node.target)]
|
||||
if not isinstance(maybe_fused_module, _FusedModule):
|
||||
continue
|
||||
|
||||
ops = list(maybe_fused_module._modules.values())
|
||||
fused_qconfig = object_type_dict.get(type(ops[0]), None)
|
||||
|
||||
# Raise an error if the modules in the fused module have
|
||||
# different qconfigs specified in the qconfig_dict
|
||||
# TODO: currently it only works for modules,
|
||||
# need to make this work for torch.nn.functional.relu
|
||||
# TODO: currently it only works for object_type configurations,
|
||||
# ideally it should work for different types of configurations,
|
||||
# maybe we want to redesign this part
|
||||
for op in ops[1:]:
|
||||
if not qconfig_equals(
|
||||
object_type_dict.get(type(op), None), fused_qconfig
|
||||
):
|
||||
raise LookupError(
|
||||
"During fusion, we need to specify the same "
|
||||
+ f"qconfigs for all module types in {type(maybe_fused_module)} "
|
||||
+ f"offending type: {type(op)}"
|
||||
)
|
||||
|
||||
if fused_qconfig is not None:
|
||||
object_type_dict[type(maybe_fused_module)] = fused_qconfig
|
||||
|
||||
|
||||
def _generate_node_name_to_qconfig(
|
||||
root: torch.nn.Module,
|
||||
modules: dict[str, torch.nn.Module],
|
||||
input_graph: Graph,
|
||||
qconfig_mapping: QConfigMapping,
|
||||
node_name_to_scope: dict[str, tuple[str, type]],
|
||||
) -> dict[str, QConfigAny]:
|
||||
global_qconfig = qconfig_mapping.global_qconfig
|
||||
node_name_to_qconfig = {}
|
||||
|
||||
# example:
|
||||
#
|
||||
# {'foo.bar': {F.linear: 0, F.conv2d: 1, ...}, ...}
|
||||
#
|
||||
# meaning in submodule 'foo.bar', we have seen 0 F.linear and
|
||||
# 1 F.conv2d invocations so far.
|
||||
submodule_to_object_type_to_cur_idx: dict[str, dict[Callable, int]] = defaultdict(
|
||||
lambda: defaultdict(int)
|
||||
)
|
||||
for node in input_graph.nodes:
|
||||
qconfig = None
|
||||
if node.op == "get_attr":
|
||||
module_name, _ = _parent_name(node.target)
|
||||
qconfig = _maybe_adjust_qconfig_for_module_type_or_name(
|
||||
qconfig_mapping, type(modules[module_name]), module_name, global_qconfig
|
||||
)
|
||||
qconfig_with_device_check = _add_module_to_qconfig_obs_ctr(
|
||||
qconfig, modules.get(node.target)
|
||||
)
|
||||
elif node.op == "call_function":
|
||||
# precedence: module_name_qconfig
|
||||
# > function_qconfig > global_qconfig
|
||||
# module_name takes precedence over function qconfig
|
||||
function_qconfig = _get_object_type_qconfig(
|
||||
qconfig_mapping, node.target, global_qconfig
|
||||
)
|
||||
module_path, module_type = node_name_to_scope[node.name]
|
||||
qconfig = _maybe_adjust_qconfig_for_module_type_or_name(
|
||||
qconfig_mapping, module_type, module_path, function_qconfig
|
||||
)
|
||||
|
||||
cur_object_type_idx = submodule_to_object_type_to_cur_idx[module_path][
|
||||
node.target
|
||||
]
|
||||
submodule_to_object_type_to_cur_idx[module_path][node.target] += 1
|
||||
qconfig = _maybe_adjust_qconfig_for_module_name_object_type_order(
|
||||
qconfig_mapping, module_path, node.target, cur_object_type_idx, qconfig
|
||||
)
|
||||
qconfig_with_device_check = _add_module_to_qconfig_obs_ctr(
|
||||
qconfig, modules.get(node.target)
|
||||
)
|
||||
|
||||
elif node.op == "call_method":
|
||||
module_path, module_type = node_name_to_scope[node.name]
|
||||
# first use node.target (string) to get the qconfig
|
||||
# this is to support configs like
|
||||
# "object_type": [("reshape", qconfig)]
|
||||
qconfig = _maybe_adjust_qconfig_for_module_type_or_name(
|
||||
qconfig_mapping, node.target, module_path, global_qconfig
|
||||
)
|
||||
# if there is no special config for the method, we'll fall back to the
|
||||
# config for the module that contains the call_method node
|
||||
qconfig = _maybe_adjust_qconfig_for_module_type_or_name(
|
||||
qconfig_mapping, module_type, module_path, qconfig
|
||||
)
|
||||
# currently call_method does not support modifying qconfig
|
||||
# by order, we can add this later if it is needed.
|
||||
qconfig_with_device_check = _add_module_to_qconfig_obs_ctr(
|
||||
qconfig, modules.get(node.target)
|
||||
)
|
||||
|
||||
elif node.op == "call_module":
|
||||
# if the node is an observer, just continue - don't add it to the qconfig_map
|
||||
if _is_activation_post_process(modules[node.target]):
|
||||
continue
|
||||
qconfig = _maybe_adjust_qconfig_for_module_type_or_name(
|
||||
qconfig_mapping, type(modules[node.target]), node.target, global_qconfig
|
||||
)
|
||||
|
||||
module_path, module_type = node_name_to_scope[node.name]
|
||||
# Note: for call_module, the module_path is the current module's name.
|
||||
# to meaningfully count invocations, we need to count them in the parent
|
||||
# module.
|
||||
parent_name, _ = _parent_name(module_path)
|
||||
cur_object_type_idx = submodule_to_object_type_to_cur_idx[parent_name][
|
||||
module_type
|
||||
]
|
||||
submodule_to_object_type_to_cur_idx[parent_name][module_type] += 1
|
||||
qconfig = _maybe_adjust_qconfig_for_module_name_object_type_order(
|
||||
qconfig_mapping, parent_name, module_type, cur_object_type_idx, qconfig
|
||||
)
|
||||
qconfig_with_device_check = _add_module_to_qconfig_obs_ctr(
|
||||
qconfig, modules.get(node.target)
|
||||
)
|
||||
|
||||
# regex is not supported eager mode propagate_qconfig_, we'll
|
||||
# need to set the qconfig explicitly here in case regex
|
||||
# is used
|
||||
modules[node.target].qconfig = qconfig_with_device_check
|
||||
else:
|
||||
qconfig_with_device_check = None
|
||||
|
||||
node_name_to_qconfig[node.name] = qconfig_with_device_check
|
||||
return node_name_to_qconfig
|
||||
|
||||
|
||||
def _check_is_valid_config_dict(
|
||||
config_dict: Any, allowed_keys: set[str], dict_name: str
|
||||
) -> None:
|
||||
r"""Checks if the given config_dict has the correct keys
|
||||
|
||||
Args:
|
||||
`config_dict`: dictionary whose keys we want to check
|
||||
"""
|
||||
|
||||
for k in config_dict:
|
||||
if k not in allowed_keys:
|
||||
raise ValueError(
|
||||
"Expected "
|
||||
+ dict_name
|
||||
+ " to have the following keys: "
|
||||
+ str(allowed_keys)
|
||||
+ ". But found '"
|
||||
+ k
|
||||
+ "' instead."
|
||||
)
|
||||
|
||||
|
||||
def _compare_prepare_convert_qconfig_mappings(
|
||||
prepare_qconfig_mapping: QConfigMapping, convert_qconfig_mapping: QConfigMapping
|
||||
):
|
||||
r"""Compare the qconfig_mapping passed in convert to the one from prepare and check the values
|
||||
|
||||
Args:
|
||||
`prepare_qconfig_mapping`: configuration for prepare quantization step
|
||||
`convert_qconfig_mapping`: configuration for convert quantization step
|
||||
"""
|
||||
if not qconfig_equals(
|
||||
prepare_qconfig_mapping.global_qconfig, convert_qconfig_mapping.global_qconfig
|
||||
):
|
||||
raise AssertionError(
|
||||
"Expected global qconfigs to be the same in the prepare and convert quantization configs"
|
||||
)
|
||||
prepare_dicts: list[OrderedDict] = [
|
||||
prepare_qconfig_mapping.object_type_qconfigs,
|
||||
prepare_qconfig_mapping.module_name_qconfigs,
|
||||
prepare_qconfig_mapping.module_name_regex_qconfigs,
|
||||
]
|
||||
convert_dicts: list[OrderedDict] = [
|
||||
convert_qconfig_mapping.object_type_qconfigs,
|
||||
convert_qconfig_mapping.module_name_qconfigs,
|
||||
convert_qconfig_mapping.module_name_regex_qconfigs,
|
||||
]
|
||||
dict_names = [
|
||||
_OBJECT_TYPE_DICT_KEY,
|
||||
_MODULE_NAME_DICT_KEY,
|
||||
_MODULE_NAME_REGEX_DICT_KEY,
|
||||
]
|
||||
for i in range(len(prepare_dicts)):
|
||||
for name in prepare_dicts[i]:
|
||||
if name not in convert_dicts[i]:
|
||||
raise AssertionError(
|
||||
f"Missing key {dict_names[i]} {name} in convert QConfigMapping when it was present in prepare"
|
||||
)
|
||||
if convert_dicts[i][name] is not None and not qconfig_equals(
|
||||
prepare_dicts[i][name], convert_dicts[i][name]
|
||||
):
|
||||
raise AssertionError(
|
||||
"Expected convert QConfigMapping to have the same qconfig as prepare for key "
|
||||
f"{dict_names[i]} {name}; prepare: {prepare_dicts[i][name]}; convert: {convert_dicts[i][name]}"
|
||||
)
|
||||
|
||||
|
||||
def _is_qconfig_supported_by_dtype_configs(
|
||||
qconfig: QConfig, dtype_configs: list[DTypeConfig]
|
||||
):
|
||||
for dtype_config in dtype_configs:
|
||||
is_dynamic = dtype_config.is_dynamic
|
||||
if is_dynamic is None:
|
||||
is_dynamic = False
|
||||
input_dtype = dtype_config.input_dtype or torch.float
|
||||
weight_dtype = dtype_config.weight_dtype or torch.float
|
||||
bias_dtype = dtype_config.bias_dtype or torch.float
|
||||
output_dtype = dtype_config.output_dtype or torch.float
|
||||
(
|
||||
qconfig_activation_dtype,
|
||||
qconfig_weight_dtype,
|
||||
qconfig_input_act_is_dynamic,
|
||||
) = get_qconfig_dtypes(qconfig)
|
||||
qconfig_bias_dtype = (
|
||||
torch.float16
|
||||
if (
|
||||
qconfig_activation_dtype == torch.float16
|
||||
and qconfig_weight_dtype == torch.float16
|
||||
and not is_dynamic
|
||||
)
|
||||
else torch.float
|
||||
)
|
||||
|
||||
if is_dynamic:
|
||||
is_match = (
|
||||
qconfig_input_act_is_dynamic
|
||||
and input_dtype == qconfig_activation_dtype
|
||||
and output_dtype == torch.float
|
||||
and weight_dtype == qconfig_weight_dtype
|
||||
)
|
||||
else:
|
||||
is_match = (
|
||||
input_dtype == qconfig_activation_dtype
|
||||
and output_dtype == qconfig_activation_dtype
|
||||
and weight_dtype == qconfig_weight_dtype
|
||||
and bias_dtype == qconfig_bias_dtype
|
||||
)
|
||||
if is_match:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _get_object_type_qconfig(
|
||||
qconfig_mapping: QConfigMapping,
|
||||
object_type: Callable | str,
|
||||
fallback_qconfig: QConfigAny,
|
||||
) -> QConfigAny:
|
||||
return qconfig_mapping.object_type_qconfigs.get(object_type, fallback_qconfig)
|
||||
|
||||
|
||||
def _get_module_name_regex_qconfig(qconfig_mapping, module_name, fallback_qconfig):
|
||||
for regex_pattern, qconfig in qconfig_mapping.module_name_regex_qconfigs.items():
|
||||
if re.match(regex_pattern, module_name):
|
||||
# first match wins
|
||||
return qconfig
|
||||
return fallback_qconfig
|
||||
|
||||
|
||||
def _get_module_name_qconfig(qconfig_mapping, module_name, fallback_qconfig):
|
||||
if module_name == "":
|
||||
# module name qconfig not found
|
||||
return fallback_qconfig
|
||||
if module_name in qconfig_mapping.module_name_qconfigs:
|
||||
return qconfig_mapping.module_name_qconfigs[module_name]
|
||||
else:
|
||||
parent, _ = _parent_name(module_name)
|
||||
return _get_module_name_qconfig(qconfig_mapping, parent, fallback_qconfig)
|
||||
|
||||
|
||||
def _maybe_adjust_qconfig_for_module_type_or_name(
|
||||
qconfig_mapping, module_type, module_name, global_qconfig
|
||||
):
|
||||
# get qconfig for module_name,
|
||||
# fallback to module_name_regex_qconfig, module_type_qconfig,
|
||||
# global_qconfig if necessary
|
||||
module_type_qconfig = _get_object_type_qconfig(
|
||||
qconfig_mapping, module_type, global_qconfig
|
||||
)
|
||||
module_name_regex_qconfig = _get_module_name_regex_qconfig(
|
||||
qconfig_mapping, module_name, module_type_qconfig
|
||||
)
|
||||
module_name_qconfig = _get_module_name_qconfig(
|
||||
qconfig_mapping, module_name, module_name_regex_qconfig
|
||||
)
|
||||
return module_name_qconfig
|
||||
|
||||
|
||||
def _get_flattened_qconfig_dict(
|
||||
qconfig_mapping: QConfigMapping,
|
||||
) -> dict[Callable | str, QConfigAny]:
|
||||
"""flatten the global, object_type and module_name qconfig
|
||||
to the same qconfig_dict so that it can be used by
|
||||
propagate_qconfig_ function.
|
||||
"module_name_regex" is ignored for now since it's not supported
|
||||
in propagate_qconfig_, but it can be fixed later.
|
||||
|
||||
For example:
|
||||
Input: {
|
||||
"": qconfig,
|
||||
"object_type": [
|
||||
(torch.add, qconfig)
|
||||
],
|
||||
"module_name": [
|
||||
("conv", qconfig)
|
||||
]
|
||||
}
|
||||
|
||||
Output: {
|
||||
"": qconfig,
|
||||
torch.add: qconfig,
|
||||
"conv": qconfig
|
||||
}
|
||||
"""
|
||||
flattened: dict[Callable | str, QConfigAny] = {"": qconfig_mapping.global_qconfig}
|
||||
flattened.update(qconfig_mapping.object_type_qconfigs)
|
||||
flattened.update(qconfig_mapping.module_name_qconfigs) # type: ignore[arg-type]
|
||||
return flattened
|
||||
|
||||
|
||||
def _update_qconfig_for_qat(
|
||||
qconfig_mapping: QConfigMapping, backend_config: BackendConfig
|
||||
):
|
||||
"""
|
||||
Update the qconfig_mapping to account for module swaps during QAT.
|
||||
During QAT we perform a module swap on the nn.Module types to the corresponding nn.qat.modules types.
|
||||
"""
|
||||
module_to_qat_module_class = get_module_to_qat_module(backend_config)
|
||||
object_type_dict = qconfig_mapping.object_type_qconfigs
|
||||
new_object_type_dict = object_type_dict.copy()
|
||||
for k, v in new_object_type_dict.items():
|
||||
if k in module_to_qat_module_class:
|
||||
object_type_dict[module_to_qat_module_class[k]] = v
|
||||
+226
@@ -0,0 +1,226 @@
|
||||
# mypy: allow-untyped-defs
|
||||
from abc import ABC
|
||||
from collections.abc import Callable
|
||||
|
||||
import torch
|
||||
from torch.ao.quantization.backend_config import (
|
||||
BackendConfig,
|
||||
DTypeConfig,
|
||||
ObservationType,
|
||||
)
|
||||
from torch.ao.quantization.utils import NodePattern, Pattern, QuantizerCls
|
||||
from torch.fx.graph import Node
|
||||
|
||||
from .utils import all_node_args_have_no_tensors
|
||||
|
||||
|
||||
__all__ = [
|
||||
"QuantizeHandler",
|
||||
"BinaryOpQuantizeHandler",
|
||||
"CatQuantizeHandler",
|
||||
"ConvReluQuantizeHandler",
|
||||
"LinearReLUQuantizeHandler",
|
||||
"BatchNormQuantizeHandler",
|
||||
"EmbeddingQuantizeHandler",
|
||||
"RNNDynamicQuantizeHandler",
|
||||
"DefaultNodeQuantizeHandler",
|
||||
"FixedQParamsOpQuantizeHandler",
|
||||
"CopyNodeQuantizeHandler",
|
||||
"GeneralTensorShapeOpQuantizeHandler",
|
||||
"CustomModuleQuantizeHandler",
|
||||
"StandaloneModuleQuantizeHandler",
|
||||
]
|
||||
|
||||
|
||||
def _default_root_node_getter(node_pattern):
|
||||
if node_pattern is None:
|
||||
return node_pattern
|
||||
while not isinstance(node_pattern, Node):
|
||||
node_pattern = node_pattern[-1]
|
||||
return node_pattern
|
||||
|
||||
|
||||
# Base Pattern Handler
|
||||
class QuantizeHandler(ABC): # noqa: B024
|
||||
"""Base handler class for the quantizer patterns"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
node_pattern: NodePattern,
|
||||
modules: dict[str, torch.nn.Module],
|
||||
root_node_getter: Callable | None = None,
|
||||
is_custom_module=False,
|
||||
is_standalone_module=False,
|
||||
):
|
||||
"""Records pattern information in __init__, which will be used
|
||||
in convert
|
||||
"""
|
||||
self.node_pattern = node_pattern
|
||||
self.modules = modules
|
||||
if root_node_getter is None:
|
||||
root_node_getter = _default_root_node_getter
|
||||
self.root_node = root_node_getter(node_pattern)
|
||||
self.is_custom_module_ = is_custom_module
|
||||
self.is_standalone_module_ = is_standalone_module
|
||||
self.num_tensor_args = 0
|
||||
# determine how many of the first two args are Tensors (versus scalars)
|
||||
# this distinguishes things like "x + y" from "x + 2" or "2 + x"
|
||||
if isinstance(self.root_node, Node):
|
||||
cache_for_no_tensor_check: dict[Node, bool] = {}
|
||||
for arg_idx in range(len(self.root_node.args)):
|
||||
arg = self.root_node.args[arg_idx]
|
||||
if isinstance(arg, Node) and (
|
||||
not all_node_args_have_no_tensors(
|
||||
arg, self.modules, cache_for_no_tensor_check
|
||||
)
|
||||
):
|
||||
self.num_tensor_args += 1
|
||||
|
||||
def is_general_tensor_value_op(self) -> bool:
|
||||
"""
|
||||
Returns True if the operator works for both floating point and
|
||||
quantized input, and does some computation based on the input Tensor,
|
||||
or the ops that only re-arranges the Tensor values or query some metadata
|
||||
about the Tensor
|
||||
so we need to insert observer/fake_quant for the output of the
|
||||
operator (same observer instance as input)
|
||||
since the distribution of values is different for input and output
|
||||
Tensors (for HistogramObserver) while they share the same quantization
|
||||
parameters
|
||||
Example operator: avgpool2d, reshape, transpose, maxpool2d
|
||||
Example observed operator:
|
||||
observer_0 - avgpool2d - observer_0 (same observer instance as input)
|
||||
"""
|
||||
return False
|
||||
|
||||
def is_custom_module(self):
|
||||
return self.is_custom_module_
|
||||
|
||||
def is_standalone_module(self):
|
||||
return self.is_standalone_module_
|
||||
|
||||
|
||||
def _get_quantize_handler_cls(
|
||||
observation_type: ObservationType,
|
||||
dtype_configs: list[DTypeConfig],
|
||||
num_tensor_args_to_observation_type: dict[int, ObservationType],
|
||||
) -> type[QuantizeHandler]:
|
||||
"""
|
||||
Return a configurable QuantizeHandler that matches the given specifications from the backend.
|
||||
"""
|
||||
|
||||
class ConfigurableQuantizeHandler(QuantizeHandler):
|
||||
def __init__(
|
||||
self,
|
||||
node_pattern: NodePattern,
|
||||
modules: dict[str, torch.nn.Module],
|
||||
root_node_getter: Callable | None = None,
|
||||
):
|
||||
super().__init__(node_pattern, modules, root_node_getter)
|
||||
if num_tensor_args_to_observation_type:
|
||||
if self.num_tensor_args not in num_tensor_args_to_observation_type:
|
||||
raise AssertionError(
|
||||
f"Must provide observation_type config for tensor number {self.num_tensor_args}"
|
||||
f" in num_tensor_args_to_observation_type for {node_pattern}"
|
||||
)
|
||||
self.observation_type = num_tensor_args_to_observation_type[
|
||||
self.num_tensor_args
|
||||
]
|
||||
else:
|
||||
self.observation_type = observation_type
|
||||
self.dtype_configs = dtype_configs
|
||||
|
||||
def is_general_tensor_value_op(self) -> bool:
|
||||
return (
|
||||
self.observation_type
|
||||
== ObservationType.OUTPUT_SHARE_OBSERVER_WITH_INPUT
|
||||
)
|
||||
|
||||
return ConfigurableQuantizeHandler
|
||||
|
||||
|
||||
def _get_pattern_to_quantize_handlers(
|
||||
backend_config: BackendConfig,
|
||||
) -> dict[Pattern, QuantizerCls]:
|
||||
"""
|
||||
Note: Quantize handler is just a holder for some check methods like
|
||||
(should_insert_observer_for_output), maybe this can be a enum as well,
|
||||
we can refactor this after we convert the path for fbgemm/qnnpack fully to the
|
||||
new path, this is not exposed to backend developers
|
||||
"""
|
||||
pattern_to_quantize_handlers = {}
|
||||
for pattern, config in backend_config._pattern_complex_format_to_config.items():
|
||||
observation_type = config.observation_type
|
||||
dtype_configs = config.dtype_configs
|
||||
num_tensor_args_to_observation_type = (
|
||||
config._num_tensor_args_to_observation_type
|
||||
)
|
||||
pattern_to_quantize_handlers[pattern] = _get_quantize_handler_cls(
|
||||
observation_type, dtype_configs, num_tensor_args_to_observation_type
|
||||
)
|
||||
return pattern_to_quantize_handlers
|
||||
|
||||
|
||||
# TODO: remove this class, this is still exposed in torch.ao.quantization
|
||||
# but we should be able to break bc
|
||||
class BinaryOpQuantizeHandler(QuantizeHandler):
|
||||
pass
|
||||
|
||||
|
||||
class CatQuantizeHandler(QuantizeHandler):
|
||||
pass
|
||||
|
||||
|
||||
# TODO: remove this class
|
||||
class ConvReluQuantizeHandler(QuantizeHandler):
|
||||
pass
|
||||
|
||||
|
||||
# TODO: remove this class
|
||||
class LinearReLUQuantizeHandler(QuantizeHandler):
|
||||
pass
|
||||
|
||||
|
||||
# TODO: remove this class
|
||||
class BatchNormQuantizeHandler(QuantizeHandler):
|
||||
pass
|
||||
|
||||
|
||||
# TODO: remove this class
|
||||
class EmbeddingQuantizeHandler(QuantizeHandler):
|
||||
pass
|
||||
|
||||
|
||||
# TODO: remove this class
|
||||
class RNNDynamicQuantizeHandler(QuantizeHandler):
|
||||
pass
|
||||
|
||||
|
||||
# TODO: remove this class
|
||||
class DefaultNodeQuantizeHandler(QuantizeHandler):
|
||||
"""Common quantized op, first input and first output will be quantized"""
|
||||
|
||||
|
||||
# TODO: remove this class
|
||||
class FixedQParamsOpQuantizeHandler(QuantizeHandler):
|
||||
pass
|
||||
|
||||
|
||||
# TODO: remove
|
||||
class CopyNodeQuantizeHandler(QuantizeHandler):
|
||||
pass
|
||||
|
||||
|
||||
# TODO: remove
|
||||
class GeneralTensorShapeOpQuantizeHandler(QuantizeHandler):
|
||||
pass
|
||||
|
||||
|
||||
# TODO: not used, can be removed after torch.ao.quantization namespace is deprecated
|
||||
class CustomModuleQuantizeHandler(QuantizeHandler):
|
||||
pass
|
||||
|
||||
|
||||
# TODO: not used, can be removed after torch.ao.quantization namespace is deprecated
|
||||
class StandaloneModuleQuantizeHandler(QuantizeHandler):
|
||||
pass
|
||||
@@ -0,0 +1,48 @@
|
||||
from collections.abc import Callable
|
||||
|
||||
import torch
|
||||
from torch.ao.nn.intrinsic import _FusedModule
|
||||
from torch.fx._symbolic_trace import Tracer
|
||||
from torch.fx.proxy import Scope
|
||||
|
||||
|
||||
__all__ = [
|
||||
"QuantizationTracer",
|
||||
]
|
||||
|
||||
|
||||
class ScopeContextManager(torch.fx.proxy.ScopeContextManager):
|
||||
def __init__(
|
||||
self, scope: Scope, current_module: torch.nn.Module, current_module_path: str
|
||||
):
|
||||
super().__init__(scope, Scope(current_module_path, type(current_module)))
|
||||
|
||||
|
||||
class QuantizationTracer(Tracer):
|
||||
def __init__(
|
||||
self, skipped_module_names: list[str], skipped_module_classes: list[Callable]
|
||||
):
|
||||
super().__init__()
|
||||
self.skipped_module_names = skipped_module_names
|
||||
self.skipped_module_classes = skipped_module_classes
|
||||
# NB: initialized the module_type of top level module to None
|
||||
# we are assuming people won't configure the model with the type of top level
|
||||
# module here, since people can use "" for global config
|
||||
# We can change this if there is a use case that configures
|
||||
# qconfig using top level module type
|
||||
self.scope = Scope("", None)
|
||||
self.record_stack_traces = not torch.fx.config.do_not_emit_stack_traces
|
||||
|
||||
def is_leaf_module(self, m: torch.nn.Module, module_qualified_name: str) -> bool:
|
||||
return (
|
||||
(
|
||||
(
|
||||
m.__module__.startswith("torch.nn")
|
||||
or m.__module__.startswith("torch.ao.nn")
|
||||
)
|
||||
and not isinstance(m, torch.nn.Sequential)
|
||||
)
|
||||
or module_qualified_name in self.skipped_module_names
|
||||
or type(m) in self.skipped_module_classes
|
||||
or isinstance(m, _FusedModule)
|
||||
)
|
||||
@@ -0,0 +1,998 @@
|
||||
# mypy: allow-untyped-defs
|
||||
import copy
|
||||
import functools
|
||||
import operator
|
||||
import warnings
|
||||
from collections import namedtuple
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
from torch.ao.quantization import QConfigAny, QuantType
|
||||
from torch.ao.quantization.backend_config import DTypeWithConstraints
|
||||
from torch.ao.quantization.fake_quantize import (
|
||||
FakeQuantizeBase,
|
||||
FixedQParamsFakeQuantize,
|
||||
)
|
||||
from torch.ao.quantization.observer import (
|
||||
_is_activation_post_process,
|
||||
FixedQParamsObserver,
|
||||
ObserverBase,
|
||||
)
|
||||
from torch.ao.quantization.qconfig import (
|
||||
float16_dynamic_qconfig,
|
||||
float16_static_qconfig,
|
||||
qconfig_equals,
|
||||
)
|
||||
from torch.ao.quantization.qconfig_mapping import QConfigMapping
|
||||
from torch.ao.quantization.stubs import DeQuantStub
|
||||
from torch.ao.quantization.utils import (
|
||||
_assert_and_get_unique_device,
|
||||
activation_is_statically_quantized,
|
||||
)
|
||||
from torch.fx import GraphModule, map_arg
|
||||
from torch.fx.graph import Graph, Node
|
||||
|
||||
# importing the lib so that the quantized_decomposed ops are registered
|
||||
from ._decomposed import quantized_decomposed_lib # noqa: F401
|
||||
from .custom_config import PrepareCustomConfig
|
||||
|
||||
|
||||
# TODO: revisit this list. Many helper methods shouldn't be public
|
||||
__all__ = [
|
||||
"all_node_args_except_first",
|
||||
"all_node_args_have_no_tensors",
|
||||
"assert_and_get_unique_device",
|
||||
"collect_producer_nodes",
|
||||
"create_getattr_from_value",
|
||||
"create_node_from_old_node_preserve_meta",
|
||||
"EMPTY_ARG_DICT",
|
||||
"get_custom_module_class_keys",
|
||||
"get_linear_prepack_op_for_dtype",
|
||||
"get_new_attr_name_with_prefix",
|
||||
"get_non_observable_arg_indexes_and_types",
|
||||
"get_qconv_prepack_op",
|
||||
"get_skipped_module_name_and_classes",
|
||||
"graph_module_from_producer_nodes",
|
||||
"maybe_get_next_module",
|
||||
"NodeInfo",
|
||||
"node_arg_is_bias",
|
||||
"node_arg_is_weight",
|
||||
"NON_OBSERVABLE_ARG_DICT",
|
||||
"NON_QUANTIZABLE_WEIGHT_OPS",
|
||||
"return_arg_list",
|
||||
"ObservedGraphModuleAttrs",
|
||||
]
|
||||
|
||||
NON_QUANTIZABLE_WEIGHT_OPS = {
|
||||
torch.nn.functional.layer_norm,
|
||||
torch.nn.functional.group_norm,
|
||||
torch.nn.functional.instance_norm,
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class ObservedGraphModuleAttrs:
|
||||
node_name_to_qconfig: dict[str, QConfigAny]
|
||||
node_name_to_scope: dict[str, tuple[str, type]]
|
||||
prepare_custom_config: PrepareCustomConfig
|
||||
equalization_node_name_to_qconfig: dict[str, Any]
|
||||
qconfig_mapping: QConfigMapping
|
||||
is_qat: bool
|
||||
observed_node_names: set[str]
|
||||
is_observed_standalone_module: bool = False
|
||||
standalone_module_input_quantized_idxs: list[int] | None = None
|
||||
standalone_module_output_quantized_idxs: list[int] | None = None
|
||||
|
||||
|
||||
def node_arg_is_weight(node: Node, arg: Any) -> bool:
|
||||
"""Returns if node arg is weight"""
|
||||
weight_index = None
|
||||
if "target_dtype_info" in node.meta:
|
||||
weight_index = node.meta["target_dtype_info"].get("weight_index", None)
|
||||
if (
|
||||
weight_index is not None
|
||||
and weight_index < len(node.args)
|
||||
and node.args[weight_index] is arg
|
||||
):
|
||||
return True
|
||||
return node.kwargs.get("weight") is arg
|
||||
|
||||
|
||||
def node_arg_is_bias(node: Node, arg: Any) -> bool:
|
||||
"""Returns if node arg is bias"""
|
||||
bias_index = None
|
||||
if "target_dtype_info" in node.meta:
|
||||
bias_index = node.meta["target_dtype_info"].get("bias_index", None)
|
||||
if (
|
||||
bias_index is not None
|
||||
and bias_index < len(node.args)
|
||||
and node.args[bias_index] is arg
|
||||
):
|
||||
return True
|
||||
return node.kwargs.get("bias") is arg
|
||||
|
||||
|
||||
def get_custom_module_class_keys(
|
||||
custom_module_mapping: dict[QuantType, dict[type, type]],
|
||||
) -> list[Any]:
|
||||
r"""Get all the unique custom module keys in the custom config dict
|
||||
e.g.
|
||||
Input:
|
||||
{
|
||||
QuantType.STATIC: {
|
||||
CustomModule1: ObservedCustomModule
|
||||
},
|
||||
QuantType.DYNAMIC: {
|
||||
CustomModule2: DynamicObservedCustomModule
|
||||
},
|
||||
QuantType.WEIGHT_ONLY: {
|
||||
CustomModule3: WeightOnlyObservedCustomModule
|
||||
},
|
||||
}
|
||||
|
||||
Output:
|
||||
# extract the keys across all inner STATIC, DYNAMIC, and WEIGHT_ONLY dicts
|
||||
[CustomModule1, CustomModule2, CustomModule3]
|
||||
"""
|
||||
# using set to dedup
|
||||
float_custom_module_classes: set[Any] = set()
|
||||
for quant_mode in [QuantType.STATIC, QuantType.DYNAMIC, QuantType.WEIGHT_ONLY]:
|
||||
quant_mode_custom_module_config = custom_module_mapping.get(quant_mode, {})
|
||||
quant_mode_custom_module_classes = set(quant_mode_custom_module_config.keys())
|
||||
float_custom_module_classes |= quant_mode_custom_module_classes
|
||||
return list(float_custom_module_classes)
|
||||
|
||||
|
||||
def get_linear_prepack_op_for_dtype(dtype):
|
||||
if dtype == torch.float16:
|
||||
return torch.ops.quantized.linear_prepack_fp16
|
||||
elif dtype == torch.qint8:
|
||||
return torch.ops.quantized.linear_prepack
|
||||
else:
|
||||
raise Exception("can't get linear prepack op for dtype:", dtype) # noqa: TRY002
|
||||
|
||||
|
||||
def get_qconv_prepack_op(conv_op: Callable) -> Callable:
|
||||
prepack_ops = {
|
||||
torch.nn.functional.conv1d: torch.ops.quantized.conv1d_prepack,
|
||||
torch.nn.functional.conv2d: torch.ops.quantized.conv2d_prepack,
|
||||
torch.nn.functional.conv3d: torch.ops.quantized.conv3d_prepack,
|
||||
torch.nn.functional.conv_transpose1d: torch.ops.quantized.conv_transpose1d_prepack,
|
||||
torch.nn.functional.conv_transpose2d: torch.ops.quantized.conv_transpose2d_prepack,
|
||||
torch.nn.functional.conv_transpose3d: torch.ops.quantized.conv_transpose3d_prepack,
|
||||
}
|
||||
prepack_op = prepack_ops.get(conv_op)
|
||||
if prepack_op is None:
|
||||
raise AssertionError(f"Didn't find prepack op for {conv_op}")
|
||||
return prepack_op
|
||||
|
||||
|
||||
# Returns a function that can get a new attribute name for module with given
|
||||
# prefix, for example,
|
||||
# >> get_new_observer_name = get_new_attr_name_with_prefix('_observer')
|
||||
# >> new_name = get_new_observer_name(module)
|
||||
# new_name will be an unused attribute name on module, e.g. `_observer_1`
|
||||
def get_new_attr_name_with_prefix(prefix: str) -> Callable:
|
||||
prefix = prefix.replace(".", "_")
|
||||
|
||||
def get_new_attr_name(module: torch.nn.Module):
|
||||
def get_attr_name(i: int):
|
||||
return prefix + str(i)
|
||||
|
||||
i = 0
|
||||
attr_name = get_attr_name(i)
|
||||
while hasattr(module, attr_name):
|
||||
i += 1
|
||||
attr_name = get_attr_name(i)
|
||||
return attr_name
|
||||
|
||||
return get_new_attr_name
|
||||
|
||||
|
||||
def collect_producer_nodes(node: Node) -> list[Node] | None:
|
||||
r"""Starting from a target node, trace back until we hit input or
|
||||
getattr node. This is used to extract the chain of operators
|
||||
starting from getattr to the target node, for example::
|
||||
|
||||
def forward(self, x):
|
||||
observed = self.observer(self.weight)
|
||||
return F.linear(x, observed)
|
||||
|
||||
collect_producer_nodes(observed) will either return a list of nodes that
|
||||
produces the observed node or None if we can't extract a self contained
|
||||
graph without free variables(inputs of the forward function).
|
||||
"""
|
||||
nodes = [node]
|
||||
frontier = [node]
|
||||
while frontier:
|
||||
node = frontier.pop()
|
||||
all_args = list(node.args) + list(node.kwargs.values())
|
||||
for arg in all_args:
|
||||
if not isinstance(arg, Node):
|
||||
continue
|
||||
if arg.op == "placeholder":
|
||||
# hit input, can't fold in this case
|
||||
return None
|
||||
nodes.append(arg)
|
||||
if not (arg.op == "call_function" and arg.target is getattr):
|
||||
frontier.append(arg)
|
||||
return nodes
|
||||
|
||||
|
||||
def graph_module_from_producer_nodes(
|
||||
root: GraphModule, producer_nodes: list[Node]
|
||||
) -> GraphModule:
|
||||
r"""Construct a graph module from extracted producer nodes
|
||||
from `collect_producer_nodes` function
|
||||
Args:
|
||||
root: the root module for the original graph
|
||||
producer_nodes: a list of nodes we use to construct the graph
|
||||
Return:
|
||||
A graph module constructed from the producer nodes
|
||||
"""
|
||||
if len(producer_nodes) == 0:
|
||||
raise AssertionError("list of producer nodes can not be empty")
|
||||
# since we traced back from node to getattr
|
||||
producer_nodes.reverse()
|
||||
graph = Graph()
|
||||
env: dict[Any, Any] = {}
|
||||
|
||||
def load_arg(a):
|
||||
return map_arg(a, lambda node: env[node])
|
||||
|
||||
for producer_node in producer_nodes:
|
||||
env[producer_node] = graph.node_copy(producer_node, load_arg)
|
||||
graph.output(load_arg(producer_nodes[-1]))
|
||||
graph_module = GraphModule(root, graph)
|
||||
return graph_module
|
||||
|
||||
|
||||
# TODO: delete
|
||||
@functools.cache
|
||||
def assert_and_get_unique_device(module: torch.nn.Module) -> Any:
|
||||
"""
|
||||
Returns the unique device for a module, or None if no device is found.
|
||||
Throws an error if multiple devices are detected.
|
||||
"""
|
||||
return _assert_and_get_unique_device(module)
|
||||
|
||||
|
||||
def create_getattr_from_value(
|
||||
module: torch.nn.Module,
|
||||
graph: Graph,
|
||||
prefix: str,
|
||||
value: Any,
|
||||
device: torch.device | None = None,
|
||||
) -> Node:
|
||||
"""
|
||||
Given a value of any type, creates a getattr node corresponding to the value and
|
||||
registers the value as a buffer to the module.
|
||||
"""
|
||||
get_new_attr_name = get_new_attr_name_with_prefix(prefix)
|
||||
attr_name = get_new_attr_name(module)
|
||||
if device is None:
|
||||
device = assert_and_get_unique_device(module)
|
||||
new_value = (
|
||||
value.detach().clone()
|
||||
if isinstance(value, torch.Tensor)
|
||||
else torch.tensor(value, device=device)
|
||||
)
|
||||
module.register_buffer(attr_name, new_value)
|
||||
# Create get_attr with value
|
||||
attr_node = graph.create_node("get_attr", attr_name)
|
||||
return attr_node
|
||||
|
||||
|
||||
def all_node_args_have_no_tensors(
|
||||
node: Node, modules: dict[str, torch.nn.Module], cache: dict[Node, bool]
|
||||
) -> bool:
|
||||
"""
|
||||
If we know for sure that all of this node's args have no
|
||||
tensors (are primitives), return True. If we either
|
||||
find a tensor or are not sure, return False. Note: this
|
||||
function is not exact.
|
||||
"""
|
||||
if cache and node in cache:
|
||||
return cache[node]
|
||||
|
||||
result = False # will be overwritten
|
||||
if not isinstance(node, Node):
|
||||
result = True
|
||||
elif node.op == "placeholder":
|
||||
result = False
|
||||
elif node.op == "call_module":
|
||||
if not isinstance(node.target, str):
|
||||
raise AssertionError("node.target must be a string for call_module nodes")
|
||||
if _is_activation_post_process(modules[node.target]):
|
||||
result = all_node_args_have_no_tensors(node.args[0], modules, cache) # type: ignore[arg-type]
|
||||
elif node.op == "call_module":
|
||||
result = False
|
||||
elif node.op == "call_function" and node.target is operator.getitem:
|
||||
result = all_node_args_have_no_tensors(node.args[0], modules, cache) # type: ignore[arg-type]
|
||||
elif node.op == "get_attr":
|
||||
result = False
|
||||
elif node.target is getattr and node.args[1] in ["ndim", "shape"]:
|
||||
# x1 = x0.ndim
|
||||
result = True
|
||||
elif node.op == "call_method" and node.target == "size":
|
||||
# x1 = x0.size(0)
|
||||
result = True
|
||||
else:
|
||||
found_one_tensor = False
|
||||
for arg in node.args:
|
||||
if isinstance(arg, list):
|
||||
for list_el in arg:
|
||||
if isinstance(list_el, Node):
|
||||
this_list_el_args_have_no_tensors = (
|
||||
all_node_args_have_no_tensors(list_el, modules, cache)
|
||||
)
|
||||
found_one_tensor = found_one_tensor or (
|
||||
not this_list_el_args_have_no_tensors
|
||||
)
|
||||
# If found_one_tensor is True, there is no point in
|
||||
# recursing further as the end result will always
|
||||
# be True.
|
||||
# TODO(future PR): remove this entire function and
|
||||
# change to dtype inference without recursion.
|
||||
if found_one_tensor:
|
||||
result = not found_one_tensor
|
||||
if cache:
|
||||
cache[node] = result
|
||||
return result
|
||||
elif isinstance(arg, int):
|
||||
pass
|
||||
else:
|
||||
if isinstance(arg, Node):
|
||||
this_arg_args_have_no_tensors = all_node_args_have_no_tensors(
|
||||
arg, modules, cache
|
||||
)
|
||||
found_one_tensor = found_one_tensor or (
|
||||
not this_arg_args_have_no_tensors
|
||||
)
|
||||
# If found_one_tensor is True, there is no point in
|
||||
# recursing further as the end result will always
|
||||
# be True.
|
||||
# TODO(future PR): remove this entire function and
|
||||
# change to dtype inference without recursion.
|
||||
if found_one_tensor:
|
||||
result = not found_one_tensor
|
||||
if cache:
|
||||
cache[node] = result
|
||||
return result
|
||||
else:
|
||||
found_one_tensor = True
|
||||
result = not found_one_tensor
|
||||
if cache:
|
||||
cache[node] = result
|
||||
return result
|
||||
|
||||
|
||||
def all_node_args_except_first(node: Node) -> list[int]:
|
||||
"""
|
||||
Returns all node arg indices after first
|
||||
"""
|
||||
return list(range(1, len(node.args)))
|
||||
|
||||
|
||||
def return_arg_list(arg_indices: list[int]) -> Callable[[Node], list[int]]:
|
||||
"""
|
||||
Constructs a function that takes a node as arg and returns the arg_indices
|
||||
that are valid for node.args
|
||||
"""
|
||||
|
||||
def arg_indices_func(node: Node) -> list[int]:
|
||||
return [i for i in arg_indices if i < len(node.args)]
|
||||
|
||||
return arg_indices_func
|
||||
|
||||
|
||||
NodeInfo = namedtuple("NodeInfo", "op target")
|
||||
|
||||
# this dict identifies which indices of a node are non tensors
|
||||
# so that they can be propagated correctly since inserting observers
|
||||
# for them would cause errors
|
||||
|
||||
NON_OBSERVABLE_ARG_DICT: dict[
|
||||
NodeInfo, dict[type | torch.dtype, Callable[[Node], list[int]]]
|
||||
] = {
|
||||
NodeInfo("call_method", "masked_fill"): {
|
||||
torch.bool: return_arg_list([1]),
|
||||
float: return_arg_list([2]),
|
||||
},
|
||||
NodeInfo("call_method", "permute"): {int: all_node_args_except_first},
|
||||
NodeInfo("call_method", "repeat"): {int: all_node_args_except_first},
|
||||
NodeInfo("call_method", "reshape"): {int: all_node_args_except_first},
|
||||
NodeInfo("call_method", "size"): {int: return_arg_list([1])},
|
||||
NodeInfo("call_method", "transpose"): {int: all_node_args_except_first},
|
||||
NodeInfo("call_method", torch.transpose): {int: all_node_args_except_first},
|
||||
NodeInfo("call_method", "unsqueeze"): {int: return_arg_list([1])},
|
||||
NodeInfo("call_method", "unsqueeze_"): {int: return_arg_list([1])},
|
||||
NodeInfo("call_method", torch.unsqueeze): {int: return_arg_list([1])},
|
||||
NodeInfo("call_method", "view"): {int: all_node_args_except_first},
|
||||
}
|
||||
|
||||
EMPTY_ARG_DICT: dict[type | torch.dtype, Callable[[Node], list[int]]] = {}
|
||||
|
||||
|
||||
def get_non_observable_arg_indexes_and_types(
|
||||
node: Node,
|
||||
) -> dict[type | torch.dtype, Callable[[Node], list[int]]]:
|
||||
"""
|
||||
Returns a dict with of non float tensor types as keys and values which correspond to a
|
||||
function to retrieve the list (which takes the node as an argument)
|
||||
"""
|
||||
info = NodeInfo(node.op, node.target)
|
||||
|
||||
return NON_OBSERVABLE_ARG_DICT.get(info, EMPTY_ARG_DICT)
|
||||
|
||||
|
||||
def maybe_get_next_module(
|
||||
node: Node,
|
||||
modules: dict[str, nn.Module],
|
||||
target_module_type: type[nn.Module] | None = None,
|
||||
target_functional_type: Any = None,
|
||||
) -> Node | None:
|
||||
"""Gets the next module that matches what is needed in
|
||||
is_target_module_type if it exists
|
||||
|
||||
Args:
|
||||
node: The node whose users we want to look at
|
||||
target_module_type: Module type that we want to check
|
||||
target_functional_type: Functional type that we want to check
|
||||
"""
|
||||
|
||||
for user in node.users:
|
||||
if (
|
||||
user.op == "call_module"
|
||||
and target_module_type is not None
|
||||
and isinstance(modules[str(user.target)], target_module_type)
|
||||
):
|
||||
return user
|
||||
elif (
|
||||
user.op == "call_function"
|
||||
and target_functional_type is not None
|
||||
and user.target == target_functional_type
|
||||
):
|
||||
return user
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def create_node_from_old_node_preserve_meta(
|
||||
quantized_graph: Graph,
|
||||
create_node_args: tuple[Any, ...],
|
||||
old_node: Node,
|
||||
) -> Node:
|
||||
"""
|
||||
Creates `new_node` and copies the necessary metadata to it from `old_node`.
|
||||
"""
|
||||
new_node = quantized_graph.create_node(*create_node_args)
|
||||
new_node.stack_trace = old_node.stack_trace
|
||||
return new_node
|
||||
|
||||
|
||||
def get_skipped_module_name_and_classes(
|
||||
prepare_custom_config: PrepareCustomConfig, is_standalone_module: bool
|
||||
) -> tuple[list[str], list[type[Any]]]:
|
||||
skipped_module_names = copy.copy(prepare_custom_config.non_traceable_module_names)
|
||||
skipped_module_classes = copy.copy(
|
||||
prepare_custom_config.non_traceable_module_classes
|
||||
)
|
||||
if not is_standalone_module:
|
||||
# standalone module and custom module config are applied in top level module
|
||||
skipped_module_names += list(
|
||||
prepare_custom_config.standalone_module_names.keys()
|
||||
)
|
||||
skipped_module_classes += list(
|
||||
prepare_custom_config.standalone_module_classes.keys()
|
||||
)
|
||||
skipped_module_classes += get_custom_module_class_keys(
|
||||
prepare_custom_config.float_to_observed_mapping
|
||||
)
|
||||
|
||||
return skipped_module_names, skipped_module_classes
|
||||
|
||||
|
||||
def _is_custom_module_lstm(
|
||||
node: Node,
|
||||
named_modules: dict[str, torch.nn.Module],
|
||||
qconfig: QConfigAny = None,
|
||||
# QuantizeHandler, but we cannot include the type here due to circular imports
|
||||
qhandler: Any | None = None,
|
||||
) -> bool:
|
||||
"""
|
||||
Return whether this refers to the custom module LSTM flow.
|
||||
"""
|
||||
mod = _get_module(node, named_modules)
|
||||
if qconfig is not None and qhandler is not None:
|
||||
if not isinstance(
|
||||
qhandler, torch.ao.quantization.fx.quantize_handler.QuantizeHandler
|
||||
): # type: ignore[attr-defined]
|
||||
raise AssertionError("qhandler must be a QuantizeHandler when provided")
|
||||
return (
|
||||
isinstance(mod, torch.nn.LSTM)
|
||||
and activation_is_statically_quantized(qconfig)
|
||||
and qhandler.is_custom_module()
|
||||
)
|
||||
else:
|
||||
return isinstance(mod, torch.ao.nn.quantizable.LSTM)
|
||||
|
||||
|
||||
def _is_custom_module_mha(
|
||||
node: Node,
|
||||
named_modules: dict[str, torch.nn.Module],
|
||||
qconfig: QConfigAny = None,
|
||||
# QuantizeHandler, but we cannot include the type here due to circular imports
|
||||
qhandler: Any | None = None,
|
||||
) -> bool:
|
||||
"""
|
||||
Return whether this refers to the custom module MultiheadAttention flow.
|
||||
"""
|
||||
mod = _get_module(node, named_modules)
|
||||
if qconfig is not None and qhandler is not None:
|
||||
if not isinstance(
|
||||
qhandler, torch.ao.quantization.fx.quantize_handler.QuantizeHandler
|
||||
): # type: ignore[attr-defined]
|
||||
raise AssertionError("qhandler must be a QuantizeHandler when provided")
|
||||
return (
|
||||
isinstance(mod, torch.nn.MultiheadAttention)
|
||||
and activation_is_statically_quantized(qconfig)
|
||||
and qhandler.is_custom_module()
|
||||
)
|
||||
else:
|
||||
return isinstance(mod, torch.ao.nn.quantizable.MultiheadAttention)
|
||||
|
||||
|
||||
def _get_module(
|
||||
node: Node, named_modules: dict[str, torch.nn.Module]
|
||||
) -> torch.nn.Module | None:
|
||||
"""
|
||||
If `node` refers to a call_module node, return the module, else None.
|
||||
"""
|
||||
if node.op == "call_module" and str(node.target) in named_modules:
|
||||
return named_modules[str(node.target)]
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def _insert_dequant_stub(
|
||||
node: Node,
|
||||
model: torch.nn.Module,
|
||||
named_modules: dict[str, torch.nn.Module],
|
||||
graph: Graph,
|
||||
) -> Node:
|
||||
"""
|
||||
Attach a `DeQuantStub` to the model and create a node that calls this
|
||||
`DeQuantStub` on the output of `node`, similar to how observers are inserted.
|
||||
"""
|
||||
prefix = "dequant_stub_"
|
||||
get_new_dequant_stub_name = get_new_attr_name_with_prefix(prefix)
|
||||
dequant_stub_name = get_new_dequant_stub_name(model)
|
||||
dequant_stub = DeQuantStub()
|
||||
setattr(model, dequant_stub_name, dequant_stub)
|
||||
named_modules[dequant_stub_name] = dequant_stub
|
||||
with graph.inserting_after(node):
|
||||
return graph.call_module(dequant_stub_name, (node,))
|
||||
|
||||
|
||||
def _insert_dequant_stubs_for_custom_module_lstm_output(
|
||||
node: Node,
|
||||
model: torch.nn.Module,
|
||||
named_modules: dict[str, torch.nn.Module],
|
||||
graph: Graph,
|
||||
) -> Node:
|
||||
"""
|
||||
Insert DeQuantStubs after each internal output node of custom module LSTM.
|
||||
|
||||
Custom module LSTM outputs are nested tuples of the structure (output, (hidden0, hidden1)),
|
||||
Since we cannot dequantize a tuple as a whole, we must first break down the tuple into its
|
||||
components through `getitem`. This function transforms the graph as follows:
|
||||
|
||||
(1) Split the LSTM node into (output, (hidden0, hidden1))
|
||||
(2) Insert a DeQuantStub after each internal node
|
||||
(3) Recombine the DeQuantStubs into the same structure as before
|
||||
(4) Reroute all consumers of the original LSTM node and its sub-nodes
|
||||
(e.g. lstm[0])
|
||||
|
||||
Before:
|
||||
lstm_output
|
||||
|
|
||||
v
|
||||
original_user(s)
|
||||
After:
|
||||
lstm_output
|
||||
/ \\
|
||||
/ (getitem) \\
|
||||
/ \\
|
||||
v v
|
||||
output hidden
|
||||
| / \\
|
||||
(DeQuantStub) (getitem)
|
||||
| / \\
|
||||
v v v
|
||||
output_dq hidden0 hidden1
|
||||
| | |
|
||||
| (DeQuantStub) (DeQuantStub)
|
||||
| | |
|
||||
| v v
|
||||
| hidden0_dq hidden1_dq
|
||||
| \\ /
|
||||
| (tuple)
|
||||
| \\ /
|
||||
| v v
|
||||
| hidden_dq
|
||||
\\ /
|
||||
\\ (tuple) /
|
||||
v v
|
||||
lstm_output_dq
|
||||
|
|
||||
v
|
||||
original_user(s)
|
||||
|
||||
For step (4), reroute all users of the original LSTM node(s) as follows:
|
||||
lstm_output -> lstm_output_dq
|
||||
lstm_output[0] -> output_dq
|
||||
lstm_output[1] -> hidden_dq
|
||||
lstm_output[1][0] -> hidden0_dq
|
||||
lstm_output[1][1] -> hidden1_dq
|
||||
|
||||
Return the node `lstm_output_dq`.
|
||||
"""
|
||||
# (1) Split the LSTM node into (output, (hidden0, hidden1))
|
||||
# (2) Insert a DeQuantStub after each internal node
|
||||
with graph.inserting_after(node):
|
||||
output = graph.call_function(operator.getitem, (node, 0))
|
||||
output_dq = _insert_dequant_stub(output, model, named_modules, graph)
|
||||
with graph.inserting_after(output_dq):
|
||||
hidden = graph.call_function(operator.getitem, (node, 1))
|
||||
with graph.inserting_after(hidden):
|
||||
hidden0 = graph.call_function(operator.getitem, (hidden, 0))
|
||||
hidden0_dq = _insert_dequant_stub(hidden0, model, named_modules, graph)
|
||||
with graph.inserting_after(hidden0_dq):
|
||||
hidden1 = graph.call_function(operator.getitem, (hidden, 1))
|
||||
hidden1_dq = _insert_dequant_stub(hidden1, model, named_modules, graph)
|
||||
|
||||
# (3) Recombine the DeQuantStubs into the same structure as before
|
||||
with graph.inserting_after(hidden1_dq):
|
||||
hidden_dq = graph.call_function(tuple, ([hidden0_dq, hidden1_dq],))
|
||||
with graph.inserting_after(hidden_dq):
|
||||
lstm_output_dq = graph.call_function(tuple, ([output_dq, hidden_dq],))
|
||||
|
||||
# (4) Reroute all consumers of the original LSTM node and its sub-nodes
|
||||
for user in list(node.users.keys()):
|
||||
if user != output and user != hidden:
|
||||
user.replace_input_with(node, lstm_output_dq)
|
||||
# The getitem and tuple nodes we added here may interfere with reference quantized
|
||||
# pattern matching, so we need to redirect the consumers of internal nodes to the
|
||||
# corresponding nodes with DeQuantStubs (e.g. lstm_output_dq[0] -> output_dq) attached,
|
||||
# in order to preserve reference patterns like "dequantize - consumer - quantize".
|
||||
_reroute_tuple_getitem_pattern(graph)
|
||||
return lstm_output_dq
|
||||
|
||||
|
||||
def _maybe_get_custom_module_lstm_from_node_arg(
|
||||
arg: Node,
|
||||
named_modules: dict[str, torch.nn.Module],
|
||||
) -> Node | None:
|
||||
"""
|
||||
Given an argument of a node, if the argument refers to the path through which the node
|
||||
is a consumer of custom module LSTM, return the custom module LSTM node, or None otherwise.
|
||||
|
||||
This is used to determine whether a node is a consumer of custom module LSTM, and, if so,
|
||||
skip inserting input observers for this node. This is because custom module LSTM produces
|
||||
quantized outputs, so inserting an input observer for the consumer of custom module LSTM
|
||||
would unnecessarily quantize the outputs again.
|
||||
|
||||
lstm -> consumer
|
||||
|
||||
In practice, however, custom module LSTM outputs a tuple (output, (hidden0, hidden1)) with
|
||||
DeQuantStubs attached to each internal node (see `_insert_dequant_stubs_for_custom_module_lstm_output`).
|
||||
This tuple can be consumed in one of four ways:
|
||||
|
||||
lstm -> getitem -> DeQuantStub -> consumer # consume lstm[0]
|
||||
lstm -> getitem -> getitem -> DeQuantStub -> tuple -> consumer # consume lstm[1]
|
||||
lstm -> getitem -> getitem -> DeQuantStub -> consumer # consume lstm[1][0] or lstm[1][1]
|
||||
lstm -> getitem -> DeQuantStub -> tuple -> consumer # consume lstm
|
||||
|
||||
Thus, we must match against the above patterns instead of simply checking the parent node
|
||||
to determine whether this node is a consumer of a custom module LSTM.
|
||||
"""
|
||||
|
||||
def match_dq(a):
|
||||
return isinstance(_get_module(a, named_modules), DeQuantStub)
|
||||
|
||||
def match_lstm(a):
|
||||
return _is_custom_module_lstm(a, named_modules)
|
||||
|
||||
def match_getitem(a):
|
||||
return a.op == "call_function" and a.target is operator.getitem
|
||||
|
||||
def match_tuple(a):
|
||||
return a.op == "call_function" and a.target is tuple
|
||||
|
||||
def _match_pattern(match_pattern: list[Callable]) -> Node | None:
|
||||
"""
|
||||
Traverse up the graph and match the args one by one.
|
||||
If there is a match, return the last matched node, or None otherwise.
|
||||
"""
|
||||
a = arg
|
||||
# pyrefly: ignore [bad-assignment]
|
||||
for i, match in enumerate(match_pattern):
|
||||
if not match(a):
|
||||
return None
|
||||
# Match next arg, for tuple the arg is a tuple of a list, e.g. ([dq_1, other_node],)
|
||||
if i < len(match_pattern) - 1:
|
||||
if match is match_tuple:
|
||||
a = a.args[0][0] # type: ignore[assignment,index]
|
||||
else:
|
||||
a = a.args[0] # type: ignore[assignment]
|
||||
# pyrefly: ignore [bad-return]
|
||||
return a
|
||||
|
||||
all_match_patterns = [
|
||||
[match_dq, match_getitem, match_lstm],
|
||||
[match_tuple, match_dq, match_getitem, match_getitem, match_lstm],
|
||||
[match_dq, match_getitem, match_getitem, match_lstm],
|
||||
[match_tuple, match_dq, match_getitem, match_lstm],
|
||||
]
|
||||
|
||||
for p in all_match_patterns:
|
||||
matched_node = _match_pattern(p)
|
||||
if matched_node is not None:
|
||||
return matched_node
|
||||
return None
|
||||
|
||||
|
||||
def _reroute_tuple_getitem_pattern(graph: Graph):
|
||||
"""
|
||||
Search for patterns where N consecutive `tuple` call_function nodes are followed by
|
||||
N consecutive `getitem` call_function nodes that are "reverses" of the `tuple` nodes.
|
||||
If we find this pattern, reroute the consumers of the last `getitem` to skip these
|
||||
N `tuple` and `getitem` nodes.
|
||||
|
||||
Before:
|
||||
|
||||
a b c
|
||||
| \\ /
|
||||
\\ tuple
|
||||
\\ /
|
||||
tuple
|
||||
|
|
||||
getitem(1)
|
||||
|
|
||||
getitem(0)
|
||||
|
|
||||
d
|
||||
|
||||
After:
|
||||
|
||||
b
|
||||
|
|
||||
d
|
||||
"""
|
||||
|
||||
def find_patterns(
|
||||
node: Node,
|
||||
index_stack: list[int],
|
||||
current_pattern: list[Node],
|
||||
matched_patterns: list[list[Node]],
|
||||
seen: set[tuple[Node, tuple[int, ...]]],
|
||||
):
|
||||
"""
|
||||
Traverse the graph recursively to match for the N-tuple - N-getitem patterns,
|
||||
starting at the given node.
|
||||
|
||||
We use a stack to keep track of the expected `getitem` indices, since these are
|
||||
reversed from the `tuple` indices. In the above example, the stack after
|
||||
(b -> tuple -> tuple) will be [0, 1], which will be popped by getitem(1) first
|
||||
and then by getitem(0).
|
||||
|
||||
TODO: traverse upwards from the output and handle the case when tuple is not a
|
||||
separate node, e.g. graph.call_function(operator.getitem, args=(a, (b, c)))
|
||||
"""
|
||||
if len(index_stack) == 0 and len(current_pattern) > 0:
|
||||
matched_patterns.append(copy.copy(current_pattern))
|
||||
current_pattern.clear()
|
||||
|
||||
# Avoid duplicating work
|
||||
state = (node, tuple(index_stack))
|
||||
if state in seen:
|
||||
return
|
||||
seen.add(state)
|
||||
|
||||
# Iterate through users of this node to find tuple/getitem nodes to match
|
||||
for user in node.users:
|
||||
if user.op == "call_function" and user.target is tuple:
|
||||
for i, user_arg in enumerate(user.args[0]): # type: ignore[arg-type]
|
||||
if user_arg == node:
|
||||
index_stack.append(i)
|
||||
current_pattern.append(user)
|
||||
find_patterns(
|
||||
user, index_stack, current_pattern, matched_patterns, seen
|
||||
)
|
||||
elif user.op == "call_function" and user.target is operator.getitem:
|
||||
if len(index_stack) > 0:
|
||||
if user.args[1] == index_stack[-1]:
|
||||
index_stack.pop()
|
||||
current_pattern.append(user)
|
||||
find_patterns(
|
||||
user, index_stack, current_pattern, matched_patterns, seen
|
||||
)
|
||||
return matched_patterns
|
||||
|
||||
# Collect all matched patterns
|
||||
matched_patterns: list[list[Node]] = []
|
||||
seen: set[tuple[Node, tuple[int, ...]]] = set() # (node, index_stack)
|
||||
for node in graph.nodes:
|
||||
find_patterns(node, [], [], matched_patterns, seen)
|
||||
|
||||
# For each pattern, redirect all consumers of the last getitem node to the correct input
|
||||
# of the first tuple node
|
||||
for pattern in matched_patterns:
|
||||
first_tuple = pattern[0]
|
||||
last_getitem = pattern[-1]
|
||||
if not (first_tuple.op == "call_function" and first_tuple.target is tuple):
|
||||
raise AssertionError(
|
||||
"first tuple node must be a call_function with target tuple"
|
||||
)
|
||||
if not (
|
||||
last_getitem.op == "call_function"
|
||||
and last_getitem.target is operator.getitem
|
||||
):
|
||||
raise AssertionError(
|
||||
"last getitem node must be a call_function with target operator.getitem"
|
||||
)
|
||||
last_getitem_index = last_getitem.args[1]
|
||||
new_input = first_tuple.args[0][last_getitem_index] # type: ignore[index]
|
||||
for user in list(last_getitem.users.keys()):
|
||||
user.replace_input_with(last_getitem, new_input) # type: ignore[arg-type]
|
||||
|
||||
|
||||
def _get_observer_from_activation_post_process(
|
||||
activation_post_process: ObserverBase | FakeQuantizeBase,
|
||||
) -> ObserverBase:
|
||||
"""
|
||||
If `activation_post_process` is an observer, return the observer.
|
||||
If `activation_post_process` is a fake quantize, return the internal observer.
|
||||
"""
|
||||
if isinstance(activation_post_process, ObserverBase):
|
||||
return activation_post_process
|
||||
else:
|
||||
if not isinstance(activation_post_process, FakeQuantizeBase):
|
||||
raise AssertionError(
|
||||
"activation_post_process must be an ObserverBase or FakeQuantizeBase"
|
||||
)
|
||||
return activation_post_process.activation_post_process # type: ignore[return-value]
|
||||
|
||||
|
||||
def _qconfig_satisfies_dtype_config_constraints(
|
||||
qconfig: QConfigAny,
|
||||
dtype_with_constraints: DTypeWithConstraints,
|
||||
is_activation: bool = True,
|
||||
) -> bool:
|
||||
"""
|
||||
Return whether `qconfig` satisfies the following constraints from the backend,
|
||||
specified through the activation and weight DTypeWithConstraints.
|
||||
|
||||
1. QConfig specified a quantization range that falls within the backend's, if any
|
||||
2. QConfig specified a min scale value that is >= the backend's, if any
|
||||
3. QConfig specified a FixedQParamsObserver or FixedQParamsFakeQuantize that has
|
||||
scale and zero point that match the backend's, if any
|
||||
|
||||
If `is_activation` is True, we check `qconfig.activation`, else we check `qconfig.weight`.
|
||||
If `qconfig` or `dtype_with_constraints.dtype` is None, or the dtypes do not match, return True.
|
||||
"""
|
||||
|
||||
# TODO: log warnings only when the user enabled a debug flag
|
||||
def _activation_post_process_satisfies_dtype_config_constraints(
|
||||
activation_post_process: ObserverBase | FakeQuantizeBase,
|
||||
dtype_with_constraints: DTypeWithConstraints,
|
||||
debug_string: str,
|
||||
) -> bool:
|
||||
observer = _get_observer_from_activation_post_process(activation_post_process)
|
||||
app_quant_min = getattr(observer, "quant_min", None)
|
||||
app_quant_max = getattr(observer, "quant_max", None)
|
||||
# TODO: for now, just use the existing eps value as scale_min. In the future, we should
|
||||
# resolve the differences between the two, either by renaming eps or some other way
|
||||
app_scale_min = getattr(observer, "eps", None)
|
||||
backend_quant_min = dtype_with_constraints.quant_min_lower_bound
|
||||
backend_quant_max = dtype_with_constraints.quant_max_upper_bound
|
||||
backend_scale_min = dtype_with_constraints.scale_min_lower_bound
|
||||
backend_scale_exact_match = dtype_with_constraints.scale_exact_match
|
||||
backend_zero_point_exact_match = dtype_with_constraints.zero_point_exact_match
|
||||
# check quantization ranges
|
||||
if backend_quant_min is not None and backend_quant_max is not None:
|
||||
if app_quant_min is None or app_quant_max is None:
|
||||
warnings.warn(
|
||||
f"QConfig {debug_string} must specify 'quant_min' and 'quant_max', ignoring {qconfig}",
|
||||
stacklevel=2,
|
||||
)
|
||||
return False
|
||||
elif app_quant_min < backend_quant_min or app_quant_max > backend_quant_max:
|
||||
warnings.warn(
|
||||
f"QConfig {debug_string} quantization range must fall within the backend's:\n"
|
||||
f"QConfig range = ({app_quant_min}, {app_quant_max}), "
|
||||
f"BackendConfig range = ({backend_quant_min}, {backend_quant_max}), "
|
||||
f"ignoring {qconfig}",
|
||||
stacklevel=2,
|
||||
)
|
||||
return False
|
||||
# check scale min
|
||||
if backend_scale_min is not None:
|
||||
if app_scale_min is None:
|
||||
warnings.warn(
|
||||
f"QConfig {debug_string} must specify 'eps', ignoring {qconfig}",
|
||||
stacklevel=2,
|
||||
)
|
||||
return False
|
||||
if app_scale_min < backend_scale_min:
|
||||
warnings.warn(
|
||||
f"QConfig {debug_string} eps ({app_scale_min}) must be greater than or equal to "
|
||||
f"the backend's min scale value ({backend_scale_min}), ignoring {qconfig}",
|
||||
stacklevel=2,
|
||||
)
|
||||
return False
|
||||
# check fixed scale and zero point
|
||||
if (
|
||||
backend_scale_exact_match is not None
|
||||
and backend_zero_point_exact_match is not None
|
||||
):
|
||||
# For tests only, accept the following qconfigs for now
|
||||
# TODO: handle fp16 qconfigs properly
|
||||
for accepted_qconfig in [float16_static_qconfig, float16_dynamic_qconfig]:
|
||||
if qconfig_equals(qconfig, accepted_qconfig):
|
||||
return True
|
||||
suggestion_str = (
|
||||
"Please use torch.ao.quantization.get_default_qconfig_mapping or "
|
||||
"torch.ao.quantization.get_default_qat_qconfig_mapping. Example:\n"
|
||||
' qconfig_mapping = get_default_qconfig_mapping("fbgemm")\n'
|
||||
" model = prepare_fx(model, qconfig_mapping, example_inputs)"
|
||||
)
|
||||
if not isinstance(
|
||||
activation_post_process, FixedQParamsObserver
|
||||
) and not isinstance(activation_post_process, FixedQParamsFakeQuantize):
|
||||
warnings.warn(
|
||||
f"QConfig must specify a FixedQParamsObserver or a FixedQParamsFakeQuantize "
|
||||
f"for fixed qparams ops, ignoring {qconfig}.\n{suggestion_str}",
|
||||
stacklevel=2,
|
||||
)
|
||||
return False
|
||||
if (
|
||||
observer.scale != backend_scale_exact_match
|
||||
or observer.zero_point != backend_zero_point_exact_match
|
||||
):
|
||||
warnings.warn(
|
||||
f"QConfig fixed scale ({observer.scale}) and zero point ({observer.zero_point}) "
|
||||
f"do not match the backend's ({backend_scale_exact_match} and {backend_zero_point_exact_match}), "
|
||||
f"ignoring {qconfig}.\n{suggestion_str}",
|
||||
stacklevel=2,
|
||||
)
|
||||
return False
|
||||
return True
|
||||
|
||||
if qconfig is None or dtype_with_constraints.dtype is None:
|
||||
return True
|
||||
|
||||
activation_post_process_ctr = (
|
||||
qconfig.activation if is_activation else qconfig.weight
|
||||
)
|
||||
debug_string = "activation" if is_activation else "weight"
|
||||
satisfies_constraints = True
|
||||
if activation_post_process_ctr is not None:
|
||||
activation_post_process = activation_post_process_ctr()
|
||||
if not _is_activation_post_process(activation_post_process):
|
||||
raise AssertionError(
|
||||
"activation_post_process must be an activation post process"
|
||||
)
|
||||
# If dtypes don't match, don't check the activation_post_process and return True early
|
||||
if activation_post_process.dtype != dtype_with_constraints.dtype:
|
||||
return True
|
||||
satisfies_constraints = (
|
||||
_activation_post_process_satisfies_dtype_config_constraints(
|
||||
activation_post_process, dtype_with_constraints, debug_string
|
||||
)
|
||||
)
|
||||
return satisfies_constraints
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,708 @@
|
||||
# mypy: allow-untyped-defs
|
||||
import copy
|
||||
import warnings
|
||||
from collections import namedtuple
|
||||
from typing import Any
|
||||
from typing_extensions import deprecated, TypeAliasType
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
from torch.ao.quantization.fake_quantize import (
|
||||
default_dynamic_fake_quant,
|
||||
default_embedding_fake_quant,
|
||||
default_embedding_fake_quant_4bit,
|
||||
default_fake_quant,
|
||||
default_fused_act_fake_quant,
|
||||
default_fused_per_channel_wt_fake_quant,
|
||||
default_fused_wt_fake_quant,
|
||||
default_per_channel_weight_fake_quant,
|
||||
default_weight_fake_quant,
|
||||
FakeQuantize,
|
||||
FakeQuantizeBase,
|
||||
fused_per_channel_wt_fake_quant_range_neg_127_to_127,
|
||||
fused_wt_fake_quant_range_neg_127_to_127,
|
||||
FusedMovingAvgObsFakeQuantize,
|
||||
)
|
||||
|
||||
from .observer import (
|
||||
_PartialWrapper,
|
||||
default_debug_observer,
|
||||
default_dynamic_quant_observer,
|
||||
default_float_qparams_observer,
|
||||
default_float_qparams_observer_4bit,
|
||||
default_observer,
|
||||
default_per_channel_weight_observer,
|
||||
default_placeholder_observer,
|
||||
default_reuse_input_observer,
|
||||
default_weight_observer,
|
||||
HistogramObserver,
|
||||
MinMaxObserver,
|
||||
MovingAverageMinMaxObserver,
|
||||
NoopObserver,
|
||||
ObserverBase,
|
||||
per_channel_weight_observer_range_neg_127_to_127,
|
||||
PlaceholderObserver,
|
||||
ReuseInputObserver,
|
||||
weight_observer_range_neg_127_to_127,
|
||||
)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"QConfig",
|
||||
# TODO: deprecated, remove
|
||||
"QConfigDynamic",
|
||||
"default_qconfig",
|
||||
"default_debug_qconfig",
|
||||
"default_per_channel_qconfig",
|
||||
"default_dynamic_qconfig",
|
||||
"float16_dynamic_qconfig",
|
||||
"float16_static_qconfig",
|
||||
"per_channel_dynamic_qconfig",
|
||||
"float_qparams_weight_only_qconfig",
|
||||
"float_qparams_weight_only_qconfig_4bit",
|
||||
"default_quint8_weight_qconfig",
|
||||
"default_qat_qconfig",
|
||||
"default_dynamic_qat_qconfig",
|
||||
"default_weight_only_qconfig",
|
||||
"default_activation_only_qconfig",
|
||||
"default_qat_qconfig_v2",
|
||||
"default_reuse_input_qconfig",
|
||||
"default_symmetric_qnnpack_qconfig",
|
||||
"default_per_channel_symmetric_qnnpack_qconfig",
|
||||
"default_symmetric_qnnpack_qat_qconfig",
|
||||
"default_per_channel_symmetric_qnnpack_qat_qconfig",
|
||||
"default_embedding_qat_qconfig",
|
||||
"default_embedding_qat_qconfig_4bit",
|
||||
"get_default_qconfig",
|
||||
"get_default_qat_qconfig",
|
||||
"get_default_qconfig_dict",
|
||||
"get_default_qat_qconfig_dict",
|
||||
"QConfigAny",
|
||||
"qconfig_equals",
|
||||
]
|
||||
|
||||
|
||||
# pyrefly: ignore [invalid-inheritance]
|
||||
class QConfig(namedtuple("QConfig", ["activation", "weight"])):
|
||||
"""
|
||||
Describes how to quantize a layer or a part of the network by providing
|
||||
settings (observer classes) for activations and weights respectively.
|
||||
|
||||
|
||||
Note that QConfig needs to contain observer **classes** (like MinMaxObserver) or a callable that returns
|
||||
instances on invocation, not the concrete observer instances themselves.
|
||||
Quantization preparation function will instantiate observers multiple times for each of the layers.
|
||||
|
||||
|
||||
Observer classes have usually reasonable default arguments, but they can be overwritten with `with_args`
|
||||
method (that behaves like functools.partial)::
|
||||
|
||||
my_qconfig = QConfig(
|
||||
activation=MinMaxObserver.with_args(dtype=torch.qint8),
|
||||
weight=default_observer.with_args(dtype=torch.qint8),
|
||||
)
|
||||
|
||||
"""
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
def __new__(cls, activation, weight):
|
||||
# catch common mistakes
|
||||
if isinstance(activation, nn.Module) or isinstance(weight, nn.Module):
|
||||
raise ValueError(
|
||||
"QConfig received observer instance, please pass observer class instead. "
|
||||
+ "Use MyObserver.with_args(x=1) to override arguments to constructor if needed"
|
||||
)
|
||||
return super().__new__(cls, activation, weight)
|
||||
|
||||
|
||||
@deprecated(
|
||||
"`QConfigDynamic` is going to be deprecated in PyTorch 1.12, please use `QConfig` instead",
|
||||
category=FutureWarning,
|
||||
)
|
||||
# pyrefly: ignore [invalid-inheritance]
|
||||
class QConfigDynamic(namedtuple("QConfigDynamic", ["activation", "weight"])):
|
||||
"""
|
||||
Describes how to dynamically quantize a layer or a part of the network by providing
|
||||
settings (observer classes) for weights.
|
||||
|
||||
It's like QConfig, but for dynamic quantization.
|
||||
|
||||
Note that QConfigDynamic needs to contain observer **classes** (like MinMaxObserver) or a callable that returns
|
||||
instances on invocation, not the concrete observer instances themselves.
|
||||
Quantization function will instantiate observers multiple times for each of the layers.
|
||||
|
||||
Observer classes have usually reasonable default arguments, but they can be overwritten with `with_args`
|
||||
method (that behaves like functools.partial)::
|
||||
|
||||
my_qconfig = QConfigDynamic(weight=default_observer.with_args(dtype=torch.qint8))
|
||||
"""
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
def __new__(cls, activation=torch.nn.Identity, weight=torch.nn.Identity):
|
||||
# catch common mistakes
|
||||
if isinstance(weight, nn.Module):
|
||||
raise ValueError(
|
||||
"QConfigDynamic received observer instance, please pass observer class instead. "
|
||||
+ "Use MyObserver.with_args(x=1) to override arguments to constructor if needed"
|
||||
)
|
||||
return super().__new__(cls, activation, weight)
|
||||
|
||||
|
||||
default_qconfig = QConfig(activation=default_observer, weight=default_weight_observer)
|
||||
"""
|
||||
Default qconfig configuration.
|
||||
"""
|
||||
|
||||
default_debug_qconfig = QConfig(
|
||||
weight=default_weight_observer, activation=default_debug_observer
|
||||
)
|
||||
"""
|
||||
Default qconfig configuration for debugging.
|
||||
"""
|
||||
|
||||
default_per_channel_qconfig = QConfig(
|
||||
activation=default_observer, weight=default_per_channel_weight_observer
|
||||
)
|
||||
"""
|
||||
Default qconfig configuration for per channel weight quantization.
|
||||
"""
|
||||
|
||||
default_dynamic_qconfig = QConfig(
|
||||
activation=default_dynamic_quant_observer, weight=default_weight_observer
|
||||
)
|
||||
"""
|
||||
Default dynamic qconfig.
|
||||
"""
|
||||
|
||||
float16_dynamic_qconfig = QConfig(
|
||||
activation=PlaceholderObserver.with_args(dtype=torch.float16, is_dynamic=True),
|
||||
weight=PlaceholderObserver.with_args(dtype=torch.float16),
|
||||
)
|
||||
"""
|
||||
Dynamic qconfig with weights quantized to `torch.float16`.
|
||||
"""
|
||||
|
||||
float16_static_qconfig = QConfig(
|
||||
activation=PlaceholderObserver.with_args(dtype=torch.float16),
|
||||
weight=PlaceholderObserver.with_args(dtype=torch.float16),
|
||||
)
|
||||
"""
|
||||
Dynamic qconfig with both activations and weights quantized to `torch.float16`.
|
||||
"""
|
||||
|
||||
per_channel_dynamic_qconfig = QConfig(
|
||||
activation=default_dynamic_quant_observer,
|
||||
weight=default_per_channel_weight_observer,
|
||||
)
|
||||
"""
|
||||
Dynamic qconfig with weights quantized per channel.
|
||||
"""
|
||||
|
||||
float_qparams_weight_only_qconfig = QConfig(
|
||||
activation=default_placeholder_observer, weight=default_float_qparams_observer
|
||||
)
|
||||
"""
|
||||
Dynamic qconfig with weights quantized with a floating point zero_point.
|
||||
"""
|
||||
|
||||
float_qparams_weight_only_qconfig_4bit = QConfig(
|
||||
activation=default_placeholder_observer, weight=default_float_qparams_observer_4bit
|
||||
)
|
||||
|
||||
default_qat_qconfig = QConfig(
|
||||
activation=default_fake_quant, weight=default_weight_fake_quant
|
||||
)
|
||||
"""
|
||||
Default qconfig for QAT.
|
||||
"""
|
||||
|
||||
default_dynamic_qat_qconfig = QConfig(
|
||||
activation=default_dynamic_fake_quant, weight=default_weight_fake_quant
|
||||
)
|
||||
"""
|
||||
Default qconfig for dynamic QAT.
|
||||
"""
|
||||
|
||||
default_weight_only_qconfig = QConfig(
|
||||
activation=torch.nn.Identity, weight=default_weight_fake_quant
|
||||
)
|
||||
"""
|
||||
Default qconfig for quantizing weights only.
|
||||
"""
|
||||
|
||||
default_activation_only_qconfig = QConfig(
|
||||
activation=default_fake_quant, weight=torch.nn.Identity
|
||||
)
|
||||
"""
|
||||
Default qconfig for quantizing activations only.
|
||||
"""
|
||||
|
||||
# QAT config that uses a fused observer + fake quant modules for optimized training performance.
|
||||
# to modify the activation/weight observers, the default entries in fake_quantize.py can be modified.
|
||||
default_qat_qconfig_v2 = QConfig(
|
||||
activation=default_fused_act_fake_quant, weight=default_fused_wt_fake_quant
|
||||
)
|
||||
"""
|
||||
Fused version of `default_qat_config`, has performance benefits.
|
||||
"""
|
||||
|
||||
default_reuse_input_qconfig = QConfig(
|
||||
activation=default_reuse_input_observer, weight=NoopObserver
|
||||
)
|
||||
"""
|
||||
Default qconfig for operators that reuse the observers from input Tensor, e.g. reshape
|
||||
"""
|
||||
|
||||
|
||||
def get_default_qconfig(backend="x86", version=0):
|
||||
"""
|
||||
Returns the default PTQ qconfig for the specified backend.
|
||||
|
||||
Args:
|
||||
* `backend` (str): a string representing the target backend. Currently supports
|
||||
`x86` (default), `fbgemm`, `qnnpack` and `onednn`.
|
||||
|
||||
Return:
|
||||
qconfig
|
||||
"""
|
||||
supported_backends = ["fbgemm", "x86", "qnnpack", "onednn"]
|
||||
if backend not in supported_backends:
|
||||
raise AssertionError(
|
||||
"backend: "
|
||||
+ str(backend)
|
||||
+ f" not supported. backend must be one of {supported_backends}"
|
||||
)
|
||||
|
||||
if version == 0:
|
||||
if backend == "fbgemm":
|
||||
qconfig = QConfig(
|
||||
activation=HistogramObserver.with_args(reduce_range=True),
|
||||
weight=default_per_channel_weight_observer,
|
||||
)
|
||||
elif backend == "qnnpack":
|
||||
# TODO: make this compatible with xnnpack constraints
|
||||
qconfig = QConfig(
|
||||
activation=HistogramObserver.with_args(reduce_range=False),
|
||||
weight=default_weight_observer,
|
||||
)
|
||||
elif backend == "onednn":
|
||||
if not torch.cpu._is_vnni_supported():
|
||||
warnings.warn(
|
||||
"Default qconfig of oneDNN backend with reduce_range of false may have accuracy issues "
|
||||
"on CPU without Vector Neural Network Instruction support.",
|
||||
stacklevel=2,
|
||||
)
|
||||
qconfig = QConfig(
|
||||
activation=HistogramObserver.with_args(reduce_range=False),
|
||||
weight=default_per_channel_weight_observer,
|
||||
)
|
||||
elif backend == "x86":
|
||||
qconfig = QConfig(
|
||||
activation=HistogramObserver.with_args(reduce_range=True),
|
||||
weight=default_per_channel_weight_observer,
|
||||
)
|
||||
else:
|
||||
# won't reach
|
||||
qconfig = default_qconfig
|
||||
else:
|
||||
raise AssertionError(
|
||||
"Version number: "
|
||||
+ str(version)
|
||||
+ " in get_default_qconfig is not supported. Version number must be 0"
|
||||
)
|
||||
|
||||
return qconfig
|
||||
|
||||
|
||||
"""
|
||||
Default, symmetric PTQ qconfig for the specified backend. And a per_channel
|
||||
variant of the same.
|
||||
|
||||
Symmetric here applies to signed weights with zero point = 0, and additional
|
||||
value restrictions. The activations are also signed 8-bit integers with this
|
||||
qconfig.
|
||||
|
||||
* Once this change is merged [as of 3/17/22], with backend or qengine =
|
||||
'qnnpack', some quantized operators with this symmetric qconfig may use
|
||||
operators from xnnpack library.
|
||||
|
||||
** Support to use xnnpack ops with `qnnpack` backed for asymmetric
|
||||
qconfig (returned by get_default_qconfig()) is not available yet.
|
||||
|
||||
* This qconfig uses signed activations and weights. Weights have added
|
||||
restrictions such as zero point is forced to be 0, making the weights
|
||||
symmetric, hence the name. And the 8-bit quantized values are
|
||||
restricting to to [-127, +127], excluding -128.
|
||||
|
||||
* xnnpack has a requantization scale value restriction, 0x1p-32 <=
|
||||
requantization_scale < 256.0 where, `requantization_scale = (input_scale
|
||||
* kernel_scale) / (output_scale)`. Using this eps (w/ assumed max value
|
||||
of 256) is to prevent requantization_scale to go below xnnpack lower
|
||||
threshold.
|
||||
"""
|
||||
default_symmetric_qnnpack_qconfig = QConfig(
|
||||
activation=HistogramObserver.with_args(
|
||||
dtype=torch.qint8, reduce_range=False, eps=2**-12
|
||||
),
|
||||
weight=weight_observer_range_neg_127_to_127,
|
||||
)
|
||||
|
||||
default_per_channel_symmetric_qnnpack_qconfig = QConfig(
|
||||
activation=HistogramObserver.with_args(
|
||||
dtype=torch.qint8, reduce_range=False, eps=2**-12
|
||||
),
|
||||
weight=per_channel_weight_observer_range_neg_127_to_127,
|
||||
)
|
||||
|
||||
default_embedding_qat_qconfig = QConfig(
|
||||
activation=NoopObserver.with_args(dtype=torch.float32),
|
||||
weight=default_embedding_fake_quant,
|
||||
)
|
||||
|
||||
default_embedding_qat_qconfig_4bit = QConfig(
|
||||
activation=NoopObserver.with_args(dtype=torch.float32),
|
||||
weight=default_embedding_fake_quant_4bit,
|
||||
)
|
||||
|
||||
default_quint8_weight_qconfig = QConfig(
|
||||
activation=HistogramObserver, weight=MinMaxObserver
|
||||
)
|
||||
|
||||
|
||||
def get_default_qat_qconfig(backend="x86", version=1):
|
||||
"""
|
||||
Returns the default QAT qconfig for the specified backend.
|
||||
|
||||
Args:
|
||||
* `backend` (str): a string representing the target backend. Currently supports
|
||||
`x86` (default), `fbgemm`, `qnnpack` and `onednn`.
|
||||
* `version`: version, for backwards compatibility. Can be `None` or `1`.
|
||||
|
||||
Return:
|
||||
qconfig
|
||||
"""
|
||||
supported_backends = ["fbgemm", "x86", "qnnpack", "onednn"]
|
||||
if backend not in supported_backends:
|
||||
raise AssertionError(
|
||||
"backend: "
|
||||
+ str(backend)
|
||||
+ f" not supported. backend must be one of {supported_backends}"
|
||||
)
|
||||
|
||||
# Histogram observer is too slow for quantization aware training
|
||||
if version == 0:
|
||||
if backend == "fbgemm":
|
||||
qconfig = QConfig(
|
||||
activation=FakeQuantize.with_args(
|
||||
observer=MovingAverageMinMaxObserver,
|
||||
quant_min=0,
|
||||
quant_max=255,
|
||||
reduce_range=True,
|
||||
),
|
||||
weight=default_per_channel_weight_fake_quant,
|
||||
)
|
||||
elif backend == "qnnpack":
|
||||
qconfig = QConfig(
|
||||
activation=FakeQuantize.with_args(
|
||||
observer=MovingAverageMinMaxObserver,
|
||||
quant_min=0,
|
||||
quant_max=255,
|
||||
reduce_range=False,
|
||||
),
|
||||
weight=default_weight_fake_quant,
|
||||
)
|
||||
elif backend == "onednn":
|
||||
qconfig = QConfig(
|
||||
activation=FakeQuantize.with_args(
|
||||
observer=MovingAverageMinMaxObserver, quant_min=0, quant_max=255
|
||||
),
|
||||
weight=default_per_channel_weight_fake_quant,
|
||||
)
|
||||
elif backend == "x86":
|
||||
qconfig = QConfig(
|
||||
activation=FakeQuantize.with_args(
|
||||
observer=MovingAverageMinMaxObserver,
|
||||
quant_min=0,
|
||||
quant_max=255,
|
||||
reduce_range=True,
|
||||
),
|
||||
weight=default_per_channel_weight_fake_quant,
|
||||
)
|
||||
else:
|
||||
qconfig = default_qat_qconfig
|
||||
# Use the fused observe + fake_quant modules for doing QAT.
|
||||
elif version == 1:
|
||||
if backend == "fbgemm":
|
||||
qconfig = QConfig(
|
||||
activation=FusedMovingAvgObsFakeQuantize.with_args(
|
||||
observer=MovingAverageMinMaxObserver,
|
||||
quant_min=0,
|
||||
quant_max=255,
|
||||
reduce_range=True,
|
||||
),
|
||||
weight=default_fused_per_channel_wt_fake_quant,
|
||||
)
|
||||
elif backend == "qnnpack":
|
||||
# TODO: make this compatible with xnnpack constraints
|
||||
qconfig = QConfig(
|
||||
activation=FusedMovingAvgObsFakeQuantize.with_args(
|
||||
observer=MovingAverageMinMaxObserver,
|
||||
quant_min=0,
|
||||
quant_max=255,
|
||||
reduce_range=False,
|
||||
),
|
||||
weight=default_fused_wt_fake_quant,
|
||||
)
|
||||
elif backend == "onednn":
|
||||
qconfig = QConfig(
|
||||
activation=FusedMovingAvgObsFakeQuantize.with_args(
|
||||
observer=MovingAverageMinMaxObserver, quant_min=0, quant_max=255
|
||||
),
|
||||
weight=default_fused_per_channel_wt_fake_quant,
|
||||
)
|
||||
elif backend == "x86":
|
||||
qconfig = QConfig(
|
||||
activation=FusedMovingAvgObsFakeQuantize.with_args(
|
||||
observer=MovingAverageMinMaxObserver,
|
||||
quant_min=0,
|
||||
quant_max=255,
|
||||
reduce_range=True,
|
||||
),
|
||||
weight=default_fused_per_channel_wt_fake_quant,
|
||||
)
|
||||
else:
|
||||
qconfig = default_qat_qconfig_v2
|
||||
else:
|
||||
raise AssertionError(
|
||||
"Version number: "
|
||||
+ str(version)
|
||||
+ "in get_default_qat_qconfig is not supported. Version number must be 0 or 1"
|
||||
)
|
||||
|
||||
return qconfig
|
||||
|
||||
|
||||
"""
|
||||
Default symmetric QAT qconfig for qnnpack. And its per channel weight variant.
|
||||
"""
|
||||
default_symmetric_qnnpack_qat_qconfig = QConfig(
|
||||
activation=FusedMovingAvgObsFakeQuantize.with_args(
|
||||
observer=MovingAverageMinMaxObserver,
|
||||
quant_min=-128,
|
||||
quant_max=127,
|
||||
dtype=torch.qint8,
|
||||
reduce_range=False,
|
||||
eps=2**-12,
|
||||
),
|
||||
weight=fused_wt_fake_quant_range_neg_127_to_127,
|
||||
)
|
||||
|
||||
default_per_channel_symmetric_qnnpack_qat_qconfig = QConfig(
|
||||
activation=FusedMovingAvgObsFakeQuantize.with_args(
|
||||
observer=MovingAverageMinMaxObserver,
|
||||
quant_min=-128,
|
||||
quant_max=127,
|
||||
dtype=torch.qint8,
|
||||
reduce_range=False,
|
||||
eps=2**-12,
|
||||
),
|
||||
weight=fused_per_channel_wt_fake_quant_range_neg_127_to_127,
|
||||
)
|
||||
|
||||
_default_fp32_placeholder_qconfig = QConfig(
|
||||
activation=PlaceholderObserver.with_args(dtype=torch.float32),
|
||||
weight=PlaceholderObserver.with_args(dtype=torch.float32),
|
||||
)
|
||||
|
||||
_default_quint8_placeholder_qconfig = QConfig(
|
||||
activation=PlaceholderObserver.with_args(dtype=torch.quint8),
|
||||
# operators using this qconfig doesn't have weights
|
||||
weight=None,
|
||||
)
|
||||
|
||||
|
||||
@deprecated(
|
||||
"`torch.ao.quantization.get_default_qconfig_dict` is deprecated and will be removed in "
|
||||
"a future version. Please use `torch.ao.quantization.get_default_qconfig_mapping` instead.",
|
||||
category=FutureWarning,
|
||||
)
|
||||
def get_default_qconfig_dict(backend="x86", version=0):
|
||||
return torch.ao.quantization.get_default_qconfig_mapping(backend, version).to_dict()
|
||||
|
||||
|
||||
@deprecated(
|
||||
"`torch.ao.quantization.get_default_qat_qconfig_dict` is deprecated and will be removed in "
|
||||
"a future version. Please use `torch.ao.quantization.get_default_qat_qconfig_mapping` instead.",
|
||||
category=FutureWarning,
|
||||
)
|
||||
def get_default_qat_qconfig_dict(backend="x86", version=1):
|
||||
return torch.ao.quantization.get_default_qat_qconfig_mapping(
|
||||
backend, version
|
||||
).to_dict()
|
||||
|
||||
|
||||
def _assert_valid_qconfig(qconfig: QConfig | None, mod: torch.nn.Module) -> None:
|
||||
"""
|
||||
Verifies that this `qconfig` is valid.
|
||||
"""
|
||||
if qconfig is None:
|
||||
return
|
||||
is_conv_transpose_mod = isinstance(
|
||||
mod,
|
||||
(torch.nn.ConvTranspose1d, torch.nn.ConvTranspose2d, torch.nn.ConvTranspose3d),
|
||||
)
|
||||
if is_conv_transpose_mod:
|
||||
if qconfig.weight is None:
|
||||
# for now, we assume that any qconfig for ConvTranspose without a weight is valid
|
||||
return
|
||||
example_observer = qconfig.weight()
|
||||
is_per_channel = isinstance(
|
||||
example_observer,
|
||||
(
|
||||
torch.ao.quantization.PerChannelMinMaxObserver,
|
||||
torch.ao.quantization.MovingAveragePerChannelMinMaxObserver,
|
||||
),
|
||||
)
|
||||
if is_per_channel:
|
||||
raise AssertionError(
|
||||
"Per channel weight observer is not supported yet for ConvTranspose{n}d."
|
||||
)
|
||||
|
||||
|
||||
QConfigAny = TypeAliasType("QConfigAny", QConfig | None)
|
||||
|
||||
|
||||
def _add_module_to_qconfig_obs_ctr(
|
||||
qconfig: QConfigAny, module: nn.Module | None
|
||||
) -> Any:
|
||||
r"""This is a helper function for use in quantization prepare that updates a qconfig so that
|
||||
the constructors stored in the qconfig will create observers on the same device that
|
||||
'module' is on. This is intended to be used when the qconfigs are propagated to each
|
||||
module in order to avoid potential device alignment issues.
|
||||
|
||||
Args:
|
||||
qconfig: QConfig with obs constructors stored in activation and weight
|
||||
module: module which the qconfig is related to
|
||||
|
||||
Return:
|
||||
qconfig: configured so that obs constructors set to construct on the same device as module
|
||||
"""
|
||||
|
||||
if module is None or qconfig is None or qconfig._fields != ("activation", "weight"):
|
||||
return qconfig
|
||||
|
||||
def get_factory_kwargs_based_on_module_device():
|
||||
if not isinstance(module, torch.nn.Module):
|
||||
raise AssertionError("module must be an instance of torch.nn.Module")
|
||||
devices = {p.device for p in module.parameters()} | {
|
||||
p.device for p in module.buffers()
|
||||
}
|
||||
device = next(iter(devices)) if len(devices) > 0 else None
|
||||
return None if device is None else {"device": device}
|
||||
|
||||
def configure_constructor_to_put_obs_on_module_device(original_constructor):
|
||||
try:
|
||||
# check if constructor can accept factory_kwargs
|
||||
check = original_constructor.with_args(factory_kwargs=None)
|
||||
check()
|
||||
return original_constructor.with_callable_args(
|
||||
factory_kwargs=get_factory_kwargs_based_on_module_device
|
||||
)
|
||||
except AttributeError: # qconfig doesn't have activation or weight
|
||||
return original_constructor
|
||||
except TypeError: # the class doesn't accept factory_kwargs argument
|
||||
return original_constructor
|
||||
|
||||
activation = configure_constructor_to_put_obs_on_module_device(qconfig.activation)
|
||||
weight = configure_constructor_to_put_obs_on_module_device(qconfig.weight)
|
||||
|
||||
return QConfig(activation, weight)
|
||||
|
||||
|
||||
_ObserverOrFakeQuantizeConstructor = (
|
||||
_PartialWrapper | type[ObserverBase] | type[FakeQuantizeBase]
|
||||
)
|
||||
|
||||
|
||||
def _obs_or_fq_ctr_equals(
|
||||
obs_or_fq1: _ObserverOrFakeQuantizeConstructor,
|
||||
obs_or_fq2: _ObserverOrFakeQuantizeConstructor,
|
||||
):
|
||||
if isinstance(obs_or_fq1, _PartialWrapper) and isinstance(
|
||||
obs_or_fq2, _PartialWrapper
|
||||
):
|
||||
return _partial_wrapper_equals(obs_or_fq1, obs_or_fq2)
|
||||
return obs_or_fq1 == obs_or_fq2
|
||||
|
||||
|
||||
def _partial_wrapper_equals(obs_or_fq1: _PartialWrapper, obs_or_fq2: _PartialWrapper):
|
||||
"""
|
||||
Return whether the two partial wrappers are equal,
|
||||
"""
|
||||
# functools.partial has no __eq__ operator defined so '==' defaults to 'is'
|
||||
obs_or_fq1_keywords = copy.copy(obs_or_fq1.p.keywords)
|
||||
obs_or_fq2_keywords = copy.copy(obs_or_fq2.p.keywords)
|
||||
keywords_equal = True
|
||||
# compare observer constructor with _obs_or_fq_ctr_equals since direct compare would fail
|
||||
if "observer" in obs_or_fq1_keywords and "observer" in obs_or_fq2_keywords:
|
||||
keywords_equal = keywords_equal and _obs_or_fq_ctr_equals(
|
||||
obs_or_fq1_keywords["observer"], obs_or_fq2_keywords["observer"]
|
||||
)
|
||||
obs_or_fq1_keywords.pop("observer")
|
||||
obs_or_fq2_keywords.pop("observer")
|
||||
keywords_equal = keywords_equal and obs_or_fq1_keywords == obs_or_fq2_keywords
|
||||
return (
|
||||
obs_or_fq1.p.func == obs_or_fq2.p.func
|
||||
and obs_or_fq1.p.args == obs_or_fq2.p.args
|
||||
and keywords_equal
|
||||
)
|
||||
|
||||
|
||||
def qconfig_equals(q1: QConfigAny, q2: QConfigAny):
|
||||
"""
|
||||
Returns `True` if `q1` equals `q2`, and `False` otherwise.
|
||||
"""
|
||||
if q1 is None or q2 is None:
|
||||
return q1 == q2
|
||||
else:
|
||||
if q1 is None or q2 is None:
|
||||
raise AssertionError(
|
||||
"Both q1 and q2 must be non-None for qconfig comparison"
|
||||
)
|
||||
try:
|
||||
# Qconfig weight and activation can be either a partial wrapper,
|
||||
# or an observer class. Special handling is required (above) for
|
||||
# comparing partial wrappers.
|
||||
activation_same = _obs_or_fq_ctr_equals(q1.activation, q2.activation)
|
||||
weight_same = _obs_or_fq_ctr_equals(q1.weight, q2.weight)
|
||||
return activation_same and weight_same
|
||||
except AttributeError:
|
||||
return q1 == q2
|
||||
|
||||
|
||||
def _activation_is_memoryless(qconfig: QConfig):
|
||||
"""
|
||||
Return whether the observer for activations defined in the given QConfig is memoryless.
|
||||
This means a MovingAverage observer with averaging constant equal to 1.
|
||||
"""
|
||||
|
||||
def _is_memoryless(observer):
|
||||
return (
|
||||
hasattr(observer, "averaging_constant") and observer.averaging_constant == 1
|
||||
)
|
||||
|
||||
act = qconfig.activation()
|
||||
if isinstance(act, FakeQuantizeBase) and hasattr(act, "activation_post_process"):
|
||||
return _is_memoryless(act.activation_post_process)
|
||||
else:
|
||||
return _is_memoryless(act)
|
||||
|
||||
|
||||
def _is_reuse_input_qconfig(qconfig: QConfig | None):
|
||||
return (
|
||||
qconfig is not None
|
||||
and isinstance(qconfig.activation(), ReuseInputObserver)
|
||||
and isinstance(qconfig.weight(), NoopObserver)
|
||||
)
|
||||
@@ -0,0 +1,385 @@
|
||||
# mypy: allow-untyped-defs
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import OrderedDict
|
||||
from typing import Any, TYPE_CHECKING
|
||||
|
||||
import torch
|
||||
|
||||
from .fake_quantize import default_weight_fake_quant, FixedQParamsFakeQuantize
|
||||
from .observer import (
|
||||
_PartialWrapper,
|
||||
default_fixed_qparams_range_0to1_observer,
|
||||
default_fixed_qparams_range_neg1to1_observer,
|
||||
default_placeholder_observer,
|
||||
default_weight_observer,
|
||||
)
|
||||
from .qconfig import (
|
||||
default_quint8_weight_qconfig,
|
||||
default_reuse_input_qconfig,
|
||||
default_symmetric_qnnpack_qat_qconfig,
|
||||
default_symmetric_qnnpack_qconfig,
|
||||
get_default_qat_qconfig,
|
||||
get_default_qconfig,
|
||||
QConfig,
|
||||
QConfigAny,
|
||||
)
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Callable
|
||||
|
||||
|
||||
__all__ = [
|
||||
"get_default_qconfig_mapping",
|
||||
"get_default_qat_qconfig_mapping",
|
||||
"QConfigMapping",
|
||||
]
|
||||
|
||||
|
||||
# TODO: replace all usages with these constants
|
||||
_GLOBAL_DICT_KEY = ""
|
||||
_OBJECT_TYPE_DICT_KEY = "object_type"
|
||||
_MODULE_NAME_REGEX_DICT_KEY = "module_name_regex"
|
||||
_MODULE_NAME_DICT_KEY = "module_name"
|
||||
_MODULE_NAME_OBJECT_TYPE_ORDER_DICT_KEY = "module_name_object_type_order"
|
||||
|
||||
# TODO: derive this map from the BackendConfig
|
||||
_FIXED_QPARAMS_OP_TO_OBSERVER: dict[Callable | str, _PartialWrapper] = {
|
||||
torch.nn.Hardsigmoid: default_fixed_qparams_range_0to1_observer,
|
||||
torch.nn.functional.hardsigmoid: default_fixed_qparams_range_0to1_observer,
|
||||
"hardsigmoid": default_fixed_qparams_range_0to1_observer,
|
||||
"hardsigmoid_": default_fixed_qparams_range_0to1_observer,
|
||||
torch.nn.Sigmoid: default_fixed_qparams_range_0to1_observer,
|
||||
torch.sigmoid: default_fixed_qparams_range_0to1_observer,
|
||||
"sigmoid": default_fixed_qparams_range_0to1_observer,
|
||||
"sigmoid_": default_fixed_qparams_range_0to1_observer,
|
||||
torch.nn.Softmax: default_fixed_qparams_range_0to1_observer,
|
||||
torch.nn.Tanh: default_fixed_qparams_range_neg1to1_observer,
|
||||
torch.tanh: default_fixed_qparams_range_neg1to1_observer,
|
||||
"tanh": default_fixed_qparams_range_neg1to1_observer,
|
||||
"tanh_": default_fixed_qparams_range_neg1to1_observer,
|
||||
}
|
||||
|
||||
|
||||
def _get_default_qconfig_mapping(
|
||||
is_qat: bool, backend: str, version: int
|
||||
) -> QConfigMapping:
|
||||
"""
|
||||
Return the default QConfigMapping for the given quantization type and backend.
|
||||
"""
|
||||
if is_qat:
|
||||
qconfig = get_default_qat_qconfig(backend, version)
|
||||
else:
|
||||
qconfig = get_default_qconfig(backend, version)
|
||||
default_weight = default_weight_fake_quant if is_qat else default_weight_observer
|
||||
|
||||
# default_per_channel_weight_observer is not currently compatible with fbgemm backend
|
||||
# so we have to modify the weight observer to default_weight_observer or another
|
||||
# per tensor supported observer.
|
||||
# see https://github.com/pytorch/pytorch/issues/47535
|
||||
if backend in ("fbgemm", "x86"):
|
||||
qconfig_transpose = QConfig(
|
||||
activation=qconfig.activation, weight=default_weight
|
||||
)
|
||||
else:
|
||||
qconfig_transpose = qconfig
|
||||
|
||||
# currently layernorm only supports float weights
|
||||
# we have to add this because otherwise there will be a extra quantize-dequantize pair
|
||||
qconfig_layernorm = QConfig(
|
||||
activation=qconfig.activation, weight=default_placeholder_observer
|
||||
)
|
||||
|
||||
qconfig_mapping = (
|
||||
QConfigMapping()
|
||||
.set_global(qconfig)
|
||||
.set_object_type("reshape", default_reuse_input_qconfig)
|
||||
.set_object_type(torch.nn.ConvTranspose1d, qconfig_transpose)
|
||||
.set_object_type(torch.nn.ConvTranspose2d, qconfig_transpose)
|
||||
.set_object_type(torch.nn.ConvTranspose3d, qconfig_transpose)
|
||||
.set_object_type(torch.nn.functional.conv_transpose1d, qconfig_transpose)
|
||||
.set_object_type(torch.nn.functional.conv_transpose2d, qconfig_transpose)
|
||||
.set_object_type(torch.nn.functional.conv_transpose3d, qconfig_transpose)
|
||||
.set_object_type(torch.nn.functional.layer_norm, qconfig_layernorm)
|
||||
.set_object_type(torch.nn.LayerNorm, qconfig_layernorm)
|
||||
.set_object_type(torch.nn.PReLU, default_quint8_weight_qconfig)
|
||||
)
|
||||
# Use special observers for ops with fixed qparams
|
||||
fixed_qparams_observer_to_qconfig: dict[Any, QConfigAny] = {}
|
||||
for fixed_qparams_op, observer in _FIXED_QPARAMS_OP_TO_OBSERVER.items():
|
||||
if observer in fixed_qparams_observer_to_qconfig:
|
||||
fixed_qparams_qconfig = fixed_qparams_observer_to_qconfig[observer]
|
||||
else:
|
||||
if is_qat:
|
||||
activation = FixedQParamsFakeQuantize.with_args(observer=observer)
|
||||
else:
|
||||
activation = observer
|
||||
fixed_qparams_qconfig = QConfig(
|
||||
activation=activation, weight=default_weight
|
||||
)
|
||||
fixed_qparams_observer_to_qconfig[observer] = fixed_qparams_qconfig
|
||||
qconfig_mapping.set_object_type(fixed_qparams_op, fixed_qparams_qconfig)
|
||||
|
||||
# TODO Currently it's required that separate ops in a fused op/module have the same qconfig.
|
||||
# Need to be able to support fusion of ops with different qconfigs
|
||||
|
||||
return qconfig_mapping
|
||||
|
||||
|
||||
def get_default_qconfig_mapping(backend="x86", version=0) -> QConfigMapping:
|
||||
"""
|
||||
Return the default QConfigMapping for post training quantization.
|
||||
|
||||
Args:
|
||||
* ``backend`` (str) : the quantization backend for the default qconfig mapping, should be
|
||||
one of ["x86" (default), "fbgemm", "qnnpack", "onednn"]
|
||||
* ``version`` (int) : the version for the default qconfig mapping
|
||||
"""
|
||||
# TODO: add assert for backend choices
|
||||
return _get_default_qconfig_mapping(False, backend, version)
|
||||
|
||||
|
||||
def get_default_qat_qconfig_mapping(backend="x86", version=1) -> QConfigMapping:
|
||||
"""
|
||||
Return the default QConfigMapping for quantization aware training.
|
||||
|
||||
Args:
|
||||
* ``backend`` (str) : the quantization backend for the default qconfig mapping, should be
|
||||
one of ["x86" (default), "fbgemm", "qnnpack", "onednn"]
|
||||
* ``version`` (int) : the version for the default qconfig mapping
|
||||
"""
|
||||
return _get_default_qconfig_mapping(True, backend, version)
|
||||
|
||||
|
||||
def _get_symmetric_qnnpack_qconfig_mapping() -> QConfigMapping:
|
||||
"""
|
||||
Return a QConfigMapping that uses `torch.ao.quantization.default_symmetric_qnnpack_qconfig`
|
||||
as the default QConfig.
|
||||
"""
|
||||
default_qconfig = default_symmetric_qnnpack_qconfig
|
||||
return _get_default_qconfig_mapping_with_default_qconfig(
|
||||
False, "qnnpack", default_qconfig
|
||||
)
|
||||
|
||||
|
||||
def _get_symmetric_qnnpack_qat_qconfig_mapping() -> QConfigMapping:
|
||||
"""
|
||||
Return a QConfigMapping that uses `torch.ao.quantization.default_symmetric_qnnpack_qat_qconfig`
|
||||
as the default QConfig.
|
||||
"""
|
||||
default_qconfig = default_symmetric_qnnpack_qat_qconfig
|
||||
return _get_default_qconfig_mapping_with_default_qconfig(
|
||||
True, "qnnpack", default_qconfig
|
||||
)
|
||||
|
||||
|
||||
def _get_default_qconfig_mapping_with_default_qconfig(
|
||||
is_qat: bool,
|
||||
backend: str,
|
||||
default_qconfig: QConfig,
|
||||
) -> QConfigMapping:
|
||||
"""
|
||||
Return a QConfigMapping that uses the provided qconfig as the default QConfig.
|
||||
"""
|
||||
if is_qat:
|
||||
qconfig_mapping = get_default_qat_qconfig_mapping(backend)
|
||||
else:
|
||||
qconfig_mapping = get_default_qconfig_mapping(backend)
|
||||
qconfig_mapping.set_global(default_qconfig)
|
||||
for pattern in qconfig_mapping.object_type_qconfigs:
|
||||
if pattern not in _FIXED_QPARAMS_OP_TO_OBSERVER:
|
||||
qconfig_mapping.set_object_type(pattern, default_qconfig)
|
||||
return qconfig_mapping
|
||||
|
||||
|
||||
_QCONFIG_STYLE_ORDER: list[str] = [
|
||||
"global_qconfig",
|
||||
"object_type_qconfigs",
|
||||
"module_name_regex_qconfigs",
|
||||
"module_name_qconfigs",
|
||||
"module_name_object_type_order_qconfigs",
|
||||
]
|
||||
|
||||
|
||||
class QConfigMapping:
|
||||
"""
|
||||
Mapping from model ops to :class:`torch.ao.quantization.QConfig` s.
|
||||
|
||||
The user can specify QConfigs using the following methods (in increasing match priority):
|
||||
|
||||
``set_global`` : sets the global (default) QConfig
|
||||
|
||||
``set_object_type`` : sets the QConfig for a given module type, function, or method name
|
||||
|
||||
``set_module_name_regex`` : sets the QConfig for modules matching the given regex string
|
||||
|
||||
``set_module_name`` : sets the QConfig for modules matching the given module name
|
||||
|
||||
``set_module_name_object_type_order`` : sets the QConfig for modules matching a combination
|
||||
of the given module name, object type, and the index at which the module appears
|
||||
|
||||
Example usage::
|
||||
|
||||
qconfig_mapping = QConfigMapping()
|
||||
.set_global(global_qconfig)
|
||||
.set_object_type(torch.nn.Linear, qconfig1)
|
||||
.set_object_type(torch.nn.ReLU, qconfig1)
|
||||
.set_module_name_regex("foo.*bar.*conv[0-9]+", qconfig1)
|
||||
.set_module_name_regex("foo.*", qconfig2)
|
||||
.set_module_name("module1", qconfig1)
|
||||
.set_module_name("module2", qconfig2)
|
||||
.set_module_name_object_type_order("foo.bar", torch.nn.functional.linear, 0, qconfig3)
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
# In increasing match priority:
|
||||
self.global_qconfig: QConfigAny = None
|
||||
self.object_type_qconfigs: OrderedDict[Callable | str, QConfigAny] = (
|
||||
OrderedDict()
|
||||
)
|
||||
self.module_name_regex_qconfigs: OrderedDict[str, QConfigAny] = OrderedDict()
|
||||
self.module_name_qconfigs: OrderedDict[str, QConfigAny] = OrderedDict()
|
||||
self.module_name_object_type_order_qconfigs: OrderedDict[
|
||||
tuple[str, Callable, int], QConfigAny
|
||||
] = OrderedDict()
|
||||
|
||||
def set_global(self, global_qconfig: QConfigAny) -> QConfigMapping:
|
||||
"""
|
||||
Set the global (default) QConfig.
|
||||
"""
|
||||
self.global_qconfig = global_qconfig
|
||||
return self
|
||||
|
||||
def set_object_type(
|
||||
self, object_type: Callable | str, qconfig: QConfigAny
|
||||
) -> QConfigMapping:
|
||||
"""
|
||||
Set the QConfig for a given module type, function, or method name.
|
||||
If the QConfig for an existing object type was already set, the new QConfig will override the old one.
|
||||
"""
|
||||
self.object_type_qconfigs[object_type] = qconfig
|
||||
return self
|
||||
|
||||
def set_module_name_regex(
|
||||
self, module_name_regex: str, qconfig: QConfigAny
|
||||
) -> QConfigMapping:
|
||||
"""
|
||||
Set the QConfig for modules matching the given regex string.
|
||||
|
||||
Regexes will be matched in the order in which they are registered through this method.
|
||||
Thus, the caller should register more specific patterns first, e.g.::
|
||||
|
||||
qconfig_mapping = QConfigMapping()
|
||||
.set_module_name_regex("foo.*bar.*conv[0-9]+", qconfig1)
|
||||
.set_module_name_regex("foo.*bar.*", qconfig2)
|
||||
.set_module_name_regex("foo.*", qconfig3)
|
||||
|
||||
In this example, "foo.bar.conv0" would match qconfig1, "foo.bar.linear" would match qconfig2,
|
||||
and "foo.baz.relu" would match qconfig3.
|
||||
|
||||
If the QConfig for an existing module name regex was already set, the new QConfig will override the
|
||||
old one while preserving the order in which the regexes were originally registered.
|
||||
"""
|
||||
self.module_name_regex_qconfigs[module_name_regex] = qconfig
|
||||
return self
|
||||
|
||||
def set_module_name(self, module_name: str, qconfig: QConfigAny) -> QConfigMapping:
|
||||
"""
|
||||
Set the QConfig for modules matching the given module name.
|
||||
If the QConfig for an existing module name was already set, the new QConfig will override the old one.
|
||||
"""
|
||||
self.module_name_qconfigs[module_name] = qconfig
|
||||
return self
|
||||
|
||||
def set_module_name_object_type_order(
|
||||
self, module_name: str, object_type: Callable, index: int, qconfig: QConfigAny
|
||||
) -> QConfigMapping:
|
||||
"""
|
||||
Set the QConfig for modules matching a combination of the given module name, object type,
|
||||
and the index at which the module appears.
|
||||
|
||||
If the QConfig for an existing (module name, object type, index) was already set, the new QConfig
|
||||
will override the old one.
|
||||
"""
|
||||
self.module_name_object_type_order_qconfigs[
|
||||
(module_name, object_type, index)
|
||||
] = qconfig
|
||||
return self
|
||||
|
||||
def __repr__(self) -> str:
|
||||
output = self.__class__.__name__ + " ("
|
||||
for style_name in _QCONFIG_STYLE_ORDER:
|
||||
output += f"\n {style_name}"
|
||||
qconfigs = getattr(self, style_name)
|
||||
if isinstance(qconfigs, OrderedDict) and len(qconfigs) > 0:
|
||||
for key, qconfig in qconfigs.items():
|
||||
output += f"\n {key}: {qconfig}"
|
||||
else:
|
||||
output += f"\n {qconfigs}"
|
||||
return output + "\n)"
|
||||
|
||||
# TODO: remove this
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
"""
|
||||
Convert this ``QConfigMapping`` to a dictionary with the following keys:
|
||||
|
||||
"" (for global QConfig)
|
||||
|
||||
"object_type"
|
||||
|
||||
"module_name_regex"
|
||||
|
||||
"module_name"
|
||||
|
||||
"module_name_object_type_order"
|
||||
|
||||
The values of this dictionary are lists of tuples.
|
||||
"""
|
||||
return {
|
||||
_GLOBAL_DICT_KEY: self.global_qconfig,
|
||||
_OBJECT_TYPE_DICT_KEY: list(self.object_type_qconfigs.items()),
|
||||
_MODULE_NAME_REGEX_DICT_KEY: list(self.module_name_regex_qconfigs.items()),
|
||||
_MODULE_NAME_DICT_KEY: list(self.module_name_qconfigs.items()),
|
||||
_MODULE_NAME_OBJECT_TYPE_ORDER_DICT_KEY: [
|
||||
(*k, v) for k, v in self.module_name_object_type_order_qconfigs.items()
|
||||
],
|
||||
}
|
||||
|
||||
# TODO: remove this
|
||||
@classmethod
|
||||
def from_dict(cls, qconfig_dict: dict[str, Any]) -> QConfigMapping:
|
||||
"""
|
||||
Create a ``QConfigMapping`` from a dictionary with the following keys (all optional):
|
||||
|
||||
"" (for global QConfig)
|
||||
|
||||
"object_type"
|
||||
|
||||
"module_name_regex"
|
||||
|
||||
"module_name"
|
||||
|
||||
"module_name_object_type_order"
|
||||
|
||||
The values of this dictionary are expected to be lists of tuples.
|
||||
"""
|
||||
conf = cls()
|
||||
if _GLOBAL_DICT_KEY in qconfig_dict:
|
||||
conf.set_global(qconfig_dict[_GLOBAL_DICT_KEY])
|
||||
for object_type, qconfig in qconfig_dict.get(_OBJECT_TYPE_DICT_KEY, []):
|
||||
conf.set_object_type(object_type, qconfig)
|
||||
for module_name_regex, qconfig in qconfig_dict.get(
|
||||
_MODULE_NAME_REGEX_DICT_KEY, []
|
||||
):
|
||||
conf.set_module_name_regex(module_name_regex, qconfig)
|
||||
for module_name, qconfig in qconfig_dict.get(_MODULE_NAME_DICT_KEY, []):
|
||||
conf.set_module_name(module_name, qconfig)
|
||||
for module_name, object_type, index, qconfig in qconfig_dict.get(
|
||||
_MODULE_NAME_OBJECT_TYPE_ORDER_DICT_KEY, []
|
||||
):
|
||||
conf.set_module_name_object_type_order(
|
||||
module_name, object_type, index, qconfig
|
||||
)
|
||||
return conf
|
||||
@@ -0,0 +1,35 @@
|
||||
import enum
|
||||
|
||||
|
||||
__all__ = [
|
||||
"QuantType",
|
||||
]
|
||||
|
||||
|
||||
# Quantization type (dynamic quantization, static quantization).
|
||||
# Should match the c++ enum in quantization_type.h
|
||||
class QuantType(enum.IntEnum):
|
||||
DYNAMIC = 0
|
||||
STATIC = 1
|
||||
QAT = 2
|
||||
WEIGHT_ONLY = 3
|
||||
|
||||
|
||||
_quant_type_to_str = {
|
||||
QuantType.STATIC: "static",
|
||||
QuantType.DYNAMIC: "dynamic",
|
||||
QuantType.QAT: "qat",
|
||||
QuantType.WEIGHT_ONLY: "weight_only",
|
||||
}
|
||||
|
||||
|
||||
# TODO: make this private
|
||||
def _get_quant_type_to_str(quant_type: QuantType) -> str:
|
||||
return _quant_type_to_str[quant_type]
|
||||
|
||||
|
||||
def _quant_type_from_str(name: str) -> QuantType:
|
||||
for quant_type, s in _quant_type_to_str.items():
|
||||
if name == s:
|
||||
return quant_type
|
||||
raise ValueError(f"Unknown QuantType name '{name}'")
|
||||
+367
@@ -0,0 +1,367 @@
|
||||
import copy
|
||||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
|
||||
import torch
|
||||
import torch.ao.nn as ao_nn
|
||||
import torch.ao.nn.intrinsic as nni
|
||||
import torch.ao.nn.intrinsic.qat as nniqat
|
||||
import torch.ao.nn.intrinsic.quantized as nniq
|
||||
import torch.ao.nn.intrinsic.quantized.dynamic as nniqd
|
||||
import torch.ao.nn.qat as nnqat
|
||||
import torch.ao.nn.qat.dynamic as nnqatd
|
||||
import torch.ao.nn.quantized as nnq
|
||||
import torch.ao.nn.quantized.dynamic as nnqd
|
||||
import torch.ao.nn.quantized.reference as nnqr
|
||||
|
||||
# Because `torch.ao.nn` uses lazy imports, we need to make
|
||||
# sure we import the contents explicitly here.
|
||||
import torch.ao.nn.sparse
|
||||
import torch.nn.functional as F
|
||||
from torch import nn
|
||||
from torch.ao.quantization.fake_quantize import (
|
||||
default_fixed_qparams_range_0to1_fake_quant,
|
||||
default_fixed_qparams_range_neg1to1_fake_quant,
|
||||
)
|
||||
from torch.ao.quantization.stubs import DeQuantStub, QuantStub
|
||||
from torch.ao.quantization.utils import get_combined_dict
|
||||
from torch.nn.utils.parametrize import type_before_parametrizations
|
||||
|
||||
|
||||
__all__ = [
|
||||
"DEFAULT_REFERENCE_STATIC_QUANT_MODULE_MAPPINGS",
|
||||
"DEFAULT_STATIC_QUANT_MODULE_MAPPINGS",
|
||||
"DEFAULT_QAT_MODULE_MAPPINGS",
|
||||
"DEFAULT_DYNAMIC_QUANT_MODULE_MAPPINGS",
|
||||
"DEFAULT_FLOAT_TO_QUANTIZED_OPERATOR_MAPPINGS",
|
||||
"DEFAULT_MODULE_TO_ACT_POST_PROCESS",
|
||||
"DEFAULT_STATIC_SPARSE_QUANT_MODULE_MAPPINGS",
|
||||
"DEFAULT_DYNAMIC_SPARSE_QUANT_MODULE_MAPPINGS",
|
||||
"no_observer_set",
|
||||
"get_default_static_quant_module_mappings",
|
||||
"get_default_static_quant_reference_module_mappings",
|
||||
"get_embedding_static_quant_module_mappings",
|
||||
"get_default_static_sparse_quant_module_mappings",
|
||||
"get_static_quant_module_class",
|
||||
"get_dynamic_quant_module_class",
|
||||
"get_default_qat_module_mappings",
|
||||
"get_embedding_qat_module_mappings",
|
||||
"get_default_dynamic_quant_module_mappings",
|
||||
"get_default_dynamic_sparse_quant_module_mappings",
|
||||
"get_default_qconfig_propagation_list",
|
||||
"get_default_compare_output_module_list",
|
||||
"get_default_float_to_quantized_operator_mappings",
|
||||
"get_quantized_operator",
|
||||
]
|
||||
|
||||
# Default map for swapping float module to reference quantized modules
|
||||
DEFAULT_REFERENCE_STATIC_QUANT_MODULE_MAPPINGS: dict[Callable, Any] = {
|
||||
QuantStub: nnq.Quantize,
|
||||
DeQuantStub: nnq.DeQuantize,
|
||||
nn.Linear: nnqr.Linear,
|
||||
nn.Conv1d: nnqr.Conv1d,
|
||||
nn.Conv2d: nnqr.Conv2d,
|
||||
nn.Conv3d: nnqr.Conv3d,
|
||||
nn.ConvTranspose1d: nnqr.ConvTranspose1d,
|
||||
nn.ConvTranspose2d: nnqr.ConvTranspose2d,
|
||||
nn.ConvTranspose3d: nnqr.ConvTranspose3d,
|
||||
nn.Embedding: nnqr.Embedding,
|
||||
nn.EmbeddingBag: nnqr.EmbeddingBag,
|
||||
nn.GRUCell: nnqr.GRUCell,
|
||||
nn.LSTMCell: nnqr.LSTMCell,
|
||||
nn.RNNCell: nnqr.RNNCell,
|
||||
nn.LSTM: nnqr.LSTM,
|
||||
}
|
||||
|
||||
# Default map for swapping float module to quantized ones
|
||||
DEFAULT_STATIC_QUANT_MODULE_MAPPINGS: dict[Callable, Any] = {
|
||||
QuantStub: nnq.Quantize,
|
||||
DeQuantStub: nnq.DeQuantize,
|
||||
nn.BatchNorm2d: nnq.BatchNorm2d,
|
||||
nn.BatchNorm3d: nnq.BatchNorm3d,
|
||||
nn.Dropout: nnq.Dropout,
|
||||
nn.Conv1d: nnq.Conv1d,
|
||||
nn.Conv2d: nnq.Conv2d,
|
||||
nn.Conv3d: nnq.Conv3d,
|
||||
nn.ConvTranspose1d: nnq.ConvTranspose1d,
|
||||
nn.ConvTranspose2d: nnq.ConvTranspose2d,
|
||||
nn.ConvTranspose3d: nnq.ConvTranspose3d,
|
||||
nn.ELU: nnq.ELU,
|
||||
nn.Embedding: nnq.Embedding,
|
||||
nn.EmbeddingBag: nnq.EmbeddingBag,
|
||||
nn.GroupNorm: nnq.GroupNorm,
|
||||
nn.Hardswish: nnq.Hardswish,
|
||||
nn.InstanceNorm1d: nnq.InstanceNorm1d,
|
||||
nn.InstanceNorm2d: nnq.InstanceNorm2d,
|
||||
nn.InstanceNorm3d: nnq.InstanceNorm3d,
|
||||
nn.LayerNorm: nnq.LayerNorm,
|
||||
nn.LeakyReLU: nnq.LeakyReLU,
|
||||
nn.modules.linear.NonDynamicallyQuantizableLinear: nnq.Linear,
|
||||
nn.Linear: nnq.Linear,
|
||||
nn.ReLU6: nnq.ReLU6,
|
||||
nn.PReLU: nnq.PReLU,
|
||||
# Wrapper Modules:
|
||||
nnq.FloatFunctional: nnq.QFunctional,
|
||||
# Intrinsic modules:
|
||||
nni.BNReLU2d: nniq.BNReLU2d,
|
||||
nni.BNReLU3d: nniq.BNReLU3d,
|
||||
nni.ConvReLU1d: nniq.ConvReLU1d,
|
||||
nni.ConvReLU2d: nniq.ConvReLU2d,
|
||||
nni.ConvReLU3d: nniq.ConvReLU3d,
|
||||
nni.ConvAdd2d: nniq.ConvAdd2d,
|
||||
nni.ConvAddReLU2d: nniq.ConvAddReLU2d,
|
||||
nni.LinearReLU: nniq.LinearReLU,
|
||||
nni.LinearLeakyReLU: nniq.LinearLeakyReLU,
|
||||
nni.LinearTanh: nniq.LinearTanh,
|
||||
nniqat.ConvBn1d: nnq.Conv1d,
|
||||
nniqat.ConvBn2d: nnq.Conv2d,
|
||||
nniqat.ConvBn3d: nnq.Conv3d,
|
||||
nniqat.ConvBnReLU1d: nniq.ConvReLU1d,
|
||||
nniqat.ConvBnReLU2d: nniq.ConvReLU2d,
|
||||
nniqat.ConvBnReLU3d: nniq.ConvReLU3d,
|
||||
nniqat.ConvReLU2d: nniq.ConvReLU2d,
|
||||
nniqat.ConvReLU3d: nniq.ConvReLU3d,
|
||||
nniqat.LinearReLU: nniq.LinearReLU,
|
||||
nniqat.LinearBn1d: nnq.Linear,
|
||||
# QAT modules:
|
||||
nnqat.Linear: nnq.Linear,
|
||||
nnqat.Conv2d: nnq.Conv2d,
|
||||
nnqat.Conv3d: nnq.Conv3d,
|
||||
}
|
||||
|
||||
# Default map for swapping float module to qat modules
|
||||
DEFAULT_QAT_MODULE_MAPPINGS: dict[Callable, Any] = {
|
||||
nn.Conv2d: nnqat.Conv2d,
|
||||
nn.Conv3d: nnqat.Conv3d,
|
||||
nn.Linear: nnqat.Linear,
|
||||
nn.modules.linear.NonDynamicallyQuantizableLinear: nnqat.Linear,
|
||||
# Intrinsic modules:
|
||||
nni.ConvBn1d: nniqat.ConvBn1d,
|
||||
nni.ConvBn2d: nniqat.ConvBn2d,
|
||||
nni.ConvBn3d: nniqat.ConvBn3d,
|
||||
nni.ConvBnReLU1d: nniqat.ConvBnReLU1d,
|
||||
nni.ConvBnReLU2d: nniqat.ConvBnReLU2d,
|
||||
nni.ConvBnReLU3d: nniqat.ConvBnReLU3d,
|
||||
nni.ConvReLU2d: nniqat.ConvReLU2d,
|
||||
nni.ConvReLU3d: nniqat.ConvReLU3d,
|
||||
nni.LinearReLU: nniqat.LinearReLU,
|
||||
nni.LinearBn1d: nniqat.LinearBn1d,
|
||||
}
|
||||
|
||||
# Default map for swapping dynamic modules
|
||||
DEFAULT_DYNAMIC_QUANT_MODULE_MAPPINGS: dict[Callable, Any] = {
|
||||
nn.GRUCell: nnqd.GRUCell,
|
||||
nn.Linear: nnqd.Linear,
|
||||
nnqatd.Linear: nnqd.Linear,
|
||||
nn.modules.linear.NonDynamicallyQuantizableLinear: nnqd.Linear,
|
||||
nn.LSTM: nnqd.LSTM,
|
||||
nn.GRU: nnqd.GRU,
|
||||
nn.LSTMCell: nnqd.LSTMCell,
|
||||
nn.RNNCell: nnqd.RNNCell,
|
||||
nni.LinearReLU: nniqd.LinearReLU,
|
||||
nn.EmbeddingBag: nnq.EmbeddingBag,
|
||||
nn.Embedding: nnq.Embedding,
|
||||
# Don't want to enable these by default because the numerical
|
||||
# accuracy is poor compared to other dynamic ops
|
||||
# nn.Conv1d: nnqd.Conv1d,
|
||||
# nn.Conv2d: nnqd.Conv2d,
|
||||
# nn.Conv3d: nnqd.Conv3d,
|
||||
# nn.ConvTranspose1d: nnqd.ConvTranspose1d,
|
||||
# nn.ConvTranspose2d: nnqd.ConvTranspose2d,
|
||||
# nn.ConvTranspose3d: nnqd.ConvTranspose3d,
|
||||
}
|
||||
|
||||
# Allowlist for propagating the qconfig
|
||||
_INCLUDE_QCONFIG_PROPAGATE_LIST: set[Callable] = {
|
||||
nn.Sequential,
|
||||
}
|
||||
|
||||
# Default mapping from floating point function or torch ops to quantized ops
|
||||
# TODO: merge with default static mapping
|
||||
DEFAULT_FLOAT_TO_QUANTIZED_OPERATOR_MAPPINGS: dict[Callable | str, Callable] = {
|
||||
F.elu: torch.ops.quantized.elu,
|
||||
F.hardswish: torch.ops.quantized.hardswish,
|
||||
F.instance_norm: torch.ops.quantized.instance_norm,
|
||||
F.layer_norm: torch.ops.quantized.layer_norm,
|
||||
F.leaky_relu: torch.ops.quantized.leaky_relu,
|
||||
F.dropout: torch.ops.quantized.dropout,
|
||||
}
|
||||
|
||||
# mapping from module to output activation post process class
|
||||
DEFAULT_MODULE_TO_ACT_POST_PROCESS: dict[Callable, Callable] = {
|
||||
nn.Hardsigmoid: default_fixed_qparams_range_0to1_fake_quant,
|
||||
nn.Sigmoid: default_fixed_qparams_range_0to1_fake_quant,
|
||||
nn.Softmax: default_fixed_qparams_range_0to1_fake_quant,
|
||||
nn.Tanh: default_fixed_qparams_range_neg1to1_fake_quant,
|
||||
}
|
||||
|
||||
# Default map for swapping float module to static sparse quantized ones
|
||||
DEFAULT_STATIC_SPARSE_QUANT_MODULE_MAPPINGS: dict[Callable, Any] = {
|
||||
nn.Linear: ao_nn.sparse.quantized.Linear
|
||||
}
|
||||
|
||||
# Default map for swapping float module to dynamic sparse quantized ones
|
||||
DEFAULT_DYNAMIC_SPARSE_QUANT_MODULE_MAPPINGS: dict[Callable, Any] = {
|
||||
nn.Linear: ao_nn.sparse.quantized.dynamic.Linear
|
||||
}
|
||||
|
||||
|
||||
def no_observer_set() -> set[Any]:
|
||||
r"""These modules cannot have observers inserted by default."""
|
||||
no_observers = {nn.quantizable.LSTM, nn.quantizable.MultiheadAttention}
|
||||
return no_observers
|
||||
|
||||
|
||||
def get_default_static_quant_module_mappings() -> dict[Callable, Any]:
|
||||
"""Get module mapping for post training static quantization"""
|
||||
return copy.deepcopy(DEFAULT_STATIC_QUANT_MODULE_MAPPINGS)
|
||||
|
||||
|
||||
def get_default_static_quant_reference_module_mappings() -> dict[Callable, Any]:
|
||||
"""Get reference module mapping for post training static quantization"""
|
||||
return copy.deepcopy(DEFAULT_REFERENCE_STATIC_QUANT_MODULE_MAPPINGS)
|
||||
|
||||
|
||||
def get_embedding_static_quant_module_mappings() -> dict[Callable, Any]:
|
||||
"""Get module mapping, including mapping for embedding QAT"""
|
||||
mapping = copy.deepcopy(DEFAULT_STATIC_QUANT_MODULE_MAPPINGS)
|
||||
mapping[nnqat.EmbeddingBag] = nnq.EmbeddingBag
|
||||
mapping[nnqat.Embedding] = nnq.Embedding
|
||||
return mapping
|
||||
|
||||
|
||||
def get_default_static_sparse_quant_module_mappings() -> dict[Callable, Any]:
|
||||
"""Get module mapping for post training static sparse quantization"""
|
||||
return copy.deepcopy(DEFAULT_STATIC_SPARSE_QUANT_MODULE_MAPPINGS)
|
||||
|
||||
|
||||
def get_static_quant_module_class(
|
||||
float_module_class: Callable,
|
||||
additional_static_quant_mapping: dict[Callable, Any] | None = None,
|
||||
is_reference: bool = False,
|
||||
) -> Any:
|
||||
r"""n Get the statically quantized module class corresponding to
|
||||
the floating point module class
|
||||
"""
|
||||
if additional_static_quant_mapping is None:
|
||||
additional_static_quant_mapping = {}
|
||||
all_mappings = get_combined_dict(
|
||||
DEFAULT_REFERENCE_STATIC_QUANT_MODULE_MAPPINGS
|
||||
if is_reference
|
||||
else DEFAULT_STATIC_QUANT_MODULE_MAPPINGS,
|
||||
additional_static_quant_mapping,
|
||||
)
|
||||
static_quant_module_class = all_mappings.get(float_module_class, None)
|
||||
if static_quant_module_class is None:
|
||||
raise AssertionError(
|
||||
f"Floating point module class {str(float_module_class)}"
|
||||
+ " does not have a corresponding quantized module class"
|
||||
)
|
||||
return copy.deepcopy(static_quant_module_class)
|
||||
|
||||
|
||||
def get_dynamic_quant_module_class(
|
||||
float_module_class: Callable,
|
||||
additional_dynamic_quant_mapping: dict[Callable, Any] | None = None,
|
||||
) -> Any:
|
||||
r"""n Get the dynamically quantized module class corresponding to
|
||||
the floating point module class
|
||||
"""
|
||||
if additional_dynamic_quant_mapping is None:
|
||||
additional_dynamic_quant_mapping = {}
|
||||
all_mappings = get_combined_dict(
|
||||
DEFAULT_DYNAMIC_QUANT_MODULE_MAPPINGS, additional_dynamic_quant_mapping
|
||||
)
|
||||
dynamic_quant_module_class = all_mappings.get(float_module_class, None)
|
||||
if dynamic_quant_module_class is None:
|
||||
raise AssertionError(
|
||||
f"Floating point module class {str(float_module_class)}"
|
||||
+ " does not have a corresponding quantized module class"
|
||||
)
|
||||
return copy.deepcopy(dynamic_quant_module_class)
|
||||
|
||||
|
||||
def get_default_qat_module_mappings() -> dict[Callable, Any]:
|
||||
"""Get default module mapping for quantization aware training"""
|
||||
return copy.deepcopy(DEFAULT_QAT_MODULE_MAPPINGS)
|
||||
|
||||
|
||||
def get_embedding_qat_module_mappings() -> dict[Callable, Any]:
|
||||
"""Get module mapping for quantization aware training
|
||||
This is includes default values in addition to
|
||||
enabling qat for embeddings.
|
||||
"""
|
||||
mapping = copy.deepcopy(DEFAULT_QAT_MODULE_MAPPINGS)
|
||||
mapping[nn.EmbeddingBag] = nnqat.EmbeddingBag
|
||||
mapping[nn.Embedding] = nnqat.Embedding
|
||||
return mapping
|
||||
|
||||
|
||||
def get_default_dynamic_quant_module_mappings() -> dict[Callable, Any]:
|
||||
"""Get module mapping for post training dynamic quantization"""
|
||||
return DEFAULT_DYNAMIC_QUANT_MODULE_MAPPINGS
|
||||
|
||||
|
||||
def get_default_dynamic_sparse_quant_module_mappings() -> dict[Callable, Any]:
|
||||
"""Get module mapping for post training dynamic sparse quantization"""
|
||||
return DEFAULT_DYNAMIC_SPARSE_QUANT_MODULE_MAPPINGS
|
||||
|
||||
|
||||
def get_default_qconfig_propagation_list() -> set[Callable]:
|
||||
"""Get the default list of module types that we'll attach qconfig
|
||||
attribute to in prepare
|
||||
"""
|
||||
QCONFIG_PROPAGATE_MODULE_CLASS_LIST = (
|
||||
set(DEFAULT_STATIC_QUANT_MODULE_MAPPINGS.keys())
|
||||
| set(DEFAULT_QAT_MODULE_MAPPINGS.keys())
|
||||
| set(DEFAULT_DYNAMIC_QUANT_MODULE_MAPPINGS.keys())
|
||||
| _INCLUDE_QCONFIG_PROPAGATE_LIST
|
||||
)
|
||||
return copy.deepcopy(QCONFIG_PROPAGATE_MODULE_CLASS_LIST)
|
||||
|
||||
|
||||
def get_default_compare_output_module_list() -> set[Callable]:
|
||||
"""Get list of module class types that we will record output
|
||||
in numeric suite
|
||||
"""
|
||||
NUMERIC_SUITE_COMPARE_MODEL_OUTPUT_MODULE_LIST = (
|
||||
set(DEFAULT_STATIC_QUANT_MODULE_MAPPINGS.values())
|
||||
| set(DEFAULT_QAT_MODULE_MAPPINGS.values())
|
||||
| set(DEFAULT_DYNAMIC_QUANT_MODULE_MAPPINGS.values())
|
||||
| set(DEFAULT_STATIC_QUANT_MODULE_MAPPINGS.keys())
|
||||
| set(DEFAULT_QAT_MODULE_MAPPINGS.keys())
|
||||
| set(DEFAULT_DYNAMIC_QUANT_MODULE_MAPPINGS.keys())
|
||||
| _INCLUDE_QCONFIG_PROPAGATE_LIST
|
||||
)
|
||||
return copy.deepcopy(NUMERIC_SUITE_COMPARE_MODEL_OUTPUT_MODULE_LIST)
|
||||
|
||||
|
||||
def get_default_float_to_quantized_operator_mappings() -> dict[
|
||||
Callable | str, Callable
|
||||
]:
|
||||
return copy.deepcopy(DEFAULT_FLOAT_TO_QUANTIZED_OPERATOR_MAPPINGS)
|
||||
|
||||
|
||||
# TODO: merge with get_static_quant_module_class
|
||||
def get_quantized_operator(float_op: Callable | str) -> Callable:
|
||||
"""Get the quantized operator corresponding to the float operator"""
|
||||
quantized_op = DEFAULT_FLOAT_TO_QUANTIZED_OPERATOR_MAPPINGS.get(float_op)
|
||||
if quantized_op is None:
|
||||
raise AssertionError(
|
||||
f"Operator {str(float_op)} does not have corresponding quantized op"
|
||||
)
|
||||
return quantized_op
|
||||
|
||||
|
||||
def _get_special_act_post_process(module: torch.nn.Module) -> Callable | None:
|
||||
r"""Get the special activation post process for `module`, this has
|
||||
higher priority than the activation post process in `qconfig`
|
||||
e.g.
|
||||
input: torch.nn.Sigmoid
|
||||
output: default_affine_fixed_qparam_fake_quant
|
||||
"""
|
||||
return DEFAULT_MODULE_TO_ACT_POST_PROCESS.get(type_before_parametrizations(module))
|
||||
|
||||
|
||||
def _has_special_act_post_process(module: torch.nn.Module) -> bool:
|
||||
return module.training and type(module) in DEFAULT_MODULE_TO_ACT_POST_PROCESS
|
||||
@@ -0,0 +1,829 @@
|
||||
# mypy: allow-untyped-defs
|
||||
import copy
|
||||
import inspect
|
||||
import itertools
|
||||
import typing_extensions
|
||||
import warnings
|
||||
|
||||
import torch
|
||||
import torch.ao.nn.quantized as nnq
|
||||
import torch.nn as nn
|
||||
from torch.ao.nn.intrinsic import _FusedModule
|
||||
from torch.ao.quantization.observer import _is_activation_post_process
|
||||
from torch.ao.quantization.qconfig import (
|
||||
_activation_is_memoryless,
|
||||
_add_module_to_qconfig_obs_ctr,
|
||||
default_dynamic_qconfig,
|
||||
float16_dynamic_qconfig,
|
||||
float_qparams_weight_only_qconfig,
|
||||
float_qparams_weight_only_qconfig_4bit,
|
||||
)
|
||||
from torch.ao.quantization.quantization_mappings import (
|
||||
_get_special_act_post_process,
|
||||
_has_special_act_post_process,
|
||||
get_default_dynamic_quant_module_mappings,
|
||||
get_default_qat_module_mappings,
|
||||
get_default_qconfig_propagation_list,
|
||||
get_default_static_quant_module_mappings,
|
||||
get_default_static_quant_reference_module_mappings,
|
||||
no_observer_set,
|
||||
)
|
||||
from torch.ao.quantization.stubs import DeQuantStub, QuantWrapper
|
||||
from torch.nn.utils.parametrize import type_before_parametrizations
|
||||
|
||||
from .utils import (
|
||||
DEPRECATION_WARNING,
|
||||
get_qparam_dict,
|
||||
has_no_children_ignoring_parametrizations,
|
||||
)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"get_default_custom_config_dict",
|
||||
"propagate_qconfig_",
|
||||
"add_quant_dequant",
|
||||
"prepare",
|
||||
"quantize",
|
||||
"quantize_dynamic",
|
||||
"prepare_qat",
|
||||
"quantize_qat",
|
||||
"convert",
|
||||
"swap_module",
|
||||
]
|
||||
|
||||
|
||||
# TODO remove this once BC is no longer required to avoid a SEV
|
||||
is_activation_post_process = _is_activation_post_process
|
||||
|
||||
|
||||
_DEFAULT_CUSTOM_CONFIG_DICT = {
|
||||
"float_to_observed_custom_module_class": {
|
||||
nn.LSTM: nn.quantizable.LSTM,
|
||||
nn.MultiheadAttention: nn.quantizable.MultiheadAttention,
|
||||
},
|
||||
"observed_to_quantized_custom_module_class": {
|
||||
nn.quantizable.LSTM: nn.quantized.LSTM,
|
||||
nn.quantizable.MultiheadAttention: nn.quantized.MultiheadAttention,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def get_default_custom_config_dict():
|
||||
r"""Defines the default custom config dict."""
|
||||
return _DEFAULT_CUSTOM_CONFIG_DICT
|
||||
|
||||
|
||||
def _propagate_qconfig_helper(
|
||||
module,
|
||||
qconfig_dict,
|
||||
qconfig_parent=None,
|
||||
prefix="",
|
||||
prepare_custom_config_dict=None,
|
||||
):
|
||||
r"""This is a helper function for `propagate_qconfig_`
|
||||
|
||||
Args:
|
||||
module: input module
|
||||
qconfig_dict: dictionary that maps from name of submodule to quantization
|
||||
configuration
|
||||
qconfig_parent: quantization config of parent module, we will fallback to
|
||||
this config when there is no specified config for current
|
||||
module
|
||||
prefix: corresponding prefix of the current module, used as key in
|
||||
qconfig_dict
|
||||
prepare_custom_config_dict: dictionary for custom handling of modules
|
||||
see docs for :func:`~torch.ao.quantization.prepare_fx`
|
||||
|
||||
Return:
|
||||
None, module is modified inplace with qconfig attached
|
||||
"""
|
||||
|
||||
module_qconfig = qconfig_dict.get(
|
||||
type_before_parametrizations(module), qconfig_parent
|
||||
)
|
||||
module_qconfig = qconfig_dict.get(prefix, module_qconfig)
|
||||
module_qconfig = getattr(module, "qconfig", module_qconfig)
|
||||
|
||||
torch.ao.quantization.qconfig._assert_valid_qconfig(module_qconfig, module)
|
||||
|
||||
qconfig_with_device_check = _add_module_to_qconfig_obs_ctr(module_qconfig, module)
|
||||
module.qconfig = qconfig_with_device_check
|
||||
|
||||
for name, child in module.named_children():
|
||||
module_prefix = prefix + "." + name if prefix else name
|
||||
# do no not propagate qconfig to child if child is non traceable
|
||||
if prepare_custom_config_dict is None or not (
|
||||
name in prepare_custom_config_dict.get("non_traceable_module_name", [])
|
||||
or type(child)
|
||||
in prepare_custom_config_dict.get("non_traceable_module_class", [])
|
||||
):
|
||||
_propagate_qconfig_helper(
|
||||
child, qconfig_dict, qconfig_with_device_check, module_prefix
|
||||
)
|
||||
|
||||
|
||||
def propagate_qconfig_(module, qconfig_dict=None, prepare_custom_config_dict=None):
|
||||
r"""Propagate qconfig through the module hierarchy and assign `qconfig`
|
||||
attribute on each leaf module
|
||||
|
||||
Args:
|
||||
module: input module
|
||||
qconfig_dict: dictionary that maps from name or type of submodule to
|
||||
quantization configuration, qconfig applies to all submodules of a
|
||||
given module unless qconfig for the submodules are specified (when
|
||||
the submodule already has qconfig attribute)
|
||||
prepare_custom_config_dict: dictionary for custom handling of modules
|
||||
see docs for :func:`~torch.ao.quantization.prepare_fx`
|
||||
|
||||
Return:
|
||||
None, module is modified inplace with qconfig attached
|
||||
"""
|
||||
if qconfig_dict is None:
|
||||
qconfig_dict = {}
|
||||
if prepare_custom_config_dict is None:
|
||||
prepare_custom_config_dict = {}
|
||||
_propagate_qconfig_helper(
|
||||
module, qconfig_dict, prepare_custom_config_dict=prepare_custom_config_dict
|
||||
)
|
||||
|
||||
|
||||
def _observer_forward_hook(self, input, output):
|
||||
r"""Forward hook that calls observer on the output"""
|
||||
return self.activation_post_process(output)
|
||||
|
||||
|
||||
def _observer_forward_pre_hook(self, input):
|
||||
r"""Forward pre hook that calls observer on the output"""
|
||||
return self.activation_post_process(input[0])
|
||||
|
||||
|
||||
def _register_activation_post_process_hook(module, pre_hook=False):
|
||||
if not hasattr(module, "activation_post_process"):
|
||||
raise AssertionError(
|
||||
"Expect activation_post_process attribute already attached to the module"
|
||||
)
|
||||
if pre_hook:
|
||||
module.register_forward_pre_hook(_observer_forward_pre_hook, prepend=True)
|
||||
else:
|
||||
module.register_forward_hook(_observer_forward_hook, prepend=True)
|
||||
|
||||
|
||||
def _add_observer_(
|
||||
module,
|
||||
qconfig_propagation_list=None,
|
||||
non_leaf_module_list=None,
|
||||
device=None,
|
||||
custom_module_class_mapping=None,
|
||||
):
|
||||
r"""Add observer for the leaf child of the module.
|
||||
|
||||
This function insert observer module to all leaf child module that
|
||||
has a valid qconfig attribute.
|
||||
|
||||
Args:
|
||||
module: input module with qconfig attributes for all the leaf modules that we want to quantize
|
||||
qconfig_propagation_list: a list of quantizable modules that will have observers added to them
|
||||
if they are leaf nodes
|
||||
device: parent device, if any
|
||||
non_leaf_module_list: list of non-leaf modules we want to add observer
|
||||
|
||||
Return:
|
||||
None, module is modified inplace with added observer modules and forward_hooks
|
||||
"""
|
||||
if qconfig_propagation_list is None:
|
||||
qconfig_propagation_list = get_default_qconfig_propagation_list()
|
||||
|
||||
if custom_module_class_mapping is None:
|
||||
custom_module_class_mapping = {}
|
||||
|
||||
# respect device affinity when adding observers
|
||||
if device is None:
|
||||
devices = _get_unique_devices_(module)
|
||||
if len(devices) > 1:
|
||||
raise AssertionError(
|
||||
f"_add_observer_ only works with cpu or single-device CUDA modules, but got devices {devices}"
|
||||
)
|
||||
device = next(iter(devices)) if len(devices) > 0 else None
|
||||
|
||||
def get_activation_post_process(qconfig, device, special_act_post_process=None):
|
||||
activation = (
|
||||
qconfig.activation()
|
||||
if special_act_post_process is None
|
||||
else special_act_post_process()
|
||||
)
|
||||
if device is not None:
|
||||
activation.to(device)
|
||||
return activation
|
||||
|
||||
def needs_observation(m):
|
||||
return hasattr(m, "qconfig") and m.qconfig is not None
|
||||
|
||||
def insert_activation_post_process(m, special_act_post_process=None):
|
||||
"""Adds an activation post process module and register
|
||||
a pre or post hook that calls the module
|
||||
"""
|
||||
# We don't insert observer/fake_quantize for DeQuantStub
|
||||
if needs_observation(m) and not isinstance(m, DeQuantStub):
|
||||
# observer and hook will be gone after we swap the module
|
||||
m.add_module(
|
||||
"activation_post_process",
|
||||
get_activation_post_process(
|
||||
m.qconfig, device, special_act_post_process
|
||||
),
|
||||
)
|
||||
# Register observer as the first entry in the hook list
|
||||
# All post forward hooks are preserved and will be executed after the observer before convert
|
||||
_register_activation_post_process_hook(
|
||||
m, pre_hook=_activation_is_memoryless(m.qconfig)
|
||||
)
|
||||
|
||||
for name, child in module.named_children():
|
||||
# TODO remove Dropout special after codebase stable
|
||||
if type_before_parametrizations(child) is nn.Dropout:
|
||||
continue
|
||||
elif issubclass(
|
||||
type_before_parametrizations(child), (nnq.FloatFunctional, nnq.QFunctional)
|
||||
):
|
||||
if needs_observation(child):
|
||||
if not hasattr(child, "activation_post_process"):
|
||||
raise AssertionError(
|
||||
f"functional class {type_before_parametrizations(child)} has no pre-defined `activation_post_process`"
|
||||
)
|
||||
child.activation_post_process = get_activation_post_process(
|
||||
child.qconfig, device
|
||||
)
|
||||
elif isinstance(child, _FusedModule):
|
||||
# activation_post_process are now added directly to nn.Sequential/_FusedModule
|
||||
if needs_observation(child):
|
||||
insert_activation_post_process(child)
|
||||
elif (
|
||||
non_leaf_module_list is not None
|
||||
and type_before_parametrizations(child) in non_leaf_module_list
|
||||
):
|
||||
if needs_observation(child):
|
||||
insert_activation_post_process(child)
|
||||
elif _has_special_act_post_process(child):
|
||||
special_act_post_process = _get_special_act_post_process(child)
|
||||
insert_activation_post_process(child, special_act_post_process)
|
||||
elif (
|
||||
needs_observation(child)
|
||||
and type_before_parametrizations(child) in custom_module_class_mapping
|
||||
):
|
||||
observed_class = custom_module_class_mapping[
|
||||
type_before_parametrizations(child)
|
||||
]
|
||||
observed_child = observed_class.from_float(child)
|
||||
setattr(module, name, observed_child)
|
||||
# TODO: These are the modules that cannot be observed
|
||||
# Once there are more, we should move them to a separate list
|
||||
if not issubclass(observed_class, tuple(no_observer_set())):
|
||||
insert_activation_post_process(observed_child)
|
||||
else:
|
||||
_add_observer_(
|
||||
child,
|
||||
qconfig_propagation_list,
|
||||
non_leaf_module_list,
|
||||
device,
|
||||
custom_module_class_mapping,
|
||||
)
|
||||
|
||||
# Insert observers only for leaf nodes, note that this observer is for
|
||||
# the output of the module, for input QuantStub will observe them
|
||||
if (
|
||||
has_no_children_ignoring_parametrizations(module)
|
||||
and not isinstance(module, torch.nn.Sequential)
|
||||
and type_before_parametrizations(module) in qconfig_propagation_list
|
||||
):
|
||||
insert_activation_post_process(module)
|
||||
# This is a special case for AdaRound eager mode
|
||||
# AdaRound contains weight_fake_quant to be propagated from API to convert
|
||||
# leaf node check with a number of children looks naive assumption that blocks
|
||||
# Adding an exception case for AdaRound
|
||||
if (
|
||||
hasattr(module, "weight_fake_quant")
|
||||
and not isinstance(module, torch.nn.Sequential)
|
||||
and type_before_parametrizations(module) in qconfig_propagation_list
|
||||
):
|
||||
insert_activation_post_process(module)
|
||||
|
||||
|
||||
def _get_unique_devices_(module):
|
||||
return {p.device for p in module.parameters() if p.device.type != "meta"} | {
|
||||
p.device for p in module.buffers() if p.device.type != "meta"
|
||||
}
|
||||
|
||||
|
||||
def add_quant_dequant(module):
|
||||
r"""Wrap the leaf child module in QuantWrapper if it has a valid qconfig
|
||||
Note that this function will modify the children of module inplace and it
|
||||
can return a new module which wraps the input module as well.
|
||||
|
||||
Args:
|
||||
module: input module with qconfig attributes for all the leaf modules
|
||||
that we want to quantize
|
||||
|
||||
Return:
|
||||
Either the inplace modified module with submodules wrapped in
|
||||
`QuantWrapper` based on qconfig or a new `QuantWrapper` module which
|
||||
wraps the input module, the latter case only happens when the input
|
||||
module is a leaf module and we want to quantize it.
|
||||
"""
|
||||
if (
|
||||
has_no_children_ignoring_parametrizations(module)
|
||||
and hasattr(module, "qconfig")
|
||||
and module.qconfig
|
||||
):
|
||||
return QuantWrapper(module)
|
||||
|
||||
for name, child in module.named_children():
|
||||
module._modules[name] = add_quant_dequant(child)
|
||||
return module
|
||||
|
||||
|
||||
@typing_extensions.deprecated(DEPRECATION_WARNING)
|
||||
def prepare(
|
||||
model,
|
||||
inplace=False,
|
||||
allow_list=None,
|
||||
observer_non_leaf_module_list=None,
|
||||
prepare_custom_config_dict=None,
|
||||
):
|
||||
r"""Prepares a copy of the model for quantization calibration or quantization-aware training.
|
||||
|
||||
Quantization configuration should be assigned preemptively
|
||||
to individual submodules in `.qconfig` attribute.
|
||||
|
||||
The model will be attached with observer or fake quant modules, and qconfig
|
||||
will be propagated.
|
||||
|
||||
Args:
|
||||
`model`: input model to be modified in-place
|
||||
`inplace`: carry out model transformations in-place, the original module is mutated
|
||||
`allow_list`: list of quantizable modules
|
||||
`observer_non_leaf_module_list`: list of non-leaf modules we want to add observer
|
||||
`prepare_custom_config_dict`: customization configuration dictionary for prepare function
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Example of prepare_custom_config_dict:
|
||||
prepare_custom_config_dict = {
|
||||
# user will manually define the corresponding observed
|
||||
# module class which has a from_float class method that converts
|
||||
# float custom module to observed custom module
|
||||
"float_to_observed_custom_module_class": {CustomModule: ObservedCustomModule}
|
||||
}
|
||||
|
||||
"""
|
||||
torch._C._log_api_usage_once("quantization_api.quantize.prepare")
|
||||
if prepare_custom_config_dict is None:
|
||||
prepare_custom_config_dict = get_default_custom_config_dict()
|
||||
custom_module_class_mapping = prepare_custom_config_dict.get(
|
||||
"float_to_observed_custom_module_class", {}
|
||||
)
|
||||
|
||||
if not inplace:
|
||||
model = copy.deepcopy(model)
|
||||
|
||||
# TODO: remove allow_list
|
||||
qconfig_propagation_list = allow_list
|
||||
if allow_list is None:
|
||||
qconfig_propagation_list = get_default_qconfig_propagation_list()
|
||||
propagate_qconfig_(model, qconfig_dict=None)
|
||||
|
||||
# sanity check common API misusage
|
||||
if not any(hasattr(m, "qconfig") and m.qconfig for m in model.modules()):
|
||||
warnings.warn(
|
||||
"None of the submodule got qconfig applied. Make sure you "
|
||||
"passed correct configuration through `qconfig_dict` or "
|
||||
"by assigning the `.qconfig` attribute directly on submodules",
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
_add_observer_(
|
||||
model,
|
||||
qconfig_propagation_list,
|
||||
observer_non_leaf_module_list,
|
||||
custom_module_class_mapping=custom_module_class_mapping,
|
||||
)
|
||||
return model
|
||||
|
||||
|
||||
def _remove_activation_post_process(module):
|
||||
# TODO: maybe we should change activation_post_process to _activation_post_process
|
||||
# to prevent it from being used by user
|
||||
if hasattr(module, "activation_post_process") and _is_activation_post_process(
|
||||
module.activation_post_process
|
||||
):
|
||||
delattr(module, "activation_post_process")
|
||||
|
||||
# remove activation_post_process pre and post hooks
|
||||
def remove_hooks(pre_hook=False):
|
||||
hook_map = module._forward_pre_hooks if pre_hook else module._forward_hooks
|
||||
observer_hook = (
|
||||
_observer_forward_pre_hook if pre_hook else _observer_forward_hook
|
||||
)
|
||||
handle_ids_to_remove = set()
|
||||
for handle_id, hook_fn in hook_map.items():
|
||||
if hook_fn is observer_hook:
|
||||
handle_ids_to_remove.add(handle_id)
|
||||
for handle_id in handle_ids_to_remove:
|
||||
hook_map.pop(handle_id)
|
||||
|
||||
remove_hooks(pre_hook=True)
|
||||
remove_hooks(pre_hook=False)
|
||||
|
||||
|
||||
# TODO: rename to something more general
|
||||
def _remove_qconfig(module):
|
||||
r"""Clean up the qconfig left in the module so that new qconfig can be
|
||||
propagated.
|
||||
|
||||
Args:
|
||||
module: module to be cleaned up
|
||||
"""
|
||||
for child in module.children():
|
||||
_remove_qconfig(child)
|
||||
|
||||
if hasattr(module, "qconfig"):
|
||||
del module.qconfig
|
||||
|
||||
_remove_activation_post_process(module)
|
||||
|
||||
|
||||
@typing_extensions.deprecated(DEPRECATION_WARNING)
|
||||
def quantize(model, run_fn, run_args, mapping=None, inplace=False):
|
||||
r"""Quantize the input float model with post training static quantization.
|
||||
|
||||
First it will prepare the model for calibration, then it calls
|
||||
`run_fn` which will run the calibration step, after that we will
|
||||
convert the model to a quantized model.
|
||||
|
||||
Args:
|
||||
model: input float model
|
||||
run_fn: a calibration function for calibrating the prepared model
|
||||
run_args: positional arguments for `run_fn`
|
||||
inplace: carry out model transformations in-place, the original module is mutated
|
||||
mapping: correspondence between original module types and quantized counterparts
|
||||
|
||||
Return:
|
||||
Quantized model.
|
||||
"""
|
||||
torch._C._log_api_usage_once("quantization_api.quantize.quantize")
|
||||
if mapping is None:
|
||||
mapping = get_default_static_quant_module_mappings()
|
||||
if not inplace:
|
||||
model = copy.deepcopy(model)
|
||||
model.eval()
|
||||
prepare(model, inplace=True)
|
||||
run_fn(model, *run_args)
|
||||
convert(model, mapping, inplace=True)
|
||||
return model
|
||||
|
||||
|
||||
@typing_extensions.deprecated(DEPRECATION_WARNING)
|
||||
def quantize_dynamic(
|
||||
model, qconfig_spec=None, dtype=torch.qint8, mapping=None, inplace=False
|
||||
):
|
||||
r"""Converts a float model to dynamic (i.e. weights-only) quantized model.
|
||||
|
||||
Replaces specified modules with dynamic weight-only quantized versions and output the quantized model.
|
||||
|
||||
For simplest usage provide `dtype` argument that can be float16 or qint8. Weight-only quantization
|
||||
by default is performed for layers with large weights size - i.e. Linear and RNN variants.
|
||||
|
||||
Fine grained control is possible with `qconfig` and `mapping` that act similarly to `quantize()`.
|
||||
If `qconfig` is provided, the `dtype` argument is ignored.
|
||||
|
||||
Args:
|
||||
model: input model
|
||||
qconfig_spec: Either:
|
||||
|
||||
- A dictionary that maps from name or type of submodule to quantization
|
||||
configuration, qconfig applies to all submodules of a given
|
||||
module unless qconfig for the submodules are specified (when the
|
||||
submodule already has qconfig attribute). Entries in the dictionary
|
||||
need to be QConfig instances.
|
||||
|
||||
- A set of types and/or submodule names to apply dynamic quantization to,
|
||||
in which case the `dtype` argument is used to specify the bit-width
|
||||
|
||||
inplace: carry out model transformations in-place, the original module is mutated
|
||||
mapping: maps type of a submodule to a type of corresponding dynamically quantized version
|
||||
with which the submodule needs to be replaced
|
||||
|
||||
"""
|
||||
torch._C._log_api_usage_once("quantization_api.quantize.quantize_dynamic")
|
||||
if qconfig_spec is None:
|
||||
if dtype == torch.qint8:
|
||||
qconfig_spec = {
|
||||
nn.Linear: default_dynamic_qconfig,
|
||||
nn.LSTM: default_dynamic_qconfig,
|
||||
nn.GRU: default_dynamic_qconfig,
|
||||
nn.LSTMCell: default_dynamic_qconfig,
|
||||
nn.RNNCell: default_dynamic_qconfig,
|
||||
nn.GRUCell: default_dynamic_qconfig,
|
||||
}
|
||||
elif dtype == torch.float16:
|
||||
qconfig_spec = {
|
||||
nn.Linear: float16_dynamic_qconfig,
|
||||
nn.LSTM: float16_dynamic_qconfig,
|
||||
nn.GRU: float16_dynamic_qconfig,
|
||||
nn.LSTMCell: float16_dynamic_qconfig,
|
||||
nn.RNNCell: float16_dynamic_qconfig,
|
||||
nn.GRUCell: float16_dynamic_qconfig,
|
||||
}
|
||||
elif dtype == torch.quint8:
|
||||
qconfig_spec = {
|
||||
nn.EmbeddingBag: float_qparams_weight_only_qconfig,
|
||||
nn.Embedding: float_qparams_weight_only_qconfig,
|
||||
}
|
||||
elif dtype == torch.quint4x2:
|
||||
qconfig_spec = {
|
||||
nn.EmbeddingBag: float_qparams_weight_only_qconfig_4bit,
|
||||
}
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Don't know how to quantize with default settings for {dtype}. Provide full qconfig please"
|
||||
)
|
||||
elif isinstance(qconfig_spec, set):
|
||||
if dtype is torch.qint8:
|
||||
default_qconfig = default_dynamic_qconfig
|
||||
elif dtype is torch.float16:
|
||||
default_qconfig = float16_dynamic_qconfig
|
||||
elif dtype is torch.quint8:
|
||||
default_qconfig = float_qparams_weight_only_qconfig
|
||||
elif dtype is torch.quint4x2:
|
||||
default_qconfig = float_qparams_weight_only_qconfig_4bit
|
||||
else:
|
||||
raise RuntimeError(
|
||||
"Unknown dtype specified for quantize_dynamic: ", str(dtype)
|
||||
)
|
||||
qconfig_spec = dict(zip(qconfig_spec, itertools.repeat(default_qconfig)))
|
||||
|
||||
if mapping is None:
|
||||
mapping = get_default_dynamic_quant_module_mappings()
|
||||
|
||||
if not inplace:
|
||||
model = copy.deepcopy(model)
|
||||
model.eval()
|
||||
propagate_qconfig_(model, qconfig_spec)
|
||||
convert(model, mapping, inplace=True)
|
||||
return model
|
||||
|
||||
|
||||
@typing_extensions.deprecated(DEPRECATION_WARNING)
|
||||
def prepare_qat(model, mapping=None, inplace=False):
|
||||
r"""
|
||||
Prepares a copy of the model for quantization calibration or
|
||||
quantization-aware training and converts it to quantized version.
|
||||
|
||||
Quantization configuration should be assigned preemptively
|
||||
to individual submodules in `.qconfig` attribute.
|
||||
|
||||
Args:
|
||||
model: input model to be modified in-place
|
||||
mapping: dictionary that maps float modules to quantized modules to be
|
||||
replaced.
|
||||
inplace: carry out model transformations in-place, the original module
|
||||
is mutated
|
||||
"""
|
||||
torch._C._log_api_usage_once("quantization_api.quantize.prepare_qat")
|
||||
if not model.training:
|
||||
raise AssertionError("prepare_qat only works on models in training mode")
|
||||
if mapping is None:
|
||||
mapping = get_default_qat_module_mappings()
|
||||
|
||||
if not inplace:
|
||||
model = copy.deepcopy(model)
|
||||
|
||||
propagate_qconfig_(model, qconfig_dict=None)
|
||||
convert(model, mapping=mapping, inplace=True, remove_qconfig=False)
|
||||
prepare(model, observer_non_leaf_module_list=set(mapping.values()), inplace=True)
|
||||
return model
|
||||
|
||||
|
||||
@typing_extensions.deprecated(DEPRECATION_WARNING)
|
||||
def quantize_qat(model, run_fn, run_args, inplace=False):
|
||||
r"""Do quantization aware training and output a quantized model
|
||||
|
||||
Args:
|
||||
model: input model
|
||||
run_fn: a function for evaluating the prepared model, can be a
|
||||
function that simply runs the prepared model or a training
|
||||
loop
|
||||
run_args: positional arguments for `run_fn`
|
||||
|
||||
Return:
|
||||
Quantized model.
|
||||
"""
|
||||
torch._C._log_api_usage_once("quantization_api.quantize.quantize_qat")
|
||||
if not inplace:
|
||||
model = copy.deepcopy(model)
|
||||
model.train()
|
||||
prepare_qat(model, inplace=True)
|
||||
run_fn(model, *run_args)
|
||||
convert(model, inplace=True)
|
||||
return model
|
||||
|
||||
|
||||
@typing_extensions.deprecated(DEPRECATION_WARNING)
|
||||
def convert(
|
||||
module,
|
||||
mapping=None,
|
||||
inplace=False,
|
||||
remove_qconfig=True,
|
||||
is_reference=False,
|
||||
convert_custom_config_dict=None,
|
||||
use_precomputed_fake_quant=False,
|
||||
):
|
||||
r"""Converts submodules in input module to a different module according to `mapping`
|
||||
by calling `from_float` method on the target module class. And remove qconfig at the
|
||||
end if remove_qconfig is set to True.
|
||||
|
||||
Args:
|
||||
`module`: prepared and calibrated module
|
||||
`mapping`: a dictionary that maps from source module type to target
|
||||
module type, can be overwritten to allow swapping user defined
|
||||
Modules
|
||||
`inplace`: carry out model transformations in-place, the original module
|
||||
is mutated
|
||||
`convert_custom_config_dict`: custom configuration dictionary for convert function
|
||||
`use_precomputed_fake_quant`: a flag to enable use of precomputed fake quant
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Example of convert_custom_config_dict:
|
||||
convert_custom_config_dict = {
|
||||
# user will manually define the corresponding quantized
|
||||
# module class which has a from_observed class method that converts
|
||||
# observed custom module to quantized custom module
|
||||
"observed_to_quantized_custom_module_class": {
|
||||
ObservedCustomModule: QuantizedCustomModule
|
||||
}
|
||||
}
|
||||
|
||||
"""
|
||||
torch._C._log_api_usage_once("quantization_api.quantize.convert")
|
||||
if not inplace:
|
||||
module = copy.deepcopy(module)
|
||||
_convert(
|
||||
module,
|
||||
mapping,
|
||||
inplace=True,
|
||||
is_reference=is_reference,
|
||||
convert_custom_config_dict=convert_custom_config_dict,
|
||||
use_precomputed_fake_quant=use_precomputed_fake_quant,
|
||||
)
|
||||
if remove_qconfig:
|
||||
_remove_qconfig(module)
|
||||
return module
|
||||
|
||||
|
||||
def _convert(
|
||||
module,
|
||||
mapping=None,
|
||||
inplace=False,
|
||||
is_reference=False,
|
||||
convert_custom_config_dict=None,
|
||||
use_precomputed_fake_quant=False,
|
||||
):
|
||||
r"""Converts submodules in input module to a different module according to `mapping`
|
||||
by calling `from_float` method on the target module class
|
||||
|
||||
Args:
|
||||
module: input module
|
||||
mapping: a dictionary that maps from source module type to target
|
||||
module type, can be overwritten to allow swapping user defined
|
||||
Modules
|
||||
inplace: carry out model transformations in-place, the original module
|
||||
is mutated
|
||||
is_reference: a flag to enable quantized reference module
|
||||
use_precomputed_fake_quant: a flag to enable use of precomputed fake quant
|
||||
|
||||
"""
|
||||
if mapping is None:
|
||||
mapping = (
|
||||
get_default_static_quant_reference_module_mappings()
|
||||
if is_reference
|
||||
else get_default_static_quant_module_mappings()
|
||||
)
|
||||
if convert_custom_config_dict is None:
|
||||
convert_custom_config_dict = get_default_custom_config_dict()
|
||||
custom_module_class_mapping = convert_custom_config_dict.get(
|
||||
"observed_to_quantized_custom_module_class", {}
|
||||
)
|
||||
|
||||
if not inplace:
|
||||
module = copy.deepcopy(module)
|
||||
reassign = {}
|
||||
for name, mod in module.named_children():
|
||||
# both fused modules and observed custom modules are
|
||||
# swapped as one unit
|
||||
if (
|
||||
not isinstance(mod, _FusedModule)
|
||||
and type_before_parametrizations(mod) not in custom_module_class_mapping
|
||||
):
|
||||
_convert(
|
||||
mod,
|
||||
mapping,
|
||||
True, # inplace
|
||||
is_reference,
|
||||
convert_custom_config_dict,
|
||||
use_precomputed_fake_quant=use_precomputed_fake_quant,
|
||||
)
|
||||
reassign[name] = swap_module(
|
||||
mod, mapping, custom_module_class_mapping, use_precomputed_fake_quant
|
||||
)
|
||||
|
||||
for key, value in reassign.items():
|
||||
module._modules[key] = value
|
||||
|
||||
return module
|
||||
|
||||
|
||||
def swap_module(
|
||||
mod, mapping, custom_module_class_mapping, use_precomputed_fake_quant=False
|
||||
):
|
||||
r"""Swaps the module if it has a quantized counterpart and it has an
|
||||
`observer` attached.
|
||||
|
||||
Args:
|
||||
mod: input module
|
||||
mapping: a dictionary that maps from nn module to nnq module
|
||||
|
||||
Return:
|
||||
The corresponding quantized module of `mod`
|
||||
"""
|
||||
new_mod = mod
|
||||
if hasattr(mod, "qconfig") and mod.qconfig is not None:
|
||||
swapped = False
|
||||
if type_before_parametrizations(mod) in custom_module_class_mapping:
|
||||
new_mod = custom_module_class_mapping[
|
||||
type_before_parametrizations(mod)
|
||||
].from_observed(mod)
|
||||
swapped = True
|
||||
elif type_before_parametrizations(mod) in mapping:
|
||||
qmod = mapping[type_before_parametrizations(mod)]
|
||||
if hasattr(qmod, "_IS_REFERENCE") and qmod._IS_REFERENCE:
|
||||
if mod.qconfig is None:
|
||||
raise AssertionError(
|
||||
"module qconfig must not be None when swapping to reference module"
|
||||
)
|
||||
weight_post_process = mod.qconfig.weight()
|
||||
weight_post_process(mod.weight)
|
||||
weight_qparams = get_qparam_dict(weight_post_process)
|
||||
new_mod = qmod.from_float(mod, weight_qparams)
|
||||
else:
|
||||
sig = inspect.signature(qmod.from_float)
|
||||
if "use_precomputed_fake_quant" in sig.parameters:
|
||||
new_mod = qmod.from_float(
|
||||
mod, use_precomputed_fake_quant=use_precomputed_fake_quant
|
||||
)
|
||||
else:
|
||||
new_mod = qmod.from_float(mod)
|
||||
swapped = True
|
||||
|
||||
if swapped:
|
||||
# Preserve module's pre forward hooks. They'll be called on quantized input
|
||||
for pre_hook_fn in mod._forward_pre_hooks.values():
|
||||
new_mod.register_forward_pre_hook(pre_hook_fn)
|
||||
# Preserve module's post forward hooks except _observer_forward_hook
|
||||
# After convert they'll work with quantized output
|
||||
for hook_fn in mod._forward_hooks.values():
|
||||
if hook_fn is not _observer_forward_hook:
|
||||
new_mod.register_forward_hook(hook_fn)
|
||||
|
||||
# respect device affinity when swapping modules
|
||||
devices = _get_unique_devices_(mod)
|
||||
if not (
|
||||
len(devices) <= 1
|
||||
or (len(devices) == 2 and torch.device("meta") in devices)
|
||||
):
|
||||
raise AssertionError(
|
||||
f"swap_module only works with cpu or single-device CUDA modules, but got devices {devices}"
|
||||
)
|
||||
device = next(iter(devices)) if len(devices) > 0 else None
|
||||
if device:
|
||||
new_mod.to(device)
|
||||
return new_mod
|
||||
|
||||
|
||||
def _get_observer_dict(mod, target_dict, prefix=""):
|
||||
r"""Traverse the modules and save all observers into dict.
|
||||
This is mainly used for quantization accuracy debug
|
||||
Args:
|
||||
mod: the top module we want to save all observers
|
||||
prefix: the prefix for the current module
|
||||
target_dict: the dictionary used to save all the observers
|
||||
"""
|
||||
|
||||
def get_prefix(prefix):
|
||||
return prefix if prefix == "" else prefix + "."
|
||||
|
||||
if hasattr(mod, "activation_post_process"):
|
||||
target_dict[get_prefix(prefix) + "activation_post_process"] = (
|
||||
mod.activation_post_process
|
||||
)
|
||||
for name, child in mod.named_children():
|
||||
module_prefix = get_prefix(prefix) + name if prefix else name
|
||||
_get_observer_dict(child, target_dict, module_prefix)
|
||||
@@ -0,0 +1,759 @@
|
||||
import copy
|
||||
import typing_extensions
|
||||
import warnings
|
||||
from typing import Any
|
||||
|
||||
import torch
|
||||
from torch.fx import GraphModule
|
||||
from torch.fx.graph_module import _USER_PRESERVED_ATTRIBUTES_KEY
|
||||
|
||||
from .backend_config import BackendConfig, get_tensorrt_backend_config # noqa: F401
|
||||
from .fx.convert import convert
|
||||
from .fx.custom_config import ConvertCustomConfig, FuseCustomConfig, PrepareCustomConfig
|
||||
from .fx.fuse import fuse # noqa: F401
|
||||
from .fx.graph_module import ObservedGraphModule # noqa: F401
|
||||
from .fx.prepare import prepare # noqa: F401
|
||||
from .fx.tracer import QuantizationTracer, Scope, ScopeContextManager # noqa: F401
|
||||
from .fx.utils import ( # noqa: F401
|
||||
get_custom_module_class_keys,
|
||||
get_skipped_module_name_and_classes,
|
||||
)
|
||||
from .qconfig_mapping import QConfigMapping
|
||||
from .utils import DEPRECATION_WARNING
|
||||
|
||||
|
||||
def attach_preserved_attrs_to_model(
|
||||
model: GraphModule | torch.nn.Module,
|
||||
preserved_attrs: dict[str, Any],
|
||||
) -> None:
|
||||
"""Store preserved attributes to the model.meta so that it can be preserved during deepcopy"""
|
||||
model.meta[_USER_PRESERVED_ATTRIBUTES_KEY] = copy.copy(preserved_attrs) # type: ignore[operator, index, assignment]
|
||||
# set the preserved attributes in the model so that user can call
|
||||
# model.attr as they do before calling fx graph mode quantization
|
||||
for attr_name, attr in model.meta[_USER_PRESERVED_ATTRIBUTES_KEY].items(): # type: ignore[index, union-attr]
|
||||
setattr(model, attr_name, attr)
|
||||
|
||||
|
||||
def _check_is_graph_module(model: torch.nn.Module) -> None:
|
||||
if not isinstance(model, GraphModule):
|
||||
raise ValueError(
|
||||
"input model must be a GraphModule, "
|
||||
+ "Got type:"
|
||||
+ str(type(model))
|
||||
+ " Please make "
|
||||
+ "sure to follow the tutorials."
|
||||
)
|
||||
|
||||
|
||||
def _attach_meta_to_node_if_not_exist(model: GraphModule) -> None:
|
||||
"""Attach meta field to all nodes of the graph if it does not exist,
|
||||
meta field is a field stores some meta information about the node, such
|
||||
as dtype and shape information for output of the node, this only exists
|
||||
if the program is captured by make_fx (used in quantize_pt2e flow), if
|
||||
the program is captured by torch.fx symbolic tracing, this field may not exist,
|
||||
so we add it here to avoid checking this all over the places
|
||||
"""
|
||||
for node in model.graph.nodes:
|
||||
if not hasattr(node, "meta"):
|
||||
node.meta = {}
|
||||
|
||||
|
||||
def _swap_ff_with_fxff(model: torch.nn.Module) -> None:
|
||||
r"""Swap FloatFunctional with FXFloatFunctional"""
|
||||
modules_to_swap = []
|
||||
for name, module in model.named_children():
|
||||
if isinstance(module, torch.ao.nn.quantized.FloatFunctional):
|
||||
modules_to_swap.append(name)
|
||||
else:
|
||||
_swap_ff_with_fxff(module)
|
||||
|
||||
for name in modules_to_swap:
|
||||
del model._modules[name]
|
||||
model._modules[name] = torch.ao.nn.quantized.FXFloatFunctional()
|
||||
|
||||
|
||||
def _fuse_fx(
|
||||
model: GraphModule,
|
||||
is_qat: bool,
|
||||
fuse_custom_config: FuseCustomConfig | dict[str, Any] | None = None,
|
||||
backend_config: BackendConfig | dict[str, Any] | None = None,
|
||||
) -> GraphModule:
|
||||
r"""Internal helper function to fuse modules in preparation for quantization
|
||||
|
||||
Args:
|
||||
model: GraphModule object from symbolic tracing (torch.fx.symbolic_trace)
|
||||
"""
|
||||
_check_is_graph_module(model)
|
||||
return fuse(model, is_qat, fuse_custom_config, backend_config) # type: ignore[operator]
|
||||
|
||||
|
||||
def _prepare_fx(
|
||||
model: torch.nn.Module,
|
||||
qconfig_mapping: QConfigMapping | dict[str, Any],
|
||||
is_qat: bool,
|
||||
example_inputs: tuple[Any, ...],
|
||||
prepare_custom_config: PrepareCustomConfig | dict[str, Any] | None = None,
|
||||
_equalization_config: QConfigMapping | dict[str, Any] | None = None,
|
||||
backend_config: BackendConfig | dict[str, Any] | None = None,
|
||||
is_standalone_module: bool = False,
|
||||
) -> GraphModule:
|
||||
r"""Internal helper function for prepare_fx
|
||||
Args:
|
||||
`model`, `qconfig_mapping`, `prepare_custom_config`, `_equalization_config`:
|
||||
see docs for :func:`~torch.ao.quantization.prepare_fx`
|
||||
`is_standalone_module`: a boolean flag indicates whether we are
|
||||
quantizing a standalone module or not, a standalone module
|
||||
is a submodule of the parent module that is not inlined in the
|
||||
forward graph of the parent module,
|
||||
the way we quantize standalone module is described in:
|
||||
:func:`~torch.ao.quantization._prepare_standalone_module_fx`
|
||||
"""
|
||||
if prepare_custom_config is None:
|
||||
prepare_custom_config = PrepareCustomConfig()
|
||||
if _equalization_config is None:
|
||||
_equalization_config = QConfigMapping()
|
||||
|
||||
if isinstance(prepare_custom_config, dict):
|
||||
warnings.warn(
|
||||
"Passing a prepare_custom_config_dict to prepare is deprecated and will not be supported "
|
||||
"in a future version. Please pass in a PrepareCustomConfig instead.",
|
||||
FutureWarning,
|
||||
stacklevel=3,
|
||||
)
|
||||
prepare_custom_config = PrepareCustomConfig.from_dict(prepare_custom_config)
|
||||
|
||||
# swap FloatFunctional with FXFloatFunctional
|
||||
_swap_ff_with_fxff(model)
|
||||
|
||||
skipped_module_names, skipped_module_classes = get_skipped_module_name_and_classes(
|
||||
prepare_custom_config, is_standalone_module
|
||||
)
|
||||
preserved_attr_names = prepare_custom_config.preserved_attributes
|
||||
preserved_attrs = {
|
||||
attr: getattr(model, attr)
|
||||
for attr in preserved_attr_names
|
||||
if hasattr(model, attr)
|
||||
}
|
||||
# symbolically trace the model
|
||||
tracer = QuantizationTracer(skipped_module_names, skipped_module_classes) # type: ignore[arg-type]
|
||||
graph_module = GraphModule(model, tracer.trace(model))
|
||||
_attach_meta_to_node_if_not_exist(graph_module)
|
||||
|
||||
fuse_custom_config = FuseCustomConfig().set_preserved_attributes(
|
||||
prepare_custom_config.preserved_attributes
|
||||
)
|
||||
graph_module = _fuse_fx(graph_module, is_qat, fuse_custom_config, backend_config)
|
||||
prepared = prepare(
|
||||
graph_module,
|
||||
qconfig_mapping,
|
||||
is_qat,
|
||||
tracer.node_name_to_scope,
|
||||
example_inputs=example_inputs,
|
||||
prepare_custom_config=prepare_custom_config,
|
||||
_equalization_config=_equalization_config,
|
||||
backend_config=backend_config,
|
||||
is_standalone_module=is_standalone_module,
|
||||
) # type: ignore[operator]
|
||||
|
||||
attach_preserved_attrs_to_model(prepared, preserved_attrs)
|
||||
return prepared
|
||||
|
||||
|
||||
def _prepare_standalone_module_fx(
|
||||
model: torch.nn.Module,
|
||||
qconfig_mapping: QConfigMapping | dict[str, Any],
|
||||
is_qat: bool,
|
||||
example_inputs: tuple[Any, ...],
|
||||
prepare_custom_config: PrepareCustomConfig | dict[str, Any] | None = None,
|
||||
backend_config: BackendConfig | dict[str, Any] | None = None,
|
||||
) -> GraphModule:
|
||||
r"""[Internal use only] Prepare a standalone module, so that it can be used when quantizing the
|
||||
parent module.
|
||||
standalone_module means it a submodule that is not inlined in parent module,
|
||||
and will be quantized separately as one unit.
|
||||
|
||||
How the standalone module is observed is specified by `input_quantized_idxs` and
|
||||
`output_quantized_idxs` in the prepare_custom_config for the standalone module
|
||||
|
||||
Returns:
|
||||
|
||||
* model(GraphModule): prepared standalone module. It has these attributes in
|
||||
model.meta:
|
||||
|
||||
* `standalone_module_input_quantized_idxs(List[Int])`: a list of
|
||||
indexes for the graph input that is expected to be quantized,
|
||||
same as input_quantized_idxs configuration provided
|
||||
for the standalone module
|
||||
* `standalone_module_output_quantized_idxs(List[Int])`: a list of
|
||||
indices for the graph output that is quantized
|
||||
same as input_quantized_idxs configuration provided
|
||||
for the standalone module
|
||||
|
||||
"""
|
||||
return _prepare_fx(
|
||||
model,
|
||||
qconfig_mapping,
|
||||
is_qat,
|
||||
example_inputs,
|
||||
prepare_custom_config,
|
||||
backend_config=backend_config,
|
||||
is_standalone_module=True,
|
||||
)
|
||||
|
||||
|
||||
def fuse_fx(
|
||||
model: torch.nn.Module,
|
||||
fuse_custom_config: FuseCustomConfig | dict[str, Any] | None = None,
|
||||
backend_config: BackendConfig | dict[str, Any] | None = None,
|
||||
) -> GraphModule:
|
||||
r"""Fuse modules like conv+bn, conv+bn+relu etc, model must be in eval mode.
|
||||
Fusion rules are defined in torch.ao.quantization.fx.fusion_pattern.py
|
||||
|
||||
Args:
|
||||
|
||||
* `model` (torch.nn.Module): a torch.nn.Module model
|
||||
* `fuse_custom_config` (FuseCustomConfig): custom configurations for fuse_fx.
|
||||
See :class:`~torch.ao.quantization.fx.custom_config.FuseCustomConfig` for more details
|
||||
Example::
|
||||
|
||||
from torch.ao.quantization import fuse_fx
|
||||
|
||||
m = Model().eval()
|
||||
m = fuse_fx(m)
|
||||
|
||||
"""
|
||||
if fuse_custom_config is None:
|
||||
fuse_custom_config = FuseCustomConfig()
|
||||
|
||||
if isinstance(fuse_custom_config, dict):
|
||||
warnings.warn(
|
||||
"Passing a fuse_custom_config_dict to fuse is deprecated and will not be supported "
|
||||
"in a future version. Please pass in a FuseCustomConfig instead.",
|
||||
FutureWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
fuse_custom_config = FuseCustomConfig.from_dict(fuse_custom_config)
|
||||
|
||||
torch._C._log_api_usage_once("quantization_api.quantize_fx.fuse_fx")
|
||||
preserved_attr_names = fuse_custom_config.preserved_attributes
|
||||
preserved_attrs = {
|
||||
attr: getattr(model, attr)
|
||||
for attr in preserved_attr_names
|
||||
if hasattr(model, attr)
|
||||
}
|
||||
|
||||
graph_module = torch.fx.symbolic_trace(model)
|
||||
_attach_meta_to_node_if_not_exist(graph_module)
|
||||
graph_module = _fuse_fx(graph_module, False, fuse_custom_config, backend_config)
|
||||
|
||||
attach_preserved_attrs_to_model(graph_module, preserved_attrs)
|
||||
return graph_module
|
||||
|
||||
|
||||
@typing_extensions.deprecated(DEPRECATION_WARNING)
|
||||
def prepare_fx(
|
||||
model: torch.nn.Module,
|
||||
qconfig_mapping: QConfigMapping | dict[str, Any],
|
||||
example_inputs: tuple[Any, ...],
|
||||
prepare_custom_config: PrepareCustomConfig | dict[str, Any] | None = None,
|
||||
_equalization_config: QConfigMapping | dict[str, Any] | None = None,
|
||||
backend_config: BackendConfig | dict[str, Any] | None = None,
|
||||
) -> GraphModule:
|
||||
r""" Prepare a model for post training quantization
|
||||
|
||||
Args:
|
||||
* `model` (torch.nn.Module): torch.nn.Module model
|
||||
|
||||
* `qconfig_mapping` (QConfigMapping): QConfigMapping object to configure how a model is
|
||||
quantized, see :class:`~torch.ao.quantization.qconfig_mapping.QConfigMapping`
|
||||
for more details
|
||||
|
||||
* `example_inputs` (Tuple[Any, ...]): Example inputs for forward function of the model,
|
||||
Tuple of positional args (keyword args can be passed as positional args as well)
|
||||
|
||||
* `prepare_custom_config` (PrepareCustomConfig): customization configuration for quantization tool.
|
||||
See :class:`~torch.ao.quantization.fx.custom_config.PrepareCustomConfig` for more details
|
||||
|
||||
* `_equalization_config`: config for specifying how to perform equalization on the model
|
||||
|
||||
* `backend_config` (BackendConfig): config that specifies how operators are quantized
|
||||
in a backend, this includes how the operators are observed,
|
||||
supported fusion patterns, how quantize/dequantize ops are
|
||||
inserted, supported dtypes etc. See :class:`~torch.ao.quantization.backend_config.BackendConfig` for more details
|
||||
|
||||
Return:
|
||||
A GraphModule with observer (configured by qconfig_mapping), ready for calibration
|
||||
|
||||
Example::
|
||||
|
||||
import torch
|
||||
from torch.ao.quantization import get_default_qconfig_mapping
|
||||
from torch.ao.quantization.quantize_fx import prepare_fx
|
||||
|
||||
class Submodule(torch.nn.Module):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.linear = torch.nn.Linear(5, 5)
|
||||
def forward(self, x):
|
||||
x = self.linear(x)
|
||||
return x
|
||||
|
||||
class M(torch.nn.Module):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.linear = torch.nn.Linear(5, 5)
|
||||
self.sub = Submodule()
|
||||
|
||||
def forward(self, x):
|
||||
x = self.linear(x)
|
||||
x = self.sub(x) + x
|
||||
return 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)
|
||||
|
||||
# qconfig is the configuration for how we insert observers for a particular
|
||||
# operator
|
||||
# qconfig = get_default_qconfig("fbgemm")
|
||||
# Example of customizing qconfig:
|
||||
# qconfig = torch.ao.quantization.QConfig(
|
||||
# activation=MinMaxObserver.with_args(dtype=torch.qint8),
|
||||
# weight=MinMaxObserver.with_args(dtype=torch.qint8))
|
||||
# `activation` and `weight` are constructors of observer module
|
||||
|
||||
# qconfig_mapping is a collection of quantization configurations, user can
|
||||
# set the qconfig for each operator (torch op calls, functional calls, module calls)
|
||||
# in the model through qconfig_mapping
|
||||
# the following call will get the qconfig_mapping that works best for models
|
||||
# that target "fbgemm" backend
|
||||
qconfig_mapping = get_default_qconfig_mapping("fbgemm")
|
||||
|
||||
# We can customize qconfig_mapping in different ways.
|
||||
# e.g. set the global qconfig, which means we will use the same qconfig for
|
||||
# all operators in the model, this can be overwritten by other settings
|
||||
# qconfig_mapping = QConfigMapping().set_global(qconfig)
|
||||
# e.g. quantize the linear submodule with a specific qconfig
|
||||
# qconfig_mapping = QConfigMapping().set_module_name("linear", qconfig)
|
||||
# e.g. quantize all nn.Linear modules with a specific qconfig
|
||||
# qconfig_mapping = QConfigMapping().set_object_type(torch.nn.Linear, qconfig)
|
||||
# for a more complete list, please see the docstring for :class:`torch.ao.quantization.QConfigMapping`
|
||||
# argument
|
||||
|
||||
# example_inputs is a tuple of inputs, that is used to infer the type of the
|
||||
# outputs in the model
|
||||
# currently it's not used, but please make sure model(*example_inputs) runs
|
||||
example_inputs = (torch.randn(1, 3, 224, 224),)
|
||||
|
||||
# TODO: add backend_config after we split the backend_config for fbgemm and qnnpack
|
||||
# e.g. backend_config = get_default_backend_config("fbgemm")
|
||||
# `prepare_fx` inserts observers in the model based on qconfig_mapping and
|
||||
# backend_config. If the configuration for an operator in qconfig_mapping
|
||||
# is supported in the backend_config (meaning it's supported by the target
|
||||
# hardware), we'll insert observer modules according to the qconfig_mapping
|
||||
# otherwise the configuration in qconfig_mapping will be ignored
|
||||
#
|
||||
# Example:
|
||||
# in qconfig_mapping, user sets linear module to be quantized with quint8 for
|
||||
# activation and qint8 for weight:
|
||||
# qconfig = torch.ao.quantization.QConfig(
|
||||
# observer=MinMaxObserver.with_args(dtype=torch.quint8),
|
||||
# weight=MinMaxObserver.with-args(dtype=torch.qint8))
|
||||
# Note: current qconfig api does not support setting output observer, but
|
||||
# we may extend this to support these more fine grained control in the
|
||||
# future
|
||||
#
|
||||
# qconfig_mapping = QConfigMapping().set_object_type(torch.nn.Linear, qconfig)
|
||||
# in backend config, linear module also supports in this configuration:
|
||||
# weighted_int8_dtype_config = DTypeConfig(
|
||||
# input_dtype=torch.quint8,
|
||||
# output_dtype=torch.quint8,
|
||||
# weight_dtype=torch.qint8,
|
||||
# bias_type=torch.float)
|
||||
|
||||
# linear_pattern_config = BackendPatternConfig(torch.nn.Linear) \
|
||||
# .set_observation_type(ObservationType.OUTPUT_USE_DIFFERENT_OBSERVER_AS_INPUT) \
|
||||
# .add_dtype_config(weighted_int8_dtype_config) \
|
||||
# ...
|
||||
|
||||
# backend_config = BackendConfig().set_backend_pattern_config(linear_pattern_config)
|
||||
# `prepare_fx` will check that the setting requested by suer in qconfig_mapping
|
||||
# is supported by the backend_config and insert observers and fake quant modules
|
||||
# in the model
|
||||
prepared_model = prepare_fx(float_model, qconfig_mapping, example_inputs)
|
||||
# Run calibration
|
||||
calibrate(prepared_model, sample_inference_data)
|
||||
"""
|
||||
torch._C._log_api_usage_once("quantization_api.quantize_fx.prepare_fx")
|
||||
return _prepare_fx(
|
||||
model,
|
||||
qconfig_mapping,
|
||||
False, # is_qat
|
||||
example_inputs,
|
||||
prepare_custom_config,
|
||||
_equalization_config,
|
||||
backend_config,
|
||||
)
|
||||
|
||||
|
||||
@typing_extensions.deprecated(DEPRECATION_WARNING)
|
||||
def prepare_qat_fx(
|
||||
model: torch.nn.Module,
|
||||
qconfig_mapping: QConfigMapping | dict[str, Any],
|
||||
example_inputs: tuple[Any, ...],
|
||||
prepare_custom_config: PrepareCustomConfig | dict[str, Any] | None = None,
|
||||
backend_config: BackendConfig | dict[str, Any] | None = None,
|
||||
) -> GraphModule:
|
||||
r"""Prepare a model for quantization aware training
|
||||
|
||||
Args:
|
||||
* `model` (torch.nn.Module): torch.nn.Module model
|
||||
* `qconfig_mapping` (QConfigMapping): see :func:`~torch.ao.quantization.prepare_fx`
|
||||
* `example_inputs` (Tuple[Any, ...]): see :func:`~torch.ao.quantization.prepare_fx`
|
||||
* `prepare_custom_config` (PrepareCustomConfig): see :func:`~torch.ao.quantization.prepare_fx`
|
||||
* `backend_config` (BackendConfig): see :func:`~torch.ao.quantization.prepare_fx`
|
||||
|
||||
Return:
|
||||
A GraphModule with fake quant modules (configured by qconfig_mapping and backend_config), ready for
|
||||
quantization aware training
|
||||
|
||||
Example::
|
||||
|
||||
import torch
|
||||
from torch.ao.quantization import get_default_qat_qconfig_mapping
|
||||
from torch.ao.quantization.quantize_fx import prepare_qat_fx
|
||||
|
||||
|
||||
class Submodule(torch.nn.Module):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.linear = torch.nn.Linear(5, 5)
|
||||
|
||||
def forward(self, x):
|
||||
x = self.linear(x)
|
||||
return x
|
||||
|
||||
|
||||
class M(torch.nn.Module):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.linear = torch.nn.Linear(5, 5)
|
||||
self.sub = Submodule()
|
||||
|
||||
def forward(self, x):
|
||||
x = self.linear(x)
|
||||
x = self.sub(x) + x
|
||||
return x
|
||||
|
||||
|
||||
# initialize a floating point model
|
||||
float_model = M().train()
|
||||
# (optional, but preferred) load the weights from pretrained model
|
||||
# float_model.load_weights(...)
|
||||
|
||||
|
||||
# define the training loop for quantization aware training
|
||||
def train_loop(model, train_data):
|
||||
model.train()
|
||||
for image, target in data_loader:
|
||||
...
|
||||
|
||||
|
||||
# qconfig is the configuration for how we insert observers for a particular
|
||||
# operator
|
||||
# qconfig = get_default_qconfig("fbgemm")
|
||||
# Example of customizing qconfig:
|
||||
# qconfig = torch.ao.quantization.QConfig(
|
||||
# activation=FakeQuantize.with_args(observer=MinMaxObserver.with_args(dtype=torch.qint8)),
|
||||
# weight=FakeQuantize.with_args(observer=MinMaxObserver.with_args(dtype=torch.qint8)))
|
||||
# `activation` and `weight` are constructors of observer module
|
||||
|
||||
# qconfig_mapping is a collection of quantization configurations, user can
|
||||
# set the qconfig for each operator (torch op calls, functional calls, module calls)
|
||||
# in the model through qconfig_mapping
|
||||
# the following call will get the qconfig_mapping that works best for models
|
||||
# that target "fbgemm" backend
|
||||
qconfig_mapping = get_default_qat_qconfig_mapping("fbgemm")
|
||||
|
||||
# We can customize qconfig_mapping in different ways, please take a look at
|
||||
# the docstring for :func:`~torch.ao.quantization.prepare_fx` for different ways
|
||||
# to configure this
|
||||
|
||||
# example_inputs is a tuple of inputs, that is used to infer the type of the
|
||||
# outputs in the model
|
||||
# currently it's not used, but please make sure model(*example_inputs) runs
|
||||
example_inputs = (torch.randn(1, 3, 224, 224),)
|
||||
|
||||
# TODO: add backend_config after we split the backend_config for fbgemm and qnnpack
|
||||
# e.g. backend_config = get_default_backend_config("fbgemm")
|
||||
# `prepare_qat_fx` inserts observers in the model based on qconfig_mapping and
|
||||
# backend_config, if the configuration for an operator in qconfig_mapping
|
||||
# is supported in the backend_config (meaning it's supported by the target
|
||||
# hardware), we'll insert fake_quantize modules according to the qconfig_mapping
|
||||
# otherwise the configuration in qconfig_mapping will be ignored
|
||||
# see :func:`~torch.ao.quantization.prepare_fx` for a detailed explanation of
|
||||
# how qconfig_mapping interacts with backend_config
|
||||
prepared_model = prepare_qat_fx(float_model, qconfig_mapping, example_inputs)
|
||||
# Run training
|
||||
train_loop(prepared_model, train_loop)
|
||||
|
||||
"""
|
||||
torch._C._log_api_usage_once("quantization_api.quantize_fx.prepare_qat_fx")
|
||||
return _prepare_fx(
|
||||
model,
|
||||
qconfig_mapping,
|
||||
True, # is_qat
|
||||
example_inputs,
|
||||
prepare_custom_config,
|
||||
backend_config=backend_config,
|
||||
)
|
||||
|
||||
|
||||
def _convert_fx(
|
||||
graph_module: GraphModule,
|
||||
is_reference: bool,
|
||||
convert_custom_config: ConvertCustomConfig | dict[str, Any] | None = None,
|
||||
is_standalone_module: bool = False,
|
||||
_remove_qconfig: bool = True,
|
||||
qconfig_mapping: QConfigMapping | dict[str, Any] | None = None,
|
||||
backend_config: BackendConfig | dict[str, Any] | None = None,
|
||||
is_decomposed: bool = False,
|
||||
keep_original_weights: bool = False,
|
||||
) -> GraphModule:
|
||||
"""`is_standalone_module`: see docs in :func:`~torch.ao.quantization.prepare_standalone_module_fx`"""
|
||||
if convert_custom_config is None:
|
||||
convert_custom_config = ConvertCustomConfig()
|
||||
|
||||
if isinstance(convert_custom_config, dict):
|
||||
warnings.warn(
|
||||
"Passing a convert_custom_config_dict to convert is deprecated and will not be supported "
|
||||
"in a future version. Please pass in a ConvertCustomConfig instead.",
|
||||
FutureWarning,
|
||||
stacklevel=3,
|
||||
)
|
||||
convert_custom_config = ConvertCustomConfig.from_dict(convert_custom_config)
|
||||
|
||||
_check_is_graph_module(graph_module)
|
||||
preserved_attr_names = convert_custom_config.preserved_attributes
|
||||
preserved_attrs = {
|
||||
attr: getattr(graph_module, attr)
|
||||
for attr in preserved_attr_names
|
||||
if hasattr(graph_module, attr)
|
||||
}
|
||||
|
||||
quantized = convert(
|
||||
graph_module,
|
||||
is_reference,
|
||||
convert_custom_config,
|
||||
is_standalone_module,
|
||||
_remove_qconfig_flag=_remove_qconfig,
|
||||
qconfig_mapping=qconfig_mapping,
|
||||
backend_config=backend_config,
|
||||
is_decomposed=is_decomposed,
|
||||
keep_original_weights=keep_original_weights,
|
||||
)
|
||||
|
||||
attach_preserved_attrs_to_model(quantized, preserved_attrs)
|
||||
return quantized
|
||||
|
||||
|
||||
@typing_extensions.deprecated(DEPRECATION_WARNING)
|
||||
def convert_fx(
|
||||
graph_module: GraphModule,
|
||||
convert_custom_config: ConvertCustomConfig | dict[str, Any] | None = None,
|
||||
_remove_qconfig: bool = True,
|
||||
qconfig_mapping: QConfigMapping | dict[str, Any] | None = None,
|
||||
backend_config: BackendConfig | dict[str, Any] | None = None,
|
||||
keep_original_weights: bool = False,
|
||||
) -> GraphModule:
|
||||
r"""Convert a calibrated or trained model to a quantized model
|
||||
|
||||
Args:
|
||||
* `graph_module` (torch.fx.GraphModule): A prepared and calibrated/trained model (GraphModule)
|
||||
|
||||
* `convert_custom_config` (ConvertCustomConfig): custom configurations for convert function.
|
||||
See :class:`~torch.ao.quantization.fx.custom_config.ConvertCustomConfig` for more details
|
||||
|
||||
* `_remove_qconfig` (bool): Option to remove the qconfig attributes in the model after convert.
|
||||
|
||||
* `qconfig_mapping` (QConfigMapping): config for specifying how to convert a model for quantization.
|
||||
|
||||
The keys must include the ones in the qconfig_mapping passed to `prepare_fx` or `prepare_qat_fx`,
|
||||
with the same values or `None`. Additional keys can be specified with values set to `None`.
|
||||
|
||||
For each entry whose value is set to None, we skip quantizing that entry in the model::
|
||||
|
||||
qconfig_mapping = QConfigMapping
|
||||
.set_global(qconfig_from_prepare)
|
||||
.set_object_type(torch.nn.functional.add, None) # skip quantizing torch.nn.functional.add
|
||||
.set_object_type(torch.nn.functional.linear, qconfig_from_prepare)
|
||||
.set_module_name("foo.bar", None) # skip quantizing module "foo.bar"
|
||||
|
||||
* `backend_config` (BackendConfig): A configuration for the backend which describes how
|
||||
operators should be quantized in the backend, this includes quantization
|
||||
mode support (static/dynamic/weight_only), dtype support (quint8/qint8 etc.),
|
||||
observer placement for each operators and fused operators.
|
||||
See :class:`~torch.ao.quantization.backend_config.BackendConfig` for more details
|
||||
|
||||
Return:
|
||||
A quantized model (torch.nn.Module)
|
||||
|
||||
Example::
|
||||
|
||||
# prepared_model: the model after prepare_fx/prepare_qat_fx and calibration/training
|
||||
# convert_fx converts a calibrated/trained model to a quantized model for the
|
||||
# target hardware, this includes converting the model first to a reference
|
||||
# quantized model, and then lower the reference quantized model to a backend
|
||||
# Currently, the supported backends are fbgemm (onednn), qnnpack (xnnpack) and
|
||||
# they share the same set of quantized operators, so we are using the same
|
||||
# lowering procedure
|
||||
#
|
||||
# backend_config defines the corresponding reference quantized module for
|
||||
# the weighted modules in the model, e.g. nn.Linear
|
||||
# TODO: add backend_config after we split the backend_config for fbgemm and qnnpack
|
||||
# e.g. backend_config = get_default_backend_config("fbgemm")
|
||||
quantized_model = convert_fx(prepared_model)
|
||||
|
||||
"""
|
||||
torch._C._log_api_usage_once("quantization_api.quantize_fx.convert_fx")
|
||||
return _convert_fx(
|
||||
graph_module,
|
||||
is_reference=False,
|
||||
convert_custom_config=convert_custom_config,
|
||||
_remove_qconfig=_remove_qconfig,
|
||||
qconfig_mapping=qconfig_mapping,
|
||||
backend_config=backend_config,
|
||||
keep_original_weights=keep_original_weights,
|
||||
)
|
||||
|
||||
|
||||
def convert_to_reference_fx(
|
||||
graph_module: GraphModule,
|
||||
convert_custom_config: ConvertCustomConfig | dict[str, Any] | None = None,
|
||||
_remove_qconfig: bool = True,
|
||||
qconfig_mapping: QConfigMapping | dict[str, Any] | None = None,
|
||||
backend_config: BackendConfig | dict[str, Any] | None = None,
|
||||
) -> GraphModule:
|
||||
r"""Convert a calibrated or trained model to a reference quantized model,
|
||||
see https://github.com/pytorch/rfcs/blob/master/RFC-0019-Extending-PyTorch-Quantization-to-Custom-Backends.md for more details,
|
||||
reference quantized model is a standard representation of a quantized model provided
|
||||
by FX Graph Mode Quantization, it can be further lowered to run on the target
|
||||
hardware, like accelerators
|
||||
|
||||
Args:
|
||||
* `graph_module` (GraphModule): A prepared and calibrated/trained model (GraphModule)
|
||||
|
||||
* `convert_custom_config` (ConvertCustomConfig): custom configurations for convert function.
|
||||
See :func:`~torch.ao.quantization.quantize_fx.convert_fx` for more details.
|
||||
|
||||
* `_remove_qconfig` (bool): Option to remove the qconfig attributes in the model after convert.
|
||||
|
||||
* `qconfig_mapping` (QConfigMapping): config for specifying how to convert a model for quantization.
|
||||
See :func:`~torch.ao.quantization.quantize_fx.convert_fx` for more details.
|
||||
|
||||
* `backend_config` (BackendConfig): A configuration for the backend which describes how
|
||||
operators should be quantized in the backend. See
|
||||
:func:`~torch.ao.quantization.quantize_fx.convert_fx` for more details.
|
||||
|
||||
Return:
|
||||
A reference quantized model (GraphModule)
|
||||
|
||||
Example::
|
||||
|
||||
# prepared_model: the model after prepare_fx/prepare_qat_fx and calibration/training
|
||||
# TODO: add backend_config after we split the backend_config for fbgemm and qnnpack
|
||||
# e.g. backend_config = get_default_backend_config("fbgemm")
|
||||
reference_quantized_model = convert_to_reference_fx(prepared_model)
|
||||
|
||||
"""
|
||||
torch._C._log_api_usage_once("quantization_api.quantize_fx.convert_to_reference_fx")
|
||||
return _convert_fx(
|
||||
graph_module,
|
||||
is_reference=True,
|
||||
convert_custom_config=convert_custom_config,
|
||||
_remove_qconfig=_remove_qconfig,
|
||||
qconfig_mapping=qconfig_mapping,
|
||||
backend_config=backend_config,
|
||||
)
|
||||
|
||||
|
||||
def _convert_to_reference_decomposed_fx(
|
||||
graph_module: GraphModule,
|
||||
convert_custom_config: ConvertCustomConfig | dict[str, Any] | None = None,
|
||||
qconfig_mapping: QConfigMapping | dict[str, Any] | None = None,
|
||||
backend_config: BackendConfig | dict[str, Any] | None = None,
|
||||
) -> GraphModule:
|
||||
r"""Convert a calibrated or trained model to a reference quantized model, with
|
||||
decomposed representation for quantized Tensor
|
||||
see https://github.com/pytorch/rfcs/blob/master/RFC-0019-Extending-PyTorch-Quantization-to-Custom-Backends.md for more details,
|
||||
reference quantized model is a standard representation of a quantized model provided
|
||||
by FX Graph Mode Quantization, it can be further lowered to run on the target
|
||||
hardware, like accelerators
|
||||
|
||||
Note: this is not public API
|
||||
|
||||
Args:
|
||||
* `graph_module` (GraphModule): A prepared and calibrated/trained model (GraphModule)
|
||||
|
||||
* `convert_custom_config` (ConvertCustomConfig): custom configurations for convert function.
|
||||
See :func:`~torch.ao.quantization.quantize_fx.convert_fx` for more details.
|
||||
|
||||
* `_remove_qconfig` (bool): Option to remove the qconfig attributes in the model after convert.
|
||||
|
||||
* `qconfig_mapping` (QConfigMapping): config for specifying how to convert a model for quantization.
|
||||
See :func:`~torch.ao.quantization.quantize_fx.convert_fx` for more details.
|
||||
|
||||
* `backend_config` (BackendConfig): A configuration for the backend which describes how
|
||||
operators should be quantized in the backend. See
|
||||
:func:`~torch.ao.quantization.quantize_fx.convert_fx` for more details.
|
||||
|
||||
Return:
|
||||
A reference quantized model (GraphModule) with operators working with decomposed quantized Tensor
|
||||
|
||||
Example::
|
||||
|
||||
# prepared_model: the model after prepare_fx/prepare_qat_fx and calibration/training
|
||||
# TODO: add backend_config after we split the backend_config for fbgemm and qnnpack
|
||||
# e.g. backend_config = get_default_backend_config("fbgemm")
|
||||
reference_quantized_model = _convert_to_reference_decomposed_fx(prepared_model)
|
||||
|
||||
"""
|
||||
torch._C._log_api_usage_once(
|
||||
"quantization_api.quantize_fx._convert_to_reference_decomposed_fx"
|
||||
)
|
||||
return _convert_fx(
|
||||
graph_module,
|
||||
is_reference=True,
|
||||
convert_custom_config=convert_custom_config,
|
||||
_remove_qconfig=False,
|
||||
qconfig_mapping=qconfig_mapping,
|
||||
backend_config=backend_config,
|
||||
is_decomposed=True,
|
||||
)
|
||||
|
||||
|
||||
def _convert_standalone_module_fx(
|
||||
graph_module: GraphModule,
|
||||
is_reference: bool = False,
|
||||
convert_custom_config: ConvertCustomConfig | dict[str, Any] | None = None,
|
||||
) -> GraphModule:
|
||||
r"""[Internal use only] Convert a model produced by :func:`~torch.ao.quantization.prepare_standalone_module_fx`
|
||||
and convert it to a quantized model
|
||||
|
||||
Returns a quantized standalone module, whether input/output is quantized is
|
||||
specified by prepare_custom_config, with
|
||||
input_quantized_idxs, output_quantized_idxs, please
|
||||
see docs for prepare_fx for details
|
||||
"""
|
||||
return _convert_fx(
|
||||
graph_module,
|
||||
is_reference,
|
||||
convert_custom_config,
|
||||
is_standalone_module=True,
|
||||
)
|
||||
@@ -0,0 +1,423 @@
|
||||
# mypy: allow-untyped-defs
|
||||
|
||||
import torch
|
||||
from torch.ao.quantization.qconfig import QConfig
|
||||
from torch.ao.quantization.quant_type import QuantType
|
||||
from torch.jit._recursive import wrap_cpp_module
|
||||
|
||||
|
||||
__all__ = [
|
||||
"script_qconfig",
|
||||
"script_qconfig_dict",
|
||||
"fuse_conv_bn_jit",
|
||||
"prepare_jit",
|
||||
"prepare_dynamic_jit",
|
||||
"convert_jit",
|
||||
"convert_dynamic_jit",
|
||||
"quantize_jit",
|
||||
"quantize_dynamic_jit",
|
||||
]
|
||||
|
||||
|
||||
def _check_is_script_module(model):
|
||||
if not isinstance(model, torch.jit.ScriptModule):
|
||||
raise ValueError("input must be a script module, got: " + str(type(model)))
|
||||
|
||||
|
||||
def _check_forward_method(model):
|
||||
if not model._c._has_method("forward"):
|
||||
raise ValueError("input script module does not have forward method")
|
||||
|
||||
|
||||
def script_qconfig(qconfig):
|
||||
r"""Instantiate the activation and weight observer modules and script
|
||||
them, these observer module instances will be deepcopied during
|
||||
prepare_jit step.
|
||||
"""
|
||||
return QConfig(
|
||||
activation=torch.jit.script(qconfig.activation())._c,
|
||||
weight=torch.jit.script(qconfig.weight())._c,
|
||||
)
|
||||
|
||||
|
||||
def script_qconfig_dict(qconfig_dict):
|
||||
r"""Helper function used by `prepare_jit`.
|
||||
Apply `script_qconfig` for all entries in `qconfig_dict` that is
|
||||
not None.
|
||||
"""
|
||||
return {k: script_qconfig(v) if v else None for k, v in qconfig_dict.items()}
|
||||
|
||||
|
||||
def fuse_conv_bn_jit(model, inplace=False):
|
||||
r"""Fuse conv - bn module
|
||||
Works for eval model only.
|
||||
|
||||
Args:
|
||||
model: TorchScript model from scripting or tracing
|
||||
"""
|
||||
torch._C._log_api_usage_once("quantization_api.quantize_jit.fuse_conv_bn_jit")
|
||||
model_c = model._c
|
||||
model_c = torch._C._jit_pass_fold_convbn(model_c)
|
||||
if inplace:
|
||||
model._reconstruct(model_c)
|
||||
else:
|
||||
model = wrap_cpp_module(model_c)
|
||||
return model
|
||||
|
||||
|
||||
def _prepare_jit(model, qconfig_dict, inplace=False, quant_type=QuantType.STATIC):
|
||||
_check_is_script_module(model)
|
||||
_check_forward_method(model)
|
||||
if not all(isinstance(x, str) for x in qconfig_dict):
|
||||
raise ValueError("qconfig_dict should only contain names(str) as keys.")
|
||||
scripted_qconfig_dict = script_qconfig_dict(qconfig_dict)
|
||||
model = fuse_conv_bn_jit(model, inplace)
|
||||
model_c = torch._C._jit_pass_insert_observers(
|
||||
model._c, "forward", scripted_qconfig_dict, inplace, quant_type
|
||||
)
|
||||
if inplace:
|
||||
model._reconstruct(model_c)
|
||||
else:
|
||||
model = wrap_cpp_module(model_c)
|
||||
return model
|
||||
|
||||
|
||||
def _prepare_ondevice_jit(
|
||||
model,
|
||||
qconfig_dict,
|
||||
method_name="forward",
|
||||
inplace=False,
|
||||
quant_type=QuantType.STATIC,
|
||||
):
|
||||
_check_is_script_module(model)
|
||||
if not all(isinstance(x, str) for x in qconfig_dict):
|
||||
raise ValueError("qconfig_dict should only contain names(str) as keys.")
|
||||
scripted_qconfig_dict = script_qconfig_dict(qconfig_dict)
|
||||
method_graph = model._c._get_method(method_name).graph
|
||||
torch._C._jit_pass_inline(method_graph)
|
||||
model = fuse_conv_bn_jit(model, inplace)
|
||||
model_c = torch._C._jit_pass_insert_observer_method_for_ondevice_ptq(
|
||||
model._c, method_name, scripted_qconfig_dict, inplace, quant_type
|
||||
)
|
||||
if inplace:
|
||||
model._reconstruct(model_c)
|
||||
else:
|
||||
model = wrap_cpp_module(model_c)
|
||||
return model
|
||||
|
||||
|
||||
def prepare_jit(model, qconfig_dict, inplace=False):
|
||||
torch._C._log_api_usage_once("quantization_api.quantize_jit.prepare_jit")
|
||||
return _prepare_jit(model, qconfig_dict, inplace, quant_type=QuantType.STATIC)
|
||||
|
||||
|
||||
def prepare_dynamic_jit(model, qconfig_dict, inplace=False):
|
||||
torch._C._log_api_usage_once("quantization_api.quantize_jit.prepare_dynamic_jit")
|
||||
return _prepare_jit(model, qconfig_dict, inplace, quant_type=QuantType.DYNAMIC)
|
||||
|
||||
|
||||
def _prepare_ondevice_dynamic_jit(
|
||||
model, qconfig_dict, method_name="forward", inplace=False
|
||||
):
|
||||
return _prepare_ondevice_jit(
|
||||
model, qconfig_dict, method_name, inplace, quant_type=QuantType.DYNAMIC
|
||||
)
|
||||
|
||||
|
||||
def _convert_jit(
|
||||
model, inplace=False, debug=False, quant_type=QuantType.STATIC, preserved_attrs=None
|
||||
):
|
||||
_check_is_script_module(model)
|
||||
model.eval()
|
||||
model_c = model._c
|
||||
model_c = torch._C._jit_pass_insert_quant_dequant(
|
||||
model_c, "forward", inplace, debug, quant_type
|
||||
)
|
||||
if not debug:
|
||||
is_xpu = all(p.device.type == "xpu" for p in model.parameters())
|
||||
if not is_xpu:
|
||||
# Moving model parameters to CPU since quantized operators
|
||||
# are only supported on CPU and XPU right now
|
||||
model.cpu()
|
||||
if preserved_attrs is None:
|
||||
preserved_attrs = []
|
||||
model_c = torch._C._jit_pass_quant_finalize(
|
||||
model_c, quant_type, preserved_attrs
|
||||
)
|
||||
if inplace:
|
||||
model._reconstruct(model_c)
|
||||
else:
|
||||
model = wrap_cpp_module(model_c)
|
||||
torch._C._jit_pass_constant_propagation(model.graph)
|
||||
torch._C._jit_pass_dce(model.graph)
|
||||
return model
|
||||
|
||||
|
||||
def _convert_ondevice_jit(
|
||||
model, method_name, inplace=False, debug=False, quant_type=QuantType.STATIC
|
||||
):
|
||||
_check_is_script_module(model)
|
||||
if quant_type != QuantType.DYNAMIC:
|
||||
raise AssertionError(
|
||||
"This API, while should work for static quant, is only tested for dynamic quant."
|
||||
)
|
||||
if method_name.startswith("observe_"):
|
||||
raise AssertionError("Pass in valid method to be quantized, e.g. forward")
|
||||
observe_method_name = "observe_" + method_name
|
||||
quantize_method_name = "quantize_" + method_name
|
||||
model_c = model._c
|
||||
model_c = torch._C._jit_pass_insert_quant_dequant_for_ondevice_ptq(
|
||||
model._c, observe_method_name, inplace, debug, QuantType.DYNAMIC
|
||||
)
|
||||
model_c = torch._C._jit_pass_quant_finalize_for_ondevice_ptq(
|
||||
model_c, QuantType.DYNAMIC, quantize_method_name
|
||||
)
|
||||
if inplace:
|
||||
model._reconstruct(model_c)
|
||||
else:
|
||||
model = wrap_cpp_module(model_c)
|
||||
return model
|
||||
|
||||
|
||||
def convert_jit(model, inplace=False, debug=False, preserved_attrs=None):
|
||||
torch._C._log_api_usage_once("quantization_api.quantize_jit.convert_jit")
|
||||
return _convert_jit(
|
||||
model,
|
||||
inplace,
|
||||
debug,
|
||||
quant_type=QuantType.STATIC,
|
||||
preserved_attrs=preserved_attrs,
|
||||
)
|
||||
|
||||
|
||||
def convert_dynamic_jit(model, inplace=False, debug=False, preserved_attrs=None):
|
||||
torch._C._log_api_usage_once("quantization_api.quantize_jit.convert_dynamic_jit")
|
||||
return _convert_jit(
|
||||
model,
|
||||
inplace,
|
||||
debug,
|
||||
quant_type=QuantType.DYNAMIC,
|
||||
preserved_attrs=preserved_attrs,
|
||||
)
|
||||
|
||||
|
||||
def _convert_ondevice_dynamic_jit(model, method_name, inplace=False, debug=False):
|
||||
return _convert_ondevice_jit(
|
||||
model, method_name, inplace, debug, quant_type=QuantType.DYNAMIC
|
||||
)
|
||||
|
||||
|
||||
def _quantize_ondevice_dynamic_jit_impl(
|
||||
model, qconfig_dict, method_name, inplace=False
|
||||
):
|
||||
model = _prepare_ondevice_dynamic_jit(model, qconfig_dict, method_name, inplace)
|
||||
model = _convert_ondevice_dynamic_jit(model, method_name, inplace)
|
||||
return model
|
||||
|
||||
|
||||
def _quantize_jit(
|
||||
model,
|
||||
qconfig_dict,
|
||||
run_fn=None,
|
||||
run_args=None,
|
||||
inplace=False,
|
||||
debug=False,
|
||||
quant_type=QuantType.STATIC,
|
||||
):
|
||||
# Always do inplace convert because the Tensor is already
|
||||
# copied in prepare_jit when inplace is False
|
||||
if quant_type == QuantType.DYNAMIC:
|
||||
model = prepare_dynamic_jit(model, qconfig_dict, inplace)
|
||||
model = convert_dynamic_jit(model, True, debug)
|
||||
else:
|
||||
if not run_fn:
|
||||
raise AssertionError(
|
||||
"Must provide calibration function for post training static quantization"
|
||||
)
|
||||
if not run_args:
|
||||
raise AssertionError(
|
||||
"Must provide calibration dataset for post training static quantization"
|
||||
)
|
||||
model = prepare_jit(model, qconfig_dict, inplace)
|
||||
run_fn(model, *run_args)
|
||||
model = convert_jit(model, True, debug)
|
||||
|
||||
torch._C._jit_pass_constant_propagation(model.graph)
|
||||
torch._C._jit_pass_dce(model.graph)
|
||||
return model
|
||||
|
||||
|
||||
def quantize_jit(model, qconfig_dict, run_fn, run_args, inplace=False, debug=False):
|
||||
r"""Quantize the input float TorchScript model with
|
||||
post training static quantization.
|
||||
|
||||
First it will prepare the model for calibration, then it calls
|
||||
`run_fn` which will run the calibration step, after that we will
|
||||
convert the model to a quantized model.
|
||||
|
||||
Args:
|
||||
`model`: input float TorchScript model
|
||||
`qconfig_dict`: qconfig_dict is a dictionary with names of sub modules as key and
|
||||
qconfig for that module as value, empty key means the qconfig will be applied
|
||||
to whole model unless it's overwritten by more specific configurations, the
|
||||
qconfig for each module is either found in the dictionary or fallback to
|
||||
the qconfig of parent module.
|
||||
|
||||
Right now qconfig_dict is the only way to configure how the model is quantized,
|
||||
and it is done in the granularity of module, that is, we only support one type
|
||||
of qconfig for each torch.nn.Module, and the qconfig for sub module will
|
||||
override the qconfig for parent module, empty string means global configuration.
|
||||
`run_fn`: a calibration function for calibrating the prepared model
|
||||
`run_args`: positional arguments for `run_fn`
|
||||
`inplace`: carry out model transformations in-place, the original module is
|
||||
mutated
|
||||
`debug`: flag for producing a debug friendly model (preserve weight attribute)
|
||||
|
||||
Return:
|
||||
Quantized TorchSciprt model.
|
||||
|
||||
Example:
|
||||
```python
|
||||
import torch
|
||||
from torch.ao.quantization import get_default_qconfig
|
||||
from torch.ao.quantization import quantize_jit
|
||||
|
||||
ts_model = torch.jit.script(
|
||||
float_model.eval()
|
||||
) # or torch.jit.trace(float_model, input)
|
||||
qconfig = get_default_qconfig("fbgemm")
|
||||
|
||||
|
||||
def calibrate(model, data_loader):
|
||||
model.eval()
|
||||
with torch.no_grad():
|
||||
for image, target in data_loader:
|
||||
model(image)
|
||||
|
||||
|
||||
quantized_model = quantize_jit(
|
||||
ts_model, {"": qconfig}, calibrate, [data_loader_test]
|
||||
)
|
||||
```
|
||||
"""
|
||||
torch._C._log_api_usage_once("quantization_api.quantize_jit.quantize_jit")
|
||||
return _quantize_jit(
|
||||
model,
|
||||
qconfig_dict,
|
||||
run_fn,
|
||||
run_args,
|
||||
inplace,
|
||||
debug,
|
||||
quant_type=QuantType.STATIC,
|
||||
)
|
||||
|
||||
|
||||
def quantize_dynamic_jit(model, qconfig_dict, inplace=False, debug=False):
|
||||
r"""Quantize the input float TorchScript model with
|
||||
post training dynamic quantization.
|
||||
Currently only qint8 quantization of torch.nn.Linear is supported.
|
||||
|
||||
Args:
|
||||
`model`: input float TorchScript model
|
||||
`qconfig_dict`: qconfig_dict is a dictionary with names of sub modules as key and
|
||||
qconfig for that module as value, please see detailed
|
||||
descriptions in :func:`~torch.ao.quantization.quantize_jit`
|
||||
`inplace`: carry out model transformations in-place, the original module is
|
||||
mutated
|
||||
`debug`: flag for producing a debug friendly model (preserve weight attribute)
|
||||
|
||||
Return:
|
||||
Quantized TorchSciprt model.
|
||||
|
||||
Example:
|
||||
```python
|
||||
import torch
|
||||
from torch.ao.quantization import per_channel_dynamic_qconfig
|
||||
from torch.ao.quantization import quantize_dynamic_jit
|
||||
|
||||
ts_model = torch.jit.script(
|
||||
float_model.eval()
|
||||
) # or torch.jit.trace(float_model, input)
|
||||
qconfig = get_default_qconfig("fbgemm")
|
||||
|
||||
|
||||
def calibrate(model, data_loader):
|
||||
model.eval()
|
||||
with torch.no_grad():
|
||||
for image, target in data_loader:
|
||||
model(image)
|
||||
|
||||
|
||||
quantized_model = quantize_dynamic_jit(
|
||||
ts_model, {"": qconfig}, calibrate, [data_loader_test]
|
||||
)
|
||||
```
|
||||
"""
|
||||
torch._C._log_api_usage_once("quantization_api.quantize_jit.quantize_dynamic_jit")
|
||||
return _quantize_jit(
|
||||
model, qconfig_dict, inplace=inplace, debug=debug, quant_type=QuantType.DYNAMIC
|
||||
)
|
||||
|
||||
|
||||
def _quantize_ondevice_dynamic_jit(
|
||||
model, qconfig_dict, method_name="forward", inplace=False
|
||||
):
|
||||
r"""Prepares the input float TorchScript model with
|
||||
*on-device* post training dynamic quantization.
|
||||
Currently only qint8 quantization of torch.nn.Linear is supported.
|
||||
|
||||
Args:
|
||||
`model`: input float TorchScript model
|
||||
`qconfig_dict`: qconfig_dict is a dictionary with names of sub modules as key and
|
||||
qconfig for that module as value, please see detailed
|
||||
`method_name`: Name of the method within the model, to be prepared for quantization
|
||||
descriptions in :func:`~torch.ao.quantization.quantize_jit`
|
||||
`inplace`: carry out model transformations in-place, the original module is
|
||||
mutated
|
||||
|
||||
Return:
|
||||
TorchScript model that is ready for on device quantization.
|
||||
This means that the returned
|
||||
model has:
|
||||
- Method is inlined.
|
||||
- Model has observer modules inserted in the model.
|
||||
- Model has packed params inserted in the model. However they are empty as in they dont
|
||||
contain valid quantized weights.
|
||||
- observe_<method_name> is added that observe the values to be quantized.
|
||||
- reset_observers_<method_name> to reset observers.
|
||||
- quantize_<method_name> is added to the model.
|
||||
- This method extract scale, zero points.
|
||||
- Quantizes observed weights.
|
||||
- Creates packed params from it and update the attribute of the model with the new values
|
||||
for the packed params.
|
||||
- Reset the original fp32 weights with empty tensor using SetAttr.
|
||||
- quantized_<method_name> is added to the model.
|
||||
- This method uses quantized weights and quantized linear ops instead of fp32 op.
|
||||
- This method should be used for inference post PTQ.
|
||||
- Note that all method's signatures should be the same as method_name.
|
||||
|
||||
Later on device:
|
||||
- Run reset_observers_<method_name>
|
||||
- Run observe_<method_name>
|
||||
- Run quantize_<method_name>
|
||||
- Now model can be saved and loaded later.
|
||||
- Run model with quantized_<method_name>
|
||||
|
||||
Example:
|
||||
```python
|
||||
import torch
|
||||
from torch.ao.quantization import per_channel_dynamic_qconfig
|
||||
from torch.ao.quantization.quantize_jit import _quantize_ondevice_dynamic_jit
|
||||
|
||||
ts_model = torch.jit.script(
|
||||
float_model.eval()
|
||||
) # or torch.jit.trace(float_model, input)
|
||||
qconfig = get_default_qconfig("fbgemm")
|
||||
quant_ready_model = _quantize_ondevice_dynamic_jit(
|
||||
ts_model, {"": qconfig}, "forward", True
|
||||
)
|
||||
```
|
||||
"""
|
||||
return _quantize_ondevice_dynamic_jit_impl(
|
||||
model, qconfig_dict, method_name, inplace=inplace
|
||||
)
|
||||
@@ -0,0 +1,74 @@
|
||||
from typing import Any
|
||||
|
||||
import torch
|
||||
from torch import nn
|
||||
from torch.ao.quantization import QConfig
|
||||
|
||||
|
||||
__all__ = ["QuantStub", "DeQuantStub", "QuantWrapper"]
|
||||
|
||||
|
||||
class QuantStub(nn.Module):
|
||||
r"""Quantize stub module, before calibration, this is same as an observer,
|
||||
it will be swapped as `nnq.Quantize` in `convert`.
|
||||
|
||||
Args:
|
||||
qconfig: quantization configuration for the tensor,
|
||||
if qconfig is not provided, we will get qconfig from parent modules
|
||||
"""
|
||||
|
||||
def __init__(self, qconfig: QConfig | None = None):
|
||||
super().__init__()
|
||||
if qconfig:
|
||||
self.qconfig = qconfig
|
||||
|
||||
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
||||
return x
|
||||
|
||||
|
||||
class DeQuantStub(nn.Module):
|
||||
r"""Dequantize stub module, before calibration, this is same as identity,
|
||||
this will be swapped as `nnq.DeQuantize` in `convert`.
|
||||
|
||||
Args:
|
||||
qconfig: quantization configuration for the tensor,
|
||||
if qconfig is not provided, we will get qconfig from parent modules
|
||||
"""
|
||||
|
||||
def __init__(self, qconfig: Any | None = None):
|
||||
super().__init__()
|
||||
if qconfig:
|
||||
self.qconfig = qconfig
|
||||
|
||||
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
||||
return x
|
||||
|
||||
|
||||
class QuantWrapper(nn.Module):
|
||||
r"""A wrapper class that wraps the input module, adds QuantStub and
|
||||
DeQuantStub and surround the call to module with call to quant and dequant
|
||||
modules.
|
||||
|
||||
This is used by the `quantization` utility functions to add the quant and
|
||||
dequant modules, before `convert` function `QuantStub` will just be observer,
|
||||
it observes the input tensor, after `convert`, `QuantStub`
|
||||
will be swapped to `nnq.Quantize` which does actual quantization. Similarly
|
||||
for `DeQuantStub`.
|
||||
"""
|
||||
|
||||
quant: QuantStub
|
||||
dequant: DeQuantStub
|
||||
module: nn.Module
|
||||
|
||||
def __init__(self, module: nn.Module):
|
||||
super().__init__()
|
||||
qconfig = getattr(module, "qconfig", None)
|
||||
self.add_module("quant", QuantStub(qconfig))
|
||||
self.add_module("dequant", DeQuantStub(qconfig))
|
||||
self.add_module("module", module)
|
||||
self.train(module.training)
|
||||
|
||||
def forward(self, X: torch.Tensor) -> torch.Tensor:
|
||||
X = self.quant(X)
|
||||
X = self.module(X)
|
||||
return self.dequant(X)
|
||||
@@ -0,0 +1,859 @@
|
||||
# mypy: allow-untyped-defs
|
||||
"""
|
||||
Utils shared by different modes of quantization (eager/graph)
|
||||
"""
|
||||
|
||||
import functools
|
||||
import warnings
|
||||
from collections import OrderedDict
|
||||
from collections.abc import Callable
|
||||
from inspect import getfullargspec, signature
|
||||
from typing import Any
|
||||
from typing_extensions import TypeAliasType
|
||||
|
||||
import torch
|
||||
from torch.ao.quantization.quant_type import QuantType
|
||||
from torch.fx import Node
|
||||
from torch.nn.utils.parametrize import is_parametrized
|
||||
|
||||
|
||||
NodePattern = TypeAliasType(
|
||||
"NodePattern", tuple[Node, Node] | tuple[Node, tuple[Node, Node]] | Any
|
||||
)
|
||||
|
||||
|
||||
# This is the Quantizer class instance from torch/quantization/fx/quantize.py.
|
||||
# Define separately to prevent circular imports.
|
||||
# TODO(future PR): improve this.
|
||||
# make this public once fixed (can't be public as is because setting the module directly
|
||||
# doesn't work)
|
||||
QuantizerCls = Any
|
||||
|
||||
# Type for fusion patterns, it can be more complicated than the following actually,
|
||||
# see pattern.md for docs
|
||||
# TODO: not sure if typing supports recursive data types
|
||||
|
||||
|
||||
Pattern = TypeAliasType(
|
||||
"Pattern",
|
||||
Callable
|
||||
| tuple[Callable, Callable]
|
||||
| tuple[Callable, tuple[Callable, Callable]]
|
||||
| Any,
|
||||
)
|
||||
|
||||
|
||||
# TODO: maybe rename this to MatchInputNode
|
||||
class MatchAllNode:
|
||||
"""A node pattern that matches all nodes, used in defining
|
||||
fusion patterns in FX Graph Mode Quantization
|
||||
"""
|
||||
|
||||
|
||||
module_type_list = {
|
||||
torch.nn.ReLU,
|
||||
torch.nn.ReLU6,
|
||||
torch.nn.AdaptiveAvgPool1d,
|
||||
torch.nn.AdaptiveAvgPool2d,
|
||||
torch.nn.AdaptiveAvgPool3d,
|
||||
torch.nn.AvgPool1d,
|
||||
torch.nn.AvgPool2d,
|
||||
torch.nn.AvgPool3d,
|
||||
torch.nn.MaxPool1d,
|
||||
torch.nn.MaxPool2d,
|
||||
torch.nn.MaxPool3d,
|
||||
torch.nn.Identity,
|
||||
torch.nn.Hardsigmoid,
|
||||
torch.nn.Sigmoid,
|
||||
torch.nn.Tanh,
|
||||
}
|
||||
func_list = {
|
||||
torch.nn.functional.adaptive_avg_pool1d,
|
||||
torch.nn.functional.adaptive_avg_pool2d,
|
||||
torch.nn.functional.adaptive_avg_pool3d,
|
||||
torch.nn.functional.elu,
|
||||
torch.nn.functional.hardswish,
|
||||
torch.nn.functional.instance_norm,
|
||||
torch.nn.functional.layer_norm,
|
||||
torch.nn.functional.leaky_relu,
|
||||
torch.nn.functional.silu,
|
||||
torch.nn.functional.mish,
|
||||
torch.nn.functional.dropout,
|
||||
torch.nn.functional.max_pool1d,
|
||||
torch.nn.functional.max_pool2d,
|
||||
torch.nn.functional.max_pool3d,
|
||||
torch.nn.functional.relu,
|
||||
torch.nn.functional.hardtanh,
|
||||
torch.nn.functional.hardtanh_,
|
||||
torch.nn.functional.hardsigmoid,
|
||||
torch.nn.functional.sigmoid,
|
||||
torch.transpose,
|
||||
torch.repeat_interleave,
|
||||
torch.sigmoid,
|
||||
torch.squeeze,
|
||||
torch.stack,
|
||||
torch.sum,
|
||||
torch.tanh,
|
||||
torch.unsqueeze,
|
||||
torch.cat,
|
||||
}
|
||||
method_list = {
|
||||
torch.mean,
|
||||
"relu",
|
||||
"relu_",
|
||||
"contiguous",
|
||||
"detach",
|
||||
"detach_",
|
||||
"hardsigmoid",
|
||||
"hardsigmoid_",
|
||||
"permute",
|
||||
"repeat",
|
||||
"repeat_interleave",
|
||||
"reshape",
|
||||
"resize_",
|
||||
"shape",
|
||||
"sigmoid",
|
||||
"sigmoid_",
|
||||
"size",
|
||||
"squeeze",
|
||||
"squeeze_",
|
||||
"tanh",
|
||||
"tanh_",
|
||||
"transpose",
|
||||
"unsqueeze",
|
||||
"unsqueeze_",
|
||||
"view",
|
||||
}
|
||||
|
||||
|
||||
# TODO: not used now, remove
|
||||
def check_node(node, modules):
|
||||
# TODO: reuse is_fixed_qparam_node after we move this function to _lower_to_native_backend.py
|
||||
is_call_function = node.op == "call_function" and node.target in func_list
|
||||
is_call_method = node.op == "call_method" and node.target in method_list
|
||||
is_call_module = (
|
||||
node.op == "call_module" and type(modules[str(node.target)]) in module_type_list
|
||||
)
|
||||
return is_call_function, is_call_method, is_call_module
|
||||
|
||||
|
||||
def get_combined_dict(default_dict, additional_dict):
|
||||
"""
|
||||
Combines two dictionaries.
|
||||
|
||||
This function takes two dictionaries as input and returns a new dictionary
|
||||
that contains all the key-value pairs from both input dictionaries.
|
||||
If there are any duplicate keys in the `additional_dict`, the values
|
||||
from the `additional_dict` will overwrite those in the `default_dict`.
|
||||
Args:
|
||||
default_dict (dict): The main dictionary that will be used as the base
|
||||
additional_dict (dict): The dictionary used to update `default_dict`
|
||||
|
||||
Returns:
|
||||
dict: The resulting dictionary
|
||||
Example:
|
||||
>>> x = dict(a=1, b=1)
|
||||
>>> y = dict(b=2, c=3)
|
||||
>>> get_combined_dict(x, y)
|
||||
{'a': 1, 'b': 2, 'c': 3}
|
||||
"""
|
||||
d = default_dict.copy()
|
||||
d.update(additional_dict)
|
||||
return d
|
||||
|
||||
|
||||
def is_per_tensor(qscheme):
|
||||
return qscheme == torch.per_tensor_affine or qscheme == torch.per_tensor_symmetric
|
||||
|
||||
|
||||
def is_per_channel(qscheme):
|
||||
return qscheme in [
|
||||
torch.per_channel_affine,
|
||||
torch.per_channel_affine_float_qparams,
|
||||
torch.per_channel_symmetric,
|
||||
]
|
||||
|
||||
|
||||
def getattr_from_fqn(obj: Any, fqn: str) -> Any:
|
||||
"""
|
||||
Given an obj and a fqn such as "foo.bar.baz", returns gm.foo.bar.baz.
|
||||
"""
|
||||
return functools.reduce(getattr, fqn.split("."), obj)
|
||||
|
||||
|
||||
def to_underlying_dtype(qdtype):
|
||||
DTYPE_MAPPING = {
|
||||
torch.quint8: torch.uint8,
|
||||
torch.qint8: torch.int8,
|
||||
torch.qint32: torch.int32,
|
||||
torch.quint4x2: torch.uint8,
|
||||
torch.quint2x4: torch.uint8,
|
||||
torch.uint8: torch.uint8,
|
||||
torch.int8: torch.int8,
|
||||
torch.uint16: torch.uint16,
|
||||
torch.int16: torch.int16,
|
||||
torch.int32: torch.int32,
|
||||
torch.float8_e5m2: torch.float8_e5m2,
|
||||
torch.float8_e4m3fn: torch.float8_e4m3fn,
|
||||
}
|
||||
if qdtype not in DTYPE_MAPPING:
|
||||
raise AssertionError("Unsupported dtype: " + str(qdtype))
|
||||
return DTYPE_MAPPING[qdtype]
|
||||
|
||||
|
||||
def get_qparam_dict(observer_or_fake_quant):
|
||||
from torch.ao.quantization.observer import PlaceholderObserver
|
||||
|
||||
qscheme = getattr(observer_or_fake_quant, "qscheme", None)
|
||||
dtype = observer_or_fake_quant.dtype
|
||||
qparams = {"qscheme": qscheme, "dtype": dtype}
|
||||
|
||||
if not qscheme or isinstance(observer_or_fake_quant, PlaceholderObserver):
|
||||
return {"qscheme": None, "dtype": dtype}
|
||||
|
||||
if is_per_tensor(qscheme):
|
||||
qscheme = torch.per_tensor_affine
|
||||
elif is_per_channel(qscheme):
|
||||
# change symmetric to affine since we do not have symmetric
|
||||
# quantized Tensor
|
||||
if qscheme == torch.per_channel_symmetric:
|
||||
qscheme = torch.per_channel_affine
|
||||
qparams["axis"] = observer_or_fake_quant.ch_axis
|
||||
else:
|
||||
raise RuntimeError(f"Unrecognized qscheme: {qscheme}")
|
||||
# update qscheme, since we don't have symmetric quant qscheme
|
||||
# in quantized Tensor
|
||||
qparams["qscheme"] = qscheme
|
||||
|
||||
scale, zero_point = observer_or_fake_quant.calculate_qparams()
|
||||
qparams["scale"] = scale
|
||||
qparams["zero_point"] = zero_point
|
||||
|
||||
if hasattr(observer_or_fake_quant, "quant_min"):
|
||||
qparams["quant_min"] = observer_or_fake_quant.quant_min
|
||||
if hasattr(observer_or_fake_quant, "quant_max"):
|
||||
qparams["quant_max"] = observer_or_fake_quant.quant_max
|
||||
|
||||
return qparams
|
||||
|
||||
|
||||
def get_swapped_custom_module_class(
|
||||
custom_module, custom_module_class_mapping, qconfig
|
||||
):
|
||||
"""Get the observed/quantized custom module class that we need
|
||||
to swap `custom_module` to
|
||||
Input:
|
||||
custom_module: input, can be an instance of either a float or observed custom module
|
||||
custom_module_class_mapping: the float to observed or observed to quantized custom module class mapping
|
||||
qconfig: qconfig configured for the custom module
|
||||
|
||||
Output:
|
||||
corresponding observed/quantized custom module class for input custom module instance
|
||||
"""
|
||||
quant_type = get_quant_type(qconfig)
|
||||
class_mapping = custom_module_class_mapping.get(quant_type, {})
|
||||
if type(custom_module) not in class_mapping:
|
||||
raise AssertionError(
|
||||
"did not find corresponding observed "
|
||||
f"module class for {type(custom_module)} in mapping: {class_mapping}"
|
||||
)
|
||||
return class_mapping[type(custom_module)]
|
||||
|
||||
|
||||
def activation_dtype(qconfig):
|
||||
if qconfig is None:
|
||||
raise AssertionError("qconfig must be provided to determine activation dtype")
|
||||
activation = qconfig.activation()
|
||||
return activation.dtype
|
||||
|
||||
|
||||
def weight_dtype(qconfig):
|
||||
if qconfig is None:
|
||||
raise AssertionError("qconfig must be provided to determine weight dtype")
|
||||
weight = qconfig.weight()
|
||||
return weight.dtype
|
||||
|
||||
|
||||
def activation_is_statically_quantized(qconfig):
|
||||
"""Given a qconfig, decide if the activation needs to be
|
||||
quantized or not, this includes quantizing to quint8, qint8 and qint32 and float16
|
||||
"""
|
||||
return activation_dtype(qconfig) in [
|
||||
torch.quint8,
|
||||
torch.qint8,
|
||||
torch.qint32,
|
||||
torch.float16,
|
||||
torch.uint8,
|
||||
torch.int8,
|
||||
torch.int16,
|
||||
torch.int32,
|
||||
torch.float8_e5m2,
|
||||
torch.float8_e4m3fn,
|
||||
] and (not activation_is_dynamically_quantized(qconfig))
|
||||
|
||||
|
||||
def activation_is_dynamically_quantized(qconfig):
|
||||
"""Given a qconfig, decide if the activation needs to be
|
||||
dynamically quantized or not, this includes dynamically quantizing to
|
||||
quint8, qint8 and float16
|
||||
"""
|
||||
_activation_dtype, _, activation_is_dynamic = get_qconfig_dtypes(qconfig)
|
||||
return activation_is_dynamic
|
||||
|
||||
|
||||
def activation_is_int8_quantized(qconfig):
|
||||
"""Given a qconfig, decide if the activation needs to be
|
||||
quantized to int8 or not, this includes quantizing to quint8, qint8
|
||||
"""
|
||||
return activation_dtype(qconfig) in [
|
||||
torch.quint8,
|
||||
torch.qint8,
|
||||
torch.uint8,
|
||||
torch.int8,
|
||||
]
|
||||
|
||||
|
||||
def activation_is_int32_quantized(qconfig):
|
||||
"""Given a qconfig, decide if the activation needs to be
|
||||
quantized to int32 or not
|
||||
"""
|
||||
return activation_dtype(qconfig) in [torch.qint32, torch.int32]
|
||||
|
||||
|
||||
def weight_is_quantized(qconfig):
|
||||
"""Given a qconfig, decide if the weight needs to be
|
||||
quantized or not
|
||||
"""
|
||||
return weight_dtype(qconfig) in [
|
||||
torch.quint8,
|
||||
torch.qint8,
|
||||
torch.float16,
|
||||
torch.quint4x2,
|
||||
torch.uint8,
|
||||
torch.int8,
|
||||
torch.int16,
|
||||
torch.int32,
|
||||
torch.float8_e5m2,
|
||||
torch.float8_e4m3fn,
|
||||
]
|
||||
|
||||
|
||||
def weight_is_statically_quantized(qconfig):
|
||||
"""Given a qconfig, decide if the weight needs to be statically
|
||||
quantized or not
|
||||
"""
|
||||
return weight_dtype(qconfig) in [torch.quint8, torch.qint8, torch.uint8, torch.int8]
|
||||
|
||||
|
||||
def op_is_int8_dynamically_quantized(qconfig) -> bool:
|
||||
"""Given a qconfig, returns True if this op is using int8 dynamic
|
||||
quantization
|
||||
"""
|
||||
activation_dtype, weight_dtype, activation_is_dynamic = get_qconfig_dtypes(qconfig)
|
||||
return (
|
||||
activation_dtype in [torch.quint8, torch.uint8]
|
||||
and
|
||||
# for now, the lines below assume fbgemm or qnnpack
|
||||
weight_dtype in [torch.qint8, torch.int8]
|
||||
and activation_is_dynamic
|
||||
)
|
||||
|
||||
|
||||
def get_qconfig_dtypes(qconfig):
|
||||
r"""returns the qconfig tuple for qconfig:
|
||||
(activation_dtype, weight_dtype, activation_is_dynamic)
|
||||
"""
|
||||
if qconfig is None:
|
||||
raise AssertionError("qconfig must be provided to extract dtypes")
|
||||
activation = qconfig.activation()
|
||||
weight = qconfig.weight()
|
||||
act_is_dynamic = getattr(activation, "is_dynamic", False)
|
||||
return (activation.dtype, weight.dtype, act_is_dynamic)
|
||||
|
||||
|
||||
def get_quant_type(qconfig):
|
||||
if qconfig is None:
|
||||
raise AssertionError("qconfig must be provided to determine quant type")
|
||||
activation = qconfig.activation()
|
||||
weight = qconfig.weight()
|
||||
static_dtypes = [
|
||||
torch.quint8,
|
||||
torch.qint8,
|
||||
torch.quint4x2,
|
||||
torch.qint32,
|
||||
torch.uint8,
|
||||
torch.int8,
|
||||
torch.int16,
|
||||
torch.int32,
|
||||
torch.float8_e5m2,
|
||||
torch.float8_e4m3fn,
|
||||
]
|
||||
if weight.dtype in static_dtypes:
|
||||
if hasattr(activation, "is_dynamic") and activation.is_dynamic:
|
||||
return QuantType.DYNAMIC
|
||||
elif activation.dtype in static_dtypes:
|
||||
return QuantType.STATIC
|
||||
else:
|
||||
return QuantType.WEIGHT_ONLY
|
||||
|
||||
if weight.dtype == torch.float16:
|
||||
if hasattr(activation, "is_dynamic") and activation.is_dynamic:
|
||||
return QuantType.DYNAMIC
|
||||
elif activation.dtype == torch.float16:
|
||||
return QuantType.STATIC
|
||||
|
||||
raise Exception( # noqa: TRY002
|
||||
f"Unrecognized dtype combination in get_quant_type: activation({activation.dtype}),"
|
||||
f"weight({weight.dtype})"
|
||||
)
|
||||
|
||||
|
||||
def check_min_max_valid(min_val: torch.Tensor, max_val: torch.Tensor) -> bool:
|
||||
"""Checks if the given minimum and maximum values are valid, meaning that
|
||||
they exist and the min value is less than the max value.
|
||||
"""
|
||||
if min_val.numel() == 0 or max_val.numel() == 0:
|
||||
warnings.warn(
|
||||
"must run observer before calling calculate_qparams. "
|
||||
+ "Returning default values.",
|
||||
stacklevel=2,
|
||||
)
|
||||
return False
|
||||
|
||||
if min_val.dim() == 0 or max_val.dim() == 0:
|
||||
if min_val == float("inf") and max_val == float("-inf"):
|
||||
warnings.warn(
|
||||
"must run observer before calling calculate_qparams. "
|
||||
+ "Returning default values.",
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
return False
|
||||
|
||||
if min_val > max_val:
|
||||
raise AssertionError(f"min {min_val} should be less than max {max_val}")
|
||||
else:
|
||||
if torch.any(min_val > max_val):
|
||||
raise AssertionError(f"min {min_val} should be less than max {max_val}")
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def calculate_qmin_qmax(
|
||||
quant_min: int,
|
||||
quant_max: int,
|
||||
has_customized_qrange: bool,
|
||||
dtype: torch.dtype,
|
||||
reduce_range: bool,
|
||||
) -> tuple[int, int]:
|
||||
r"""Calculates actual qmin and qmax based on the quantization range,
|
||||
observer datatype and if range is reduced.
|
||||
"""
|
||||
# TODO(jerryzh): Figure out why custom quant_min/quant_max are still adjusted.
|
||||
if has_customized_qrange:
|
||||
# This initialization here is to be resolve TorchScript compilation issues and allow
|
||||
# using of refinement to decouple initial_qmin and initial_qmax from quantization range.
|
||||
# The actual values of initial_qmin and initial_qmax will be reset below.
|
||||
if dtype in [torch.qint32, torch.int32]:
|
||||
initial_quant_min, initial_quant_max = 0, 2**32 - 1
|
||||
else:
|
||||
initial_quant_min, initial_quant_max = 0, 255
|
||||
# The following assignment of self.qmin and self.qmax to the local variables and the if check refine the
|
||||
# attribute from Optional valid integers for use, based on TorchScript's requirements.
|
||||
custom_quant_min, custom_quant_max = quant_min, quant_max
|
||||
if custom_quant_min is not None and custom_quant_max is not None:
|
||||
initial_quant_min, initial_quant_max = (
|
||||
custom_quant_min,
|
||||
custom_quant_max,
|
||||
)
|
||||
|
||||
qrange_len = initial_quant_max - initial_quant_min + 1
|
||||
if dtype in [torch.qint8, torch.int8]:
|
||||
if not (0 < qrange_len <= 256):
|
||||
raise AssertionError(
|
||||
"quantization range should be positive and not exceed the maximum bit range (=256)."
|
||||
)
|
||||
elif dtype in [torch.qint32, torch.int32]:
|
||||
if not (0 < qrange_len <= 2**32):
|
||||
raise AssertionError(
|
||||
"quantization range should be positive and not exceed the maximum bit range (=4294967296)."
|
||||
)
|
||||
if reduce_range:
|
||||
quant_min, quant_max = quant_min // 2, quant_max // 2
|
||||
else:
|
||||
# Fallback onto default 8-bit qmin and qmax calculation if dynamic range is not used.
|
||||
if dtype in [torch.qint8, torch.int8]:
|
||||
if reduce_range:
|
||||
quant_min, quant_max = -64, 63
|
||||
else:
|
||||
quant_min, quant_max = -128, 127
|
||||
elif dtype in [torch.quint8, torch.uint8]:
|
||||
if reduce_range:
|
||||
quant_min, quant_max = 0, 127
|
||||
else:
|
||||
quant_min, quant_max = 0, 255
|
||||
elif dtype in [torch.qint32, torch.int32]:
|
||||
quant_min, quant_max = -1 * (2**31), (2**31) - 1
|
||||
elif dtype == torch.uint16:
|
||||
quant_min, quant_max = 0, 2**16 - 1
|
||||
elif dtype == torch.int16:
|
||||
quant_min, quant_max = -(2**15), 2**15 - 1
|
||||
else:
|
||||
quant_min, quant_max = 0, 15
|
||||
return quant_min, quant_max
|
||||
|
||||
|
||||
def _parent_name(target):
|
||||
"""
|
||||
Turn 'foo.bar' into ['foo', 'bar']
|
||||
"""
|
||||
r = target.rsplit(".", 1)
|
||||
if len(r) == 1:
|
||||
return "", r[0]
|
||||
else:
|
||||
return r[0], r[1]
|
||||
|
||||
|
||||
def has_no_children_ignoring_parametrizations(module):
|
||||
"""
|
||||
Checks if module._modules is empty or
|
||||
if module is a parametrization, checks that module._modules only has
|
||||
the 'parametrizations' module
|
||||
"""
|
||||
if len(module._modules) == 0:
|
||||
return True
|
||||
elif is_parametrized(module):
|
||||
return len(module._modules) == 1 and "parametrizations" in module._modules
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def _get_path_of_module(
|
||||
root: torch.nn.Module, submodule: torch.nn.Module
|
||||
) -> str | None:
|
||||
"""Get the path (fully qualified name) of a submodule
|
||||
|
||||
Example::
|
||||
|
||||
>> class M(torch.nn.Module):
|
||||
def __init__(self) -> None:
|
||||
self.linear = torch.nn.Linear(5, 5)
|
||||
def forward(self, x):
|
||||
return self.linear(x)
|
||||
|
||||
>> m = M()
|
||||
>> l = m.linear
|
||||
>> _get_path_of_module(m, l)
|
||||
"linear"
|
||||
"""
|
||||
for n, p in root.named_modules():
|
||||
if submodule is p:
|
||||
return n
|
||||
return None
|
||||
|
||||
|
||||
def _get_signature_locals(f: Callable, loc: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Get local keyword arguments
|
||||
|
||||
Example::
|
||||
|
||||
>> def f(self, a, b=9):
|
||||
pass
|
||||
>> loc = {"a": 6, "c": 7}
|
||||
>> _get_signature_locals(f, loc)
|
||||
{"a": 6}
|
||||
"""
|
||||
return {k: v for k, v in loc.items() if k in signature(f).parameters}
|
||||
|
||||
|
||||
def _get_default_kwargs(f: Callable) -> "OrderedDict[str, Any]":
|
||||
"""Get all default keyword arguments from function signature
|
||||
|
||||
Example::
|
||||
|
||||
>> def f(self, a, b=9):
|
||||
pass
|
||||
>> _get_default_kwargs(f)
|
||||
{"b": 9}
|
||||
"""
|
||||
kwargs = {}
|
||||
for name, param in signature(f).parameters.items():
|
||||
if param.default is not param.empty:
|
||||
kwargs[name] = param.default
|
||||
elif param.kind is param.VAR_POSITIONAL:
|
||||
kwargs[name] = ()
|
||||
elif param.kind is param.VAR_KEYWORD:
|
||||
kwargs[name] = {}
|
||||
return OrderedDict(kwargs)
|
||||
|
||||
|
||||
def _normalize_kwargs(func: Callable, loc: dict[str, Any]) -> "OrderedDict[str, Any]":
|
||||
"""Given a function and local function arguments, normalize the keyword
|
||||
arguments by filling in default arguments from function signature
|
||||
|
||||
Example::
|
||||
|
||||
>> def f(self, key1=3, key2=3):
|
||||
pass
|
||||
>> loc = {"key2": 6}
|
||||
>> _normalize_kwargs(f, loc)
|
||||
{"key1": 3, "key2": 6}
|
||||
"""
|
||||
default_kwargs = _get_default_kwargs(func)
|
||||
local_kwargs = _get_signature_locals(func, loc)
|
||||
normalized_kwargs = default_kwargs.copy()
|
||||
for attr, val in local_kwargs.items():
|
||||
if attr in normalized_kwargs:
|
||||
# override the default keyword arguments
|
||||
normalized_kwargs[attr] = val
|
||||
return normalized_kwargs
|
||||
|
||||
|
||||
def validate_qmin_qmax(quant_min: int, quant_max: int) -> None:
|
||||
r"""Validates that the user-specified quantization range is properly initialized
|
||||
and within the given bound supported by the observer dtype.
|
||||
|
||||
To accommodate lower-bit quantization with respect to the existing torch.qint8 and
|
||||
torch.quint8 datatypes, the user can choose to use dynamic quantization range by passing
|
||||
in a tuple of initial qmin and qmax values. One use case is these customized qmin and qmax
|
||||
values are used to calculate static estimates of the scale and zero point for aggressive lower-bit
|
||||
fake quantization. These estimates are compared against parameters learned through backpropagation.
|
||||
The related literatures for scale and zero point via backpropagation are as follows:
|
||||
|
||||
Learned Step Size Quantization: https://openreview.net/pdf?id=rkgO66VKDS
|
||||
Trained Quantization Thresholds: https://arxiv.org/pdf/1903.08066.pdf
|
||||
"""
|
||||
# The variable names are prefixed with "initial" because their values (qmin and qmax) might be adjusted
|
||||
# based on whether quantization range is reduced and the datatype (signed/unsigned) used by the observer.
|
||||
if not (quant_min <= 0 <= quant_max):
|
||||
raise AssertionError("Used-specified quantization range must include 0.")
|
||||
if quant_min >= quant_max:
|
||||
raise AssertionError(
|
||||
"qmin must be strictly less than qmax for user-specified quantization range."
|
||||
)
|
||||
|
||||
|
||||
# Functionally equivalent to '_calculate_qparams' in observer.py. Observers must be torchscriptable however and qscheme
|
||||
# as far as I can tell is not allowed to passed as a parameter in torchscript functions. This makes refactoring observer
|
||||
# to use this utility a massive pain and very gross. For now Im opting just to duplicate as this code seems unlikely to change
|
||||
# (last update over 1 year ago) and when torchscript is fully deprecated we can refactor. TODO(jakeszwe, jerryzh168)
|
||||
def determine_qparams(
|
||||
min_val: torch.Tensor,
|
||||
max_val: torch.Tensor,
|
||||
quant_min: int,
|
||||
quant_max: int,
|
||||
dtype: torch.dtype,
|
||||
eps: torch.Tensor,
|
||||
has_customized_qrange: bool,
|
||||
qscheme: torch.qscheme = torch.per_tensor_affine,
|
||||
) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
r"""Calculates the quantization parameters, given min and max
|
||||
value tensors. Works for both per tensor and per channel cases
|
||||
|
||||
Args:
|
||||
min_val: Minimum values per channel
|
||||
max_val: Maximum values per channel
|
||||
|
||||
Returns:
|
||||
scales: Scales tensor of shape (#channels,)
|
||||
zero_points: Zero points tensor of shape (#channels,)
|
||||
"""
|
||||
if not check_min_max_valid(min_val, max_val):
|
||||
return torch.tensor([1.0], device=min_val.device.type), torch.tensor(
|
||||
[0], device=min_val.device.type
|
||||
)
|
||||
|
||||
min_val_neg = torch.min(min_val, torch.zeros_like(min_val))
|
||||
max_val_pos = torch.max(max_val, torch.zeros_like(max_val))
|
||||
|
||||
device = min_val_neg.device
|
||||
scale = torch.ones(min_val_neg.size(), dtype=torch.double, device=device)
|
||||
zero_point = torch.zeros(min_val_neg.size(), dtype=torch.int64, device=device)
|
||||
eps = eps.to(device)
|
||||
|
||||
if qscheme == torch.per_tensor_symmetric or qscheme == torch.per_channel_symmetric:
|
||||
max_val_pos = torch.max(-min_val_neg, max_val_pos)
|
||||
scale = max_val_pos / (float(quant_max - quant_min) / 2)
|
||||
scale = torch.max(scale, eps)
|
||||
if dtype in [torch.uint8, torch.quint8]:
|
||||
if has_customized_qrange:
|
||||
# When customized quantization range is used, down-rounded midpoint of the range is chosen.
|
||||
zero_point = zero_point.new_full(
|
||||
zero_point.size(), (quant_min + quant_max) // 2
|
||||
)
|
||||
else:
|
||||
zero_point = zero_point.new_full(zero_point.size(), 128)
|
||||
elif qscheme == torch.per_channel_affine_float_qparams:
|
||||
scale = (max_val - min_val) / float(quant_max - quant_min)
|
||||
scale = torch.where(scale > eps, scale, torch.ones_like(scale))
|
||||
# We use the quantize function
|
||||
# xq = Round(Xf * inv_scale + zero_point),
|
||||
# setting zero_point to (-1 * min *inv_scale) we get
|
||||
# Xq = Round((Xf - min) * inv_scale)
|
||||
zero_point = -1 * min_val / scale
|
||||
else:
|
||||
scale = (max_val_pos - min_val_neg) / float(quant_max - quant_min)
|
||||
scale = torch.max(scale, eps)
|
||||
zero_point = quant_min - torch.round(min_val_neg / scale).to(torch.int)
|
||||
zero_point = torch.clamp(zero_point, quant_min, quant_max)
|
||||
|
||||
# For scalar values, cast them to Tensors of size 1 to keep the shape
|
||||
# consistent with default values in FakeQuantize.
|
||||
if len(scale.shape) == 0:
|
||||
# TODO: switch to scale.item() after adding JIT support
|
||||
scale = torch.tensor([float(scale)], dtype=scale.dtype, device=device)
|
||||
if len(zero_point.shape) == 0:
|
||||
# TODO: switch to zero_point.item() after adding JIT support
|
||||
zero_point = torch.tensor(
|
||||
[int(zero_point)], dtype=zero_point.dtype, device=device
|
||||
)
|
||||
if qscheme == torch.per_channel_affine_float_qparams:
|
||||
zero_point = torch.tensor(
|
||||
[float(zero_point)], dtype=zero_point.dtype, device=device
|
||||
)
|
||||
|
||||
return scale.to(torch.double), zero_point.to(torch.int64)
|
||||
|
||||
|
||||
def _get_num_pos_args(f: Callable) -> int:
|
||||
"""Get number of positional args for a function
|
||||
|
||||
Example::
|
||||
|
||||
>> def f(self, key1=3, key2=3):
|
||||
pass
|
||||
>> _get_num_pos_args(f)
|
||||
3
|
||||
"""
|
||||
return len(getfullargspec(f).args)
|
||||
|
||||
|
||||
def get_fqn_to_example_inputs(
|
||||
model: torch.nn.Module, example_inputs: tuple[Any, ...]
|
||||
) -> dict[str, tuple[Any, ...]]:
|
||||
"""Given a model and its example inputs, return a dictionary from
|
||||
fully qualified name of submodules to example_inputs for that submodule,
|
||||
e.g. {"linear1": (tensor1,), "linear2": (tensor2,), "sub": (tensor3,),
|
||||
"sub.linear1": (tensor4,), ...}
|
||||
|
||||
Used to make quantizing submodules easier now that FX Graph Mode Quantization requires
|
||||
example inputs.
|
||||
|
||||
Also works for keyword arguments with default values, we would flatten keyword
|
||||
arguments as positional arguments and fill in the missing keyword args with default
|
||||
values, e.g. if we have a forward function:
|
||||
def forward(self, x, key1=3, key2=3):
|
||||
...
|
||||
|
||||
and we call it with self.submodule(x, key2=6)
|
||||
we'll get example_inputs: (x, 3, 6)
|
||||
|
||||
user can also override `key1` with positional arguments as well:
|
||||
for self.submodule(x, 5, key2=6)
|
||||
we'll get: (x, 5, 6)
|
||||
|
||||
variable positional arguments and variable positional keyword arguments in forward
|
||||
function are not supported currently, so please make sure no submodules is using
|
||||
them.
|
||||
"""
|
||||
root = model
|
||||
fqn_to_example_inputs = {}
|
||||
|
||||
def _patched_module_call(self, *args, **kwargs):
|
||||
submodule_example_inputs = list(args).copy()
|
||||
normalized_kwargs = _normalize_kwargs(self.forward, kwargs)
|
||||
# minus 1 to skipping counting `self`
|
||||
num_args = _get_num_pos_args(self.forward) - 1
|
||||
num_to_pop = num_args - len(submodule_example_inputs)
|
||||
while num_to_pop and normalized_kwargs:
|
||||
normalized_kwargs.popitem(last=False)
|
||||
num_to_pop -= 1
|
||||
submodule_example_inputs.extend(normalized_kwargs.values())
|
||||
submodule_example_inputs_tuple = tuple(submodule_example_inputs)
|
||||
fqn = _get_path_of_module(root, self)
|
||||
if fqn is not None:
|
||||
fqn_to_example_inputs[fqn] = submodule_example_inputs_tuple
|
||||
return orig_module_call(self, *args, **kwargs)
|
||||
|
||||
orig_module_call = torch.nn.Module.__call__
|
||||
torch.nn.Module.__call__ = _patched_module_call # type: ignore[method-assign]
|
||||
try:
|
||||
model(*example_inputs)
|
||||
finally:
|
||||
# restore the module call even if there is an exception
|
||||
torch.nn.Module.__call__ = orig_module_call # type: ignore[method-assign]
|
||||
return fqn_to_example_inputs
|
||||
|
||||
|
||||
def _assert_and_get_unique_device(module: torch.nn.Module) -> Any:
|
||||
"""
|
||||
Returns the unique device for a module, or None if no device is found.
|
||||
Throws an error if multiple devices are detected.
|
||||
"""
|
||||
devices = {p.device for p in module.parameters()} | {
|
||||
p.device for p in module.buffers()
|
||||
}
|
||||
"""
|
||||
As a temp workaround for AIMP HHC publish we added CPU check.remove it later. T163614564
|
||||
"""
|
||||
if {torch.device("cpu"), torch.device("meta")} == devices:
|
||||
warnings.warn(
|
||||
"Both 'meta' and 'cpu' are present in the list of devices. Module can have one device. We Select 'cpu'.",
|
||||
stacklevel=2,
|
||||
)
|
||||
devices = {torch.device("cpu")}
|
||||
""
|
||||
if len(devices) > 1:
|
||||
raise AssertionError(
|
||||
"prepare only works with cpu or single-device CUDA modules, "
|
||||
f"but got devices {devices}"
|
||||
)
|
||||
device = next(iter(devices)) if len(devices) > 0 else None
|
||||
return device
|
||||
|
||||
|
||||
DEPRECATION_WARNING = (
|
||||
"torch.ao.quantization is deprecated and will be removed in 2.10. \n"
|
||||
"For migrations of users: \n"
|
||||
"1. Eager mode quantization (torch.ao.quantization.quantize, "
|
||||
"torch.ao.quantization.quantize_dynamic), please migrate to use torchao eager mode "
|
||||
"quantize_ API instead \n"
|
||||
"2. FX graph mode quantization (torch.ao.quantization.quantize_fx.prepare_fx,"
|
||||
"torch.ao.quantization.quantize_fx.convert_fx, please migrate to use torchao pt2e quantization "
|
||||
"API instead (prepare_pt2e, convert_pt2e) \n"
|
||||
"3. pt2e quantization has been migrated to torchao (https://github.com/pytorch/ao/tree/main/torchao/quantization/pt2e) \n"
|
||||
"see https://github.com/pytorch/ao/issues/2259 for more details"
|
||||
)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"NodePattern",
|
||||
"Pattern",
|
||||
"MatchAllNode",
|
||||
"check_node",
|
||||
"get_combined_dict",
|
||||
"is_per_tensor",
|
||||
"is_per_channel",
|
||||
"getattr_from_fqn",
|
||||
"get_qparam_dict",
|
||||
"get_swapped_custom_module_class",
|
||||
"activation_dtype",
|
||||
"weight_dtype",
|
||||
"activation_is_statically_quantized",
|
||||
"activation_is_dynamically_quantized",
|
||||
"activation_is_int8_quantized",
|
||||
"activation_is_int32_quantized",
|
||||
"weight_is_quantized",
|
||||
"weight_is_statically_quantized",
|
||||
"op_is_int8_dynamically_quantized",
|
||||
"get_qconfig_dtypes",
|
||||
"get_quant_type",
|
||||
"check_min_max_valid",
|
||||
"calculate_qmin_qmax",
|
||||
"has_no_children_ignoring_parametrizations",
|
||||
"get_fqn_to_example_inputs",
|
||||
"to_underlying_dtype",
|
||||
"determine_qparams",
|
||||
"validate_qmin_qmax",
|
||||
"DEPRECATION_WARNING",
|
||||
]
|
||||
Reference in New Issue
Block a user