Initial import: grid-bot — grid trading bot for BTC-USDT on Cifra Markets

This commit is contained in:
Kolp
2026-09-24 13:22:23 +07:00
commit 642cc11a9f
18968 changed files with 5683248 additions and 0 deletions
@@ -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)
@@ -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
@@ -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",
]
@@ -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")]
@@ -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())
)
@@ -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
@@ -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())
)
@@ -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)
)
)
@@ -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()
@@ -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",
]
@@ -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)
)
)
@@ -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()
@@ -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,
)
@@ -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
@@ -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
@@ -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"
)
@@ -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
@@ -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
@@ -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}'")
@@ -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",
]