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,31 @@
# torch.ao is a package with a lot of interdependencies.
# We will use lazy import to avoid cyclic dependencies here.
from typing import TYPE_CHECKING as _TYPE_CHECKING
if _TYPE_CHECKING:
from types import ModuleType
from torch.ao import ( # noqa: TC004
nn as nn,
ns as ns,
pruning as pruning,
quantization as quantization,
)
__all__ = [
"nn",
"ns",
"pruning",
"quantization",
]
def __getattr__(name: str) -> "ModuleType":
if name in __all__:
import importlib
return importlib.import_module("." + name, __name__)
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
@@ -0,0 +1,35 @@
# We are exposing all subpackages to the end-user.
# Because of possible inter-dependency, we want to avoid
# the cyclic imports, thus implementing lazy version
# as per https://peps.python.org/pep-0562/
from typing import TYPE_CHECKING as _TYPE_CHECKING
if _TYPE_CHECKING:
from types import ModuleType
from torch.ao.nn import ( # noqa: TC004
intrinsic as intrinsic,
qat as qat,
quantizable as quantizable,
quantized as quantized,
sparse as sparse,
)
__all__ = [
"intrinsic",
"qat",
"quantizable",
"quantized",
"sparse",
]
def __getattr__(name: str) -> "ModuleType":
if name in __all__:
import importlib
return importlib.import_module("." + name, __name__)
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
@@ -0,0 +1,41 @@
import types
from .modules import * # noqa: F403
from .modules.fused import _FusedModule # noqa: F403
# # Subpackages
# from . import qat # noqa: F403
# from . import quantized # noqa: F403
__all__ = [
"ConvBn1d",
"ConvBn2d",
"ConvBn3d",
"ConvBnReLU1d",
"ConvBnReLU2d",
"ConvBnReLU3d",
"ConvReLU1d",
"ConvReLU2d",
"ConvReLU3d",
"LinearReLU",
"BNReLU2d",
"BNReLU3d",
"LinearBn1d",
"LinearLeakyReLU",
"LinearTanh",
"ConvAdd2d",
"ConvAddReLU2d",
]
# We are exposing all subpackages to the end-user.
# Because of possible inter-dependency, we want to avoid
# the cyclic imports, thus implementing lazy version
# as per https://peps.python.org/pep-0562/
def __getattr__(name: str) -> types.ModuleType:
if name in __all__:
import importlib
return importlib.import_module("." + name, __name__)
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
@@ -0,0 +1,41 @@
from .fused import ( # noqa: F401
_FusedModule,
BNReLU2d,
BNReLU3d,
ConvAdd2d,
ConvAddReLU2d,
ConvBn1d,
ConvBn2d,
ConvBn3d,
ConvBnReLU1d,
ConvBnReLU2d,
ConvBnReLU3d,
ConvReLU1d,
ConvReLU2d,
ConvReLU3d,
LinearBn1d,
LinearLeakyReLU,
LinearReLU,
LinearTanh,
)
__all__ = [
"ConvBn1d",
"ConvBn2d",
"ConvBn3d",
"ConvBnReLU1d",
"ConvBnReLU2d",
"ConvBnReLU3d",
"ConvReLU1d",
"ConvReLU2d",
"ConvReLU3d",
"LinearReLU",
"BNReLU2d",
"BNReLU3d",
"LinearBn1d",
"LinearLeakyReLU",
"LinearTanh",
"ConvAdd2d",
"ConvAddReLU2d",
]
@@ -0,0 +1,319 @@
# mypy: allow-untyped-defs
import torch
from torch.nn import (
BatchNorm1d,
BatchNorm2d,
BatchNorm3d,
Conv1d,
Conv2d,
Conv3d,
Linear,
ReLU,
)
from torch.nn.utils.parametrize import type_before_parametrizations
__all__ = [
"ConvReLU1d",
"ConvReLU2d",
"ConvReLU3d",
"LinearReLU",
"ConvBn1d",
"ConvBn2d",
"ConvBnReLU1d",
"ConvBnReLU2d",
"ConvBn3d",
"ConvBnReLU3d",
"BNReLU2d",
"BNReLU3d",
"LinearBn1d",
"LinearLeakyReLU",
"LinearTanh",
"ConvAdd2d",
"ConvAddReLU2d",
]
# Used for identifying intrinsic modules used in quantization
class _FusedModule(torch.nn.Sequential):
pass
class ConvReLU1d(_FusedModule):
r"""This is a sequential container which calls the Conv1d and ReLU modules.
During quantization this will be replaced with the corresponding fused module."""
def __init__(self, conv, relu):
if not (
type_before_parametrizations(conv) == Conv1d
and type_before_parametrizations(relu) == ReLU
):
raise AssertionError(
f"Incorrect types for input modules: "
f"{type_before_parametrizations(conv).__name__} and "
f"{type_before_parametrizations(relu).__name__}"
)
super().__init__(conv, relu)
class ConvReLU2d(_FusedModule):
r"""This is a sequential container which calls the Conv2d and ReLU modules.
During quantization this will be replaced with the corresponding fused module."""
def __init__(self, conv, relu):
if not (
type_before_parametrizations(conv) == Conv2d
and type_before_parametrizations(relu) == ReLU
):
raise AssertionError(
f"Incorrect types for input modules: "
f"{type_before_parametrizations(conv).__name__} and "
f"{type_before_parametrizations(relu).__name__}"
)
super().__init__(conv, relu)
class ConvReLU3d(_FusedModule):
r"""This is a sequential container which calls the Conv3d and ReLU modules.
During quantization this will be replaced with the corresponding fused module."""
def __init__(self, conv, relu):
if not (
type_before_parametrizations(conv) == Conv3d
and type_before_parametrizations(relu) == ReLU
):
raise AssertionError(
f"Incorrect types for input modules: "
f"{type_before_parametrizations(conv).__name__} and "
f"{type_before_parametrizations(relu).__name__}"
)
super().__init__(conv, relu)
class LinearReLU(_FusedModule):
r"""This is a sequential container which calls the Linear and ReLU modules.
During quantization this will be replaced with the corresponding fused module."""
def __init__(self, linear, relu):
if not (
type_before_parametrizations(linear) == Linear
and type_before_parametrizations(relu) == ReLU
):
raise AssertionError(
f"Incorrect types for input modules: "
f"{type_before_parametrizations(linear).__name__} and "
f"{type_before_parametrizations(relu).__name__}"
)
super().__init__(linear, relu)
class ConvBn1d(_FusedModule):
r"""This is a sequential container which calls the Conv 1d and Batch Norm 1d modules.
During quantization this will be replaced with the corresponding fused module."""
def __init__(self, conv, bn):
if not (
type_before_parametrizations(conv) == Conv1d
and type_before_parametrizations(bn) == BatchNorm1d
):
raise AssertionError(
f"Incorrect types for input modules: "
f"{type_before_parametrizations(conv).__name__} and "
f"{type_before_parametrizations(bn).__name__}"
)
super().__init__(conv, bn)
class ConvBn2d(_FusedModule):
r"""This is a sequential container which calls the Conv 2d and Batch Norm 2d modules.
During quantization this will be replaced with the corresponding fused module."""
def __init__(self, conv, bn):
if not (
type_before_parametrizations(conv) == Conv2d
and type_before_parametrizations(bn) == BatchNorm2d
):
raise AssertionError(
f"Incorrect types for input modules: "
f"{type_before_parametrizations(conv).__name__} and "
f"{type_before_parametrizations(bn).__name__}"
)
super().__init__(conv, bn)
class ConvBnReLU1d(_FusedModule):
r"""This is a sequential container which calls the Conv 1d, Batch Norm 1d, and ReLU modules.
During quantization this will be replaced with the corresponding fused module."""
def __init__(self, conv, bn, relu):
if not (
type_before_parametrizations(conv) == Conv1d
and type_before_parametrizations(bn) == BatchNorm1d
and type_before_parametrizations(relu) == ReLU
):
raise AssertionError(
f"Incorrect types for input modules: "
f"{type_before_parametrizations(conv).__name__}, "
f"{type_before_parametrizations(bn).__name__}, and "
f"{type_before_parametrizations(relu).__name__}"
)
super().__init__(conv, bn, relu)
class ConvBnReLU2d(_FusedModule):
r"""This is a sequential container which calls the Conv 2d, Batch Norm 2d, and ReLU modules.
During quantization this will be replaced with the corresponding fused module."""
def __init__(self, conv, bn, relu):
if not (
type_before_parametrizations(conv) == Conv2d
and type_before_parametrizations(bn) == BatchNorm2d
and type_before_parametrizations(relu) == ReLU
):
raise AssertionError(
f"Incorrect types for input modules: "
f"{type_before_parametrizations(conv).__name__}, "
f"{type_before_parametrizations(bn).__name__}, and "
f"{type_before_parametrizations(relu).__name__}"
)
super().__init__(conv, bn, relu)
class ConvBn3d(_FusedModule):
r"""This is a sequential container which calls the Conv 3d and Batch Norm 3d modules.
During quantization this will be replaced with the corresponding fused module."""
def __init__(self, conv, bn):
if not (
type_before_parametrizations(conv) == Conv3d
and type_before_parametrizations(bn) == BatchNorm3d
):
raise AssertionError(
f"Incorrect types for input modules: "
f"{type_before_parametrizations(conv).__name__} and "
f"{type_before_parametrizations(bn).__name__}"
)
super().__init__(conv, bn)
class ConvBnReLU3d(_FusedModule):
r"""This is a sequential container which calls the Conv 3d, Batch Norm 3d, and ReLU modules.
During quantization this will be replaced with the corresponding fused module."""
def __init__(self, conv, bn, relu):
if not (
type_before_parametrizations(conv) == Conv3d
and type_before_parametrizations(bn) == BatchNorm3d
and type_before_parametrizations(relu) == ReLU
):
raise AssertionError(
f"Incorrect types for input modules: "
f"{type_before_parametrizations(conv).__name__}, "
f"{type_before_parametrizations(bn).__name__}, and "
f"{type_before_parametrizations(relu).__name__}"
)
super().__init__(conv, bn, relu)
class BNReLU2d(_FusedModule):
r"""This is a sequential container which calls the BatchNorm 2d and ReLU modules.
During quantization this will be replaced with the corresponding fused module."""
def __init__(self, batch_norm, relu):
if not (
type_before_parametrizations(batch_norm) == BatchNorm2d
and type_before_parametrizations(relu) == ReLU
):
raise AssertionError(
f"Incorrect types for input modules: "
f"{type_before_parametrizations(batch_norm).__name__} and "
f"{type_before_parametrizations(relu).__name__}"
)
super().__init__(batch_norm, relu)
class BNReLU3d(_FusedModule):
r"""This is a sequential container which calls the BatchNorm 3d and ReLU modules.
During quantization this will be replaced with the corresponding fused module."""
def __init__(self, batch_norm, relu):
if not (
type_before_parametrizations(batch_norm) == BatchNorm3d
and type_before_parametrizations(relu) == ReLU
):
raise AssertionError(
f"Incorrect types for input modules: "
f"{type_before_parametrizations(batch_norm).__name__} and "
f"{type_before_parametrizations(relu).__name__}"
)
super().__init__(batch_norm, relu)
class LinearBn1d(_FusedModule):
r"""This is a sequential container which calls the Linear and BatchNorm1d modules.
During quantization this will be replaced with the corresponding fused module."""
def __init__(self, linear, bn):
if not (
type_before_parametrizations(linear) == Linear
and type_before_parametrizations(bn) == BatchNorm1d
):
raise AssertionError(
f"Incorrect types for input modules: "
f"{type_before_parametrizations(linear).__name__} and "
f"{type_before_parametrizations(bn).__name__}"
)
super().__init__(linear, bn)
class LinearLeakyReLU(_FusedModule):
r"""This is a sequential container which calls the Linear and LeakyReLU modules.
During quantization this will be replaced with the corresponding fused module."""
def __init__(self, linear, leaky_relu):
if not (type(linear) is Linear and type(leaky_relu) is torch.nn.LeakyReLU):
raise AssertionError(
f"Incorrect types for input modules: "
f"{type(linear).__name__} and {type(leaky_relu).__name__}"
)
super().__init__(linear, leaky_relu)
class LinearTanh(_FusedModule):
r"""This is a sequential container which calls the Linear and Tanh modules.
During quantization this will be replaced with the corresponding fused module."""
def __init__(self, linear, tanh):
if not (type(linear) is Linear and type(tanh) is torch.nn.Tanh):
raise AssertionError(
f"Incorrect types for input modules: "
f"{type(linear).__name__} and {type(tanh).__name__}"
)
super().__init__(linear, tanh)
class ConvAdd2d(_FusedModule):
r"""This is a sequential container which calls the Conv2d modules with extra Add.
During quantization this will be replaced with the corresponding fused module."""
def __init__(self, conv, add):
super().__init__(conv)
self.add = add
def forward(self, x1, x2): # type: ignore[override]
r"""Applies convolution to x1 and adds the result to x2."""
return self.add(self[0](x1), x2)
class ConvAddReLU2d(_FusedModule):
r"""This is a sequential container which calls the Conv2d, add, Relu.
During quantization this will be replaced with the corresponding fused module."""
def __init__(self, conv, add, relu):
super().__init__(conv)
self.add = add
self.relu = relu
def forward(self, x1, x2): # type: ignore[override]
r"""Applies convolution to x1, adds the result to x2, and applies ReLU."""
return self.relu(self.add(self[0](x1), x2))
@@ -0,0 +1 @@
from .modules import * # noqa: F403
@@ -0,0 +1,32 @@
from .conv_fused import (
ConvBn1d,
ConvBn2d,
ConvBn3d,
ConvBnReLU1d,
ConvBnReLU2d,
ConvBnReLU3d,
ConvReLU1d,
ConvReLU2d,
ConvReLU3d,
freeze_bn_stats,
update_bn_stats,
)
from .linear_fused import LinearBn1d
from .linear_relu import LinearReLU
__all__ = [
"LinearReLU",
"LinearBn1d",
"ConvReLU1d",
"ConvReLU2d",
"ConvReLU3d",
"ConvBn1d",
"ConvBn2d",
"ConvBn3d",
"ConvBnReLU1d",
"ConvBnReLU2d",
"ConvBnReLU3d",
"update_bn_stats",
"freeze_bn_stats",
]
@@ -0,0 +1,971 @@
# mypy: allow-untyped-defs
import math
from typing import ClassVar
import torch
import torch.ao.nn.intrinsic as nni
import torch.ao.nn.qat as nnqat
import torch.nn as nn
import torch.nn.functional as F
from torch.nn import init
from torch.nn.modules.utils import _pair, _single, _triple
from torch.nn.parameter import Parameter
from torch.nn.utils import fuse_conv_bn_weights
__all__ = [
"ConvBn1d",
"ConvBnReLU1d",
"ConvReLU1d",
"ConvBn2d",
"ConvBnReLU2d",
"ConvReLU2d",
"ConvBn3d",
"ConvBnReLU3d",
"ConvReLU3d",
"update_bn_stats",
"freeze_bn_stats",
]
_BN_CLASS_MAP = {
1: nn.BatchNorm1d,
2: nn.BatchNorm2d,
3: nn.BatchNorm3d,
}
class _ConvBnNd(nn.modules.conv._ConvNd, nni._FusedModule):
_version = 2
_FLOAT_MODULE: ClassVar[type[nn.modules.conv._ConvNd]]
def __init__(
self,
# ConvNd args
in_channels,
out_channels,
kernel_size,
stride,
padding,
dilation,
transposed,
output_padding,
groups,
bias,
padding_mode,
# BatchNormNd args
# num_features: out_channels
eps=1e-05,
momentum=0.1,
# affine: True
# track_running_stats: True
# Args for this module
freeze_bn=False,
qconfig=None,
dim=2,
):
nn.modules.conv._ConvNd.__init__(
self,
in_channels,
out_channels,
kernel_size,
stride,
padding,
dilation,
transposed,
output_padding,
groups,
False,
padding_mode,
)
if not qconfig:
raise AssertionError("qconfig must be provided for QAT module")
self.qconfig = qconfig
self.freeze_bn = freeze_bn if self.training else True
self.bn = _BN_CLASS_MAP[dim](out_channels, eps, momentum, True, True)
self.weight_fake_quant = self.qconfig.weight()
if bias:
self.bias = Parameter(torch.empty(out_channels))
else:
self.register_parameter("bias", None)
self.reset_bn_parameters()
# this needs to be called after reset_bn_parameters,
# as they modify the same state
if self.training:
if freeze_bn:
self.freeze_bn_stats()
else:
self.update_bn_stats()
else:
self.freeze_bn_stats()
self._enable_slow_path_for_better_numerical_stability = False
def reset_running_stats(self):
self.bn.reset_running_stats()
def reset_bn_parameters(self):
self.bn.reset_running_stats()
init.uniform_(self.bn.weight)
init.zeros_(self.bn.bias)
# note: below is actually for conv, not BN
if self.bias is not None:
fan_in, _ = init._calculate_fan_in_and_fan_out(self.weight)
bound = 1 / math.sqrt(fan_in)
init.uniform_(self.bias, -bound, bound)
def update_bn_stats(self):
self.freeze_bn = False
self.bn.training = True
return self
def freeze_bn_stats(self):
self.freeze_bn = True
self.bn.training = False
return self
def _forward(self, input):
if self._enable_slow_path_for_better_numerical_stability:
return self._forward_slow(input)
return self._forward_approximate(input)
def _forward_approximate(self, input):
"""Approximated method to fuse conv and bn. It requires only one forward pass.
conv_orig = conv / scale_factor where scale_factor = bn.weight / running_std
"""
if self.bn.running_var is None:
raise AssertionError("self.bn.running_var must not be None")
running_std = torch.sqrt(self.bn.running_var + self.bn.eps)
scale_factor = self.bn.weight / running_std
weight_shape = [1] * len(self.weight.shape)
weight_shape[0] = -1
bias_shape = [1] * len(self.weight.shape)
bias_shape[1] = -1
scaled_weight = self.weight_fake_quant(
self.weight * scale_factor.reshape(weight_shape)
)
# using zero bias here since the bias for original conv
# will be added later
if self.bias is not None:
zero_bias = torch.zeros_like(self.bias, dtype=input.dtype)
else:
zero_bias = torch.zeros(
self.out_channels, device=scaled_weight.device, dtype=input.dtype
)
conv = self._conv_forward(input, scaled_weight, zero_bias)
conv_orig = conv / scale_factor.reshape(bias_shape)
if self.bias is not None:
conv_orig = conv_orig + self.bias.reshape(bias_shape)
conv = self.bn(conv_orig)
return conv
def _forward_slow(self, input):
"""
A more accurate but slow method to compute conv bn fusion, following https://arxiv.org/pdf/1806.08342.pdf
It requires two forward passes but handles the case bn.weight == 0
Conv: Y = WX + B_c
Conv without bias: Y0 = WX = Y - B_c, Y = Y0 + B_c
Batch statistics:
mean_Y = Y.mean()
= Y0.mean() + B_c
var_Y = (Y - mean_Y)^2.mean()
= (Y0 - Y0.mean())^2.mean()
BN (r: bn.weight, beta: bn.bias):
Z = r * (Y - mean_Y) / sqrt(var_Y + eps) + beta
= r * (Y0 - Y0.mean()) / sqrt(var_Y + eps) + beta
Fused Conv BN training (std_Y = sqrt(var_Y + eps)):
Z = (r * W / std_Y) * X + r * (B_c - mean_Y) / std_Y + beta
= (r * W / std_Y) * X - r * Y0.mean() / std_Y + beta
Fused Conv BN inference (running_std = sqrt(running_var + eps)):
Z = (r * W / running_std) * X - r * (running_mean - B_c) / running_std + beta
QAT with fused conv bn:
Z_train = fake_quant(r * W / running_std) * X * (running_std / std_Y) - r * Y0.mean() / std_Y + beta
= conv(X, fake_quant(r * W / running_std)) * (running_std / std_Y) - r * Y0.mean() / std_Y + beta
Z_inference = conv(X, fake_quant(r * W / running_std)) - r * (running_mean - B_c) / running_std + beta
"""
if self.bn.running_var is None:
raise AssertionError("self.bn.running_var must not be None")
if self.bn.running_mean is None:
raise AssertionError("self.bn.running_mean must not be None")
# using zero bias here since the bias for original conv
# will be added later
zero_bias = torch.zeros(
self.out_channels, device=self.weight.device, dtype=input.dtype
)
weight_shape = [1] * len(self.weight.shape)
weight_shape[0] = -1
bias_shape = [1] * len(self.weight.shape)
bias_shape[1] = -1
if self.bn.training:
# needed to compute batch mean/std
conv_out = self._conv_forward(input, self.weight, zero_bias)
# update bn statistics
with torch.no_grad():
conv_out_bias = (
conv_out
if self.bias is None
else conv_out + self.bias.reshape(bias_shape)
)
self.bn(conv_out_bias)
# fused conv + bn without bias using bn running statistics
running_std = torch.sqrt(self.bn.running_var + self.bn.eps)
scale_factor = self.bn.weight / running_std
scaled_weight = self.weight_fake_quant(
self.weight * scale_factor.reshape(weight_shape)
)
# fused conv without bias for inference: (r * W / running_std) * X
conv_bn = self._conv_forward(input, scaled_weight, zero_bias)
avg_dims = [0] + list(range(2, len(self.weight.shape)))
batch_mean = conv_out.mean(avg_dims)
batch_var = torch.square(conv_out - batch_mean.reshape(bias_shape)).mean(
avg_dims
)
batch_std = torch.sqrt(batch_var + self.bn.eps)
# scale to use batch std in training mode
# conv(X, r * W / std_Y) = conv(X, r * W / running_std) * (running_std / std_Y)
unscale_factor = running_std / batch_std
conv_bn *= unscale_factor.reshape(bias_shape)
fused_mean = batch_mean
fused_std = batch_std
else:
# fused conv + bn without bias using bn running statistics
running_std = torch.sqrt(self.bn.running_var + self.bn.eps)
scale_factor = self.bn.weight / running_std
scaled_weight = self.weight_fake_quant(
self.weight * scale_factor.reshape(weight_shape)
)
# fused conv without bias for inference: (r * W / running_std) * X
conv_bn = self._conv_forward(input, scaled_weight, zero_bias)
fused_mean = self.bn.running_mean - (
self.bias if self.bias is not None else 0
)
fused_std = running_std
# fused bias = beta - r * mean / std
fused_bias = self.bn.bias - self.bn.weight * fused_mean / fused_std
conv_bn += fused_bias.reshape(bias_shape)
# HACK to let conv bias participate in loss to avoid DDP error (parameters
# were not used in producing loss)
if self.bias is not None:
conv_bn += (self.bias - self.bias).reshape(bias_shape)
return conv_bn
def forward(self, input):
return self._forward(input)
def train(self, mode=True):
"""
Batchnorm's training behavior is using the self.training flag. Prevent
changing it if BN is frozen. This makes sure that calling `model.train()`
on a model with a frozen BN will behave properly.
"""
self.training = mode
if not self.freeze_bn:
for module in self.children():
module.train(mode)
return self
# ===== Serialization version history =====
#
# Version 1/None
# self
# |--- weight : Tensor
# |--- bias : Tensor
# |--- gamma : Tensor
# |--- beta : Tensor
# |--- running_mean : Tensor
# |--- running_var : Tensor
# |--- num_batches_tracked : Tensor
#
# Version 2
# self
# |--- weight : Tensor
# |--- bias : Tensor
# |--- bn : Module
# |--- weight : Tensor (moved from v1.self.gamma)
# |--- bias : Tensor (moved from v1.self.beta)
# |--- running_mean : Tensor (moved from v1.self.running_mean)
# |--- running_var : Tensor (moved from v1.self.running_var)
# |--- num_batches_tracked : Tensor (moved from v1.self.num_batches_tracked)
def _load_from_state_dict(
self,
state_dict,
prefix,
local_metadata,
strict,
missing_keys,
unexpected_keys,
error_msgs,
):
version = local_metadata.get("version", None)
if version is None or version == 1:
# BN related parameters and buffers were moved into the BN module for v2
v2_to_v1_names = {
"bn.weight": "gamma",
"bn.bias": "beta",
"bn.running_mean": "running_mean",
"bn.running_var": "running_var",
"bn.num_batches_tracked": "num_batches_tracked",
}
for v2_name, v1_name in v2_to_v1_names.items():
if prefix + v1_name in state_dict:
state_dict[prefix + v2_name] = state_dict[prefix + v1_name]
state_dict.pop(prefix + v1_name)
elif prefix + v2_name in state_dict:
# there was a brief period where forward compatibility
# for this module was broken (between
# https://github.com/pytorch/pytorch/pull/38478
# and https://github.com/pytorch/pytorch/pull/38820)
# and modules emitted the v2 state_dict format while
# specifying that version == 1. This patches the forward
# compatibility issue by allowing the v2 style entries to
# be used.
pass
elif strict:
missing_keys.append(prefix + v2_name)
super()._load_from_state_dict(
state_dict,
prefix,
local_metadata,
strict,
missing_keys,
unexpected_keys,
error_msgs,
)
@classmethod
def from_float(cls, mod, use_precomputed_fake_quant=False):
r"""Create a qat module from a float module or qparams_dict
Args: `mod` a float module, either produced by torch.ao.quantization utilities
or directly from user
"""
# The ignore is because _FLOAT_MODULE is a TypeVar here where the bound
# has no __name__ (code is fine though)
if type(mod) is not cls._FLOAT_MODULE:
raise AssertionError(
"qat."
+ cls.__name__
+ ".from_float only works for "
+ cls._FLOAT_MODULE.__name__
)
if not hasattr(mod, "qconfig"):
raise AssertionError("Input float module must have qconfig defined")
if not mod.qconfig:
raise AssertionError("Input float module must have a valid qconfig")
qconfig = mod.qconfig
conv, bn = mod[0], mod[1] # type: ignore[index]
qat_convbn = cls(
conv.in_channels,
conv.out_channels,
conv.kernel_size,
conv.stride,
conv.padding,
conv.dilation,
conv.groups,
conv.bias is not None,
conv.padding_mode,
bn.eps,
bn.momentum,
False,
qconfig,
)
qat_convbn.weight = conv.weight
qat_convbn.bias = conv.bias
qat_convbn.bn.weight = bn.weight
qat_convbn.bn.bias = bn.bias
qat_convbn.bn.running_mean = bn.running_mean
qat_convbn.bn.running_var = bn.running_var
# mypy error: Cannot determine type of 'num_batches_tracked'
qat_convbn.bn.num_batches_tracked = bn.num_batches_tracked
return qat_convbn
def to_float(self):
cls = type(self)
conv = cls._FLOAT_CONV_MODULE( # type: ignore[attr-defined]
self.in_channels,
self.out_channels,
self.kernel_size,
self.stride,
self.padding,
self.dilation,
self.groups,
self.bias is not None,
self.padding_mode,
)
conv.weight = torch.nn.Parameter(self.weight.detach())
if self.bias is not None:
conv.bias = torch.nn.Parameter(self.bias.detach())
if cls._FLOAT_BN_MODULE: # type: ignore[attr-defined]
# fuse bn into conv
if self.bn.running_var is None or self.bn.running_mean is None:
raise AssertionError(
"self.bn.running_var and self.bn.running_mean must not be None"
)
conv.weight, conv.bias = fuse_conv_bn_weights(
conv.weight,
conv.bias,
self.bn.running_mean,
self.bn.running_var,
self.bn.eps,
self.bn.weight,
self.bn.bias,
)
if cls._FLOAT_RELU_MODULE: # type: ignore[attr-defined]
modules = []
modules.append(conv)
relu = cls._FLOAT_RELU_MODULE() # type: ignore[attr-defined]
modules.append(relu)
conv_relu = cls._FUSED_FLOAT_MODULE(*modules) # type: ignore[attr-defined]
conv_relu.train(self.training)
return conv_relu
else:
conv.train(self.training)
return conv
class ConvBn1d(_ConvBnNd, nn.Conv1d):
r"""
A ConvBn1d module is a module fused from Conv1d and BatchNorm1d,
attached with FakeQuantize modules for weight,
used in quantization aware training.
We combined the interface of :class:`torch.nn.Conv1d` and
:class:`torch.nn.BatchNorm1d`.
Similar to :class:`torch.nn.Conv1d`, with FakeQuantize modules initialized
to default.
Attributes:
freeze_bn:
weight_fake_quant: fake quant module for weight
"""
_FLOAT_BN_MODULE: ClassVar[type[nn.BatchNorm1d]] = nn.BatchNorm1d
_FLOAT_RELU_MODULE: ClassVar[type[nn.Module] | None] = None
_FLOAT_MODULE: ClassVar[type[nn.Module]] = nni.ConvBn1d # type: ignore[assignment]
_FLOAT_CONV_MODULE: ClassVar[type[nn.Conv1d]] = nn.Conv1d
def __init__(
self,
# Conv1d args
in_channels,
out_channels,
kernel_size,
stride=1,
padding=0,
dilation=1,
groups=1,
bias=None,
padding_mode="zeros",
# BatchNorm1d args
# num_features: out_channels
eps=1e-05,
momentum=0.1,
# affine: True
# track_running_stats: True
# Args for this module
freeze_bn=False,
qconfig=None,
):
kernel_size = _single(kernel_size)
stride = _single(stride)
padding = _single(padding)
dilation = _single(dilation)
_ConvBnNd.__init__(
self,
in_channels,
out_channels,
kernel_size,
stride,
padding,
dilation,
False,
_single(0),
groups,
bias,
padding_mode,
eps,
momentum,
freeze_bn,
qconfig,
dim=1,
)
class ConvBnReLU1d(ConvBn1d):
r"""
A ConvBnReLU1d module is a module fused from Conv1d, BatchNorm1d and ReLU,
attached with FakeQuantize modules for weight,
used in quantization aware training.
We combined the interface of :class:`torch.nn.Conv1d` and
:class:`torch.nn.BatchNorm1d` and :class:`torch.nn.ReLU`.
Similar to `torch.nn.Conv1d`, with FakeQuantize modules initialized to
default.
Attributes:
weight_fake_quant: fake quant module for weight
"""
# base class defines _FLOAT_MODULE as "ConvBn1d"
_FLOAT_MODULE: ClassVar[type[nn.Module]] = nni.ConvBnReLU1d
_FLOAT_CONV_MODULE: ClassVar[type[nn.Conv1d]] = nn.Conv1d
_FLOAT_BN_MODULE: ClassVar[type[nn.BatchNorm1d]] = nn.BatchNorm1d
_FLOAT_RELU_MODULE: ClassVar[type[nn.Module] | None] = nn.ReLU
# module class after fusing bn into conv
_FUSED_FLOAT_MODULE: ClassVar[type[nn.Module] | None] = nni.ConvReLU1d
def forward(self, input):
r"""Performs forward pass through fused Conv1d, BatchNorm1d, and ReLU."""
return F.relu(self._forward(input))
@classmethod
def from_float(cls, mod, use_precomputed_fake_quant=False):
r"""Creates a QAT module from a floating point module."""
return super().from_float(mod, use_precomputed_fake_quant)
class ConvReLU1d(nnqat.Conv1d, nni._FusedModule):
r"""A ConvReLU1d module is a fused module of Conv1d and ReLU, attached with
FakeQuantize modules for weight for
quantization aware training.
We combined the interface of :class:`~torch.nn.Conv1d` and
:class:`~torch.nn.BatchNorm1d`.
Attributes:
weight_fake_quant: fake quant module for weight
"""
_FLOAT_MODULE: ClassVar[type[nni.ConvReLU1d]] = nni.ConvReLU1d # type: ignore[assignment]
_FLOAT_CONV_MODULE: ClassVar[type[nn.Conv1d]] = nn.Conv1d
_FLOAT_BN_MODULE: ClassVar[type[nn.Module] | None] = None
_FLOAT_RELU_MODULE: ClassVar[type[nn.Module] | None] = nn.ReLU
def __init__(
self,
in_channels,
out_channels,
kernel_size,
stride=1,
padding=0,
dilation=1,
groups=1,
bias=True,
padding_mode="zeros",
qconfig=None,
):
super().__init__(
in_channels,
out_channels,
kernel_size,
stride=stride,
padding=padding,
dilation=dilation,
groups=groups,
bias=bias,
# pyrefly: ignore [bad-argument-type]
padding_mode=padding_mode,
qconfig=qconfig,
)
if not qconfig:
raise AssertionError("qconfig must be provided for QAT module")
self.qconfig = qconfig
self.weight_fake_quant = self.qconfig.weight()
def forward(self, input):
r"""Performs forward pass through fused Conv1d and ReLU."""
return F.relu(
self._conv_forward(input, self.weight_fake_quant(self.weight), self.bias)
)
@classmethod
def from_float(cls, mod, use_precomputed_fake_quant=False): # type: ignore[override]
r"""Creates a QAT module from a floating point module."""
return super().from_float(
mod, use_precomputed_fake_quant=use_precomputed_fake_quant
)
class ConvBn2d(_ConvBnNd, nn.Conv2d):
r"""
A ConvBn2d module is a module fused from Conv2d and BatchNorm2d,
attached with FakeQuantize modules for weight,
used in quantization aware training.
We combined the interface of :class:`torch.nn.Conv2d` and
:class:`torch.nn.BatchNorm2d`.
Similar to :class:`torch.nn.Conv2d`, with FakeQuantize modules initialized
to default.
Attributes:
freeze_bn:
weight_fake_quant: fake quant module for weight
"""
_FLOAT_MODULE: ClassVar[type[nni.ConvBn2d]] = nni.ConvBn2d # type: ignore[assignment]
_FLOAT_CONV_MODULE: ClassVar[type[nn.Conv2d]] = nn.Conv2d
_FLOAT_BN_MODULE: ClassVar[type[nn.Module] | None] = nn.BatchNorm2d
_FLOAT_RELU_MODULE: ClassVar[type[nn.Module] | None] = None
def __init__(
self,
# ConvNd args
in_channels,
out_channels,
kernel_size,
stride=1,
padding=0,
dilation=1,
groups=1,
bias=None,
padding_mode="zeros",
# BatchNorm2d args
# num_features: out_channels
eps=1e-05,
momentum=0.1,
# affine: True
# track_running_stats: True
# Args for this module
freeze_bn=False,
qconfig=None,
):
kernel_size = _pair(kernel_size)
stride = _pair(stride)
padding = _pair(padding)
dilation = _pair(dilation)
_ConvBnNd.__init__(
self,
in_channels,
out_channels,
kernel_size,
stride,
padding,
dilation,
False,
_pair(0),
groups,
bias,
padding_mode,
eps,
momentum,
freeze_bn,
qconfig,
dim=2,
)
class ConvBnReLU2d(ConvBn2d):
r"""
A ConvBnReLU2d module is a module fused from Conv2d, BatchNorm2d and ReLU,
attached with FakeQuantize modules for weight,
used in quantization aware training.
We combined the interface of :class:`torch.nn.Conv2d` and
:class:`torch.nn.BatchNorm2d` and :class:`torch.nn.ReLU`.
Similar to `torch.nn.Conv2d`, with FakeQuantize modules initialized to
default.
Attributes:
weight_fake_quant: fake quant module for weight
"""
# base class defines _FLOAT_MODULE as "ConvBn2d"
_FLOAT_MODULE: ClassVar[type[nni.ConvBnReLU2d]] = nni.ConvBnReLU2d # type: ignore[assignment]
_FLOAT_CONV_MODULE: ClassVar[type[nn.Conv2d]] = nn.Conv2d
_FLOAT_BN_MODULE: ClassVar[type[nn.BatchNorm2d]] = nn.BatchNorm2d
_FLOAT_RELU_MODULE: ClassVar[type[nn.Module] | None] = nn.ReLU
# module class after fusing bn into conv
_FUSED_FLOAT_MODULE: ClassVar[type[nni.ConvReLU2d] | None] = nni.ConvReLU2d
def forward(self, input):
r"""Performs forward pass through fused Conv2d, BatchNorm2d, and ReLU."""
return F.relu(self._forward(input))
@classmethod
def from_float(cls, mod, use_precomputed_fake_quant=False):
r"""Creates a QAT module from a floating point module."""
return super().from_float(mod, use_precomputed_fake_quant)
class ConvReLU2d(nnqat.Conv2d, nni._FusedModule):
r"""A ConvReLU2d module is a fused module of Conv2d and ReLU, attached with
FakeQuantize modules for weight for
quantization aware training.
We combined the interface of :class:`~torch.nn.Conv2d` and
:class:`~torch.nn.BatchNorm2d`.
Attributes:
weight_fake_quant: fake quant module for weight
"""
_FLOAT_MODULE: ClassVar[type[nn.Module]] = nni.ConvReLU2d # type: ignore[assignment]
_FLOAT_CONV_MODULE: ClassVar[type[nn.Conv2d]] = nn.Conv2d
_FLOAT_BN_MODULE: ClassVar[type[nn.Module] | None] = None
_FLOAT_RELU_MODULE: ClassVar[type[nn.Module] | None] = nn.ReLU
def __init__(
self,
in_channels,
out_channels,
kernel_size,
stride=1,
padding=0,
dilation=1,
groups=1,
bias=True,
padding_mode="zeros",
qconfig=None,
):
super().__init__(
in_channels,
out_channels,
kernel_size,
stride=stride,
padding=padding,
dilation=dilation,
groups=groups,
bias=bias,
# pyrefly: ignore [bad-argument-type]
padding_mode=padding_mode,
qconfig=qconfig,
)
if not qconfig:
raise AssertionError("qconfig must be provided for QAT module")
self.qconfig = qconfig
self.weight_fake_quant = self.qconfig.weight()
def forward(self, input):
r"""Performs forward pass through fused Conv2d and ReLU."""
return F.relu(
self._conv_forward(input, self.weight_fake_quant(self.weight), self.bias)
)
@classmethod
def from_float(cls, mod, use_precomputed_fake_quant=False): # type: ignore[override]
r"""Creates a QAT module from a floating point module."""
return super().from_float(
mod, use_precomputed_fake_quant=use_precomputed_fake_quant
)
class ConvBn3d(_ConvBnNd, nn.Conv3d):
r"""
A ConvBn3d module is a module fused from Conv3d and BatchNorm3d,
attached with FakeQuantize modules for weight,
used in quantization aware training.
We combined the interface of :class:`torch.nn.Conv3d` and
:class:`torch.nn.BatchNorm3d`.
Similar to :class:`torch.nn.Conv3d`, with FakeQuantize modules initialized
to default.
Attributes:
freeze_bn:
weight_fake_quant: fake quant module for weight
"""
_FLOAT_MODULE: ClassVar[type[nni.ConvBn3d]] = nni.ConvBn3d # type: ignore[assignment]
_FLOAT_CONV_MODULE: ClassVar[type[nn.Conv3d]] = nn.Conv3d
_FLOAT_BN_MODULE: ClassVar[type[nn.Module] | None] = nn.BatchNorm3d
_FLOAT_RELU_MODULE: ClassVar[type[nn.Module] | None] = None
def __init__(
self,
# ConvNd args
in_channels,
out_channels,
kernel_size,
stride=1,
padding=0,
dilation=1,
groups=1,
bias=None,
padding_mode="zeros",
# BatchNorm3d args
# num_features: out_channels
eps=1e-05,
momentum=0.1,
# affine: True
# track_running_stats: True
# Args for this module
freeze_bn=False,
qconfig=None,
):
kernel_size = _triple(kernel_size)
stride = _triple(stride)
padding = _triple(padding)
dilation = _triple(dilation)
_ConvBnNd.__init__(
self,
in_channels,
out_channels,
kernel_size,
stride,
padding,
dilation,
False,
_triple(0),
groups,
bias,
padding_mode,
eps,
momentum,
freeze_bn,
qconfig,
dim=3,
)
class ConvBnReLU3d(ConvBn3d):
r"""
A ConvBnReLU3d module is a module fused from Conv3d, BatchNorm3d and ReLU,
attached with FakeQuantize modules for weight,
used in quantization aware training.
We combined the interface of :class:`torch.nn.Conv3d` and
:class:`torch.nn.BatchNorm3d` and :class:`torch.nn.ReLU`.
Similar to `torch.nn.Conv3d`, with FakeQuantize modules initialized to
default.
Attributes:
weight_fake_quant: fake quant module for weight
"""
_FLOAT_MODULE: ClassVar[type[nni.ConvBnReLU3d]] = nni.ConvBnReLU3d # type: ignore[assignment]
_FLOAT_CONV_MODULE: ClassVar[type[nn.Conv3d]] = nn.Conv3d
_FLOAT_BN_MODULE: ClassVar[type[nn.BatchNorm3d]] = nn.BatchNorm3d
_FLOAT_RELU_MODULE: ClassVar[type[nn.ReLU] | None] = nn.ReLU
# module class after fusing bn into conv
_FUSED_FLOAT_MODULE: ClassVar[type[nni.ConvReLU3d] | None] = nni.ConvReLU3d
def forward(self, input):
r"""Performs forward pass through fused Conv3d, BatchNorm3d, and ReLU."""
return F.relu(ConvBn3d._forward(self, input))
@classmethod
def from_float(cls, mod, use_precomputed_fake_quant=False):
r"""Creates a QAT module from a floating point module."""
return super().from_float(
mod, use_precomputed_fake_quant=use_precomputed_fake_quant
)
class ConvReLU3d(nnqat.Conv3d, nni._FusedModule):
r"""A ConvReLU3d module is a fused module of Conv3d and ReLU, attached with
FakeQuantize modules for weight for
quantization aware training.
We combined the interface of :class:`~torch.nn.Conv3d` and
:class:`~torch.nn.BatchNorm3d`.
Attributes:
weight_fake_quant: fake quant module for weight
"""
_FLOAT_MODULE: ClassVar[type[nni.ConvReLU3d]] = nni.ConvReLU3d # type: ignore[assignment]
_FLOAT_CONV_MODULE: ClassVar[type[nn.Conv3d]] = nn.Conv3d
_FLOAT_BN_MODULE: ClassVar[type[nn.Module] | None] = None
_FLOAT_RELU_MODULE: ClassVar[type[nn.Module] | None] = nn.ReLU
def __init__(
self,
in_channels,
out_channels,
kernel_size,
stride=1,
padding=0,
dilation=1,
groups=1,
bias=True,
padding_mode="zeros",
qconfig=None,
):
super().__init__(
in_channels,
out_channels,
kernel_size,
stride=stride,
padding=padding,
dilation=dilation,
groups=groups,
bias=bias,
# pyrefly: ignore [bad-argument-type]
padding_mode=padding_mode,
qconfig=qconfig,
)
if not qconfig:
raise AssertionError("qconfig must be provided for QAT module")
self.qconfig = qconfig
self.weight_fake_quant = self.qconfig.weight()
def forward(self, input):
r"""Performs forward pass through fused Conv3d and ReLU."""
return F.relu(
self._conv_forward(input, self.weight_fake_quant(self.weight), self.bias)
)
@classmethod
def from_float(cls, mod, use_precomputed_fake_quant=False): # type: ignore[override]
r"""Creates a QAT module from a floating point module."""
return super().from_float(
mod, use_precomputed_fake_quant=use_precomputed_fake_quant
)
def update_bn_stats(mod):
if type(mod) in {
ConvBnReLU1d,
ConvBnReLU2d,
ConvBnReLU3d,
ConvBn1d,
ConvBn2d,
ConvBn3d,
}:
mod.update_bn_stats()
def freeze_bn_stats(mod):
if type(mod) in {
ConvBnReLU1d,
ConvBnReLU2d,
ConvBnReLU3d,
ConvBn1d,
ConvBn2d,
ConvBn3d,
}:
mod.freeze_bn_stats()
@@ -0,0 +1,199 @@
# mypy: allow-untyped-defs
import torch
import torch.ao.nn.intrinsic as nni
import torch.nn as nn
import torch.nn.functional as F
from torch.nn import init
from torch.nn.parameter import Parameter
from torch.nn.utils.fusion import fuse_linear_bn_weights
__all__ = [
"LinearBn1d",
]
class LinearBn1d(nn.modules.linear.Linear, nni._FusedModule):
r"""
A LinearBn1d module is a module fused from Linear and BatchNorm1d, attached
with FakeQuantize modules for weight, used in quantization aware training.
We combined the interface of :class:`torch.nn.Linear` and
:class:torch.nn.BatchNorm1d`.
Similar to :class:`torch.nn.Linear`, with FakeQuantize modules initialized
to default.
Attributes:
freeze_bn:
weight_fake_quant: fake quant module for weight
"""
def __init__(
self,
# Linear args
in_features,
out_features,
bias=True,
# BatchNorm1d args
# num_features: out_features
eps=1e-05,
momentum=0.1,
# affine: True
# track_running_stats: True
# Args for this module
freeze_bn=False,
qconfig=None,
):
nn.modules.linear.Linear.__init__(self, in_features, out_features, bias)
if not qconfig:
raise AssertionError("qconfig must be provided for QAT module")
self.qconfig = qconfig
self.freeze_bn = freeze_bn if self.training else True
self.bn = nn.BatchNorm1d(out_features, eps, momentum, True, True)
self.weight_fake_quant = self.qconfig.weight()
if bias:
self.bias = Parameter(torch.empty(out_features))
else:
self.register_parameter("bias", None)
self.reset_bn_parameters()
# this needs to be called after reset_bn_parameters,
# as they modify the same state
if self.training:
if freeze_bn:
self.freeze_bn_stats()
else:
self.update_bn_stats()
else:
self.freeze_bn_stats()
def reset_running_stats(self):
self.bn.reset_running_stats()
def reset_bn_parameters(self):
self.bn.reset_running_stats()
init.uniform_(self.bn.weight)
init.zeros_(self.bn.bias)
def update_bn_stats(self):
self.freeze_bn = False
self.bn.training = True
return self
def freeze_bn_stats(self):
self.freeze_bn = True
self.bn.training = False
return self
def forward(self, input):
if self.bn.running_var is None:
raise AssertionError("self.bn.running_var must not be None")
# Scale the linear weights by BN's running statistics to reduce
# weight jitter, see https://arxiv.org/pdf/1806.08342.pdf, page 18
# for motivation.
#
# Instead of
#
# x1 = F.linear(x0, fq(w), b)
# x2 = self.bn(x1)
#
# We have
#
# # scale the weight by previous batch's running statistics
# scale_factor = bn.w / bn.running_std_from_prev_batch
# # do the linear transformation without bias
# x1_scaled = F.linear(x0, fq(w * scale_factor), 0)
# # reverse the scaling and add original bias
# x1_orig = x1_scaled / scale_factor + b
# x2 = self.bn(x1_orig)
running_std = torch.sqrt(self.bn.running_var + self.bn.eps)
scale_factor = self.bn.weight / running_std
weight_shape = [1] * len(self.weight.shape)
weight_shape[0] = -1
bias_shape = [1] * len(self.weight.shape)
bias_shape[1] = -1
scaled_weight = self.weight_fake_quant(
self.weight * scale_factor.reshape(weight_shape)
)
if self.bias is not None:
zero_bias = torch.zeros_like(self.bias)
else:
zero_bias = torch.zeros(self.out_features, device=scaled_weight.device)
linear_out = F.linear(input, scaled_weight, zero_bias)
linear_out_orig = linear_out / scale_factor.reshape(bias_shape)
if self.bias is not None:
linear_out_orig = linear_out_orig + self.bias.reshape(bias_shape)
bn_out = self.bn(linear_out_orig)
return bn_out
def train(self, mode=True):
"""
Batchnorm's training behavior is using the self.training flag. Prevent
changing it if BN is frozen. This makes sure that calling `model.train()`
on a model with a frozen BN will behave properly.
"""
self.training = mode
if not self.freeze_bn:
for module in self.children():
module.train(mode)
return self
@classmethod
def from_float(cls, mod, use_precomputed_fake_quant=False):
r"""Create a qat module from a float module or qparams_dict
Args:
mod: A float module, either produced by torch.ao.quantization
utilities or directly from the user.
"""
if type(mod) is not nni.LinearBn1d:
raise AssertionError(
"qat."
+ cls.__name__
+ ".from_float only works for "
+ nni.LinearBn1d.__name__
)
if not hasattr(mod, "qconfig"):
raise AssertionError("Input float module must have qconfig defined")
if not mod.qconfig:
raise AssertionError("Input float module must have a valid config")
qconfig = mod.qconfig
linear, bn = mod[0], mod[1]
qat_linearbn = cls(
linear.in_features,
linear.out_features,
linear.bias is not None,
bn.eps,
bn.momentum,
False,
qconfig,
)
qat_linearbn.weight = linear.weight # type: ignore[assignment]
qat_linearbn.bias = linear.bias # type: ignore[assignment]
qat_linearbn.bn.weight = bn.weight # type: ignore[assignment]
qat_linearbn.bn.bias = bn.bias # type: ignore[assignment]
qat_linearbn.bn.running_mean = bn.running_mean # type: ignore[assignment]
qat_linearbn.bn.running_var = bn.running_var # type: ignore[assignment]
qat_linearbn.bn.num_batches_tracked = bn.num_batches_tracked # type: ignore[assignment]
return qat_linearbn
def to_float(self):
linear = torch.nn.Linear(self.in_features, self.out_features)
if self.bn.running_var is None or self.bn.running_mean is None:
raise AssertionError(
"self.bn.running_var and self.bn.running_mean must not be None"
)
linear.weight, linear.bias = fuse_linear_bn_weights(
self.weight,
self.bias,
self.bn.running_mean,
self.bn.running_var,
self.bn.eps,
self.bn.weight,
self.bn.bias,
)
return linear
@@ -0,0 +1,74 @@
from __future__ import annotations
from typing import TYPE_CHECKING
import torch
import torch.ao.nn.intrinsic as nni
import torch.ao.nn.qat as nnqat
import torch.nn.functional as F
from torch.ao.nn.intrinsic.modules.fused import _FusedModule
if TYPE_CHECKING:
from torch.ao.quantization.qconfig import QConfigAny
__all__ = ["LinearReLU"]
class LinearReLU(nnqat.Linear, _FusedModule):
r"""
A LinearReLU module fused from Linear and ReLU modules, attached with
FakeQuantize modules for weight, used in
quantization aware training.
We adopt the same interface as :class:`torch.nn.Linear`.
Similar to `torch.ao.nn.intrinsic.LinearReLU`, with FakeQuantize modules initialized to
default.
Attributes:
weight: fake quant module for weight
Examples::
>>> # xdoctest: +SKIP
>>> m = nn.qat.LinearReLU(20, 30)
>>> input = torch.randn(128, 20)
>>> output = m(input)
>>> print(output.size())
torch.Size([128, 30])
"""
# pyrefly: ignore [bad-override]
_FLOAT_MODULE = nni.LinearReLU
def __init__(
self,
in_features: int,
out_features: int,
bias: bool = True,
qconfig: QConfigAny = None,
) -> None:
super().__init__(in_features, out_features, bias, qconfig)
def forward(self, input: torch.Tensor) -> torch.Tensor:
return F.relu(F.linear(input, self.weight_fake_quant(self.weight), self.bias))
@classmethod
def from_float(
cls,
mod: torch.nn.Module,
use_precomputed_fake_quant: bool = False,
) -> LinearReLU:
return super().from_float(mod, use_precomputed_fake_quant) # type: ignore[no-untyped-call,no-any-return]
def to_float(self) -> nni.LinearReLU:
linear = torch.nn.Linear(
self.in_features, self.out_features, self.bias is not None
)
linear.weight = torch.nn.Parameter(self.weight.detach())
if self.bias is not None:
linear.bias = torch.nn.Parameter(self.bias.detach())
relu = torch.nn.ReLU()
return torch.ao.nn.intrinsic.LinearReLU(linear, relu) # type: ignore[no-untyped-call]
@@ -0,0 +1,15 @@
from .modules import * # noqa: F403
__all__ = [
"BNReLU2d",
"BNReLU3d",
"ConvReLU1d",
"ConvReLU2d",
"ConvReLU3d",
"LinearReLU",
"LinearLeakyReLU",
"LinearTanh",
"ConvAdd2d",
"ConvAddReLU2d",
]
@@ -0,0 +1 @@
from .modules import * # noqa: F403
@@ -0,0 +1,6 @@
from .linear_relu import LinearReLU
__all__ = [
"LinearReLU",
]
@@ -0,0 +1,72 @@
from typing import Any
from typing_extensions import Self
import torch
import torch.ao.nn.intrinsic as nni
import torch.ao.nn.quantized.dynamic as nnqd
__all__ = ["LinearReLU"]
class LinearReLU(nnqd.Linear):
r"""
A LinearReLU module fused from Linear and ReLU modules that can be used
for dynamic quantization.
Supports both, FP16 and INT8 quantization.
We adopt the same interface as :class:`torch.ao.nn.quantized.dynamic.Linear`.
Attributes:
Same as torch.ao.nn.quantized.dynamic.Linear
Examples::
>>> # xdoctest: +SKIP
>>> m = nn.intrinsic.quantized.dynamic.LinearReLU(20, 30)
>>> input = torch.randn(128, 20)
>>> output = m(input)
>>> print(output.size())
torch.Size([128, 30])
"""
# pyrefly: ignore [bad-override]
_FLOAT_MODULE = nni.LinearReLU
def __init__(
self,
in_features: int,
out_features: int,
bias: bool = True,
dtype: torch.dtype = torch.qint8,
) -> None:
super().__init__(in_features, out_features, bias, dtype)
def forward(self, x: torch.Tensor) -> torch.Tensor:
if self._packed_params.dtype == torch.qint8:
# TODO check if we should set reduce_rage = True by default here
Y = torch.ops.quantized.linear_relu_dynamic(
x, self._packed_params._packed_params, reduce_range=True
)
elif self._packed_params.dtype == torch.float16:
Y = torch.ops.quantized.linear_relu_dynamic_fp16(
x, self._packed_params._packed_params
)
else:
raise RuntimeError("Unsupported dtype on dynamic quantized linear relu!")
return Y.to(x.dtype)
def _get_name(self) -> str:
return "DynamicQuantizedLinearReLU"
@classmethod
def from_float(
cls, mod: torch.nn.Module, use_precomputed_fake_quant: bool = False
) -> Self:
return super().from_float(
mod, use_precomputed_fake_quant=use_precomputed_fake_quant
)
@classmethod
def from_reference(cls, ref_qlinear_relu: Any) -> Self: # type: ignore[override]
return super().from_reference(ref_qlinear_relu[0])
@@ -0,0 +1,18 @@
from .bn_relu import BNReLU2d, BNReLU3d
from .conv_add import ConvAdd2d, ConvAddReLU2d
from .conv_relu import ConvReLU1d, ConvReLU2d, ConvReLU3d
from .linear_relu import LinearLeakyReLU, LinearReLU, LinearTanh
__all__ = [
"LinearReLU",
"ConvReLU1d",
"ConvReLU2d",
"ConvReLU3d",
"BNReLU2d",
"BNReLU3d",
"LinearLeakyReLU",
"LinearTanh",
"ConvAdd2d",
"ConvAddReLU2d",
]
@@ -0,0 +1,113 @@
# mypy: allow-untyped-defs
import torch
import torch.ao.nn.intrinsic
import torch.ao.nn.intrinsic.qat
import torch.ao.nn.quantized as nnq
__all__ = ["BNReLU2d", "BNReLU3d"]
class BNReLU2d(nnq.BatchNorm2d):
r"""
A BNReLU2d module is a fused module of BatchNorm2d and ReLU
We adopt the same interface as :class:`torch.ao.nn.quantized.BatchNorm2d`.
Attributes:
Same as torch.ao.nn.quantized.BatchNorm2d
"""
_FLOAT_MODULE = torch.ao.nn.intrinsic.BNReLU2d
def __init__(self, num_features, eps=1e-5, momentum=0.1, device=None, dtype=None):
super().__init__(
num_features, eps=eps, momentum=momentum, device=device, dtype=dtype
)
def forward(self, input):
r"""Applies fused BatchNorm2d and ReLU."""
# Temporarily using len(shape) instead of ndim due to JIT issue
# https://github.com/pytorch/pytorch/issues/23890
if len(input.shape) != 4:
raise ValueError("Input shape must be `(N, C, H, W)`!")
return torch.ops.quantized.batch_norm2d_relu(
input,
self.weight,
self.bias,
self.running_mean,
self.running_var,
self.eps,
self.scale,
self.zero_point,
)
def _get_name(self):
return "QuantizedBNReLU2d"
@classmethod
def from_float(cls, mod, use_precomputed_fake_quant=False): # type: ignore[override]
r"""Creates a quantized module from a float module."""
# TODO: Add qat support for BNReLU2d
return super().from_float(
mod, use_precomputed_fake_quant=use_precomputed_fake_quant
)
@classmethod
def from_reference(cls, bn_relu, output_scale, output_zero_point):
r"""Creates a quantized module from a reference module."""
return super().from_reference(bn_relu[0], output_scale, output_zero_point)
class BNReLU3d(nnq.BatchNorm3d):
r"""
A BNReLU3d module is a fused module of BatchNorm3d and ReLU
We adopt the same interface as :class:`torch.ao.nn.quantized.BatchNorm3d`.
Attributes:
Same as torch.ao.nn.quantized.BatchNorm3d
"""
_FLOAT_MODULE = torch.ao.nn.intrinsic.BNReLU3d
def __init__(self, num_features, eps=1e-5, momentum=0.1, device=None, dtype=None):
super().__init__(
num_features, eps=eps, momentum=momentum, device=device, dtype=dtype
)
def forward(self, input):
r"""Applies fused BatchNorm3d and ReLU."""
# Temporarily using len(shape) instead of ndim due to JIT issue
# https://github.com/pytorch/pytorch/issues/23890
if len(input.shape) != 5:
raise ValueError("Input shape must be `(N, C, D, H, W)`!")
return torch.ops.quantized.batch_norm3d_relu(
input,
self.weight,
self.bias,
self.running_mean,
self.running_var,
self.eps,
self.scale,
self.zero_point,
)
def _get_name(self):
return "QuantizedBNReLU3d"
@classmethod
def from_float(cls, mod, use_precomputed_fake_quant=False): # type: ignore[override]
r"""Creates a quantized module from a float module."""
# TODO: Add qat support for BNReLU3d
return super().from_float(
mod, use_precomputed_fake_quant=use_precomputed_fake_quant
)
@classmethod
def from_reference(cls, bn_relu, output_scale, output_zero_point):
r"""Creates a quantized module from a reference module."""
return super().from_reference(bn_relu[0], output_scale, output_zero_point)
@@ -0,0 +1,153 @@
# mypy: allow-untyped-defs
import torch
import torch.ao.nn.intrinsic
import torch.ao.nn.intrinsic.qat
import torch.ao.nn.quantized as nnq
import torch.nn.functional as F
_reverse_repeat_padding = nnq.modules.conv._reverse_repeat_padding
class ConvAdd2d(nnq.Conv2d):
r"""
A ConvAdd2d module is a fused module of Conv2d and Add
We adopt the same interface as :class:`torch.ao.nn.quantized.Conv2d`.
Attributes:
Same as torch.ao.nn.quantized.Conv2d
"""
_FLOAT_MODULE = torch.ao.nn.intrinsic.ConvAdd2d # type: ignore[assignment]
def __init__(
self,
in_channels,
out_channels,
kernel_size,
stride=1,
padding=0,
dilation=1,
groups=1,
bias=True,
padding_mode="zeros",
device=None,
dtype=None,
):
super().__init__(
in_channels,
out_channels,
kernel_size,
stride=stride,
padding=padding,
dilation=dilation,
groups=groups,
bias=bias,
padding_mode=padding_mode,
device=device,
dtype=dtype,
)
def forward(self, input, extra_input): # type: ignore[override]
r"""Applies fused quantized Conv2d and addition."""
# Temporarily using len(shape) instead of ndim due to JIT issue
# https://github.com/pytorch/pytorch/issues/23890
if len(input.shape) != 4:
raise ValueError("Input shape must be `(N, C, H, W)`!")
if self.padding_mode != "zeros":
_reversed_padding_repeated_twice = _reverse_repeat_padding(self.padding)
input = F.pad(
input, _reversed_padding_repeated_twice, mode=self.padding_mode
)
return torch.ops.quantized.conv2d_add(
input, extra_input, self._packed_params, self.scale, self.zero_point
)
def _get_name(self):
return "QuantizedConvAdd2d"
@classmethod
def from_float(cls, mod, use_precomputed_fake_quant=False): # type: ignore[override]
r"""Creates a quantized module from a float module."""
return super().from_float(
mod, use_precomputed_fake_quant=use_precomputed_fake_quant
)
@classmethod
def from_reference(cls, ref_qconv, output_scale, output_zero_point):
r"""Creates a quantized module from a reference module."""
return super().from_reference(ref_qconv[0], output_scale, output_zero_point)
class ConvAddReLU2d(nnq.Conv2d):
r"""
A ConvAddReLU2d module is a fused module of Conv2d, Add and Relu
We adopt the same interface as :class:`torch.ao.nn.quantized.Conv2d`.
Attributes:
Same as torch.ao.nn.quantized.Conv2d
"""
_FLOAT_MODULE = torch.ao.nn.intrinsic.ConvAddReLU2d # type: ignore[assignment]
def __init__(
self,
in_channels,
out_channels,
kernel_size,
stride=1,
padding=0,
dilation=1,
groups=1,
bias=True,
padding_mode="zeros",
device=None,
dtype=None,
):
super().__init__(
in_channels,
out_channels,
kernel_size,
stride=stride,
padding=padding,
dilation=dilation,
groups=groups,
bias=bias,
padding_mode=padding_mode,
device=device,
dtype=dtype,
)
def forward(self, input, extra_input): # type: ignore[override]
r"""Applies fused quantized Conv2d, addition, and ReLU."""
# Temporarily using len(shape) instead of ndim due to JIT issue
# https://github.com/pytorch/pytorch/issues/23890
if len(input.shape) != 4:
raise ValueError("Input shape must be `(N, C, H, W)`!")
if self.padding_mode != "zeros":
_reversed_padding_repeated_twice = _reverse_repeat_padding(self.padding)
input = F.pad(
input, _reversed_padding_repeated_twice, mode=self.padding_mode
)
return torch.ops.quantized.conv2d_add_relu(
input, extra_input, self._packed_params, self.scale, self.zero_point
)
def _get_name(self):
return "QuantizedConvAddReLU2d"
@classmethod
def from_float(cls, mod, use_precomputed_fake_quant=False): # type: ignore[override]
r"""Creates a quantized module from a float module."""
return super().from_float(
mod, use_precomputed_fake_quant=use_precomputed_fake_quant
)
@classmethod
def from_reference(cls, ref_qconv, output_scale, output_zero_point):
r"""Creates a quantized module from a reference module."""
return super().from_reference(ref_qconv[0], output_scale, output_zero_point)
@@ -0,0 +1,289 @@
# mypy: allow-untyped-defs
import torch
import torch.ao.nn.intrinsic
import torch.ao.nn.intrinsic.qat
import torch.ao.nn.quantized as nnq
import torch.nn.functional as F
from torch.nn.utils import fuse_conv_bn_weights
__all__ = [
"ConvReLU1d",
"ConvReLU2d",
"ConvReLU3d",
]
_reverse_repeat_padding = nnq.modules.conv._reverse_repeat_padding
# TODO: factor out the common parts to ConvNd
class ConvReLU1d(nnq.Conv1d):
r"""
A ConvReLU1d module is a fused module of Conv1d and ReLU
We adopt the same interface as :class:`torch.ao.nn.quantized.Conv1d`.
Attributes:
Same as torch.ao.nn.quantized.Conv1d
"""
_FLOAT_MODULE = torch.ao.nn.intrinsic.ConvReLU1d # type: ignore[assignment]
def __init__(
self,
in_channels,
out_channels,
kernel_size,
stride=1,
padding=0,
dilation=1,
groups=1,
bias=True,
padding_mode="zeros",
device=None,
dtype=None,
):
super().__init__(
in_channels,
out_channels,
kernel_size,
stride=stride,
padding=padding,
dilation=dilation,
groups=groups,
bias=bias,
# pyrefly: ignore [bad-argument-type]
padding_mode=padding_mode,
device=device,
dtype=dtype,
)
def forward(self, input):
r"""Applies fused quantized Conv1d and ReLU."""
# Temporarily using len(shape) instead of ndim due to JIT issue
# https://github.com/pytorch/pytorch/issues/23890
if len(input.shape) != 3:
raise ValueError("Input shape must be `(N, C, L)`!")
if self.padding_mode != "zeros":
# Padding in Conv1d is stored as (p, p), need to get (p,)
_reversed_padding_repeated_twice = _reverse_repeat_padding(self.padding[:1])
input = F.pad(
input, _reversed_padding_repeated_twice, mode=self.padding_mode
)
return torch.ops.quantized.conv1d_relu(
input, self._packed_params, self.scale, self.zero_point
)
def _get_name(self):
return "QuantizedConvReLU1d"
@classmethod
def from_float(cls, mod, use_precomputed_fake_quant=False): # type: ignore[override]
r"""Creates a quantized module from a float module."""
if type(mod) is torch.ao.nn.intrinsic.qat.ConvBnReLU1d:
if mod.bn.running_var is None or mod.bn.running_mean is None:
raise AssertionError(
"mod.bn.running_var and mod.bn.running_mean must not be None"
)
mod.weight, mod.bias = fuse_conv_bn_weights(
mod.weight,
mod.bias,
mod.bn.running_mean,
mod.bn.running_var,
mod.bn.eps,
mod.bn.weight,
mod.bn.bias,
)
return super().from_float(mod, use_precomputed_fake_quant)
@classmethod
def from_reference(cls, ref_qconv, output_scale, output_zero_point):
r"""Creates a quantized module from a reference module."""
if type(ref_qconv) is torch.ao.nn.intrinsic.ConvBnReLU1d:
raise AssertionError(
"BatchNorm1d should be fused into Conv1d before converting to reference module"
)
return super().from_reference(ref_qconv[0], output_scale, output_zero_point)
class ConvReLU2d(nnq.Conv2d):
r"""
A ConvReLU2d module is a fused module of Conv2d and ReLU
We adopt the same interface as :class:`torch.ao.nn.quantized.Conv2d`.
Attributes:
Same as torch.ao.nn.quantized.Conv2d
"""
_FLOAT_MODULE = torch.ao.nn.intrinsic.ConvReLU2d # type: ignore[assignment]
def __init__(
self,
in_channels,
out_channels,
kernel_size,
stride=1,
padding=0,
dilation=1,
groups=1,
bias=True,
padding_mode="zeros",
device=None,
dtype=None,
):
super().__init__(
in_channels,
out_channels,
kernel_size,
stride=stride,
padding=padding,
dilation=dilation,
groups=groups,
bias=bias,
padding_mode=padding_mode,
device=device,
dtype=dtype,
)
def forward(self, input):
r"""Applies fused quantized Conv2d and ReLU."""
# Temporarily using len(shape) instead of ndim due to JIT issue
# https://github.com/pytorch/pytorch/issues/23890
if len(input.shape) != 4:
raise ValueError("Input shape must be `(N, C, H, W)`!")
if self.padding_mode != "zeros":
_reversed_padding_repeated_twice = _reverse_repeat_padding(self.padding)
input = F.pad(
input, _reversed_padding_repeated_twice, mode=self.padding_mode
)
return torch.ops.quantized.conv2d_relu(
input, self._packed_params, self.scale, self.zero_point
)
def _get_name(self):
return "QuantizedConvReLU2d"
@classmethod
def from_float(cls, mod, use_precomputed_fake_quant=False): # type: ignore[override]
r"""Creates a quantized module from a float module."""
if type(mod) is torch.ao.nn.intrinsic.qat.ConvBnReLU2d:
if mod.bn.running_var is None or mod.bn.running_mean is None:
raise AssertionError(
"mod.bn.running_var and mod.bn.running_mean must not be None"
)
mod.weight, mod.bias = fuse_conv_bn_weights(
mod.weight,
mod.bias,
mod.bn.running_mean,
mod.bn.running_var,
mod.bn.eps,
mod.bn.weight,
mod.bn.bias,
)
return super().from_float(
mod, use_precomputed_fake_quant=use_precomputed_fake_quant
)
@classmethod
def from_reference(cls, ref_qconv, output_scale, output_zero_point):
r"""Creates a quantized module from a reference module."""
if type(ref_qconv) is torch.ao.nn.intrinsic.ConvBnReLU2d:
raise AssertionError(
"BatchNorm2d should be fused into Conv2d before converting to reference module"
)
return super().from_reference(ref_qconv[0], output_scale, output_zero_point)
class ConvReLU3d(nnq.Conv3d):
r"""
A ConvReLU3d module is a fused module of Conv3d and ReLU
We adopt the same interface as :class:`torch.ao.nn.quantized.Conv3d`.
Attributes: Same as torch.ao.nn.quantized.Conv3d
"""
_FLOAT_MODULE = torch.ao.nn.intrinsic.ConvReLU3d # type: ignore[assignment]
def __init__(
self,
in_channels,
out_channels,
kernel_size,
stride=1,
padding=0,
dilation=1,
groups=1,
bias=True,
padding_mode="zeros",
device=None,
dtype=None,
):
if padding_mode == "reflect":
raise AssertionError("Conv3d does not support reflection padding")
super().__init__(
in_channels,
out_channels,
kernel_size,
stride=stride,
padding=padding,
dilation=dilation,
groups=groups,
bias=bias,
padding_mode=padding_mode,
device=device,
dtype=dtype,
)
def forward(self, input):
r"""Applies fused quantized Conv3d and ReLU."""
# Temporarily using len(shape) instead of ndim due to JIT issue
# https://github.com/pytorch/pytorch/issues/23890
if len(input.shape) != 5:
raise ValueError("Input shape must be `(N, C, D, H, W)`!")
if self.padding_mode != "zeros":
_reversed_padding_repeated_twice = _reverse_repeat_padding(self.padding)
input = F.pad(
input, _reversed_padding_repeated_twice, mode=self.padding_mode
)
return torch.ops.quantized.conv3d_relu(
input, self._packed_params, self.scale, self.zero_point
)
def _get_name(self):
return "QuantizedConvReLU3d"
@classmethod
def from_float(cls, mod, use_precomputed_fake_quant=False): # type: ignore[override]
r"""Creates a quantized module from a float module."""
if type(mod) is torch.ao.nn.intrinsic.qat.ConvBnReLU3d:
if mod.bn.running_var is None or mod.bn.running_mean is None:
raise AssertionError(
"mod.bn.running_var and mod.bn.running_mean must not be None"
)
mod.weight, mod.bias = fuse_conv_bn_weights(
mod.weight,
mod.bias,
mod.bn.running_mean,
mod.bn.running_var,
mod.bn.eps,
mod.bn.weight,
mod.bn.bias,
)
return super().from_float(
mod, use_precomputed_fake_quant=use_precomputed_fake_quant
)
@classmethod
def from_reference(cls, ref_qconv, output_scale, output_zero_point):
r"""Creates a quantized module from a reference module."""
if type(ref_qconv) is torch.ao.nn.intrinsic.ConvBnReLU3d:
raise AssertionError(
"BatchNorm3d should be fused into Conv3d before converting to reference module"
)
return super().from_reference(ref_qconv[0], output_scale, output_zero_point)
@@ -0,0 +1,198 @@
# mypy: allow-untyped-defs
import torch
import torch.ao.nn.intrinsic as nni
import torch.ao.nn.quantized as nnq
from torch.ao.nn.quantized.modules.utils import _quantize_weight
__all__ = [
"LinearReLU",
"LinearLeakyReLU",
"LinearTanh",
]
class LinearReLU(nnq.Linear):
r"""
A LinearReLU module fused from Linear and ReLU modules
We adopt the same interface as :class:`torch.ao.nn.quantized.Linear`.
Attributes:
Same as torch.ao.nn.quantized.Linear
Examples::
>>> # xdoctest: +SKIP
>>> m = nn.intrinsic.LinearReLU(20, 30)
>>> input = torch.randn(128, 20)
>>> output = m(input)
>>> print(output.size())
torch.Size([128, 30])
"""
_FLOAT_MODULE = nni.LinearReLU # type: ignore[assignment]
def __init__(self, in_features, out_features, bias=True, dtype=torch.qint8):
super().__init__(in_features, out_features, bias, dtype)
def forward(self, x: torch.Tensor) -> torch.Tensor:
return torch.ops.quantized.linear_relu(
x, self._packed_params._packed_params, self.scale, self.zero_point
)
def _get_name(self):
return "QuantizedLinearReLU"
@classmethod
def from_float(cls, mod, use_precomputed_fake_quant=False):
return super().from_float(mod, use_precomputed_fake_quant)
@classmethod
def from_reference(cls, ref_linear_relu, output_scale, output_zero_point):
return super().from_reference(
ref_linear_relu[0], output_scale, output_zero_point
)
class LinearLeakyReLU(nnq.Linear):
r"""
For onednn backend only
A LinearLeakyReLU module fused from Linear and LeakyReLU modules
We adopt the same interface as :class:`torch.ao.nn.quantized.Linear`.
Attributes:
Same as torch.ao.nn.quantized.Linear
+ negative_slope
Examples::
>>> # xdoctest: +SKIP
>>> m = nn.intrinsic.LinearLeakyReLU(20, 30, 0.01)
>>> input = torch.randn(128, 20)
>>> output = m(input)
>>> print(output.size())
torch.Size([128, 30])
"""
_FLOAT_MODULE = nni.LinearLeakyReLU # type: ignore[assignment]
def __init__(
self, in_features, out_features, negative_slope, bias=True, dtype=torch.qint8
):
super().__init__(in_features, out_features, bias, dtype)
self.negative_slope = negative_slope
def forward(self, x: torch.Tensor) -> torch.Tensor:
return torch.ops.quantized.linear_leaky_relu(
x,
self._packed_params._packed_params,
self.scale,
self.zero_point,
self.negative_slope,
)
def _get_name(self):
return "QuantizedLinearLeakyReLU"
@classmethod
def from_float(cls, mod, use_precomputed_fake_quant=False):
if type(mod) is not nni.LinearLeakyReLU:
raise AssertionError("Input float module should be LinearLeakyReLU")
if not hasattr(mod, "qconfig"):
raise AssertionError("Input float module must have qconfig defined")
activation_post_process = mod.activation_post_process
leaky_relu = mod[1]
mod = mod[0]
weight_post_process = mod.qconfig.weight() # type: ignore[union-attr, operator]
weight_post_process(mod.weight)
dtype = weight_post_process.dtype
act_scale, act_zp = activation_post_process.calculate_qparams() # type: ignore[union-attr,operator]
if dtype != torch.qint8:
raise AssertionError(
f"Weight observer must have dtype torch.qint8, got {dtype}"
)
qweight = _quantize_weight(mod.weight.float(), weight_post_process)
qlinear_leaky_relu = cls(
mod.in_features, mod.out_features, leaky_relu.negative_slope, dtype=dtype
)
qlinear_leaky_relu.set_weight_bias(qweight, mod.bias) # type: ignore[arg-type]
qlinear_leaky_relu.scale = float(act_scale)
qlinear_leaky_relu.zero_point = int(act_zp)
return qlinear_leaky_relu
@classmethod
def from_reference(cls, ref_mod, output_scale, output_zero_point):
linear = ref_mod[0]
leaky_relu = ref_mod[1]
qlinear_leaky_relu = cls(
linear.in_features, linear.out_features, leaky_relu.negative_slope
)
qweight = linear.get_quantized_weight()
qlinear_leaky_relu.set_weight_bias(qweight, linear.bias)
qlinear_leaky_relu.scale = float(output_scale)
qlinear_leaky_relu.zero_point = int(output_zero_point)
return qlinear_leaky_relu
class LinearTanh(nnq.Linear):
r"""
A LinearTanh module fused from Linear and Tanh modules
We adopt the same interface as :class:`torch.ao.nn.quantized.Linear`.
Attributes:
Same as torch.ao.nn.quantized.Linear
Examples::
>>> # xdoctest: +SKIP
>>> m = nn.intrinsic.LinearTanh(20, 30)
>>> input = torch.randn(128, 20)
>>> output = m(input)
>>> print(output.size())
torch.Size([128, 30])
"""
_FLOAT_MODULE = nni.LinearTanh # type: ignore[assignment]
def __init__(self, in_features, out_features, bias=True, dtype=torch.qint8):
super().__init__(in_features, out_features, bias, dtype)
def forward(self, x: torch.Tensor) -> torch.Tensor:
return torch.ops.quantized.linear_tanh(
x, self._packed_params._packed_params, self.scale, self.zero_point
)
def _get_name(self):
return "QuantizedLinearTanh"
@classmethod
def from_float(cls, mod, use_precomputed_fake_quant=False):
if type(mod) is not nni.LinearTanh:
raise AssertionError("Input float module should be LinearTanh")
if not hasattr(mod, "qconfig"):
raise AssertionError("Input float module must have qconfig defined")
activation_post_process = mod.activation_post_process
mod = mod[0]
weight_post_process = mod.qconfig.weight() # type: ignore[union-attr,operator]
weight_post_process(mod.weight)
dtype = weight_post_process.dtype
act_scale, act_zp = activation_post_process.calculate_qparams() # type: ignore[union-attr,operator]
if dtype != torch.qint8:
raise AssertionError(
f"Weight observer must have dtype torch.qint8, got {dtype}"
)
qweight = _quantize_weight(mod.weight.float(), weight_post_process)
qlinear_tanh = cls(mod.in_features, mod.out_features, dtype=dtype)
qlinear_tanh.set_weight_bias(qweight, mod.bias) # type: ignore[arg-type]
qlinear_tanh.scale = float(act_scale)
qlinear_tanh.zero_point = int(act_zp)
return qlinear_tanh
@classmethod
def from_reference(cls, ref_mod, output_scale, output_zero_point):
linear = ref_mod[0]
qlinear_tanh = cls(linear.in_features, linear.out_features)
qweight = linear.get_quantized_weight()
qlinear_tanh.set_weight_bias(qweight, linear.bias)
qlinear_tanh.scale = float(output_scale)
qlinear_tanh.zero_point = int(output_zero_point)
return qlinear_tanh
@@ -0,0 +1 @@
from .modules import * # noqa: F403
@@ -0,0 +1 @@
from .modules import * # noqa: F403
@@ -0,0 +1,4 @@
from .linear import Linear
__all__ = ["Linear"]
@@ -0,0 +1,40 @@
from typing import Optional, TYPE_CHECKING
import torch
if TYPE_CHECKING:
from torch.ao.quantization.qconfig import QConfig
__all__ = ["Linear"]
class Linear(torch.ao.nn.qat.Linear):
r"""
A linear module attached with FakeQuantize modules for weight,
used for dynamic quantization aware training.
We adopt the same interface as `torch.nn.Linear`, please see
https://pytorch.org/docs/stable/nn.html#torch.nn.Linear
for documentation.
Similar to `torch.nn.Linear`, with FakeQuantize modules initialized to
default.
"""
def __init__(
self,
in_features: int,
out_features: int,
bias: bool = True,
qconfig: Optional["QConfig"] = None,
device: int | str | torch.device | None = None,
dtype: str | None = None,
) -> None:
super().__init__(in_features, out_features, bias, qconfig, device, dtype)
if not torch.ao.quantization.qconfig._activation_is_memoryless(qconfig): # type: ignore[arg-type]
raise ValueError(
"Dynamic QAT requires a memoryless observer."
+ "This means a MovingAverage observer with averaging constant equal to 1"
)
@@ -0,0 +1,13 @@
from .conv import Conv1d, Conv2d, Conv3d
from .embedding_ops import Embedding, EmbeddingBag
from .linear import Linear
__all__ = [
"Linear",
"Conv1d",
"Conv2d",
"Conv3d",
"Embedding",
"EmbeddingBag",
]
@@ -0,0 +1,318 @@
# mypy: allow-untyped-defs
from typing import ClassVar, Literal
import torch
import torch.nn as nn
from torch.ao.nn.intrinsic import _FusedModule
from torch.nn.common_types import _size_1_t, _size_2_t, _size_3_t
from torch.nn.modules.utils import _pair, _single, _triple
__all__ = ["Conv1d", "Conv2d", "Conv3d"]
class _ConvNd(nn.modules.conv._ConvNd):
_FLOAT_MODULE: ClassVar[type[nn.modules.conv._ConvNd]]
def __init__(
self,
in_channels: int,
out_channels: int,
kernel_size: tuple[int, ...],
stride: tuple[int, ...],
padding: str | tuple[int, ...],
dilation: tuple[int, ...],
transposed: bool,
output_padding: tuple[int, ...],
groups: int,
bias: bool,
padding_mode: Literal["zeros", "reflect", "replicate", "circular"],
qconfig=None,
device=None,
dtype=None,
) -> None:
factory_kwargs = {"device": device, "dtype": dtype}
nn.modules.conv._ConvNd.__init__(
self,
in_channels,
out_channels,
kernel_size,
stride,
padding,
dilation,
transposed,
output_padding,
groups,
bias,
padding_mode,
**factory_kwargs,
)
if not qconfig:
raise AssertionError("qconfig must be provided for QAT module")
self.qconfig = qconfig
self.weight_fake_quant = qconfig.weight(factory_kwargs=factory_kwargs)
def forward(self, input):
return self._conv_forward(input, self.weight_fake_quant(self.weight), self.bias)
@staticmethod
def from_float(cls, mod, use_precomputed_fake_quant=False):
r"""Create a qat module from a float module
Args:
`mod`: a float module, either produced by torch.ao.quantization utilities
or directly from user
"""
if type(mod) is not cls._FLOAT_MODULE:
raise AssertionError(
f"qat.{cls.__name__}.from_float only works for "
f"{cls._FLOAT_MODULE.__name__}, got {type(mod).__name__}"
)
if not hasattr(mod, "qconfig"):
raise AssertionError("Input float module must have qconfig defined")
if not mod.qconfig:
raise AssertionError("Input float module must have a valid qconfig")
if issubclass(type(mod), _FusedModule):
mod = mod[0]
qconfig = mod.qconfig
qat_conv = cls(
mod.in_channels,
mod.out_channels,
mod.kernel_size,
stride=mod.stride,
padding=mod.padding,
dilation=mod.dilation,
groups=mod.groups,
bias=mod.bias is not None,
padding_mode=mod.padding_mode,
qconfig=qconfig,
)
qat_conv.weight = mod.weight
qat_conv.bias = mod.bias
return qat_conv
def to_float(self):
"""This works for both single qat conv, and the qat conv - relu modules
to convert the qat module to a floating point module
"""
cls = type(self)
conv = cls._FLOAT_CONV_MODULE( # type: ignore[attr-defined]
self.in_channels,
self.out_channels,
self.kernel_size,
self.stride,
self.padding,
self.dilation,
self.groups,
self.bias is not None,
self.padding_mode,
)
conv.weight = torch.nn.Parameter(self.weight.detach())
if self.bias is not None:
conv.bias = torch.nn.Parameter(self.bias.detach())
# conv relu
if issubclass(cls, _FusedModule):
modules = [conv]
if not hasattr(cls, "_FLOAT_RELU_MODULE"):
raise AssertionError(
f"{cls.__name__} must have _FLOAT_RELU_MODULE attribute"
)
relu = cls._FLOAT_RELU_MODULE()
modules.append(relu)
# pyrefly: ignore [missing-attribute]
fused = cls._FLOAT_MODULE(*modules)
fused.train(self.training)
return fused
else:
return conv
class Conv1d(_ConvNd, nn.Conv1d):
r"""
A Conv1d module attached with FakeQuantize modules for weight,
used for quantization aware training.
We adopt the same interface as :class:`~torch.nn.Conv1d`
Similar to :class:`~torch.nn.Conv2d`, with FakeQuantize modules initialized to
default.
Attributes:
weight_fake_quant: fake quant module for weight
"""
_FLOAT_MODULE: ClassVar[type[nn.Conv1d]] = nn.Conv1d
_FLOAT_CONV_MODULE: ClassVar[type[nn.Conv1d]] = nn.Conv1d
def __init__(
self,
in_channels: int,
out_channels: int,
kernel_size: _size_1_t,
stride: _size_1_t = 1,
padding: str | _size_1_t = 0,
dilation: _size_1_t = 1,
groups: int = 1,
bias: bool = True,
padding_mode: Literal["zeros", "reflect", "replicate", "circular"] = "zeros",
qconfig=None,
device=None,
dtype=None,
) -> None:
kernel_size_ = _single(kernel_size)
stride_ = _single(stride)
padding_ = padding if isinstance(padding, str) else _single(padding)
dilation_ = _single(dilation)
super().__init__(
in_channels,
out_channels,
kernel_size_,
stride=stride_,
padding=padding_,
dilation=dilation_,
transposed=False,
output_padding=_single(0),
groups=groups,
bias=bias,
padding_mode=padding_mode,
qconfig=qconfig,
device=device,
dtype=dtype,
)
@classmethod
def from_float(cls, mod, use_precomputed_fake_quant=False): # type: ignore[override]
return super().from_float(
cls, mod, use_precomputed_fake_quant=use_precomputed_fake_quant
)
class Conv2d(_ConvNd, nn.Conv2d):
r"""
A Conv2d module attached with FakeQuantize modules for weight,
used for quantization aware training.
We adopt the same interface as `torch.nn.Conv2d`, please see
https://pytorch.org/docs/stable/nn.html?highlight=conv2d#torch.nn.Conv2d
for documentation.
Similar to `torch.nn.Conv2d`, with FakeQuantize modules initialized to
default.
Attributes:
weight_fake_quant: fake quant module for weight
"""
_FLOAT_MODULE: ClassVar[type[nn.Conv2d]] = nn.Conv2d
_FLOAT_CONV_MODULE: ClassVar[type[nn.Conv2d]] = nn.Conv2d
def __init__(
self,
in_channels: int,
out_channels: int,
kernel_size: _size_2_t,
stride: _size_2_t = 1,
padding: str | _size_2_t = 0,
dilation: _size_2_t = 1,
groups: int = 1,
bias: bool = True,
padding_mode: Literal["zeros", "reflect", "replicate", "circular"] = "zeros",
qconfig=None,
device=None,
dtype=None,
) -> None:
kernel_size_ = _pair(kernel_size)
stride_ = _pair(stride)
padding_ = padding if isinstance(padding, str) else _pair(padding)
dilation_ = _pair(dilation)
super().__init__(
in_channels,
out_channels,
kernel_size_,
stride=stride_,
padding=padding_,
dilation=dilation_,
transposed=False,
output_padding=_pair(0),
groups=groups,
bias=bias,
padding_mode=padding_mode,
qconfig=qconfig,
device=device,
dtype=dtype,
)
def forward(self, input):
return self._conv_forward(input, self.weight_fake_quant(self.weight), self.bias)
@classmethod
def from_float(cls, mod, use_precomputed_fake_quant=False): # type: ignore[override]
return super().from_float(
cls, mod, use_precomputed_fake_quant=use_precomputed_fake_quant
)
class Conv3d(_ConvNd, nn.Conv3d):
r"""
A Conv3d module attached with FakeQuantize modules for weight,
used for quantization aware training.
We adopt the same interface as `torch.nn.Conv3d`, please see
https://pytorch.org/docs/stable/nn.html?highlight=conv3d#torch.nn.Conv3d
for documentation.
Similar to `torch.nn.Conv3d`, with FakeQuantize modules initialized to
default.
Attributes:
weight_fake_quant: fake quant module for weight
"""
_FLOAT_MODULE: ClassVar[type[nn.Conv3d]] = nn.Conv3d
_FLOAT_CONV_MODULE: ClassVar[type[nn.Conv3d]] = nn.Conv3d
def __init__(
self,
in_channels: int,
out_channels: int,
kernel_size: _size_3_t,
stride: _size_3_t = 1,
padding: str | _size_3_t = 0,
dilation: _size_3_t = 1,
groups: int = 1,
bias: bool = True,
padding_mode: Literal["zeros", "reflect", "replicate", "circular"] = "zeros",
qconfig=None,
device=None,
dtype=None,
) -> None:
kernel_size_ = _triple(kernel_size)
stride_ = _triple(stride)
padding_ = padding if isinstance(padding, str) else _triple(padding)
dilation_ = _triple(dilation)
super().__init__(
in_channels,
out_channels,
kernel_size_,
stride=stride_,
padding=padding_,
dilation=dilation_,
transposed=False,
output_padding=_triple(0),
groups=groups,
bias=bias,
padding_mode=padding_mode,
qconfig=qconfig,
device=device,
dtype=dtype,
)
def forward(self, input):
return self._conv_forward(input, self.weight_fake_quant(self.weight), self.bias)
@classmethod
def from_float(cls, mod, use_precomputed_fake_quant=False): # type: ignore[override]
return super().from_float(
cls, mod, use_precomputed_fake_quant=use_precomputed_fake_quant
)
@@ -0,0 +1,264 @@
# mypy: allow-untyped-defs
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch import Tensor
__all__ = ["Embedding", "EmbeddingBag"]
class Embedding(nn.Embedding):
r"""
An embedding bag module attached with FakeQuantize modules for weight,
used for quantization aware training.
We adopt the same interface as `torch.nn.Embedding`, please see
https://pytorch.org/docs/stable/generated/torch.nn.Embedding.html#torch.nn.Embedding
for documentation.
Similar to `torch.nn.Embedding`, with FakeQuantize modules initialized to
default.
Attributes:
weight: fake quant module for weight
"""
_FLOAT_MODULE = nn.Embedding
def __init__(
self,
num_embeddings,
embedding_dim,
padding_idx=None,
max_norm=None,
norm_type=2.0,
scale_grad_by_freq=False,
sparse=False,
_weight=None,
device=None,
dtype=None,
qconfig=None,
) -> None:
factory_kwargs = {"device": device, "dtype": dtype}
super().__init__(
num_embeddings,
embedding_dim,
padding_idx,
max_norm,
norm_type,
scale_grad_by_freq,
sparse,
_weight,
**factory_kwargs,
)
if not qconfig:
raise AssertionError("qconfig must be provided for QAT module")
weight_qscheme = qconfig.weight().qscheme
if weight_qscheme != torch.per_channel_affine_float_qparams:
raise AssertionError(
"Embedding weights requires a qscheme of torch.per_channel_affine_float_qparams Got "
+ str(weight_qscheme)
)
self.qconfig = qconfig
self.weight_fake_quant = qconfig.weight(factory_kwargs=factory_kwargs)
def forward(self, input) -> Tensor:
return F.embedding(
input,
self.weight_fake_quant(self.weight),
self.padding_idx,
self.max_norm,
self.norm_type,
self.scale_grad_by_freq,
self.sparse,
)
@classmethod
def from_float(cls, mod, use_precomputed_fake_quant=False):
r"""Create a qat module from a float module
Args: `mod` a float module, either produced by torch.ao.quantization utilities
or directly from user
"""
if type(mod) is not cls._FLOAT_MODULE:
raise AssertionError(
" qat."
+ cls.__name__
+ ".from_float only works for "
+ cls._FLOAT_MODULE.__name__
)
if not hasattr(mod, "qconfig"):
raise AssertionError("Input float module must have qconfig defined")
if not mod.qconfig:
raise AssertionError("Input float module must have a valid qconfig")
weight_qscheme = mod.qconfig.weight().qscheme # type: ignore[union-attr, operator]
if weight_qscheme != torch.per_channel_affine_float_qparams:
raise AssertionError(
"Embedding weights requires a qscheme of torch.per_channel_affine_float_qparams Got "
+ str(weight_qscheme)
)
qconfig = mod.qconfig
qat_embedding_bag = cls(
mod.num_embeddings,
mod.embedding_dim,
mod.padding_idx,
mod.max_norm,
mod.norm_type,
mod.scale_grad_by_freq,
mod.sparse,
mod.weight,
qconfig=qconfig,
)
return qat_embedding_bag
def to_float(self):
embedding_bag = torch.nn.Embedding(
self.num_embeddings,
self.embedding_dim,
self.padding_idx,
self.max_norm,
self.norm_type,
self.scale_grad_by_freq,
self.sparse,
None,
)
embedding_bag.weight = torch.nn.Parameter(self.weight.detach())
embedding_bag.train(self.training)
return embedding_bag
class EmbeddingBag(nn.EmbeddingBag):
r"""
An embedding bag module attached with FakeQuantize modules for weight,
used for quantization aware training.
We adopt the same interface as `torch.nn.EmbeddingBag`, please see
https://pytorch.org/docs/stable/generated/torch.nn.EmbeddingBag.html#torch.nn.EmbeddingBag
for documentation.
Similar to `torch.nn.EmbeddingBag`, with FakeQuantize modules initialized to
default.
Attributes:
weight: fake quant module for weight
"""
_FLOAT_MODULE = nn.EmbeddingBag
def __init__(
self,
num_embeddings,
embedding_dim,
max_norm=None,
norm_type=2.0,
scale_grad_by_freq=False,
mode="mean",
sparse=False,
_weight=None,
include_last_offset=False,
padding_idx=None,
qconfig=None,
device=None,
dtype=None,
) -> None:
factory_kwargs = {"device": device, "dtype": dtype}
super().__init__(
num_embeddings,
embedding_dim,
max_norm,
norm_type,
scale_grad_by_freq,
mode,
sparse,
_weight,
include_last_offset,
padding_idx,
**factory_kwargs,
)
if not qconfig:
raise AssertionError("qconfig must be provided for QAT module")
weight_qscheme = qconfig.weight().qscheme
if weight_qscheme != torch.per_channel_affine_float_qparams:
raise AssertionError(
"Embedding Bag weights requires a qscheme of torch.per_channel_affine_float_qparams Got "
+ str(weight_qscheme)
)
self.qconfig = qconfig
self.weight_fake_quant = qconfig.weight(factory_kwargs=factory_kwargs)
def forward(self, input, offsets=None, per_sample_weights=None) -> Tensor:
return F.embedding_bag(
input,
self.weight_fake_quant(self.weight),
offsets,
self.max_norm,
self.norm_type,
self.scale_grad_by_freq,
self.mode,
self.sparse,
per_sample_weights,
self.include_last_offset,
self.padding_idx,
)
@classmethod
def from_float(cls, mod, use_precomputed_fake_quant=False):
r"""Create a qat module from a float module
Args: `mod` a float module, either produced by torch.ao.quantization utilities
or directly from user
"""
if type(mod) is not cls._FLOAT_MODULE:
raise AssertionError(
" qat."
+ cls.__name__
+ ".from_float only works for "
+ cls._FLOAT_MODULE.__name__
)
if not hasattr(mod, "qconfig"):
raise AssertionError("Input float module must have qconfig defined")
if not mod.qconfig:
raise AssertionError("Input float module must have a valid qconfig")
weight_qscheme = mod.qconfig.weight().qscheme # type: ignore[union-attr, operator]
if weight_qscheme != torch.per_channel_affine_float_qparams:
raise AssertionError(
"Embedding Bag weights requires a qscheme of torch.per_channel_affine_float_qparams Got "
+ str(weight_qscheme)
)
qconfig = mod.qconfig
qat_embedding_bag = cls(
mod.num_embeddings,
mod.embedding_dim,
mod.max_norm,
mod.norm_type,
mod.scale_grad_by_freq,
mod.mode,
mod.sparse,
mod.weight,
mod.include_last_offset,
mod.padding_idx,
qconfig=qconfig,
)
return qat_embedding_bag
def to_float(self):
embedding_bag = torch.nn.EmbeddingBag(
self.num_embeddings,
self.embedding_dim,
self.max_norm,
self.norm_type,
self.scale_grad_by_freq,
self.mode,
self.sparse,
None,
self.include_last_offset,
self.padding_idx,
)
embedding_bag.weight = torch.nn.Parameter(self.weight.detach())
embedding_bag.train(self.training)
return embedding_bag
@@ -0,0 +1,99 @@
# mypy: allow-untyped-defs
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.ao.nn.intrinsic import LinearReLU
from torch.nn.utils.parametrize import (
is_parametrized,
transfer_parametrizations_and_params,
type_before_parametrizations,
)
__all__ = ["Linear"]
class Linear(nn.Linear):
r"""
A linear module attached with FakeQuantize modules for weight,
used for quantization aware training.
We adopt the same interface as `torch.nn.Linear`, please see
https://pytorch.org/docs/stable/nn.html#torch.nn.Linear
for documentation.
Similar to `torch.nn.Linear`, with FakeQuantize modules initialized to
default.
Attributes:
weight: fake quant module for weight
"""
_FLOAT_MODULE = nn.Linear
def __init__(
self,
in_features,
out_features,
bias=True,
qconfig=None,
device=None,
dtype=None,
) -> None:
factory_kwargs = {"device": device, "dtype": dtype}
super().__init__(in_features, out_features, bias, **factory_kwargs)
if not qconfig:
raise AssertionError("qconfig must be provided for QAT module")
self.qconfig = qconfig
self.weight_fake_quant = qconfig.weight(factory_kwargs=factory_kwargs)
def forward(self, input):
return F.linear(input, self.weight_fake_quant(self.weight), self.bias)
@classmethod
def from_float(cls, mod, use_precomputed_fake_quant=False):
r"""Create a qat module from a float module or qparams_dict
Args: `mod` a float module, either produced by torch.ao.quantization utilities
or directly from user
"""
if type_before_parametrizations(mod) != cls._FLOAT_MODULE:
raise AssertionError(
f"qat.{cls.__name__}.from_float only works for "
f"{cls._FLOAT_MODULE.__name__}, got {type_before_parametrizations(mod).__name__}"
)
if not hasattr(mod, "qconfig"):
raise AssertionError("Input float module must have qconfig defined")
if not mod.qconfig:
raise AssertionError("Input float module must have a valid qconfig")
if type_before_parametrizations(mod) == LinearReLU:
mod = mod[0]
qconfig = mod.qconfig
qat_linear = cls(
mod.in_features,
mod.out_features,
bias=mod.bias is not None,
qconfig=qconfig,
)
if is_parametrized(mod, "weight"):
transfer_parametrizations_and_params(mod, qat_linear, "weight")
else:
qat_linear.weight = mod.weight
if is_parametrized(mod, "bias"):
transfer_parametrizations_and_params(mod, qat_linear, "bias")
else:
qat_linear.bias = mod.bias
return qat_linear
def to_float(self):
linear = torch.nn.Linear(
self.in_features, self.out_features, self.bias is not None
)
linear.weight = torch.nn.Parameter(self.weight.detach())
if self.bias is not None:
linear.bias = torch.nn.Parameter(self.bias.detach())
linear.train(self.training)
return linear
@@ -0,0 +1 @@
from .modules import * # noqa: F403
@@ -0,0 +1,9 @@
from .activation import MultiheadAttention
from .rnn import LSTM, LSTMCell
__all__ = [
"LSTM",
"LSTMCell",
"MultiheadAttention",
]
@@ -0,0 +1,623 @@
# mypy: allow-untyped-defs
import warnings
import torch
import torch.jit # this is needed to avoid a circular import
import torch.nn.functional as F
from torch import nn, Tensor
__all__ = ["MultiheadAttention"]
class MultiheadAttention(nn.MultiheadAttention):
_FLOAT_MODULE = nn.MultiheadAttention
r"""Quantizable implementation of the MultiheadAttention.
Note::
Please, refer to :class:`~torch.nn.MultiheadAttention` for more
information
Allows the model to jointly attend to information from different
representation subspaces.
See reference: Attention Is All You Need
The original MHA module is not quantizable.
This reimplements it by explicitly instantiating the linear layers.
.. math::
\text{MultiHead}(Q, K, V) = \text{Concat}(head_1,\dots,head_h)W^O
\text{where} head_i = \text{Attention}(QW_i^Q, KW_i^K, VW_i^V)
Args:
embed_dim: total dimension of the model.
num_heads: parallel attention heads.
dropout: a Dropout layer on attn_output_weights. Default: 0.0.
bias: add bias as module parameter. Default: True.
add_bias_kv: add bias to the key and value sequences at dim=0.
add_zero_attn: add a new batch of zeros to the key and
value sequences at dim=1.
kdim: total number of features in key. Default: None.
vdim: total number of features in value. Default: None.
batch_first: If ``True``, then the input and output tensors are provided
as (batch, seq, feature). Default: ``False`` (seq, batch, feature).
Note that if :attr:`kdim` and :attr:`vdim` are None, they will be set
to :attr:`embed_dim` such that query, key, and value have the same
number of features.
Examples::
>>> import torch.ao.nn.quantizable as nnqa
>>> multihead_attn = nnqa.MultiheadAttention(embed_dim, num_heads)
>>> attn_output, attn_output_weights = multihead_attn(query, key, value)
Note::
Please, follow the quantization flow to convert the quantizable MHA.
"""
__constants__ = ["batch_first"]
def __init__(
self,
embed_dim: int,
num_heads: int,
dropout: float = 0.0,
bias: bool = True,
add_bias_kv: bool = False,
add_zero_attn: bool = False,
kdim: int | None = None,
vdim: int | None = None,
batch_first: bool = False,
device=None,
dtype=None,
) -> None:
factory_kwargs = {"device": device, "dtype": dtype}
super().__init__(
embed_dim,
num_heads,
dropout,
bias,
add_bias_kv,
add_zero_attn,
kdim,
vdim,
batch_first,
**factory_kwargs,
)
self.linear_Q = nn.Linear(
self.embed_dim, self.embed_dim, bias=bias, **factory_kwargs
)
self.linear_K = nn.Linear(
self.kdim, self.embed_dim, bias=bias, **factory_kwargs
)
self.linear_V = nn.Linear(
self.vdim, self.embed_dim, bias=bias, **factory_kwargs
)
# for the type: ignore, see https://github.com/pytorch/pytorch/issues/58969
# pyrefly: ignore [bad-assignment]
self.out_proj = nn.Linear(
self.embed_dim, self.embed_dim, bias=bias, **factory_kwargs
) # type: ignore[assignment]
# Functionals
self.q_scaling_product = torch.ao.nn.quantized.FloatFunctional()
# note: importing torch.ao.nn.quantized at top creates a circular import
# Quant/Dequant
self.quant_attn_output = torch.ao.quantization.QuantStub()
self.quant_attn_output_weights = torch.ao.quantization.QuantStub()
self.dequant_q = torch.ao.quantization.DeQuantStub()
self.dequant_k = torch.ao.quantization.DeQuantStub()
self.dequant_v = torch.ao.quantization.DeQuantStub()
def _get_name(self):
return "QuantizableMultiheadAttention"
@classmethod
def from_float(cls, other):
if type(other) is not cls._FLOAT_MODULE:
raise AssertionError(
f"Expected type {cls._FLOAT_MODULE}, got {type(other)}"
)
if not hasattr(other, "qconfig"):
raise AssertionError("The float module must have 'qconfig'")
# Setting the dropout to 0.0!
observed = cls(
other.embed_dim,
other.num_heads,
other.dropout,
(other.in_proj_bias is not None),
(other.bias_k is not None),
other.add_zero_attn,
other.kdim,
other.vdim,
other.batch_first,
)
observed.bias_k = other.bias_k
observed.bias_v = other.bias_v
observed.qconfig = other.qconfig
# Set the linear weights
# for the type: ignores, see https://github.com/pytorch/pytorch/issues/58969
observed.out_proj.weight = other.out_proj.weight
observed.out_proj.bias = other.out_proj.bias
if other._qkv_same_embed_dim:
# Use separate params
bias = other.in_proj_bias
_start = 0
_end = _start + other.embed_dim
weight = other.in_proj_weight[_start:_end, :]
if bias is not None:
bias = torch.nn.Parameter(bias[_start:_end], bias.requires_grad)
observed.linear_Q.weight = torch.nn.Parameter(weight, weight.requires_grad)
observed.linear_Q.bias = bias
bias = other.in_proj_bias
_start = _end
_end = _start + other.embed_dim
weight = other.in_proj_weight[_start:_end, :]
if bias is not None:
bias = torch.nn.Parameter(bias[_start:_end], bias.requires_grad)
observed.linear_K.weight = torch.nn.Parameter(weight, weight.requires_grad)
observed.linear_K.bias = bias
bias = other.in_proj_bias
_start = _end
weight = other.in_proj_weight[_start:, :]
if bias is not None:
bias = torch.nn.Parameter(bias[_start:], bias.requires_grad)
observed.linear_V.weight = torch.nn.Parameter(weight, weight.requires_grad)
observed.linear_V.bias = bias
else:
observed.linear_Q.weight = nn.Parameter(other.q_proj_weight)
observed.linear_K.weight = nn.Parameter(other.k_proj_weight)
observed.linear_V.weight = nn.Parameter(other.v_proj_weight)
if other.in_proj_bias is None:
# pyrefly: ignore [bad-assignment]
observed.linear_Q.bias = None
# pyrefly: ignore [bad-assignment]
observed.linear_K.bias = None
# pyrefly: ignore [bad-assignment]
observed.linear_V.bias = None
else:
observed.linear_Q.bias = nn.Parameter(
other.in_proj_bias[0 : other.embed_dim]
)
observed.linear_K.bias = nn.Parameter(
other.in_proj_bias[other.embed_dim : (other.embed_dim * 2)]
)
observed.linear_V.bias = nn.Parameter(
other.in_proj_bias[(other.embed_dim * 2) :]
)
observed.eval()
# Explicit prepare
observed = torch.ao.quantization.prepare(observed, inplace=True)
return observed
@torch.jit.unused
def dequantize(self):
r"""Utility to convert the quantized MHA back to float.
The motivation for this is that it is not trivial to convert the weights
from the format that is used in the quantized version back to the
float.
"""
fp = self._FLOAT_MODULE(
self.embed_dim,
self.num_heads,
self.dropout,
(self.linear_Q._weight_bias()[1] is not None), # type: ignore[operator]
(self.bias_k is not None),
self.add_zero_attn,
self.kdim,
self.vdim,
self.batch_first,
)
if fp._qkv_same_embed_dim != self._qkv_same_embed_dim:
raise AssertionError(
f"_qkv_same_embed_dim mismatch: {fp._qkv_same_embed_dim} != {self._qkv_same_embed_dim}"
)
if self.bias_k is not None:
fp.bias_k = nn.Parameter(self.bias_k.dequantize())
if self.bias_v is not None:
fp.bias_v = nn.Parameter(self.bias_v.dequantize())
# Set the linear weights
# Note: Because the linear layers are quantized, mypy does not know how
# to deal with them -- might need to ignore the typing checks.
# for the type: ignore[has-type], see https://github.com/pytorch/pytorch/issues/58969
w, b = self.out_proj._weight_bias() # type: ignore[operator, has-type]
fp.out_proj.weight = nn.Parameter(w.dequantize())
if b is not None:
fp.out_proj.bias = nn.Parameter(b)
wQ, bQ = self.linear_Q._weight_bias() # type: ignore[operator]
wQ = wQ.dequantize()
wK, bK = self.linear_K._weight_bias() # type: ignore[operator]
wK = wK.dequantize()
wV, bV = self.linear_V._weight_bias() # type: ignore[operator]
wV = wV.dequantize()
if fp._qkv_same_embed_dim:
# Use separate params
_start = 0
_end = _start + fp.embed_dim
fp.in_proj_weight[_start:_end, :] = wQ
if fp.in_proj_bias is not None:
# pyrefly: ignore [bad-argument-type]
if not all(bQ == 0):
raise AssertionError("Expected all bQ elements to be 0")
fp.in_proj_bias[_start:_end] = bQ
_start = _end
_end = _start + fp.embed_dim
fp.in_proj_weight[_start:_end, :] = wK
if fp.in_proj_bias is not None:
# pyrefly: ignore [bad-argument-type]
if not all(bK == 0):
raise AssertionError("Expected all bK elements to be 0")
fp.in_proj_bias[_start:_end] = bK
_start = _end
fp.in_proj_weight[_start:, :] = wV
if fp.in_proj_bias is not None:
# pyrefly: ignore [bad-argument-type]
if not all(bV == 0):
raise AssertionError("Expected all bV elements to be 0")
fp.in_proj_bias[_start:] = bV
else:
fp.q_proj_weight = nn.Parameter(wQ)
fp.k_proj_weight = nn.Parameter(wK)
fp.v_proj_weight = nn.Parameter(wV)
if fp.in_proj_bias is None:
# pyrefly: ignore [bad-assignment]
self.linear_Q.bias = None
# pyrefly: ignore [bad-assignment]
self.linear_K.bias = None
# pyrefly: ignore [bad-assignment]
self.linear_V.bias = None
else:
fp.in_proj_bias[0 : fp.embed_dim] = bQ
fp.in_proj_bias[fp.embed_dim : (fp.embed_dim * 2)] = bK
fp.in_proj_bias[(fp.embed_dim * 2) :] = bV
return fp
@classmethod
def from_observed(cls, other):
# The whole flow is float -> observed -> quantized
# This class does float -> observed only
# See nn.quantized.MultiheadAttention
raise NotImplementedError(
"It looks like you are trying to prepare an "
"MHA module. Please, see "
"the examples on quantizable MHAs."
)
def forward(
self,
query: Tensor,
key: Tensor,
value: Tensor,
key_padding_mask: Tensor | None = None,
need_weights: bool = True,
attn_mask: Tensor | None = None,
average_attn_weights: bool = True,
is_causal: bool = False,
) -> tuple[Tensor, Tensor | None]:
r"""
Note::
Please, refer to :func:`~torch.nn.MultiheadAttention.forward` for more
information
Args:
query, key, value: map a query and a set of key-value pairs to an output.
See "Attention Is All You Need" for more details.
key_padding_mask: if provided, specified padding elements in the key will
be ignored by the attention. When given a binary mask and a value is True,
the corresponding value on the attention layer will be ignored.
need_weights: output attn_output_weights.
attn_mask: 2D or 3D mask that prevents attention to certain positions. A 2D mask will be broadcasted for all
the batches while a 3D mask allows to specify a different mask for the entries of each batch.
Shape:
- Inputs:
- query: :math:`(L, N, E)` where L is the target sequence length, N is the batch size, E is
the embedding dimension. :math:`(N, L, E)` if ``batch_first`` is ``True``.
- key: :math:`(S, N, E)`, where S is the source sequence length, N is the batch size, E is
the embedding dimension. :math:`(N, S, E)` if ``batch_first`` is ``True``.
- value: :math:`(S, N, E)` where S is the source sequence length, N is the batch size, E is
the embedding dimension. :math:`(N, S, E)` if ``batch_first`` is ``True``.
- key_padding_mask: :math:`(N, S)` where N is the batch size, S is the source sequence length.
If a BoolTensor is provided, the positions with the
value of ``True`` will be ignored while the position with the value of ``False`` will be unchanged.
- attn_mask: 2D mask :math:`(L, S)` where L is the target sequence length, S is the source sequence length.
3D mask :math:`(N*num_heads, L, S)` where N is the batch size, L is the target sequence length,
S is the source sequence length. attn_mask ensure that position i is allowed to attend the unmasked
positions. If a BoolTensor is provided, positions with ``True``
is not allowed to attend while ``False`` values will be unchanged. If a FloatTensor
is provided, it will be added to the attention weight.
- is_causal: If specified, applies a causal mask as attention mask. Mutually exclusive with providing attn_mask.
Default: ``False``.
- average_attn_weights: If true, indicates that the returned ``attn_weights`` should be averaged across
heads. Otherwise, ``attn_weights`` are provided separately per head. Note that this flag only has an
effect when ``need_weights=True.``. Default: True (i.e. average weights across heads)
- Outputs:
- attn_output: :math:`(L, N, E)` where L is the target sequence length, N is the batch size,
E is the embedding dimension. :math:`(N, L, E)` if ``batch_first`` is ``True``.
- attn_output_weights: If ``average_attn_weights=True``, returns attention weights averaged
across heads of shape :math:`(N, L, S)`, where N is the batch size, L is the target sequence length,
S is the source sequence length. If ``average_attn_weights=False``, returns attention weights per
head of shape :math:`(N, num_heads, L, S)`.
"""
return self._forward_impl(
query,
key,
value,
key_padding_mask,
need_weights,
attn_mask,
average_attn_weights,
is_causal,
)
def _forward_impl(
self,
query: Tensor,
key: Tensor,
value: Tensor,
key_padding_mask: Tensor | None = None,
need_weights: bool = True,
attn_mask: Tensor | None = None,
average_attn_weights: bool = True,
is_causal: bool = False,
) -> tuple[Tensor, Tensor | None]:
# This version will not deal with the static key/value pairs.
# Keeping it here for future changes.
#
# TODO: This method has some duplicate lines with the
# `torch.nn.functional.multi_head_attention`. Will need to refactor.
static_k = None
static_v = None
if attn_mask is not None and is_causal:
raise AssertionError("Only allow causal mask or attn_mask")
if is_causal:
raise AssertionError("causal mask not supported by AO MHA module")
if self.batch_first:
query, key, value = (x.transpose(0, 1) for x in (query, key, value))
tgt_len, bsz, embed_dim_to_check = query.size()
if self.embed_dim != embed_dim_to_check:
raise AssertionError(
f"embed_dim mismatch: {self.embed_dim} != {embed_dim_to_check}"
)
# allow MHA to have different sizes for the feature dimension
if key.size(0) != value.size(0) or key.size(1) != value.size(1):
raise AssertionError(
f"key and value size mismatch: key.size()={key.size()}, value.size()={value.size()}"
)
head_dim = self.embed_dim // self.num_heads
if head_dim * self.num_heads != self.embed_dim:
raise AssertionError("embed_dim must be divisible by num_heads")
scaling = float(head_dim) ** -0.5
q = self.linear_Q(query)
k = self.linear_K(key)
v = self.linear_V(value)
q = self.q_scaling_product.mul_scalar(q, scaling)
if attn_mask is not None:
if attn_mask.dtype == torch.uint8:
warnings.warn(
"Byte tensor for `attn_mask` in `nn.MultiheadAttention` is deprecated. "
"Use bool tensor instead.",
stacklevel=3,
)
attn_mask = attn_mask.to(torch.bool)
if not attn_mask.is_floating_point() and attn_mask.dtype != torch.bool:
raise AssertionError(
f"Only float and bool types are supported for attn_mask, not {attn_mask.dtype}"
)
if attn_mask.dim() == 2:
attn_mask = attn_mask.unsqueeze(0)
if list(attn_mask.size()) != [1, query.size(0), key.size(0)]:
raise RuntimeError("The size of the 2D attn_mask is not correct.")
elif attn_mask.dim() == 3:
if list(attn_mask.size()) != [
bsz * self.num_heads,
query.size(0),
key.size(0),
]:
raise RuntimeError("The size of the 3D attn_mask is not correct.")
else:
raise RuntimeError(
f"attn_mask's dimension {attn_mask.dim()} is not supported"
)
# attn_mask's dim is 3 now.
# convert ByteTensor key_padding_mask to bool
if key_padding_mask is not None and key_padding_mask.dtype == torch.uint8:
warnings.warn(
"Byte tensor for `key_padding_mask` in `nn.MultiheadAttention` is deprecated. "
"Use bool tensor instead.",
stacklevel=3,
)
key_padding_mask = key_padding_mask.to(torch.bool)
if self.bias_k is not None and self.bias_v is not None:
if static_k is None and static_v is None:
# Explicitly check that bias_k and bias_v are not None
# in a way that TorchScript can understand.
bias_k = self.bias_k
if bias_k is None:
raise AssertionError("bias_k must not be None")
bias_v = self.bias_v
if bias_v is None:
raise AssertionError("bias_v must not be None")
k = torch.cat([k, bias_k.repeat(1, bsz, 1)])
v = torch.cat([v, bias_v.repeat(1, bsz, 1)])
if attn_mask is not None:
attn_mask = F.pad(attn_mask, (0, 1))
if key_padding_mask is not None:
key_padding_mask = F.pad(key_padding_mask, (0, 1))
else:
if static_k is not None:
raise AssertionError("bias cannot be added to static key.")
if static_v is not None:
raise AssertionError("bias cannot be added to static value.")
else:
if self.bias_k is not None:
raise AssertionError(
"self.bias_k must be None when self.bias_v is None"
)
if self.bias_v is not None:
raise AssertionError(
"self.bias_v must be None when self.bias_k is None"
)
q = q.contiguous().view(tgt_len, bsz * self.num_heads, head_dim).transpose(0, 1)
if k is not None:
k = k.contiguous().view(-1, bsz * self.num_heads, head_dim).transpose(0, 1)
if v is not None:
v = v.contiguous().view(-1, bsz * self.num_heads, head_dim).transpose(0, 1)
if static_k is not None:
if static_k.size(0) != bsz * self.num_heads:
raise AssertionError(
f"static_k.size(0) must be {bsz * self.num_heads}, got {static_k.size(0)}"
)
if static_k.size(2) != head_dim:
raise AssertionError(
f"static_k.size(2) must be {head_dim}, got {static_k.size(2)}"
)
k = static_k
if static_v is not None:
if static_v.size(0) != bsz * self.num_heads:
raise AssertionError(
f"static_v.size(0) must be {bsz * self.num_heads}, got {static_v.size(0)}"
)
if static_v.size(2) != head_dim:
raise AssertionError(
f"static_v.size(2) must be {head_dim}, got {static_v.size(2)}"
)
v = static_v
src_len = k.size(1)
if key_padding_mask is not None:
if key_padding_mask.size(0) != bsz:
raise AssertionError(
f"key_padding_mask.size(0) must be {bsz}, got {key_padding_mask.size(0)}"
)
if key_padding_mask.size(1) != src_len:
raise AssertionError(
f"key_padding_mask.size(1) must be {src_len}, got {key_padding_mask.size(1)}"
)
if self.add_zero_attn:
src_len += 1
k_zeros = torch.zeros((k.size(0), 1) + k.size()[2:])
if k.is_quantized:
k_zeros = torch.quantize_per_tensor(
k_zeros,
k.q_scale(),
k.q_zero_point(),
k.dtype,
)
k = torch.cat([k, k_zeros], dim=1)
v_zeros = torch.zeros((v.size(0), 1) + k.size()[2:])
if v.is_quantized:
v_zeros = torch.quantize_per_tensor(
v_zeros,
v.q_scale(),
v.q_zero_point(),
v.dtype,
)
v = torch.cat([v, v_zeros], dim=1)
if attn_mask is not None:
attn_mask = F.pad(attn_mask, (0, 1))
if key_padding_mask is not None:
key_padding_mask = F.pad(key_padding_mask, (0, 1))
# Leaving the quantized zone here
q = self.dequant_q(q)
k = self.dequant_k(k)
v = self.dequant_v(v)
attn_output_weights = torch.bmm(q, k.transpose(1, 2))
expected_size = [bsz * self.num_heads, tgt_len, src_len]
if list(attn_output_weights.size()) != expected_size:
raise AssertionError(
f"attn_output_weights size mismatch: expected {expected_size}, "
f"got {list(attn_output_weights.size())}"
)
if attn_mask is not None:
if attn_mask.dtype == torch.bool:
attn_output_weights.masked_fill_(attn_mask, float("-inf"))
else:
attn_output_weights += attn_mask
if key_padding_mask is not None:
attn_output_weights = attn_output_weights.view(
bsz, self.num_heads, tgt_len, src_len
)
attn_output_weights = attn_output_weights.masked_fill(
key_padding_mask.unsqueeze(1).unsqueeze(2),
float("-inf"),
)
attn_output_weights = attn_output_weights.view(
bsz * self.num_heads, tgt_len, src_len
)
attn_output_weights = F.softmax(attn_output_weights, dim=-1)
attn_output_weights = F.dropout(
attn_output_weights, p=self.dropout, training=self.training
)
attn_output = torch.bmm(attn_output_weights, v)
expected_output_size = [bsz * self.num_heads, tgt_len, head_dim]
if list(attn_output.size()) != expected_output_size:
raise AssertionError(
f"attn_output size mismatch: expected {expected_output_size}, "
f"got {list(attn_output.size())}"
)
if self.batch_first:
attn_output = attn_output.view(bsz, tgt_len, self.embed_dim)
else:
attn_output = (
attn_output.transpose(0, 1)
.contiguous()
.view(tgt_len, bsz, self.embed_dim)
)
# Reentering the quantized zone
attn_output = self.quant_attn_output(attn_output)
# for the type: ignore[has-type], see https://github.com/pytorch/pytorch/issues/58969
attn_output = self.out_proj(attn_output) # type: ignore[has-type]
attn_output_weights = self.quant_attn_output_weights(attn_output_weights)
if need_weights:
# average attention weights over heads
attn_output_weights = attn_output_weights.view(
bsz, self.num_heads, tgt_len, src_len
)
if average_attn_weights:
attn_output_weights = attn_output_weights.mean(dim=1)
return attn_output, attn_output_weights
else:
return attn_output, None
@@ -0,0 +1,618 @@
"""
We will recreate all the RNN modules as we require the modules to be decomposed
into its building blocks to be able to observe.
"""
# mypy: allow-untyped-defs
import numbers
import warnings
import torch
from torch import Tensor
__all__ = ["LSTMCell", "LSTM"]
class LSTMCell(torch.nn.Module):
r"""A quantizable long short-term memory (LSTM) cell.
For the description and the argument types, please, refer to :class:`~torch.nn.LSTMCell`
`split_gates`: specify True to compute the input/forget/cell/output gates separately
to avoid an intermediate tensor which is subsequently chunk'd. This optimization can
be beneficial for on-device inference latency. This flag is cascaded down from the
parent classes.
Examples::
>>> import torch.ao.nn.quantizable as nnqa
>>> rnn = nnqa.LSTMCell(10, 20)
>>> input = torch.randn(6, 10)
>>> hx = torch.randn(3, 20)
>>> cx = torch.randn(3, 20)
>>> output = []
>>> for i in range(6):
... hx, cx = rnn(input[i], (hx, cx))
... output.append(hx)
"""
_FLOAT_MODULE = torch.nn.LSTMCell
__constants__ = ["split_gates"] # for jit.script
def __init__(
self,
input_dim: int,
hidden_dim: int,
bias: bool = True,
device=None,
dtype=None,
*,
split_gates=False,
) -> None:
factory_kwargs = {"device": device, "dtype": dtype}
super().__init__()
self.input_size = input_dim
self.hidden_size = hidden_dim
self.bias = bias
self.split_gates = split_gates
if not split_gates:
self.igates: torch.nn.Module = torch.nn.Linear(
input_dim, 4 * hidden_dim, bias=bias, **factory_kwargs
)
self.hgates: torch.nn.Module = torch.nn.Linear(
hidden_dim, 4 * hidden_dim, bias=bias, **factory_kwargs
)
self.gates: torch.nn.Module = torch.ao.nn.quantized.FloatFunctional()
else:
# keep separate Linear layers for each gate
self.igates = torch.nn.ModuleDict()
self.hgates = torch.nn.ModuleDict()
self.gates = torch.nn.ModuleDict()
for g in ["input", "forget", "cell", "output"]:
# pyre-fixme[29]: `Union[torch._tensor.Tensor, torch.nn.modules.module.Module]`
self.igates[g] = torch.nn.Linear(
input_dim, hidden_dim, bias=bias, **factory_kwargs
)
# pyre-fixme[29]: `Union[torch._tensor.Tensor, torch.nn.modules.module.Module]`
self.hgates[g] = torch.nn.Linear(
hidden_dim, hidden_dim, bias=bias, **factory_kwargs
)
# pyre-fixme[29]: `Union[torch._tensor.Tensor, torch.nn.modules.module.Module]`
self.gates[g] = torch.ao.nn.quantized.FloatFunctional()
self.input_gate = torch.nn.Sigmoid()
self.forget_gate = torch.nn.Sigmoid()
self.cell_gate = torch.nn.Tanh()
self.output_gate = torch.nn.Sigmoid()
self.fgate_cx = torch.ao.nn.quantized.FloatFunctional()
self.igate_cgate = torch.ao.nn.quantized.FloatFunctional()
self.fgate_cx_igate_cgate = torch.ao.nn.quantized.FloatFunctional()
self.ogate_cy = torch.ao.nn.quantized.FloatFunctional()
self.initial_hidden_state_qparams: tuple[float, int] = (1.0, 0)
self.initial_cell_state_qparams: tuple[float, int] = (1.0, 0)
self.hidden_state_dtype: torch.dtype = torch.quint8
self.cell_state_dtype: torch.dtype = torch.quint8
def forward(
self, x: Tensor, hidden: tuple[Tensor, Tensor] | None = None
) -> tuple[Tensor, Tensor]:
if hidden is None or hidden[0] is None or hidden[1] is None:
hidden = self.initialize_hidden(x.shape[0], x.is_quantized)
hx, cx = hidden
if not self.split_gates:
igates = self.igates(x)
hgates = self.hgates(hx)
gates = self.gates.add(igates, hgates) # type: ignore[operator]
input_gate, forget_gate, cell_gate, out_gate = gates.chunk(4, 1)
input_gate = self.input_gate(input_gate)
forget_gate = self.forget_gate(forget_gate)
cell_gate = self.cell_gate(cell_gate)
out_gate = self.output_gate(out_gate)
else:
# apply each input + hidden projection and add together
gate = {}
for (key, gates), igates, hgates in zip(
self.gates.items(), # type: ignore[operator]
self.igates.values(), # type: ignore[operator]
self.hgates.values(), # type: ignore[operator]
):
gate[key] = gates.add(igates(x), hgates(hx))
input_gate = self.input_gate(gate["input"])
forget_gate = self.forget_gate(gate["forget"])
cell_gate = self.cell_gate(gate["cell"])
out_gate = self.output_gate(gate["output"])
fgate_cx = self.fgate_cx.mul(forget_gate, cx)
igate_cgate = self.igate_cgate.mul(input_gate, cell_gate)
fgate_cx_igate_cgate = self.fgate_cx_igate_cgate.add(fgate_cx, igate_cgate)
cy = fgate_cx_igate_cgate
# TODO: make this tanh a member of the module so its qparams can be configured
tanh_cy = torch.tanh(cy)
hy = self.ogate_cy.mul(out_gate, tanh_cy)
return hy, cy
def initialize_hidden(
self, batch_size: int, is_quantized: bool = False
) -> tuple[Tensor, Tensor]:
h, c = (
torch.zeros((batch_size, self.hidden_size)),
torch.zeros((batch_size, self.hidden_size)),
)
if is_quantized:
(h_scale, h_zp) = self.initial_hidden_state_qparams
(c_scale, c_zp) = self.initial_cell_state_qparams
h = torch.quantize_per_tensor(
h, scale=h_scale, zero_point=h_zp, dtype=self.hidden_state_dtype
)
c = torch.quantize_per_tensor(
c, scale=c_scale, zero_point=c_zp, dtype=self.cell_state_dtype
)
return h, c
def _get_name(self):
return "QuantizableLSTMCell"
@classmethod
def from_params(cls, wi, wh, bi=None, bh=None, split_gates=False):
"""Uses the weights and biases to create a new LSTM cell.
Args:
wi, wh: Weights for the input and hidden layers
bi, bh: Biases for the input and hidden layers
"""
if (bi is None) != (bh is None):
raise AssertionError("bi and bh must both be None or both have values")
input_size = wi.shape[1]
hidden_size = wh.shape[1]
cell = cls(
input_dim=input_size,
hidden_dim=hidden_size,
bias=(bi is not None),
split_gates=split_gates,
)
if not split_gates:
cell.igates.weight = torch.nn.Parameter(wi)
if bi is not None:
cell.igates.bias = torch.nn.Parameter(bi)
cell.hgates.weight = torch.nn.Parameter(wh)
if bh is not None:
cell.hgates.bias = torch.nn.Parameter(bh)
else:
# split weight/bias
for w, b, gates in zip([wi, wh], [bi, bh], [cell.igates, cell.hgates]):
for w_chunk, gate in zip(w.chunk(4, dim=0), gates.values()): # type: ignore[operator]
gate.weight = torch.nn.Parameter(w_chunk)
if b is not None:
for b_chunk, gate in zip(b.chunk(4, dim=0), gates.values()): # type: ignore[operator]
gate.bias = torch.nn.Parameter(b_chunk)
return cell
@classmethod
def from_float(cls, other, use_precomputed_fake_quant=False, split_gates=False):
if type(other) is not cls._FLOAT_MODULE:
raise AssertionError(
f"Expected module type {cls._FLOAT_MODULE}, got {type(other)}"
)
if not hasattr(other, "qconfig"):
raise AssertionError("The float module must have 'qconfig'")
observed = cls.from_params(
other.weight_ih,
other.weight_hh,
other.bias_ih,
other.bias_hh,
split_gates=split_gates,
)
observed.qconfig = other.qconfig
observed.igates.qconfig = other.qconfig
observed.hgates.qconfig = other.qconfig
if split_gates:
# also apply qconfig directly to Linear modules
for g in observed.igates.values():
g.qconfig = other.qconfig
for g in observed.hgates.values():
g.qconfig = other.qconfig
return observed
class _LSTMSingleLayer(torch.nn.Module):
r"""A single one-directional LSTM layer.
The difference between a layer and a cell is that the layer can process a
sequence, while the cell only expects an instantaneous value.
"""
def __init__(
self,
input_dim: int,
hidden_dim: int,
bias: bool = True,
device=None,
dtype=None,
*,
split_gates=False,
) -> None:
factory_kwargs = {"device": device, "dtype": dtype}
super().__init__()
self.cell = LSTMCell(
input_dim, hidden_dim, bias=bias, split_gates=split_gates, **factory_kwargs
)
def forward(self, x: Tensor, hidden: tuple[Tensor, Tensor] | None = None):
result = []
seq_len = x.shape[0]
for i in range(seq_len):
hidden = self.cell(x[i], hidden)
result.append(hidden[0]) # type: ignore[index]
result_tensor = torch.stack(result, 0)
return result_tensor, hidden
@classmethod
def from_params(cls, *args, **kwargs):
cell = LSTMCell.from_params(*args, **kwargs)
layer = cls(
cell.input_size, cell.hidden_size, cell.bias, split_gates=cell.split_gates
)
layer.cell = cell
return layer
class _LSTMLayer(torch.nn.Module):
r"""A single bi-directional LSTM layer."""
def __init__(
self,
input_dim: int,
hidden_dim: int,
bias: bool = True,
batch_first: bool = False,
bidirectional: bool = False,
device=None,
dtype=None,
*,
split_gates=False,
) -> None:
factory_kwargs = {"device": device, "dtype": dtype}
super().__init__()
self.batch_first = batch_first
self.bidirectional = bidirectional
self.layer_fw = _LSTMSingleLayer(
input_dim, hidden_dim, bias=bias, split_gates=split_gates, **factory_kwargs
)
if self.bidirectional:
self.layer_bw = _LSTMSingleLayer(
input_dim,
hidden_dim,
bias=bias,
split_gates=split_gates,
**factory_kwargs,
)
def forward(self, x: Tensor, hidden: tuple[Tensor, Tensor] | None = None):
if self.batch_first:
x = x.transpose(0, 1)
if hidden is None:
hx_fw, cx_fw = (None, None)
else:
hx_fw, cx_fw = hidden
hidden_bw: tuple[Tensor, Tensor] | None = None
if self.bidirectional:
if hx_fw is None:
hx_bw = None
else:
hx_bw = hx_fw[1]
hx_fw = hx_fw[0]
if cx_fw is None:
cx_bw = None
else:
cx_bw = cx_fw[1]
cx_fw = cx_fw[0]
if hx_bw is not None and cx_bw is not None:
hidden_bw = hx_bw, cx_bw
if hx_fw is None and cx_fw is None:
hidden_fw = None
else:
hidden_fw = (
torch.jit._unwrap_optional(hx_fw),
torch.jit._unwrap_optional(cx_fw),
)
result_fw, hidden_fw = self.layer_fw(x, hidden_fw)
if hasattr(self, "layer_bw") and self.bidirectional:
x_reversed = x.flip(0)
result_bw, hidden_bw = self.layer_bw(x_reversed, hidden_bw)
result_bw = result_bw.flip(0)
result = torch.cat([result_fw, result_bw], result_fw.dim() - 1)
if hidden_fw is None and hidden_bw is None:
h = None
c = None
elif hidden_fw is None:
(h, c) = torch.jit._unwrap_optional(hidden_bw)
elif hidden_bw is None:
(h, c) = torch.jit._unwrap_optional(hidden_fw)
else:
h = torch.stack([hidden_fw[0], hidden_bw[0]], 0) # type: ignore[list-item]
c = torch.stack([hidden_fw[1], hidden_bw[1]], 0) # type: ignore[list-item]
else:
result = result_fw
h, c = torch.jit._unwrap_optional(hidden_fw) # type: ignore[assignment]
if self.batch_first:
result.transpose_(0, 1)
return result, (h, c)
@classmethod
def from_float(cls, other, layer_idx=0, qconfig=None, **kwargs):
r"""
There is no FP equivalent of this class. This function is here just to
mimic the behavior of the `prepare` within the `torch.ao.quantization`
flow.
"""
if not hasattr(other, "qconfig") and qconfig is None:
raise AssertionError("other must have qconfig or qconfig must be provided")
input_size = kwargs.get("input_size", other.input_size)
hidden_size = kwargs.get("hidden_size", other.hidden_size)
bias = kwargs.get("bias", other.bias)
batch_first = kwargs.get("batch_first", other.batch_first)
bidirectional = kwargs.get("bidirectional", other.bidirectional)
split_gates = kwargs.get("split_gates", False)
layer = cls(
# pyrefly: ignore [bad-argument-type]
input_size,
# pyrefly: ignore [bad-argument-type]
hidden_size,
# pyrefly: ignore [bad-argument-type]
bias,
# pyrefly: ignore [bad-argument-type]
batch_first,
# pyrefly: ignore [bad-argument-type]
bidirectional,
split_gates=split_gates,
)
# pyrefly: ignore [bad-argument-type]
layer.qconfig = getattr(other, "qconfig", qconfig)
wi = getattr(other, f"weight_ih_l{layer_idx}")
wh = getattr(other, f"weight_hh_l{layer_idx}")
bi = getattr(other, f"bias_ih_l{layer_idx}", None)
bh = getattr(other, f"bias_hh_l{layer_idx}", None)
layer.layer_fw = _LSTMSingleLayer.from_params(
wi, wh, bi, bh, split_gates=split_gates
)
if other.bidirectional:
wi = getattr(other, f"weight_ih_l{layer_idx}_reverse")
wh = getattr(other, f"weight_hh_l{layer_idx}_reverse")
bi = getattr(other, f"bias_ih_l{layer_idx}_reverse", None)
bh = getattr(other, f"bias_hh_l{layer_idx}_reverse", None)
layer.layer_bw = _LSTMSingleLayer.from_params(
wi, wh, bi, bh, split_gates=split_gates
)
return layer
class LSTM(torch.nn.Module):
r"""A quantizable long short-term memory (LSTM).
For the description and the argument types, please, refer to :class:`~torch.nn.LSTM`
Attributes:
layers : instances of the `_LSTMLayer`
.. note::
To access the weights and biases, you need to access them per layer.
See examples below.
Examples::
>>> import torch.ao.nn.quantizable as nnqa
>>> rnn = nnqa.LSTM(10, 20, 2)
>>> input = torch.randn(5, 3, 10)
>>> h0 = torch.randn(2, 3, 20)
>>> c0 = torch.randn(2, 3, 20)
>>> output, (hn, cn) = rnn(input, (h0, c0))
>>> # To get the weights:
>>> # xdoctest: +SKIP
>>> print(rnn.layers[0].weight_ih)
tensor([[...]])
>>> print(rnn.layers[0].weight_hh)
AssertionError: There is no reverse path in the non-bidirectional layer
"""
_FLOAT_MODULE = torch.nn.LSTM
def __init__(
self,
input_size: int,
hidden_size: int,
num_layers: int = 1,
bias: bool = True,
batch_first: bool = False,
dropout: float = 0.0,
bidirectional: bool = False,
device=None,
dtype=None,
*,
split_gates: bool = False,
) -> None:
factory_kwargs = {"device": device, "dtype": dtype}
super().__init__()
self.input_size = input_size
self.hidden_size = hidden_size
self.num_layers = num_layers
self.bias = bias
self.batch_first = batch_first
self.dropout = float(dropout)
self.bidirectional = bidirectional
self.training = False # Default to eval mode. If we want to train, we will explicitly set to training.
if (
not isinstance(dropout, numbers.Number)
or not 0 <= dropout <= 1
or isinstance(dropout, bool)
):
raise ValueError(
"dropout should be a number in range [0, 1] "
"representing the probability of an element being "
"zeroed"
)
if dropout > 0:
warnings.warn(
"dropout option for quantizable LSTM is ignored. "
"If you are training, please, use nn.LSTM version "
"followed by `prepare` step.",
stacklevel=2,
)
if num_layers == 1:
warnings.warn(
"dropout option adds dropout after all but last "
"recurrent layer, so non-zero dropout expects "
f"num_layers greater than 1, but got dropout={dropout} "
f"and num_layers={num_layers}",
stacklevel=2,
)
layers = [
_LSTMLayer(
self.input_size,
self.hidden_size,
self.bias,
batch_first=False,
bidirectional=self.bidirectional,
split_gates=split_gates,
**factory_kwargs,
)
]
layers.extend(
_LSTMLayer(
self.hidden_size,
self.hidden_size,
self.bias,
batch_first=False,
bidirectional=self.bidirectional,
split_gates=split_gates,
**factory_kwargs,
)
for _ in range(1, num_layers)
)
self.layers = torch.nn.ModuleList(layers)
def forward(self, x: Tensor, hidden: tuple[Tensor, Tensor] | None = None):
if self.batch_first:
x = x.transpose(0, 1)
max_batch_size = x.size(1)
num_directions = 2 if self.bidirectional else 1
if hidden is None:
zeros = torch.zeros(
num_directions,
max_batch_size,
self.hidden_size,
dtype=torch.float,
device=x.device,
)
zeros.squeeze_(0)
if x.is_quantized:
zeros = torch.quantize_per_tensor(
zeros, scale=1.0, zero_point=0, dtype=x.dtype
)
hxcx = [(zeros, zeros) for _ in range(self.num_layers)]
else:
hidden_non_opt = torch.jit._unwrap_optional(hidden)
if isinstance(hidden_non_opt[0], Tensor):
hx = hidden_non_opt[0].reshape(
self.num_layers, num_directions, max_batch_size, self.hidden_size
)
cx = hidden_non_opt[1].reshape(
self.num_layers, num_directions, max_batch_size, self.hidden_size
)
hxcx = [
(hx[idx].squeeze(0), cx[idx].squeeze(0))
for idx in range(self.num_layers)
]
else:
hxcx = hidden_non_opt
hx_list = []
cx_list = []
for idx, layer in enumerate(self.layers):
x, (h, c) = layer(x, hxcx[idx])
hx_list.append(torch.jit._unwrap_optional(h))
cx_list.append(torch.jit._unwrap_optional(c))
hx_tensor = torch.stack(hx_list)
cx_tensor = torch.stack(cx_list)
# We are creating another dimension for bidirectional case
# need to collapse it
hx_tensor = hx_tensor.reshape(-1, hx_tensor.shape[-2], hx_tensor.shape[-1])
cx_tensor = cx_tensor.reshape(-1, cx_tensor.shape[-2], cx_tensor.shape[-1])
if self.batch_first:
x = x.transpose(0, 1)
return x, (hx_tensor, cx_tensor)
def _get_name(self):
return "QuantizableLSTM"
@classmethod
def from_float(cls, other, qconfig=None, split_gates=False):
if not isinstance(other, cls._FLOAT_MODULE):
raise AssertionError(
f"Expected module type {cls._FLOAT_MODULE}, got {type(other)}"
)
if not hasattr(other, "qconfig") and not qconfig:
raise AssertionError("other must have qconfig or qconfig must be provided")
observed = cls(
other.input_size,
other.hidden_size,
other.num_layers,
other.bias,
other.batch_first,
other.dropout,
other.bidirectional,
split_gates=split_gates,
)
# pyrefly: ignore [bad-argument-type]
observed.qconfig = getattr(other, "qconfig", qconfig)
for idx in range(other.num_layers):
observed.layers[idx] = _LSTMLayer.from_float(
other, idx, qconfig, batch_first=False, split_gates=split_gates
)
# Prepare the model
if other.training:
observed.train()
observed = torch.ao.quantization.prepare_qat(observed, inplace=True)
else:
observed.eval()
observed = torch.ao.quantization.prepare(observed, inplace=True)
return observed
@classmethod
def from_observed(cls, other):
# The whole flow is float -> observed -> quantized
# This class does float -> observed only
raise NotImplementedError(
"It looks like you are trying to convert a "
"non-quantizable LSTM module. Please, see "
"the examples on quantizable LSTMs."
)
@@ -0,0 +1,39 @@
from . import functional
from .modules import * # noqa: F403
from .modules import MaxPool2d
__all__ = [
"BatchNorm2d",
"BatchNorm3d",
"Conv1d",
"Conv2d",
"Conv3d",
"ConvTranspose1d",
"ConvTranspose2d",
"ConvTranspose3d",
"DeQuantize",
"ELU",
"Embedding",
"EmbeddingBag",
"GroupNorm",
"Hardswish",
"InstanceNorm1d",
"InstanceNorm2d",
"InstanceNorm3d",
"LayerNorm",
"LeakyReLU",
"Linear",
"LSTM",
"MultiheadAttention",
"Quantize",
"ReLU6",
"Sigmoid",
"Softmax",
"Dropout",
"PReLU",
# Wrapper modules
"FloatFunctional",
"FXFloatFunctional",
"QFunctional",
]
@@ -0,0 +1 @@
from .modules import * # noqa: F403
@@ -0,0 +1,26 @@
from .conv import (
Conv1d,
Conv2d,
Conv3d,
ConvTranspose1d,
ConvTranspose2d,
ConvTranspose3d,
)
from .linear import Linear
from .rnn import GRU, GRUCell, LSTM, LSTMCell, RNNCell
__all__ = [
"Linear",
"LSTM",
"GRU",
"LSTMCell",
"RNNCell",
"GRUCell",
"Conv1d",
"Conv2d",
"Conv3d",
"ConvTranspose1d",
"ConvTranspose2d",
"ConvTranspose3d",
]
@@ -0,0 +1,531 @@
# mypy: allow-untyped-defs
r"""Dynamically quantized convolution modules."""
import warnings
from typing import ClassVar, Literal
import torch
import torch.ao.nn.quantized as nnq
import torch.nn as nn
import torch.nn.functional as F
from torch import Tensor
from torch._ops import ops
from torch.ao.nn.quantized.modules.conv import _reverse_repeat_padding
from torch.nn.common_types import _size_1_t
from torch.nn.modules.utils import _pair, _single, _triple
__all__ = [
"Conv1d",
"Conv2d",
"Conv3d",
"ConvTranspose1d",
"ConvTranspose2d",
"ConvTranspose3d",
]
class Conv1d(nnq.Conv1d):
r"""A dynamically quantized conv module with floating point tensors as inputs and outputs.
For details on input arguments, parameters, and implementation see
:class:`~torch.nn.Conv1d` and :class:`~torch.ao.nn.quantized.dynamic.Conv1d` and
Attributes:
weight (Tensor): packed tensor derived from the learnable weight
parameter.
scale (Tensor): scalar for the output scale
zero_point (Tensor): scalar for the output zero point
See :class:`~torch.nn.Conv1d` for other attributes.
Examples::
>>> # xdoctest: +SKIP
>>> m = nn.quantized.dynamic.Conv1d(16, 33, 3, stride=2)
>>> input = torch.randn(20, 16, 100)
>>> output = m(input)
"""
_FLOAT_MODULE: ClassVar[type[nn.Conv1d]] = nn.Conv1d
_NNIQAT_CONV_BN_MODULE: ClassVar[type[nn.Module] | None] = None
_NNI_CONV_RELU_MODULE: ClassVar[type[nn.Module] | None] = None
def __init__(
self,
in_channels: int,
out_channels: int,
kernel_size: _size_1_t,
stride: _size_1_t = 1,
padding: _size_1_t = 0,
dilation: _size_1_t = 1,
groups: int = 1,
bias: bool = True,
padding_mode: Literal["zeros", "reflect", "replicate", "circular"] = "zeros",
device=None,
dtype=None,
reduce_range=True,
):
warnings.warn(
f"The current implementation of the {self._get_name()} module has poor numerical accuracy and its use is not recommended", # noqa: B950
stacklevel=2,
)
factory_kwargs = {"device": device, "dtype": dtype}
kernel_size = _single(kernel_size)
stride = _single(stride)
padding = padding if isinstance(padding, str) else _single(padding)
dilation = _single(dilation)
super().__init__(
in_channels,
out_channels,
kernel_size,
stride,
padding,
dilation,
groups,
bias,
padding_mode,
**factory_kwargs,
)
def _get_name(self):
return "DynamicQuantizedConv1d"
def forward(self, input: Tensor, reduce_range: bool = True) -> Tensor:
# Temporarily using len(shape) instead of ndim due to JIT issue
# https://github.com/pytorch/pytorch/issues/23890
if len(input.shape) != 3:
raise ValueError("Input shape must be `(N, C, L)`!")
if self.padding_mode != "zeros":
# Padding in Conv1d is stored as (p, p), need to get (p,)
_reversed_padding_repeated_twice = _reverse_repeat_padding(self.padding[:1])
input = F.pad(
input, _reversed_padding_repeated_twice, mode=self.padding_mode
)
return ops.quantized.conv1d_dynamic(input, self._packed_params, reduce_range)
class Conv2d(nnq.Conv2d):
r"""A dynamically quantized conv module with floating point tensors as inputs and outputs.
For details on input arguments, parameters, and implementation see
:class:`~torch.nn.Conv2d` and :class:`~torch.ao.nn.quantized.dynamic.Conv2d` and
Attributes:
weight (Tensor): packed tensor derived from the learnable weight
parameter.
scale (Tensor): scalar for the output scale
zero_point (Tensor): scalar for the output zero point
See :class:`~torch.nn.Conv2d` for other attributes.
Examples::
>>> # xdoctest: +SKIP
>>> # With square kernels and equal stride
>>> m = nn.quantized.dynamic.Conv2d(16, 33, 3, stride=2)
>>> # non-square kernels and unequal stride and with padding
>>> m = nn.quantized.dynamic.Conv2d(16, 33, (3, 5), stride=(2, 1), padding=(4, 2))
>>> # non-square kernels and unequal stride and with padding and dilation
>>> m = nn.quantized.dynamic.Conv2d(16, 33, (3, 5), stride=(2, 1), padding=(4, 2), dilation=(3, 1))
>>> input = torch.randn(20, 16, 50, 100)
>>> output = m(input)
"""
_FLOAT_MODULE: ClassVar[type[nn.Conv2d]] = nn.Conv2d
_NNIQAT_CONV_BN_MODULE: ClassVar[type[nn.Module] | None] = None
_NNI_CONV_RELU_MODULE: ClassVar[type[nn.Module] | None] = None
def __init__(
self,
in_channels,
out_channels,
kernel_size,
stride=1,
padding=0,
dilation=1,
groups=1,
bias=True,
padding_mode="zeros",
device=None,
dtype=None,
):
warnings.warn(
f"The current implementation of the {self._get_name()} module "
"has poor numerical accuracy and its use is not recommended",
stacklevel=2,
)
factory_kwargs = {"device": device, "dtype": dtype}
kernel_size = _pair(kernel_size)
stride = _pair(stride)
padding = _pair(padding)
dilation = _pair(dilation)
super().__init__(
in_channels,
out_channels,
kernel_size,
stride,
padding,
dilation,
groups,
bias,
padding_mode,
**factory_kwargs,
)
def _get_name(self):
return "DynamicQuantizedConv2d"
def forward(self, input: Tensor, reduce_range: bool = True) -> Tensor:
# Temporarily using len(shape) instead of ndim due to JIT issue
# https://github.com/pytorch/pytorch/issues/23890
if len(input.shape) != 4:
raise ValueError("Input shape must be `(N, C, H, W)`!")
if self.padding_mode != "zeros":
_reversed_padding_repeated_twice = _reverse_repeat_padding(self.padding)
input = F.pad(
input, _reversed_padding_repeated_twice, mode=self.padding_mode
)
return ops.quantized.conv2d_dynamic(input, self._packed_params, reduce_range)
class Conv3d(nnq.Conv3d):
r"""A dynamically quantized conv module with floating point tensors as inputs and outputs.
For details on input arguments, parameters, and implementation see
:class:`~torch.nn.Conv3d` and :class:`~torch.ao.nn.quantized.dynamic.Conv3d` and
Attributes:
weight (Tensor): packed tensor derived from the learnable weight
parameter.
scale (Tensor): scalar for the output scale
zero_point (Tensor): scalar for the output zero point
See :class:`~torch.nn.Conv3d` for other attributes.
Examples::
>>> # xdoctest: +SKIP
>>> # With square kernels and equal stride
>>> m = nn.quantized.dynamic.Conv3d(16, 33, 3, stride=2)
>>> # non-square kernels and unequal stride and with padding
>>> m = nn.quantized.dynamic.Conv3d(16, 33, (3, 5, 5), stride=(1, 2, 2), padding=(1, 2, 2))
>>> # non-square kernels and unequal stride and with padding and dilation
>>> m = nn.quantized.dynamic.Conv3d(16, 33, (3, 5, 5), stride=(1, 2, 2), padding=(1, 2, 2), dilation=(1, 2, 2))
>>> input = torch.randn(20, 16, 56, 56, 56)
>>> output = m(input)
"""
_FLOAT_MODULE: ClassVar[type[nn.Conv3d]] = nn.Conv3d
_NNIQAT_CONV_BN_MODULE: ClassVar[type[nn.Module] | None] = None
_NNI_CONV_RELU_MODULE: ClassVar[type[nn.Module] | None] = None
def __init__(
self,
in_channels,
out_channels,
kernel_size,
stride=1,
padding=0,
dilation=1,
groups=1,
bias=True,
padding_mode="zeros",
device=None,
dtype=None,
):
warnings.warn(
f"The current implementation of the {self._get_name()} module has poor numerical accuracy and its use is not recommended", # noqa: B950
stacklevel=2,
)
if padding_mode == "reflect":
raise AssertionError("Conv3d does not support reflection padding")
factory_kwargs = {"device": device, "dtype": dtype}
kernel_size = _triple(kernel_size)
stride = _triple(stride)
padding = _triple(padding)
dilation = _triple(dilation)
super()._init(
in_channels,
out_channels,
kernel_size,
stride,
padding,
dilation,
False,
_triple(0),
groups,
bias,
padding_mode,
**factory_kwargs,
)
def _get_name(self):
return "DynamicQuantizedConv3d"
def forward(self, input: Tensor, reduce_range: bool = True) -> Tensor:
# Temporarily using len(shape) instead of ndim due to JIT issue
# https://github.com/pytorch/pytorch/issues/23890
if len(input.shape) != 5:
raise ValueError("Input shape must be `(N, C, D, H, W)`!")
if self.padding_mode != "zeros":
_reversed_padding_repeated_twice = _reverse_repeat_padding(self.padding)
input = F.pad(
input, _reversed_padding_repeated_twice, mode=self.padding_mode
)
return ops.quantized.conv3d_dynamic(input, self._packed_params, reduce_range)
class ConvTranspose1d(nnq.ConvTranspose1d):
r"""A dynamically quantized transposed convolution module with floating point tensors as inputs and outputs.
For details on input arguments, parameters, and implementation see
:class:`~torch.nn.ConvTranspose1d`.
For special notes, please, see :class:`~torch.ao.nn.quantized.dynamic.Conv1d`
Attributes:
weight (Tensor): packed tensor derived from the learnable weight
parameter.
scale (Tensor): scalar for the output scale
zero_point (Tensor): scalar for the output zero point
See :class:`~torch.nn.ConvTranspose1d` for other attributes.
Examples::
>>> # xdoctest: +SKIP
>>> # With square kernels and equal stride
>>> m = nndq.ConvTranspose1d(16, 33, 3, stride=2)
>>> # non-square kernels and unequal stride and with padding
>>> m = nndq.ConvTranspose1d(16, 33, (3, 5), stride=(2, 1), padding=(4, 2))
>>> output = m(input)
>>> # exact output size can be also specified as an argument
>>> downsample = nndq.Conv1d(16, 16, 3, stride=2, padding=1)
>>> upsample = nndq.ConvTranspose1d(16, 16, 3, stride=2, padding=1)
>>> h = downsample(input)
>>> h.size()
torch.Size([1, 16, 6])
>>> output = upsample(h, output_size=input.size())
>>> output.size()
torch.Size([1, 16, 12])
"""
_FLOAT_MODULE: ClassVar[type[nn.ConvTranspose1d]] = nn.ConvTranspose1d
def __init__(
self,
in_channels,
out_channels,
kernel_size,
stride=1,
padding=0,
output_padding=0,
groups=1,
bias=True,
dilation=1,
padding_mode="zeros",
device=None,
dtype=None,
):
warnings.warn(
f"The current implementation of the {self._get_name()} module has poor numerical accuracy and its use is not recommended", # noqa: B950
stacklevel=2,
)
factory_kwargs = {"device": device, "dtype": dtype}
super().__init__(
in_channels,
out_channels,
kernel_size,
stride,
padding,
output_padding,
groups,
bias,
dilation,
padding_mode,
**factory_kwargs,
)
def _get_name(self):
return "DynamicQuantizedConvTranspose1d"
def forward(self, input: Tensor, reduce_range: bool = True) -> Tensor:
# Temporarily using len(shape) instead of ndim due to JIT issue
# https://github.com/pytorch/pytorch/issues/23890
if len(input.shape) != 3:
raise ValueError("Input shape must be `(N, C, L)`!")
return torch.ops.quantized.conv_transpose1d_dynamic(
input, self._packed_params, reduce_range
)
class ConvTranspose2d(nnq.ConvTranspose2d):
r"""A dynamically quantized transposed convolution module with floating point tensors as inputs and outputs.
For details on input arguments, parameters, and implementation see
:class:`~torch.nn.ConvTranspose2d`.
For special notes, please, see :class:`~torch.ao.nn.quantized.dynamic.Conv2d`
Attributes:
weight (Tensor): packed tensor derived from the learnable weight
parameter.
scale (Tensor): scalar for the output scale
zero_point (Tensor): scalar for the output zero point
See :class:`~torch.nn.ConvTranspose2d` for other attributes.
Examples::
>>> # xdoctest: +SKIP
>>> # With square kernels and equal stride
>>> m = nnq.ConvTranspose2d(16, 33, 3, stride=2)
>>> # non-square kernels and unequal stride and with padding
>>> m = nnq.ConvTranspose2d(16, 33, (3, 5), stride=(2, 1), padding=(4, 2))
>>> output = m(input)
>>> # exact output size can be also specified as an argument
>>> downsample = nnq.Conv2d(16, 16, 3, stride=2, padding=1)
>>> upsample = nnq.ConvTranspose2d(16, 16, 3, stride=2, padding=1)
>>> h = downsample(input)
>>> h.size()
torch.Size([1, 16, 6, 6])
>>> output = upsample(h, output_size=input.size())
>>> output.size()
torch.Size([1, 16, 12, 12])
"""
_FLOAT_MODULE: ClassVar[type[nn.ConvTranspose2d]] = nn.ConvTranspose2d
def __init__(
self,
in_channels,
out_channels,
kernel_size,
stride=1,
padding=0,
output_padding=0,
groups=1,
bias=True,
dilation=1,
padding_mode="zeros",
device=None,
dtype=None,
):
warnings.warn(
f"The current implementation of the {self._get_name()} module has poor numerical accuracy and its use is not recommended", # noqa: B950
stacklevel=2,
)
factory_kwargs = {"device": device, "dtype": dtype}
super().__init__(
in_channels,
out_channels,
kernel_size,
stride,
padding,
output_padding,
groups,
bias,
dilation,
padding_mode,
**factory_kwargs,
)
def _get_name(self):
return "DynamicQuantizedConvTranspose2d"
def forward(self, input: Tensor, reduce_range: bool = True) -> Tensor:
# Temporarily using len(shape) instead of ndim due to JIT issue
# https://github.com/pytorch/pytorch/issues/23890
if len(input.shape) != 4:
raise ValueError("Input shape must be `(N, C, H, W)`!")
return ops.quantized.conv_transpose2d_dynamic(
input, self._packed_params, reduce_range
)
class ConvTranspose3d(nnq.ConvTranspose3d):
r"""A dynamically quantized transposed convolution module with floating point tensors as inputs and outputs.
For details on input arguments, parameters, and implementation see
:class:`~torch.nn.ConvTranspose3d`.
For special notes, please, see :class:`~torch.ao.nn.quantized.dynamic.Conv3d`
Attributes:
weight (Tensor): packed tensor derived from the learnable weight
parameter.
scale (Tensor): scalar for the output scale
zero_point (Tensor): scalar for the output zero point
See :class:`~torch.nn.ConvTranspose3d` for other attributes.
Examples::
>>> # xdoctest: +SKIP
>>> # With cubic kernels and equal stride
>>> m = nnq.ConvTranspose3d(16, 33, 3, stride=2)
>>> # non-cubic kernels and unequal stride and with padding
>>> m = nnq.ConvTranspose3d(16, 33, (3, 3, 5), stride=(2, 1, 1), padding=(4, 2, 2))
>>> output = m(input)
>>> # exact output size can be also specified as an argument
>>> downsample = nnq.Conv3d(16, 16, 3, stride=2, padding=1)
>>> upsample = nnq.ConvTranspose3d(16, 16, 3, stride=2, padding=1)
>>> h = downsample(input)
>>> h.size()
torch.Size([1, 16, 6, 6, 6])
>>> output = upsample(h, output_size=input.size())
>>> output.size()
torch.Size([1, 16, 12, 12, 12])
"""
_FLOAT_MODULE: ClassVar[type[nn.ConvTranspose3d]] = nn.ConvTranspose3d
def __init__(
self,
in_channels,
out_channels,
kernel_size,
stride=1,
padding=0,
output_padding=0,
groups=1,
bias=True,
dilation=1,
padding_mode="zeros",
device=None,
dtype=None,
):
warnings.warn(
f"The current implementation of the {self._get_name()} module has poor numerical accuracy and its use is not recommended", # noqa: B950
stacklevel=2,
)
factory_kwargs = {"device": device, "dtype": dtype}
super().__init__(
in_channels,
out_channels,
kernel_size,
stride,
padding,
output_padding,
groups,
bias,
dilation,
padding_mode,
**factory_kwargs,
)
def _get_name(self):
return "DynamicQuantizedConvTranspose3d"
def forward(self, input: Tensor, reduce_range: bool = True) -> Tensor:
# Temporarily using len(shape) instead of ndim due to JIT issue
# https://github.com/pytorch/pytorch/issues/23890
if len(input.shape) != 5:
raise ValueError("Input shape must be `(N, C, T, H, W)`!")
return ops.quantized.conv_transpose3d_dynamic(
input, self._packed_params, reduce_range
)
@@ -0,0 +1,170 @@
# mypy: allow-untyped-defs
import torch
import torch.ao.nn.intrinsic as nni
import torch.ao.nn.quantized as nnq
from torch.ao.nn.quantized.modules.utils import _quantize_weight
__all__ = [
"Linear",
]
class Linear(nnq.Linear):
r"""
A dynamic quantized linear module with floating point tensor as inputs and outputs.
We adopt the same interface as `torch.nn.Linear`, please see
https://pytorch.org/docs/stable/nn.html#torch.nn.Linear for documentation.
Similar to :class:`torch.nn.Linear`, attributes will be randomly
initialized at module creation time and will be overwritten later
Attributes:
weight (Tensor): the non-learnable quantized weights of the module which are of
shape :math:`(\text{out\_features}, \text{in\_features})`.
bias (Tensor): the non-learnable floating point bias of the module of shape
:math:`(\text{out\_features})`. If :attr:`bias` is ``True``,
the values are initialized to zero.
Examples::
>>> # xdoctest: +SKIP
>>> m = nn.quantized.dynamic.Linear(20, 30)
>>> input = torch.randn(128, 20)
>>> output = m(input)
>>> print(output.size())
torch.Size([128, 30])
"""
# version used in this class is different from the parent class nnq.Linear
_version = 4
def __init__(self, in_features, out_features, bias_=True, dtype=torch.qint8):
super().__init__(in_features, out_features, bias_, dtype=dtype)
# We don't muck around with buffers or attributes or anything here
# to keep the module simple. *everything* is simply a Python attribute.
# Serialization logic is explicitly handled in the below serialization and
# deserialization modules
self.version = 4
def forward(self, x):
# Note that we can handle self.bias == None case.
if self._packed_params.dtype == torch.qint8:
if self.version is None or self.version < 4:
Y = torch.ops.quantized.linear_dynamic(
x, self._packed_params._packed_params
)
else:
Y = torch.ops.quantized.linear_dynamic(
x, self._packed_params._packed_params, reduce_range=True
)
elif self._packed_params.dtype == torch.float16:
Y = torch.ops.quantized.linear_dynamic_fp16(
x, self._packed_params._packed_params
)
else:
raise RuntimeError("Unsupported dtype on dynamic quantized linear!")
return Y.to(x.dtype)
def _get_name(self):
return "DynamicQuantizedLinear"
def extra_repr(self):
extra_repr_str = f"in_features={self.in_features}, out_features={self.out_features}, dtype={self._packed_params.dtype}"
if self._packed_params.dtype == torch.qint8:
extra_repr_str += f", qscheme={self.weight().qscheme()}"
return extra_repr_str
def _load_from_state_dict(
self,
state_dict,
prefix,
local_metadata,
strict,
missing_keys,
unexpected_keys,
error_msgs,
):
version = local_metadata.get("version", None)
self.version = version
super()._load_from_state_dict(
state_dict,
prefix,
local_metadata,
False,
missing_keys,
unexpected_keys,
error_msgs,
)
@classmethod
def from_float(cls, mod, use_precomputed_fake_quant=False):
r"""Create a dynamic quantized module from a float module or qparams_dict
Args:
mod (Module): a float module, either produced by torch.ao.quantization
utilities or provided by the user
"""
float_modules = [
torch.nn.Linear,
torch.nn.modules.linear.NonDynamicallyQuantizableLinear,
torch.ao.nn.intrinsic.modules.fused.LinearReLU,
torch.ao.nn.qat.dynamic.Linear,
]
if type(mod) not in float_modules:
raise AssertionError(
"nn.quantized.dynamic.Linear.from_float only works for one of"
+ str([float_mod.__name__ for float_mod in float_modules])
+ f", got {type(mod)}"
)
if not hasattr(mod, "qconfig"):
raise AssertionError("Input float module must have qconfig defined")
if type(mod) is nni.LinearReLU:
mod = mod[0]
if mod.qconfig is not None and mod.qconfig.weight is not None:
weight_observer = mod.qconfig.weight()
else:
# We have the circular import issues if we import the qconfig in the beginning of this file:
# https://github.com/pytorch/pytorch/pull/24231. The current workaround is to postpone the
# import until we need it.
from torch.ao.quantization.qconfig import default_dynamic_qconfig
weight_observer = default_dynamic_qconfig.weight()
dtype = weight_observer.dtype
if dtype not in [torch.qint8, torch.float16]:
raise AssertionError(
f"The only supported dtypes for dynamic quantized linear are qint8 and float16, got: {dtype}"
)
weight_observer(mod.weight)
if dtype == torch.qint8:
qweight = _quantize_weight(mod.weight.float(), weight_observer)
elif dtype == torch.float16:
qweight = mod.weight.float()
else:
raise RuntimeError(
"Unsupported dtype specified for dynamic quantized Linear!"
)
qlinear = cls(mod.in_features, mod.out_features, dtype=dtype)
qlinear.set_weight_bias(qweight, mod.bias)
return qlinear
@classmethod
def from_reference(cls, ref_qlinear): # type: ignore[override]
"""Create a (fbgemm/qnnpack) dynamic quantized module from a reference quantized
module
Args:
ref_qlinear (Module): a reference quantized module, either produced by
torch.ao.quantization functions or provided by the user
"""
qlinear = cls(
ref_qlinear.in_features,
ref_qlinear.out_features,
dtype=ref_qlinear.weight_dtype,
)
qweight = ref_qlinear.get_quantized_weight()
bias = ref_qlinear.bias
qlinear.set_weight_bias(qweight, bias)
return qlinear
@@ -0,0 +1,782 @@
# mypy: allow-untyped-defs
r"""Functional interface (quantized)."""
import warnings
import torch
from torch import Tensor
from torch.jit.annotations import BroadcastingList2
from torch.nn.modules.utils import _pair, _triple
from .modules.utils import _pair_from_first
# Although some of the functions and docstrings are mirrored from the torch.nn,
# we want to have them here for future changes.
__all__ = [
"avg_pool2d",
"avg_pool3d",
"adaptive_avg_pool2d",
"adaptive_avg_pool3d",
"conv1d",
"conv2d",
"conv3d",
"interpolate",
"linear",
"max_pool1d",
"max_pool2d",
"celu",
"leaky_relu",
"hardtanh",
"hardswish",
"threshold",
"elu",
"hardsigmoid",
"clamp",
"upsample",
"upsample_bilinear",
"upsample_nearest",
]
def avg_pool2d(
input,
kernel_size,
stride=None,
padding=0,
ceil_mode=False,
count_include_pad=True,
divisor_override=None,
):
r"""
Applies 2D average-pooling operation in :math:`kH \times kW` regions by step size
:math:`sH \times sW` steps. The number of output features is equal to the number of
input planes.
.. note:: The input quantization parameters propagate to the output.
See :class:`~torch.ao.nn.quantized.AvgPool2d` for details and output shape.
Args:
input: quantized input tensor :math:`(\text{minibatch} , \text{in\_channels} , iH , iW)`
kernel_size: size of the pooling region. Can be a single number or a
tuple `(kH, kW)`
stride: stride of the pooling operation. Can be a single number or a
tuple `(sH, sW)`. Default: :attr:`kernel_size`
padding: implicit zero paddings on both sides of the input. Can be a
single number or a tuple `(padH, padW)`. Default: 0
ceil_mode: when True, will use `ceil` instead of `floor` in the formula
to compute the output shape. Default: ``False``
count_include_pad: when True, will include the zero-padding in the
averaging calculation. Default: ``True``
divisor_override: if specified, it will be used as divisor, otherwise
size of the pooling region will be used. Default: None
"""
if not input.is_quantized:
raise ValueError("Input to 'quantized.avg_pool2d' must be quantized!")
return torch.nn.functional.avg_pool2d(
input,
kernel_size,
stride,
padding,
ceil_mode,
count_include_pad,
divisor_override,
)
def avg_pool3d(
input,
kernel_size,
stride=None,
padding=0,
ceil_mode=False,
count_include_pad=True,
divisor_override=None,
):
r"""
Applies 3D average-pooling operation in :math:`kD \ times kH \times kW` regions by step size
:math:`sD \times sH \times sW` steps. The number of output features is equal to the number of
input planes.
.. note:: The input quantization parameters propagate to the output.
Args:
input: quantized input tensor :math:`(\text{minibatch} , \text{in\_channels} , iH , iW)`
kernel_size: size of the pooling region. Can be a single number or a
tuple `(kD, kH, kW)`
stride: stride of the pooling operation. Can be a single number or a
tuple `(sD, sH, sW)`. Default: :attr:`kernel_size`
padding: implicit zero paddings on both sides of the input. Can be a
single number or a tuple `(padD, padH, padW)`. Default: 0
ceil_mode: when True, will use `ceil` instead of `floor` in the formula
to compute the output shape. Default: ``False``
count_include_pad: when True, will include the zero-padding in the
averaging calculation. Default: ``True``
divisor_override: if specified, it will be used as divisor, otherwise
size of the pooling region will be used. Default: None
"""
if not input.is_quantized:
raise ValueError("Input to 'quantized.avg_pool3d' must be quantized!")
return torch.nn.functional.avg_pool3d(
input,
kernel_size,
stride,
padding,
ceil_mode,
count_include_pad,
divisor_override,
)
def adaptive_avg_pool2d(input: Tensor, output_size: BroadcastingList2[int]) -> Tensor:
r"""
Applies a 2D adaptive average pooling over a quantized input signal composed
of several quantized input planes.
.. note:: The input quantization parameters propagate to the output.
See :class:`~torch.ao.nn.quantized.AdaptiveAvgPool2d` for details and output shape.
Args:
output_size: the target output size (single integer or
double-integer tuple)
"""
if not input.is_quantized:
raise ValueError(
"Input to 'quantized.functional.adaptive_avg_pool2d' must be quantized!"
)
return torch.nn.functional.adaptive_avg_pool2d(input, output_size)
def adaptive_avg_pool3d(input: Tensor, output_size: BroadcastingList2[int]) -> Tensor:
r"""
Applies a 3D adaptive average pooling over a quantized input signal composed
of several quantized input planes.
.. note:: The input quantization parameters propagate to the output.
See :class:`~torch.ao.nn.quantized.AdaptiveAvgPool3d` for details and output shape.
Args:
output_size: the target output size (single integer or
double-integer tuple)
"""
if not input.is_quantized:
raise ValueError(
"Input to 'quantized.functional.adaptive_avg_pool3d' must be quantized!"
)
return torch.nn.functional.adaptive_avg_pool3d(input, output_size)
def conv1d(
input,
weight,
bias,
stride=1,
padding=0,
dilation=1,
groups=1,
padding_mode="zeros",
scale=1.0,
zero_point=0,
dtype=torch.quint8,
):
r"""
Applies a 1D convolution over a quantized 1D input composed of several input
planes.
See :class:`~torch.ao.nn.quantized.Conv1d` for details and output shape.
Args:
input: quantized input tensor of shape :math:`(\text{minibatch} , \text{in\_channels} , iW)`
weight: quantized filters of shape :math:`(\text{out\_channels} , \frac{\text{in\_channels}}{\text{groups}} , iW)`
bias: **non-quantized** bias tensor of shape :math:`(\text{out\_channels})`. The tensor type must be `torch.float`.
stride: the stride of the convolving kernel. Can be a single number or a
tuple `(sW,)`. Default: 1
padding: implicit paddings on both sides of the input. Can be a
single number or a tuple `(padW,)`. Default: 0
dilation: the spacing between kernel elements. Can be a single number or
a tuple `(dW,)`. Default: 1
groups: split input into groups, :math:`\text{in\_channels}` should be divisible by the
number of groups. Default: 1
padding_mode: the padding mode to use. Only "zeros" is supported for quantized convolution at the moment. Default: "zeros"
scale: quantization scale for the output. Default: 1.0
zero_point: quantization zero_point for the output. Default: 0
dtype: quantization data type to use. Default: ``torch.quint8``
Examples::
>>> # xdoctest: +REQUIRES(env:TORCH_DOCTEST_QENGINE)
>>> from torch.ao.nn.quantized import functional as qF
>>> filters = torch.randn(33, 16, 3, dtype=torch.float)
>>> inputs = torch.randn(20, 16, 50, dtype=torch.float)
>>> bias = torch.randn(33, dtype=torch.float)
>>>
>>> scale, zero_point = 1.0, 0
>>> dtype_inputs = torch.quint8
>>> dtype_filters = torch.qint8
>>>
>>> q_filters = torch.quantize_per_tensor(filters, scale, zero_point, dtype_filters)
>>> q_inputs = torch.quantize_per_tensor(inputs, scale, zero_point, dtype_inputs)
>>> qF.conv1d(q_inputs, q_filters, bias, padding=1, scale=scale, zero_point=zero_point)
""" # noqa: E501
if padding_mode != "zeros":
raise NotImplementedError("Only zero-padding is supported!")
if input.dtype != torch.quint8:
raise NotImplementedError(
"Only torch.quint8 is supported for activation tensor!"
)
if weight.dtype != torch.qint8:
raise NotImplementedError("Only torch.qint8 is supported for weight tensor!")
if input.ndim != 3:
raise ValueError("Input shape must be `(N, C, L)`!")
stride = _pair_from_first(stride)
padding = _pair_from_first(padding)
dilation = _pair_from_first(dilation)
packed_params = torch.ops.quantized.conv1d_prepack(
weight, bias, stride, padding, dilation, groups
)
return torch.ops.quantized.conv1d(input, packed_params, scale, zero_point)
def conv2d(
input,
weight,
bias,
stride=1,
padding=0,
dilation=1,
groups=1,
padding_mode="zeros",
scale=1.0,
zero_point=0,
dtype=torch.quint8,
):
r"""
Applies a 2D convolution over a quantized 2D input composed of several input
planes.
See :class:`~torch.ao.nn.quantized.Conv2d` for details and output shape.
Args:
input: quantized input tensor of shape :math:`(\text{minibatch} , \text{in\_channels} , iH , iW)`
weight: quantized filters of shape :math:`(\text{out\_channels} , \frac{\text{in\_channels}}{\text{groups}} , kH , kW)`
bias: **non-quantized** bias tensor of shape :math:`(\text{out\_channels})`. The tensor type must be `torch.float`.
stride: the stride of the convolving kernel. Can be a single number or a
tuple `(sH, sW)`. Default: 1
padding: implicit paddings on both sides of the input. Can be a
single number or a tuple `(padH, padW)`. Default: 0
dilation: the spacing between kernel elements. Can be a single number or
a tuple `(dH, dW)`. Default: 1
groups: split input into groups, :math:`\text{in\_channels}` should be divisible by the
number of groups. Default: 1
padding_mode: the padding mode to use. Only "zeros" is supported for quantized convolution at the moment. Default: "zeros"
scale: quantization scale for the output. Default: 1.0
zero_point: quantization zero_point for the output. Default: 0
dtype: quantization data type to use. Default: ``torch.quint8``
Examples::
>>> # xdoctest: +REQUIRES(env:TORCH_DOCTEST_QENGINE)
>>> from torch.ao.nn.quantized import functional as qF
>>> filters = torch.randn(8, 4, 3, 3, dtype=torch.float)
>>> inputs = torch.randn(1, 4, 5, 5, dtype=torch.float)
>>> bias = torch.randn(8, dtype=torch.float)
>>>
>>> scale, zero_point = 1.0, 0
>>> dtype_inputs = torch.quint8
>>> dtype_filters = torch.qint8
>>>
>>> q_filters = torch.quantize_per_tensor(filters, scale, zero_point, dtype_filters)
>>> q_inputs = torch.quantize_per_tensor(inputs, scale, zero_point, dtype_inputs)
>>> qF.conv2d(q_inputs, q_filters, bias, padding=1, scale=scale, zero_point=zero_point)
""" # noqa: E501
if padding_mode != "zeros":
raise NotImplementedError("Only zero-padding is supported!")
if input.dtype != torch.quint8:
raise NotImplementedError(
"Only torch.quint8 is supported for activation tensor!"
)
if weight.dtype != torch.qint8:
raise NotImplementedError("Only torch.qint8 is supported for weight tensor!")
if input.ndim != 4:
raise ValueError("Input shape must be `(N, C, H, W)`!")
stride = _pair(stride)
padding = _pair(padding)
dilation = _pair(dilation)
packed_params = torch.ops.quantized.conv2d_prepack(
weight, bias, stride, padding, dilation, groups
)
return torch.ops.quantized.conv2d(input, packed_params, scale, zero_point)
def conv3d(
input,
weight,
bias,
stride=1,
padding=0,
dilation=1,
groups=1,
padding_mode="zeros",
scale=1.0,
zero_point=0,
dtype=torch.quint8,
):
r"""
Applies a 3D convolution over a quantized 3D input composed of several input
planes.
See :class:`~torch.ao.nn.quantized.Conv3d` for details and output shape.
Args:
input: quantized input tensor of shape
:math:`(\text{minibatch} , \text{in\_channels} , iD , iH , iW)`
weight: quantized filters of shape
:math:`(\text{out\_channels} , \frac{\text{in\_channels}}{\text{groups}} , kD , kH , kW)`
bias: **non-quantized** bias tensor of shape
:math:`(\text{out\_channels})`. The tensor type must be `torch.float`.
stride: the stride of the convolving kernel. Can be a single number or a
tuple `(sD, sH, sW)`. Default: 1
padding: implicit paddings on both sides of the input. Can be a
single number or a tuple `(padD, padH, padW)`. Default: 0
dilation: the spacing between kernel elements. Can be a single number or
a tuple `(dD, dH, dW)`. Default: 1
groups: split input into groups, :math:`\text{in\_channels}` should be
divisible by the number of groups. Default: 1
padding_mode: the padding mode to use. Only "zeros" is supported for
quantized convolution at the moment. Default: "zeros"
scale: quantization scale for the output. Default: 1.0
zero_point: quantization zero_point for the output. Default: 0
dtype: quantization data type to use. Default: ``torch.quint8``
Examples::
>>> # xdoctest: +REQUIRES(env:TORCH_DOCTEST_QENGINE)
>>> from torch.ao.nn.quantized import functional as qF
>>> filters = torch.randn(8, 4, 3, 3, 3, dtype=torch.float)
>>> inputs = torch.randn(1, 4, 5, 5, 5, dtype=torch.float)
>>> bias = torch.randn(8, dtype=torch.float)
>>>
>>> scale, zero_point = 1.0, 0
>>> dtype_inputs = torch.quint8
>>> dtype_filters = torch.qint8
>>>
>>> q_filters = torch.quantize_per_tensor(filters, scale, zero_point, dtype_filters)
>>> q_inputs = torch.quantize_per_tensor(inputs, scale, zero_point, dtype_inputs)
>>> qF.conv3d(q_inputs, q_filters, bias, padding=1, scale=scale, zero_point=zero_point)
""" # noqa: E501
if padding_mode != "zeros":
raise NotImplementedError("Only zero-padding is supported!")
if input.dtype != torch.quint8:
raise NotImplementedError(
"Only torch.quint8 is supported for activation tensor!"
)
if weight.dtype != torch.qint8:
raise NotImplementedError("Only torch.qint8 is supported for weight tensor!")
if input.ndim != 5:
raise ValueError("Input shape must be `(N, C, D, H, W)`!")
stride = _triple(stride)
padding = _triple(padding)
dilation = _triple(dilation)
packed_params = torch.ops.quantized.conv3d_prepack(
weight, bias, stride, padding, dilation, groups
)
return torch.ops.quantized.conv3d(input, packed_params, scale, zero_point)
def interpolate(
input, size=None, scale_factor=None, mode="nearest", align_corners=None
):
r"""Down/up samples the input to either the given :attr:`size` or the given
:attr:`scale_factor`
See :func:`torch.nn.functional.interpolate` for implementation details.
The input dimensions are interpreted in the form:
`mini-batch x channels x [optional depth] x [optional height] x width`.
.. note:: The input quantization parameters propagate to the output.
.. note:: Only 2D/3D input is supported for quantized inputs
.. note:: Only the following modes are supported for the quantized inputs:
- `bilinear`
- `nearest`
Args:
input (Tensor): the input tensor
size (int or Tuple[int] or Tuple[int, int] or Tuple[int, int, int]):
output spatial size.
scale_factor (float or Tuple[float]): multiplier for spatial size. Has to match input size if it is a tuple.
mode (str): algorithm used for upsampling:
``'nearest'`` | ``'bilinear'``
align_corners (bool, optional): Geometrically, we consider the pixels of the
input and output as squares rather than points.
If set to ``True``, the input and output tensors are aligned by the
center points of their corner pixels, preserving the values at the corner pixels.
If set to ``False``, the input and output tensors are aligned by the corner
points of their corner pixels, and the interpolation uses edge value padding
for out-of-boundary values, making this operation *independent* of input size
when :attr:`scale_factor` is kept the same. This only has an effect when :attr:`mode`
is ``'bilinear'``.
Default: ``False``
"""
if not input.is_quantized:
raise ValueError("Input to 'quantized.interpolate' must be quantized!")
return torch.nn.functional.interpolate(
input, size, scale_factor, mode, align_corners
)
def linear(
input: Tensor,
weight: Tensor,
bias: Tensor | None = None,
scale: float | None = None,
zero_point: int | None = None,
) -> Tensor:
r"""
Applies a linear transformation to the incoming quantized data:
:math:`y = xA^T + b`.
See :class:`~torch.ao.nn.quantized.Linear`
.. note::
Current implementation packs weights on every call, which has penalty on performance.
If you want to avoid the overhead, use :class:`~torch.ao.nn.quantized.Linear`.
Args:
input (Tensor): Quantized input of type `torch.quint8`
weight (Tensor): Quantized weight of type `torch.qint8`
bias (Tensor): None or fp32 bias of type `torch.float`
scale (double): output scale. If None, derived from the input scale
zero_point (long): output zero point. If None, derived from the input zero_point
Shape:
- Input: :math:`(N, *, in\_features)` where `*` means any number of
additional dimensions
- Weight: :math:`(out\_features, in\_features)`
- Bias: :math:`(out\_features)`
- Output: :math:`(N, *, out\_features)`
"""
if scale is None:
scale = input.q_scale()
if zero_point is None:
zero_point = input.q_zero_point()
_packed_params = torch.ops.quantized.linear_prepack(weight, bias)
return torch.ops.quantized.linear(input, _packed_params, scale, zero_point)
def max_pool1d(
input,
kernel_size,
stride=None,
padding=0,
dilation=1,
ceil_mode=False,
return_indices=False,
):
r"""Applies a 1D max pooling over a quantized input signal composed of
several quantized input planes.
.. note:: The input quantization parameters are propagated to the output.
See :class:`~torch.ao.nn.quantized.MaxPool1d` for details.
"""
if return_indices:
raise NotImplementedError("return_indices is not yet implemented!")
if stride is None:
stride = torch.jit.annotate(list[int], [])
return torch.nn.functional.max_pool1d(
input,
kernel_size,
stride,
padding,
dilation,
ceil_mode=ceil_mode,
return_indices=return_indices,
)
def max_pool2d(
input,
kernel_size,
stride=None,
padding=0,
dilation=1,
ceil_mode=False,
return_indices=False,
):
r"""Applies a 2D max pooling over a quantized input signal composed of
several quantized input planes.
.. note:: The input quantization parameters are propagated to the output.
See :class:`~torch.ao.nn.quantized.MaxPool2d` for details.
"""
if return_indices:
raise NotImplementedError("return_indices is not yet implemented!")
if stride is None:
stride = torch.jit.annotate(list[int], [])
return torch.nn.functional.max_pool2d(
input,
kernel_size,
stride,
padding,
dilation,
ceil_mode=ceil_mode,
return_indices=return_indices,
)
def celu(input: Tensor, scale: float, zero_point: int, alpha: float = 1.0) -> Tensor:
r"""celu(input, scale, zero_point, alpha=1.) -> Tensor
Applies the quantized CELU function element-wise.
.. math::
\text{CELU}(x) = \max(0,x) + \min(0, \alpha * (\exp(x / \alpha) - 1))
Args:
input: quantized input
alpha: the :math:`\alpha` value for the CELU formulation. Default: 1.0
"""
if not input.is_quantized:
raise ValueError("Input to 'quantized.celu' must be quantized!")
return torch.ops.quantized.celu(input, scale, zero_point, alpha)
def leaky_relu(
input: Tensor,
negative_slope: float = 0.01,
inplace: bool = False,
scale: float | None = None,
zero_point: int | None = None,
):
r"""
Quantized version of the.
leaky_relu(input, negative_slope=0.01, inplace=False, scale, zero_point) -> Tensor
Applies element-wise,
:math:`\text{LeakyReLU}(x) = \max(0, x) + \text{negative\_slope} * \min(0, x)`
Args:
input: Quantized input
negative_slope: The slope of the negative input
inplace: Inplace modification of the input tensor
scale, zero_point: Scale and zero point of the output tensor.
See :class:`~torch.nn.LeakyReLU` for more details.
"""
if scale is not None and zero_point is not None:
if inplace:
raise AssertionError("Cannot rescale with `inplace`")
output = torch._empty_affine_quantized(
input.shape, scale=scale, zero_point=int(zero_point), dtype=input.dtype
)
torch._C._nn.leaky_relu(input, negative_slope, out=output)
return output
if inplace:
result = torch._C._nn.leaky_relu_(input, negative_slope)
else:
result = torch._C._nn.leaky_relu(input, negative_slope)
return result
def hardtanh(
input: Tensor, min_val: float = -1.0, max_val: float = 1.0, inplace: bool = False
) -> Tensor:
r"""This is the quantized version of :func:`~torch.nn.functional.hardtanh`."""
if not input.is_quantized:
raise ValueError("Input to 'quantized.hardtanh' must be quantized!")
if inplace:
return torch._C._nn.hardtanh_(input, min_val, max_val)
return torch._C._nn.hardtanh(input, min_val, max_val)
def hardswish(input: Tensor, scale: float, zero_point: int) -> Tensor:
r"""This is the quantized version of :func:`~torch.nn.functional.hardswish`.
Args:
input: quantized input
scale: quantization scale of the output tensor
zero_point: quantization zero point of the output tensor
"""
if not input.is_quantized:
raise ValueError("Input to 'quantized.hardswish' must be quantized!")
return torch._ops.ops.quantized.hardswish(input, scale, zero_point)
def threshold(input: Tensor, threshold: float, value: float) -> Tensor:
r"""Applies the quantized version of the threshold function element-wise:
.. math::
x = \begin{cases}
x & \text{if~} x > \text{threshold} \\
\text{value} & \text{otherwise}
\end{cases}
See :class:`~torch.nn.Threshold` for more details.
"""
if not input.is_quantized:
raise ValueError("Input to 'quantized.threshold' must be quantized!")
if threshold is None:
raise ValueError("Input to 'threshold' must be specified!")
if value is None:
raise ValueError("Input to 'value' must be specified!")
return torch._ops.ops.quantized.threshold(input, threshold, value)
def elu(input: Tensor, scale: float, zero_point: int, alpha: float = 1.0) -> Tensor:
r"""This is the quantized version of :func:`~torch.nn.functional.elu`.
Args:
input: quantized input
scale: quantization scale of the output tensor
zero_point: quantization zero point of the output tensor
alpha: the alpha constant
"""
if not input.is_quantized:
raise ValueError("Input to 'quantized.elu' must be quantized!")
return torch.ops.quantized.elu(input, scale, zero_point, alpha)
def hardsigmoid(input: Tensor, inplace: bool = False) -> Tensor:
r"""This is the quantized version of :func:`~torch.nn.functional.hardsigmoid`."""
if not input.is_quantized:
raise ValueError("Input to 'quantized.hardsigmoid' must be quantized!")
if inplace:
return torch._C._nn.hardsigmoid_(input) # type: ignore[attr-defined]
return torch._C._nn.hardsigmoid(input)
def clamp(input: Tensor, min_: float, max_: float) -> Tensor:
r"""float(input, min\_, max\_) -> Tensor
Applies the clamp function element-wise.
See :class:`~torch.ao.nn.quantized.clamp` for more details.
Args:
input: quantized input
min_: minimum value for clamping
max_: maximum value for clamping
"""
if not input.is_quantized:
raise ValueError("Input to 'quantized.clamp' must be quantized!")
return torch.clamp(input, min_, max_)
def upsample(input, size=None, scale_factor=None, mode="nearest", align_corners=None):
r"""Upsamples the input to either the given :attr:`size` or the given
:attr:`scale_factor`
.. warning::
This function is deprecated in favor of
:func:`torch.ao.nn.quantized.functional.interpolate`.
This is equivalent with ``nn.quantized.functional.interpolate(...)``.
See :func:`torch.nn.functional.interpolate` for implementation details.
The input dimensions are interpreted in the form:
`mini-batch x channels x [optional depth] x [optional height] x width`.
.. note:: The input quantization parameters propagate to the output.
.. note:: Only 2D input is supported for quantized inputs
.. note:: Only the following modes are supported for the quantized inputs:
- `bilinear`
- `nearest`
Args:
input (Tensor): quantized input tensor
size (int or Tuple[int] or Tuple[int, int] or Tuple[int, int, int]):
output spatial size.
scale_factor (float or Tuple[float]): multiplier for spatial size. Has to be an integer.
mode (str): algorithm used for upsampling:
``'nearest'`` | ``'bilinear'``
align_corners (bool, optional): Geometrically, we consider the pixels of the
input and output as squares rather than points.
If set to ``True``, the input and output tensors are aligned by the
center points of their corner pixels, preserving the values at the corner pixels.
If set to ``False``, the input and output tensors are aligned by the corner
points of their corner pixels, and the interpolation uses edge value padding
for out-of-boundary values, making this operation *independent* of input size
when :attr:`scale_factor` is kept the same. This only has an effect when :attr:`mode`
is ``'bilinear'``.
Default: ``False``
.. warning::
With ``align_corners = True``, the linearly interpolating modes
(`bilinear`) don't proportionally align the
output and input pixels, and thus the output values can depend on the
input size. This was the default behavior for these modes up to version
0.3.1. Since then, the default behavior is ``align_corners = False``.
See :class:`~torch.nn.Upsample` for concrete examples on how this
affects the outputs.
"""
warnings.warn(
"nn.quantized.functional.upsample is deprecated. Use nn.quantized.functional.interpolate instead.",
stacklevel=2,
)
return interpolate(input, size, scale_factor, mode, align_corners)
def upsample_bilinear(input, size=None, scale_factor=None):
r"""Upsamples the input, using bilinear upsampling.
.. warning::
This function is deprecated in favor of
:func:`torch.ao.nn.quantized.functional.interpolate`.
This is equivalent with
``nn.quantized.functional.interpolate(..., mode='bilinear', align_corners=True)``.
.. note:: The input quantization parameters propagate to the output.
.. note:: Only 2D inputs are supported
Args:
input (Tensor): quantized input
size (int or Tuple[int, int]): output spatial size.
scale_factor (int or Tuple[int, int]): multiplier for spatial size
"""
# DeprecationWarning is ignored by default
warnings.warn(
"nn.quantized.functional.upsample_bilinear is deprecated. Use nn.quantized.functional.interpolate instead.",
stacklevel=2,
)
return interpolate(input, size, scale_factor, mode="bilinear", align_corners=True)
def upsample_nearest(input, size=None, scale_factor=None):
r"""Upsamples the input, using nearest neighbours' pixel values.
.. warning::
This function is deprecated in favor of
:func:`torch.ao.nn.quantized.functional.interpolate`.
This is equivalent with ``nn.quantized.functional.interpolate(..., mode='nearest')``.
.. note:: The input quantization parameters propagate to the output.
.. note:: Only 2D inputs are supported
Args:
input (Tensor): quantized input
size (int or Tuple[int, int] or Tuple[int, int, int]): output spatial
size.
scale_factor (int): multiplier for spatial size. Has to be an integer.
"""
# DeprecationWarning is ignored by default
warnings.warn(
"nn.quantized.functional.upsample_nearest is deprecated. Use nn.quantized.functional.interpolate instead.",
stacklevel=2,
)
return interpolate(input, size, scale_factor, mode="nearest")
@@ -0,0 +1,165 @@
# mypy: allow-untyped-defs
import torch
# The quantized modules use `torch.nn` and `torch.ao.nn.quantizable`
# packages. However, the `quantizable` package uses "lazy imports"
# to avoid circular dependency.
# Hence we need to include it here to make sure it is resolved before
# they are used in the modules.
import torch.ao.nn.quantizable
from torch.nn.modules.pooling import MaxPool2d
from .activation import (
ELU,
Hardswish,
LeakyReLU,
MultiheadAttention,
PReLU,
ReLU6,
Sigmoid,
Softmax,
)
from .batchnorm import BatchNorm2d, BatchNorm3d
from .conv import (
Conv1d,
Conv2d,
Conv3d,
ConvTranspose1d,
ConvTranspose2d,
ConvTranspose3d,
)
from .dropout import Dropout
from .embedding_ops import Embedding, EmbeddingBag
from .functional_modules import FloatFunctional, FXFloatFunctional, QFunctional
from .linear import Linear
from .normalization import (
GroupNorm,
InstanceNorm1d,
InstanceNorm2d,
InstanceNorm3d,
LayerNorm,
)
from .rnn import LSTM
__all__ = [
"BatchNorm2d",
"BatchNorm3d",
"Conv1d",
"Conv2d",
"Conv3d",
"ConvTranspose1d",
"ConvTranspose2d",
"ConvTranspose3d",
"DeQuantize",
"ELU",
"Embedding",
"EmbeddingBag",
"GroupNorm",
"Hardswish",
"InstanceNorm1d",
"InstanceNorm2d",
"InstanceNorm3d",
"LayerNorm",
"LeakyReLU",
"Linear",
"LSTM",
"MultiheadAttention",
"Quantize",
"ReLU6",
"Sigmoid",
"Softmax",
"Dropout",
"PReLU",
# Wrapper modules
"FloatFunctional",
"FXFloatFunctional",
"QFunctional",
]
class Quantize(torch.nn.Module):
r"""Quantizes an incoming tensor
Args:
`scale`: scale of the output Quantized Tensor
`zero_point`: zero_point of output Quantized Tensor
`dtype`: data type of output Quantized Tensor
`factory_kwargs`: Dictionary of kwargs used for configuring initialization
of internal buffers. Currently, `device` and `dtype` are supported.
Example: `factory_kwargs={'device': 'cuda', 'dtype': torch.float64}`
will initialize internal buffers as type `torch.float64` on the current CUDA device.
Note that `dtype` only applies to floating-point buffers.
Examples::
>>> t = torch.tensor([[1., -1.], [1., -1.]])
>>> scale, zero_point, dtype = 1.0, 2, torch.qint8
>>> qm = Quantize(scale, zero_point, dtype)
>>> # xdoctest: +SKIP
>>> qt = qm(t)
>>> print(qt)
tensor([[ 1., -1.],
[ 1., -1.]], size=(2, 2), dtype=torch.qint8, scale=1.0, zero_point=2)
"""
scale: torch.Tensor
zero_point: torch.Tensor
def __init__(self, scale, zero_point, dtype, factory_kwargs=None):
factory_kwargs = torch.nn.factory_kwargs(factory_kwargs)
super().__init__()
self.register_buffer("scale", torch.tensor([scale], **factory_kwargs))
self.register_buffer(
"zero_point",
torch.tensor(
[zero_point],
dtype=torch.long,
**{k: v for k, v in factory_kwargs.items() if k != "dtype"},
),
)
self.dtype = dtype
def forward(self, X):
return torch.quantize_per_tensor(
X, float(self.scale), int(self.zero_point), self.dtype
)
@staticmethod
def from_float(mod, use_precomputed_fake_quant=False):
if not hasattr(mod, "activation_post_process"):
raise AssertionError(
f"Module {type(mod).__name__} must have activation_post_process attribute"
)
scale, zero_point = mod.activation_post_process.calculate_qparams()
return Quantize(
scale.float().item(),
zero_point.long().item(),
mod.activation_post_process.dtype,
)
def extra_repr(self):
return f"scale={self.scale}, zero_point={self.zero_point}, dtype={self.dtype}"
class DeQuantize(torch.nn.Module):
r"""Dequantizes an incoming tensor
Examples::
>>> input = torch.tensor([[1., -1.], [1., -1.]])
>>> scale, zero_point, dtype = 1.0, 2, torch.qint8
>>> qm = Quantize(scale, zero_point, dtype)
>>> # xdoctest: +SKIP
>>> quantized_input = qm(input)
>>> dqm = DeQuantize()
>>> dequantized = dqm(quantized_input)
>>> print(dequantized)
tensor([[ 1., -1.],
[ 1., -1.]], dtype=torch.float32)
"""
def forward(self, Xq):
return Xq.dequantize()
@staticmethod
def from_float(mod, use_precomputed_fake_quant=False):
return DeQuantize()
@@ -0,0 +1,346 @@
# mypy: allow-untyped-defs
from warnings import warn
import torch
__all__ = [
"ReLU6",
"Hardswish",
"ELU",
"LeakyReLU",
"Sigmoid",
"Softmax",
"MultiheadAttention",
"PReLU",
]
class ReLU6(torch.nn.ReLU):
r"""Applies the element-wise function:
:math:`\text{ReLU6}(x) = \min(\max(x_0, x), q(6))`, where :math:`x_0` is the
zero_point, and :math:`q(6)` is the quantized representation of number 6.
Args:
inplace: can optionally do the operation in-place. Default: ``False``
Shape:
- Input: :math:`(N, *)` where `*` means, any number of additional
dimensions
- Output: :math:`(N, *)`, same shape as the input
.. image:: ../scripts/activation_images/ReLU6.png
Examples::
>>> m = nn.quantized.ReLU6()
>>> input = torch.randn(2)
>>> # xdoctest: +SKIP
>>> input = torch.quantize_per_tensor(input, 1.0, 0, dtype=torch.qint32)
>>> output = m(input)
"""
def __init__(self, inplace=False):
super().__init__(inplace)
self.inplace = inplace
def forward(self, input):
return torch.ops.quantized.relu6(input, self.inplace)
def _get_name(self):
return "QuantizedReLU6"
@staticmethod
def from_float(mod, use_precomputed_fake_quant=False):
return ReLU6(mod.inplace)
class Hardswish(torch.nn.Hardswish):
r"""This is the quantized version of :class:`~torch.nn.Hardswish`.
Args:
scale: quantization scale of the output tensor
zero_point: quantization zero point of the output tensor
"""
def __init__(self, scale, zero_point, device=None, dtype=None):
factory_kwargs = {"device": device, "dtype": dtype}
super().__init__()
self.register_buffer("scale", torch.tensor(scale, **factory_kwargs))
self.register_buffer("zero_point", torch.tensor(zero_point, **factory_kwargs))
def forward(self, input):
return torch.ops.quantized.hardswish(input, self.scale, self.zero_point)
def _get_name(self):
return "QuantizedHardswish"
@staticmethod
def from_float(mod, use_precomputed_fake_quant=False):
scale, zero_point = mod.activation_post_process.calculate_qparams()
return Hardswish(float(scale), int(zero_point))
@classmethod
def from_reference(cls, mod, scale, zero_point):
return cls(float(scale), int(zero_point))
class ELU(torch.nn.ELU):
r"""This is the quantized equivalent of :class:`~torch.nn.ELU`.
Args:
scale: quantization scale of the output tensor
zero_point: quantization zero point of the output tensor
alpha: the alpha constant
"""
def __init__(self, scale, zero_point, alpha=1.0):
super().__init__(alpha)
self.scale = scale
self.zero_point = zero_point
def forward(self, input):
return torch.ao.nn.quantized.functional.elu(
input, self.scale, self.zero_point, self.alpha
)
def _get_name(self):
return "QuantizedELU"
@staticmethod
def from_float(mod, use_precomputed_fake_quant=False):
scale, zero_point = mod.activation_post_process.calculate_qparams()
return ELU(float(scale), int(zero_point), mod.alpha)
@classmethod
def from_reference(cls, mod, scale, zero_point):
return cls(float(scale), int(zero_point), mod.alpha)
class LeakyReLU(torch.nn.LeakyReLU):
r"""This is the quantized equivalent of :class:`~torch.nn.LeakyReLU`.
Args:
scale: quantization scale of the output tensor
zero_point: quantization zero point of the output tensor
negative_slope: Controls the angle of the negative slope. Default: 1e-2
"""
def __init__(
self,
scale: float,
zero_point: int,
negative_slope: float = 1e-2,
inplace: bool = False,
device=None,
dtype=None,
) -> None:
factory_kwargs = {"device": device, "dtype": dtype}
super().__init__(negative_slope, inplace)
self.register_buffer("scale", torch.tensor(scale, **factory_kwargs))
self.register_buffer("zero_point", torch.tensor(zero_point, **factory_kwargs))
def forward(self, input):
return torch.ops.quantized.leaky_relu(
input, self.negative_slope, self.inplace, self.scale, self.zero_point
)
def _get_name(self):
return "QuantizedLeakyReLU"
@classmethod
def from_float(cls, mod, use_precomputed_fake_quant=False):
scale, zero_point = mod.activation_post_process.calculate_qparams()
return cls(float(scale), int(zero_point), mod.negative_slope, mod.inplace)
@classmethod
def from_reference(cls, mod, scale, zero_point):
return cls(float(scale), int(zero_point), mod.negative_slope, mod.inplace)
class Sigmoid(torch.nn.Sigmoid):
r"""This is the quantized equivalent of :class:`~torch.nn.Sigmoid`.
Args:
scale: quantization scale of the output tensor
zero_point: quantization zero point of the output tensor
"""
def __init__(self, output_scale: float, output_zero_point: int):
super().__init__()
self.output_scale = output_scale
self.output_zero_point = output_zero_point
def forward(self, input):
return torch.ops.quantized.sigmoid(
input, self.output_scale, self.output_zero_point
)
@classmethod
def from_float(cls, mod, use_precomputed_fake_quant=False):
(
output_scale,
output_zero_point,
) = mod.activation_post_process.calculate_qparams()
return cls(float(output_scale), int(output_zero_point))
class Softmax(torch.nn.Softmax):
r"""This is the quantized version of :class:`~torch.nn.Softmax`.
Args:
dim: A dimension along which Softmax will be computed (so every slice along dim will sum to 1).
scale: quantization scale of the output tensor
zero_point: quantization zero point of the output tensor
"""
def __init__(self, dim=None, scale=1.0, zero_point=0):
super().__init__()
self.dim = dim
self.scale = scale
self.zero_point = zero_point
def forward(self, input):
dim = self.dim
if dim is None:
stacklevel = 3
# Note: adding the mypy ignore on _get_softmax_dim seems less bad
# than making `_get_softmax_dim` an official API.
dim = torch.nn.functional._get_softmax_dim( # type: ignore[attr-defined]
"softmax", input.dim(), stacklevel
)
return torch.ops.quantized.softmax(input, dim, self.scale, self.zero_point)
def _get_name(self):
return "QuantizedSoftmax"
@staticmethod
def from_float(mod, use_precomputed_fake_quant=False):
scale, zero_point = mod.activation_post_process.calculate_qparams()
return Softmax(mod.dim, float(scale), int(zero_point))
@classmethod
def from_reference(cls, mod, scale, zero_point):
return cls(mod.dim, float(scale), int(zero_point))
class MultiheadAttention(torch.ao.nn.quantizable.MultiheadAttention):
_FLOAT_MODULE = torch.ao.nn.quantizable.MultiheadAttention
def _get_name(self):
return "QuantizedMultiheadAttention"
@classmethod
def from_float(cls, other):
# The whole flow is float -> observed -> quantized
# This class does observed -> quantized only
raise NotImplementedError(
"It looks like you are trying to convert a "
"non-observed MHA module. Please, see "
"the examples on quantizable MHAs."
)
@classmethod
def from_observed(cls, other):
converted = torch.ao.quantization.convert(
other,
mapping=None,
inplace=False,
remove_qconfig=True,
convert_custom_config_dict=None,
)
converted.__class__ = cls
# Remove the parameters for the bias_k and bias_v to quantize them
# TODO: This is a potential source of accuracy drop.
# quantized cat takes the scale and zp of the first
# element, which might lose the precision in the bias_k
# and the bias_v (which are cat'ed with k/v being first).
if converted.bias_k is not None:
bias_k = converted._parameters.pop("bias_k")
sc, zp = torch._choose_qparams_per_tensor(bias_k, reduce_range=False)
bias_k = torch.quantize_per_tensor(bias_k, sc, zp, torch.quint8)
setattr(converted, "bias_k", bias_k) # noqa: B010
if converted.bias_v is not None:
bias_v = converted._parameters.pop("bias_v")
sc, zp = torch._choose_qparams_per_tensor(
bias_k, # type: ignore[possibly-undefined]
reduce_range=False,
)
bias_v = torch.quantize_per_tensor(bias_v, sc, zp, torch.quint8)
setattr(converted, "bias_v", bias_v) # noqa: B010
del converted.in_proj_weight
del converted.in_proj_bias
return converted
class PReLU(torch.nn.Module):
r"""This is the quantized equivalent of :class:`~torch.nn.PReLU`.
Args:
scale: quantization scale of the output tensor
zero_point: quantization zero point of the output tensor
num_parameters: number of parameters: 1, or the number of channels at input. Default: 1
"""
def __init__(
self, output_scale: float, output_zero_point: int, num_parameters: int = 1
) -> None:
super().__init__()
self.num_parameters = num_parameters
self.scale = output_scale
self.zero_point = output_zero_point
w = torch.randn(num_parameters, dtype=torch.float)
qw = torch.quantize_per_tensor(w, scale=1.0, zero_point=0, dtype=torch.quint8)
self.set_weight(qw)
def set_weight(self, w: torch.Tensor) -> None:
self.weight = w
def forward(self, input: torch.Tensor) -> torch.Tensor:
return torch.ops.quantized.prelu(
input, self.weight, self.scale, self.zero_point
)
def _get_name(self):
return "QuantizedPReLU"
@classmethod
def from_float(cls, mod, use_precomputed_fake_quant=False):
scale, zero_point = mod.activation_post_process.calculate_qparams()
qprelu = cls(float(scale), int(zero_point), mod.num_parameters)
float_wt = mod.weight.float()
observer = mod.qconfig.weight()
observer(float_wt)
if observer.dtype != torch.quint8:
warn(
f"PReLU's weight observer should have dtype quint8 but got {observer.dtype}",
stacklevel=2,
)
wt_scale, wt_zp = observer.calculate_qparams()
qweight = torch.quantize_per_tensor(
float_wt, float(wt_scale), int(wt_zp), torch.quint8
)
qprelu.set_weight(qweight)
return qprelu
@classmethod
def from_reference(cls, mod, scale, zero_point):
qprelu = cls(float(scale), int(zero_point), mod.num_parameters)
float_wt = mod.weight.float()
observer = mod.qconfig.weight()
observer(float_wt)
if observer.dtype != torch.quint8:
warn(
f"PReLU's weight observer should have dtype quint8 but got {observer.dtype}",
stacklevel=2,
)
wt_scale, wt_zp = observer.calculate_qparams()
qweight = torch.quantize_per_tensor(
float_wt, float(wt_scale), int(wt_zp), torch.quint8
)
qprelu.set_weight(qweight)
return qprelu
@@ -0,0 +1,128 @@
# mypy: allow-untyped-defs
import torch
import torch.ao.nn.intrinsic as nni
__all__ = ["BatchNorm2d", "BatchNorm3d"]
class _BatchNorm(torch.nn.modules.batchnorm._BatchNorm):
def __init__(
self, num_features, eps=1e-5, momentum=0.1, device=None, dtype=None
) -> None:
factory_kwargs = {"device": device, "dtype": dtype}
super().__init__(num_features, eps, momentum, True, True, **factory_kwargs)
self.register_buffer("scale", torch.tensor(1.0, **factory_kwargs))
self.register_buffer("zero_point", torch.tensor(0, **factory_kwargs))
@staticmethod
def from_float(cls, mod, use_precomputed_fake_quant=False):
activation_post_process = mod.activation_post_process
if type(mod) is cls._NNI_BN_RELU_MODULE:
mod = mod[0]
scale, zero_point = activation_post_process.calculate_qparams()
new_mod = cls(mod.num_features, mod.eps)
new_mod.weight = mod.weight
new_mod.bias = mod.bias
new_mod.running_mean = mod.running_mean
new_mod.running_var = mod.running_var
new_mod.scale = scale
new_mod.zero_point = zero_point
return new_mod
@classmethod
def from_reference(cls, bn, output_scale, output_zero_point):
qbn = cls(
bn.num_features,
bn.eps,
bn.momentum,
device=bn.weight.device,
dtype=bn.weight.dtype,
)
qbn.weight = bn.weight
qbn.bias = bn.bias
qbn.running_mean = bn.running_mean
qbn.running_var = bn.running_var
qbn.scale = output_scale
qbn.zero_point = output_zero_point
return qbn
class BatchNorm2d(_BatchNorm):
r"""This is the quantized version of :class:`~torch.nn.BatchNorm2d`."""
_NNI_BN_RELU_MODULE = nni.BNReLU2d
def __init__(
self, num_features, eps=1e-5, momentum=0.1, device=None, dtype=None
) -> None:
factory_kwargs = {"device": device, "dtype": dtype}
super().__init__(num_features, eps, momentum, **factory_kwargs)
def _get_name(self):
return "QuantizedBatchNorm2d"
def _check_input_dim(self, input):
# Temporarily using len(shape) instead of ndim due to JIT issue
# https://github.com/pytorch/pytorch/issues/23890
if len(input.shape) != 4:
raise ValueError("Input shape must be `(N, C, H, W)`!")
def forward(self, input: torch.Tensor) -> torch.Tensor:
# disabling this since this is not symbolically traceable
# self._check_input_dim(input)
return torch.ops.quantized.batch_norm2d(
input,
self.weight,
self.bias,
self.running_mean,
self.running_var,
self.eps,
self.scale,
self.zero_point,
)
@classmethod
def from_float(cls, mod, use_precomputed_fake_quant=False): # type: ignore[override]
return _BatchNorm.from_float(
cls, mod, use_precomputed_fake_quant=use_precomputed_fake_quant
)
class BatchNorm3d(_BatchNorm):
r"""This is the quantized version of :class:`~torch.nn.BatchNorm3d`."""
_NNI_BN_RELU_MODULE = nni.BNReLU3d
def __init__(self, num_features, eps=1e-5, momentum=0.1, device=None, dtype=None):
factory_kwargs = {"device": device, "dtype": dtype}
super().__init__(num_features, eps, momentum, **factory_kwargs)
def _get_name(self):
return "QuantizedBatchNorm3d"
def _check_input_dim(self, input):
# Temporarily using len(shape) instead of ndim due to JIT issue
# https://github.com/pytorch/pytorch/issues/23890
if len(input.shape) != 5:
raise ValueError("Input shape must be `(N, C, H, W)`!")
def forward(self, input: torch.Tensor) -> torch.Tensor:
# disabling this since this is not symbolically traceable
# self._check_input_dim(input)
return torch.ops.quantized.batch_norm3d(
input,
self.weight,
self.bias,
self.running_mean,
self.running_var,
self.eps,
self.scale,
self.zero_point,
)
@classmethod
def from_float(cls, mod, use_precomputed_fake_quant=False): # type: ignore[override]
return _BatchNorm.from_float(
cls, mod, use_precomputed_fake_quant=use_precomputed_fake_quant
)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,30 @@
# mypy: allow-untyped-defs
import torch
__all__ = ["Dropout"]
class Dropout(torch.nn.Dropout):
r"""This is the quantized equivalent of :class:`~torch.nn.Dropout`.
And this is a placeholder to enable models where fp32 tensors
had dropout to work with quantized tensors in train and eval mode.
Args:
p: probability of an element to be zeroed
inplace: can optionally do the operation in-place. Default: ``False``
"""
def forward(self, input):
return input
def _get_name(self):
return "QuantizedDropout"
@classmethod
def from_float(cls, mod, use_precomputed_fake_quant=False):
return cls(mod.p, mod.inplace)
@classmethod
def from_reference(cls, mod, scale, zero_point):
return cls(mod.p, mod.inplace)
@@ -0,0 +1,424 @@
# mypy: allow-untyped-defs
import torch
import torch.nn as nn
from torch import Tensor # noqa: F401
from torch._jit_internal import List, Optional # noqa: F401
from .utils import _hide_packed_params_repr, _quantize_weight
__all__ = ["EmbeddingPackedParams", "Embedding", "EmbeddingBag"]
class EmbeddingPackedParams(torch.nn.Module):
_version = 1
def __init__(self, num_embeddings, embedding_dim, dtype=torch.quint8):
super().__init__()
self.dtype = dtype
if self.dtype in [torch.quint8, torch.quint4x2]:
scales = torch.ones(num_embeddings, dtype=torch.float)
zero_points = torch.zeros(num_embeddings, dtype=torch.float)
wq = torch._empty_per_channel_affine_quantized(
[num_embeddings, embedding_dim],
scales=scales,
zero_points=zero_points,
axis=0,
dtype=self.dtype,
)
self.set_weight(wq)
else:
raise NotImplementedError(
f"Unsupported dtype on quantized embedding! Supports quint8 and quint4x2. Got dtype: {dtype}"
)
@torch.jit.export
def set_weight(self, weight: torch.Tensor) -> None:
if self.dtype in [torch.quint8, torch.quint4x2]:
self._packed_weight = torch.ops.quantized.embedding_bag_prepack(weight)
else:
raise NotImplementedError(
"Unsupported dtype for quantized embedding prepack! Supports quint8 and quint4x2."
)
@torch.jit.export
def _weight(self):
if self.dtype in [torch.quint8, torch.quint4x2]:
return torch.ops.quantized.embedding_bag_unpack(self._packed_weight)
else:
raise NotImplementedError(
"Unsupported dtype for quantized embedding unpack! Supports quint8 and quint4x2."
)
def forward(self, x):
return x
# Version 1
# self
# |--- _packed_weight : Tensor representing weight of EmbeddingPackedParamsBase
# |--- dtype : torch.dtype
def _save_to_state_dict(self, destination, prefix, keep_vars):
super()._save_to_state_dict(destination, prefix, keep_vars)
destination[prefix + "dtype"] = self.dtype
destination[prefix + "_packed_weight"] = self._weight()
def _load_from_state_dict(
self,
state_dict,
prefix,
local_metadata,
strict,
missing_keys,
unexpected_keys,
error_msgs,
):
self.dtype = state_dict[prefix + "dtype"]
state_dict.pop(prefix + "dtype")
weight = state_dict[prefix + "_packed_weight"]
state_dict.pop(prefix + "_packed_weight")
self.set_weight(weight)
super()._load_from_state_dict(
state_dict,
prefix,
local_metadata,
False,
missing_keys,
unexpected_keys,
error_msgs,
)
def __repr__(self):
return self._weight().__repr__()
class Embedding(torch.nn.Module):
r"""
A quantized Embedding module with quantized packed weights as inputs.
We adopt the same interface as `torch.nn.Embedding`, please see
https://pytorch.org/docs/stable/generated/torch.nn.Embedding.html for documentation.
Similar to :class:`~torch.nn.Embedding`, attributes will be randomly
initialized at module creation time and will be overwritten later
Attributes:
weight (Tensor): the non-learnable quantized weights of the module of
shape :math:`(\text{num\_embeddings}, \text{embedding\_dim})`.
Examples::
>>> m = nn.quantized.Embedding(num_embeddings=10, embedding_dim=12)
>>> indices = torch.tensor([9, 6, 5, 7, 8, 8, 9, 2, 8])
>>> output = m(indices)
>>> print(output.size())
torch.Size([9, 12])
"""
_version = 1
def __init__(
self,
num_embeddings: int,
embedding_dim: int,
padding_idx: Optional[int] = None,
max_norm: Optional[float] = None,
norm_type: float = 2.0,
scale_grad_by_freq: bool = False,
sparse: bool = False,
_weight: Optional[Tensor] = None,
dtype=torch.quint8,
) -> None:
super().__init__()
self.num_embeddings = num_embeddings
self.embedding_dim = embedding_dim
self.dtype = dtype
if _weight is None:
scales = torch.ones(num_embeddings, dtype=torch.float)
zero_points = torch.zeros(num_embeddings, dtype=torch.float)
qweight = torch._empty_per_channel_affine_quantized(
[num_embeddings, embedding_dim],
scales=scales,
zero_points=zero_points,
axis=0,
dtype=torch.quint8,
)
else:
expected_shape = [num_embeddings, embedding_dim]
if list(_weight.shape) != expected_shape:
raise AssertionError(
f"Shape of weight does not match num_embeddings and embedding_dim: "
f"expected {expected_shape}, got {list(_weight.shape)}"
)
qweight = _weight
self._packed_params = EmbeddingPackedParams(
num_embeddings, embedding_dim, dtype
)
self._packed_params.set_weight(qweight)
def forward(self, indices: Tensor) -> Tensor:
if self.dtype == torch.quint4x2:
return torch.ops.quantized.embedding_4bit(
self._packed_params._packed_weight, indices
)
else:
return torch.ops.quantized.embedding_byte(
self._packed_params._packed_weight, indices
)
def _get_name(self):
return "QuantizedEmbedding"
def __repr__(self):
return _hide_packed_params_repr(self, EmbeddingPackedParams)
def extra_repr(self):
extra_repr_str = (
f"num_embeddings={self.num_embeddings}, embedding_dim={self.embedding_dim}, "
f"dtype={self._packed_params.dtype}, qscheme={self.weight().qscheme()}"
)
return extra_repr_str
def set_weight(self, w: torch.Tensor) -> None:
self._packed_params.set_weight(w)
def weight(self):
return self._packed_params._weight()
@classmethod
def from_float(cls, mod, use_precomputed_fake_quant=False):
r"""Create a quantized embedding module from a float module
Args:
mod (Module): a float module, either produced by torch.ao.quantization
utilities or provided by user
"""
if hasattr(mod, "weight_fake_quant"):
if type(mod) is not torch.ao.nn.qat.Embedding:
raise AssertionError(
"nnq."
+ cls.__name__
+ ".from_float "
+ "with fake quant only works for "
+ torch.ao.nn.qat.Embedding.__name__
)
weight_observer = mod.weight_fake_quant
else:
if type(mod) is not nn.Embedding:
raise AssertionError(
"nnq."
+ cls.__name__
+ ".from_float only works for "
+ nn.Embedding.__name__
)
if not hasattr(mod, "qconfig"):
raise AssertionError(
"Embedding input float module must have qconfig defined"
)
from torch.ao.quantization import float_qparams_weight_only_qconfig
if mod.qconfig is not None and mod.qconfig.weight is not None: # type: ignore[union-attr]
weight_observer = mod.qconfig.weight() # type: ignore[union-attr, operator]
else:
weight_observer = float_qparams_weight_only_qconfig.weight()
dtype = weight_observer.dtype
is_float_qparams_qconfig = (
weight_observer.qscheme == torch.per_channel_affine_float_qparams
)
if not is_float_qparams_qconfig:
raise AssertionError(
"Embedding quantization is only supported with float_qparams_weight_only_qconfig."
)
if dtype != torch.quint8 and dtype != torch.quint4x2:
raise AssertionError(
f"The only supported dtype for nnq.Embedding is torch.quint8 and torch.quint4x2, got {dtype}"
)
# Run the observer to calculate qparams.
weight_observer(mod.weight)
qweight = _quantize_weight(mod.weight.float(), weight_observer)
# Create quantized Embedding module and pass in the quantized weight
qembedding = Embedding(mod.num_embeddings, mod.embedding_dim)
qembedding.set_weight(qweight)
return qembedding
@classmethod
def from_reference(cls, ref_embedding):
qembedding = cls(
ref_embedding.num_embeddings,
ref_embedding.embedding_dim,
ref_embedding.padding_idx,
ref_embedding.max_norm,
ref_embedding.norm_type,
ref_embedding.scale_grad_by_freq,
ref_embedding.sparse,
ref_embedding.get_quantized_weight(),
ref_embedding.weight_dtype,
)
return qembedding
class EmbeddingBag(Embedding):
r"""
A quantized EmbeddingBag module with quantized packed weights as inputs.
We adopt the same interface as `torch.nn.EmbeddingBag`, please see
https://pytorch.org/docs/stable/generated/torch.nn.EmbeddingBag.html for documentation.
Similar to :class:`~torch.nn.EmbeddingBag`, attributes will be randomly
initialized at module creation time and will be overwritten later
Attributes:
weight (Tensor): the non-learnable quantized weights of the module of
shape :math:`(\text{num\_embeddings}, \text{embedding\_dim})`.
Examples::
>>> m = nn.quantized.EmbeddingBag(num_embeddings=10, embedding_dim=12, include_last_offset=True, mode='sum')
>>> indices = torch.tensor([9, 6, 5, 7, 8, 8, 9, 2, 8, 6, 6, 9, 1, 6, 8, 8, 3, 2, 3, 6, 3, 6, 5, 7, 0, 8, 4, 6, 5, 8, 2, 3])
>>> offsets = torch.tensor([0, 19, 20, 28, 28, 32])
>>> output = m(indices, offsets)
>>> print(output.size())
torch.Size([5, 12])
"""
_version = 1
def __init__(
self,
num_embeddings: int,
embedding_dim: int,
max_norm: Optional[float] = None,
norm_type: float = 2.0,
scale_grad_by_freq: bool = False,
mode: str = "sum",
sparse: bool = False,
_weight: Optional[Tensor] = None,
include_last_offset: bool = False,
dtype=torch.quint8,
) -> None:
super().__init__(num_embeddings, embedding_dim, _weight=_weight, dtype=dtype)
self.mode = mode
self.pruned_weights = False
self.include_last_offset = include_last_offset
self.dtype = dtype
def forward(
self,
indices: Tensor,
offsets: Optional[Tensor] = None,
per_sample_weights: Optional[Tensor] = None,
compressed_indices_mapping: Optional[Tensor] = None,
) -> Tensor:
if self.dtype == torch.quint4x2:
return torch.ops.quantized.embedding_bag_4bit(
self._packed_params._packed_weight,
indices,
offsets,
False,
0,
self.pruned_weights,
per_sample_weights,
compressed_indices_mapping,
self.include_last_offset,
)
else:
return torch.ops.quantized.embedding_bag_byte(
self._packed_params._packed_weight,
indices,
offsets,
False,
0,
self.pruned_weights,
per_sample_weights,
compressed_indices_mapping,
self.include_last_offset,
)
def _get_name(self):
return "QuantizedEmbeddingBag"
@classmethod
def from_float(cls, mod, use_precomputed_fake_quant=False):
r"""Create a quantized embedding_bag module from a float module
Args:
mod (Module): a float module, either produced by torch.ao.quantization
utilities or provided by user
"""
if hasattr(mod, "weight_fake_quant"):
weight_observer = mod.weight_fake_quant
else:
if type(mod) is not nn.EmbeddingBag:
raise AssertionError(
"nnq."
+ cls.__name__
+ ".from_float only works for "
+ nn.EmbeddingBag.__name__
)
if not hasattr(mod, "qconfig"):
raise AssertionError(
"EmbeddingBag input float module must have qconfig defined"
)
from torch.ao.quantization.qconfig import float_qparams_weight_only_qconfig
if mod.qconfig is not None and mod.qconfig.weight is not None: # type: ignore[union-attr]
weight_observer = mod.qconfig.weight() # type: ignore[union-attr, operator]
else:
weight_observer = float_qparams_weight_only_qconfig.weight()
dtype = weight_observer.dtype
is_float_qparams_qconfig = (
weight_observer.qscheme == torch.per_channel_affine_float_qparams
)
if not is_float_qparams_qconfig:
raise AssertionError(
"EmbeddingBag quantization is only supported with float_qparams_weight_only_qconfig."
)
if dtype != torch.quint8 and dtype != torch.quint4x2:
raise AssertionError(
f"The only supported dtype for nnq.EmbeddingBag is torch.quint8 and torch.quint4x2, got {dtype}"
)
# Run the observer to calculate qparams.
weight_observer(mod.weight)
qweight = _quantize_weight(mod.weight.float(), weight_observer)
# Create quantized EmbeddingBag module and pass in the quantized weight
qembedding_bag = EmbeddingBag(
mod.num_embeddings,
mod.embedding_dim,
max_norm=mod.max_norm,
norm_type=mod.norm_type,
scale_grad_by_freq=mod.scale_grad_by_freq,
mode=mod.mode,
sparse=mod.sparse,
include_last_offset=mod.include_last_offset,
dtype=dtype,
)
qembedding_bag.set_weight(qweight)
return qembedding_bag
@classmethod
def from_reference(cls, ref_embedding_bag):
qembedding_bag = cls(
ref_embedding_bag.num_embeddings,
ref_embedding_bag.embedding_dim,
ref_embedding_bag.max_norm,
ref_embedding_bag.norm_type,
ref_embedding_bag.scale_grad_by_freq,
ref_embedding_bag.mode,
ref_embedding_bag.sparse,
ref_embedding_bag.get_quantized_weight(),
ref_embedding_bag.include_last_offset,
ref_embedding_bag.weight_dtype,
)
return qembedding_bag
@@ -0,0 +1,300 @@
# mypy: allow-untyped-defs
import torch
from torch import Tensor
from torch._ops import ops
__all__ = ["FloatFunctional", "FXFloatFunctional", "QFunctional"]
class FloatFunctional(torch.nn.Module):
r"""State collector class for float operations.
The instance of this class can be used instead of the ``torch.`` prefix for
some operations. See example usage below.
.. note::
This class does not provide a ``forward`` hook. Instead, you must use
one of the underlying functions (e.g. ``add``).
Examples::
>>> f_add = FloatFunctional()
>>> a = torch.tensor(3.0)
>>> b = torch.tensor(4.0)
>>> f_add.add(a, b) # Equivalent to ``torch.add(a, b)``
Valid operation names:
- add
- cat
- mul
- add_relu
- add_scalar
- mul_scalar
"""
def __init__(self) -> None:
super().__init__()
self.activation_post_process = torch.nn.Identity()
def forward(self, x):
raise RuntimeError(
"FloatFunctional is not intended to use the "
+ "'forward'. Please use the underlying operation"
)
r"""Operation equivalent to ``torch.add(Tensor, Tensor)``"""
def add(self, x: Tensor, y: Tensor) -> Tensor:
r = torch.add(x, y)
r = self.activation_post_process(r)
return r
r"""Operation equivalent to ``torch.add(Tensor, float)``"""
def add_scalar(self, x: Tensor, y: float) -> Tensor:
r = torch.add(x, y)
# Note: this operation is not observed because the observation is not
# needed for the quantized op.
return r
r"""Operation equivalent to ``torch.mul(Tensor, Tensor)``"""
def mul(self, x: Tensor, y: Tensor) -> Tensor:
r = torch.mul(x, y)
r = self.activation_post_process(r)
return r
r"""Operation equivalent to ``torch.mul(Tensor, float)``"""
def mul_scalar(self, x: Tensor, y: float) -> Tensor:
r = torch.mul(x, y)
# Note: this operation is not observed because the observation is not
# needed for the quantized op.
return r
r"""Operation equivalent to ``torch.cat``"""
def cat(self, x: list[Tensor], dim: int = 0) -> Tensor:
r = torch.cat(x, dim=dim)
r = self.activation_post_process(r)
return r
r"""Operation equivalent to ``relu(torch.add(x,y))``"""
def add_relu(self, x: Tensor, y: Tensor) -> Tensor:
r = torch.add(x, y)
r = torch.nn.functional.relu(r)
r = self.activation_post_process(r)
return r
r"""Operation equivalent to ``torch.matmul(Tensor, Tensor)``"""
def matmul(self, x: Tensor, y: Tensor) -> Tensor:
r = torch.matmul(x, y)
r = self.activation_post_process(r)
return r
class FXFloatFunctional(torch.nn.Module):
r"""module to replace FloatFunctional module before FX graph mode quantization,
since activation_post_process will be inserted in top level module directly
Valid operation names:
- add
- cat
- mul
- add_relu
- add_scalar
- mul_scalar
"""
def forward(self, x):
raise RuntimeError(
"FloatFunctional is not intended to use the "
+ "'forward'. Please use the underlying operation"
)
r"""Operation equivalent to ``torch.add(Tensor, Tensor)``"""
def add(self, x: Tensor, y: Tensor) -> Tensor:
r = torch.add(x, y)
return r
r"""Operation equivalent to ``torch.add(Tensor, float)``"""
def add_scalar(self, x: Tensor, y: float) -> Tensor:
r = torch.add(x, y)
return r
r"""Operation equivalent to ``torch.mul(Tensor, Tensor)``"""
def mul(self, x: Tensor, y: Tensor) -> Tensor:
r = torch.mul(x, y)
return r
r"""Operation equivalent to ``torch.mul(Tensor, float)``"""
def mul_scalar(self, x: Tensor, y: float) -> Tensor:
r = torch.mul(x, y)
return r
r"""Operation equivalent to ``torch.cat``"""
def cat(self, x: list[Tensor], dim: int = 0) -> Tensor:
r = torch.cat(x, dim=dim)
return r
r"""Operation equivalent to ``relu(torch.add(x,y))``"""
def add_relu(self, x: Tensor, y: Tensor) -> Tensor:
r = torch.add(x, y)
r = torch.nn.functional.relu(r)
return r
r"""Operation equivalent to ``torch.matmul(Tensor, Tensor)``"""
def matmul(self, x: Tensor, y: Tensor) -> Tensor:
r = torch.matmul(x, y)
return r
class QFunctional(torch.nn.Module):
r"""Wrapper class for quantized operations.
The instance of this class can be used instead of the
``torch.ops.quantized`` prefix. See example usage below.
.. note::
This class does not provide a ``forward`` hook. Instead, you must use
one of the underlying functions (e.g. ``add``).
Examples::
>>> q_add = QFunctional()
>>> # xdoctest: +SKIP
>>> a = torch.quantize_per_tensor(torch.tensor(3.0), 1.0, 0, torch.qint32)
>>> b = torch.quantize_per_tensor(torch.tensor(4.0), 1.0, 0, torch.qint32)
>>> q_add.add(a, b) # Equivalent to ``torch.ops.quantized.add(a, b, 1.0, 0)``
Valid operation names:
- add
- cat
- mul
- add_relu
- add_scalar
- mul_scalar
"""
def __init__(self) -> None:
super().__init__()
self.scale = 1.0
self.zero_point = 0
self.activation_post_process = torch.nn.Identity()
def _save_to_state_dict(self, destination, prefix, keep_vars):
super()._save_to_state_dict(destination, prefix, keep_vars)
destination[prefix + "scale"] = torch.tensor(self.scale)
destination[prefix + "zero_point"] = torch.tensor(self.zero_point)
def _load_from_state_dict(
self,
state_dict,
prefix,
local_metadata,
strict,
missing_keys,
unexpected_keys,
error_msgs,
):
self.scale = float(state_dict.pop(prefix + "scale"))
self.zero_point = int(state_dict.pop(prefix + "zero_point"))
super()._load_from_state_dict(
state_dict,
prefix,
local_metadata,
False,
missing_keys,
unexpected_keys,
error_msgs,
)
def _get_name(self):
return "QFunctional"
def extra_repr(self):
return f"scale={self.scale}, zero_point={self.zero_point}"
def forward(self, x):
raise RuntimeError(
"Functional is not intended to use the "
+ "'forward'. Please use the underlying operation"
)
r"""Operation equivalent to ``torch.ops.quantized.add``"""
def add(self, x: Tensor, y: Tensor) -> Tensor:
r = ops.quantized.add(x, y, scale=self.scale, zero_point=self.zero_point)
r = self.activation_post_process(r)
return r
r"""Operation equivalent to ``torch.ops.quantized.add(Tensor, float)``"""
def add_scalar(self, x: Tensor, y: float) -> Tensor:
r = ops.quantized.add_scalar(x, y)
# Note: this operation is not observed because the observation is not
# needed for the quantized op.
return r
r"""Operation equivalent to ``torch.ops.quantized.mul(Tensor, Tensor)``"""
def mul(self, x: Tensor, y: Tensor) -> Tensor:
r = ops.quantized.mul(x, y, scale=self.scale, zero_point=self.zero_point)
r = self.activation_post_process(r)
return r
r"""Operation equivalent to ``torch.ops.quantized.mul(Tensor, float)``"""
def mul_scalar(self, x: Tensor, y: float) -> Tensor:
r = ops.quantized.mul_scalar(x, y)
# Note: this operation is not observed because the observation is not
# needed for the quantized op.
return r
r"""Operation equivalent to ``torch.ops.quantized.cat``"""
def cat(self, x: list[Tensor], dim: int = 0) -> Tensor:
r = ops.quantized.cat(x, scale=self.scale, zero_point=self.zero_point, dim=dim)
r = self.activation_post_process(r)
return r
r"""Operation equivalent to ``torch.ops.quantized.add_relu``"""
def add_relu(self, x: Tensor, y: Tensor) -> Tensor:
r = ops.quantized.add_relu(x, y, scale=self.scale, zero_point=self.zero_point)
r = self.activation_post_process(r)
return r
r"""Operation equivalent to ``torch.ops.quantized.matmul(Tensor, Tensor)``"""
def matmul(self, x: Tensor, y: Tensor) -> Tensor:
r = ops.quantized.matmul(x, y, scale=self.scale, zero_point=self.zero_point)
# Note: this operation is not observed because the observation is not
# needed for the quantized op.
return r
@classmethod
def from_float(cls, mod, use_precomputed_fake_quant=False):
if type(mod) is not FloatFunctional:
raise AssertionError(
f"QFunctional.from_float expects an instance of FloatFunctional, "
f"got {type(mod).__name__}"
)
scale, zero_point = mod.activation_post_process.calculate_qparams() # type: ignore[operator]
new_mod = QFunctional()
new_mod.scale = float(scale)
new_mod.zero_point = int(zero_point)
return new_mod
@@ -0,0 +1,362 @@
# mypy: allow-untyped-decorators
# mypy: allow-untyped-defs
from collections.abc import Iterable
import torch
import torch.ao.nn.intrinsic as nni
import torch.ao.nn.intrinsic.qat as nniqat
import torch.nn as nn
from torch.nn.utils.fusion import fuse_linear_bn_weights
from torch.nn.utils.parametrize import type_before_parametrizations
from .utils import _hide_packed_params_repr, _quantize_weight, WeightedQuantizedModule
__all__ = ["LinearPackedParams", "Linear"]
class LinearPackedParams(torch.nn.Module):
_version = 3
def __init__(self, dtype=torch.qint8):
super().__init__()
self.dtype = dtype
if self.dtype == torch.qint8:
wq = torch._empty_affine_quantized(
[1, 1], scale=1.0, zero_point=0, dtype=torch.qint8
)
elif self.dtype == torch.float16:
wq = torch.zeros([1, 1], dtype=torch.float)
self.set_weight_bias(wq, None) # type: ignore[possibly-undefined]
@torch.jit.export
def set_weight_bias(self, weight: torch.Tensor, bias: torch.Tensor | None) -> None:
if self.dtype == torch.qint8:
self._packed_params = torch.ops.quantized.linear_prepack(weight, bias)
elif self.dtype == torch.float16:
self._packed_params = torch.ops.quantized.linear_prepack_fp16(weight, bias)
else:
raise RuntimeError("Unsupported dtype on dynamic quantized linear!")
@torch.jit.export
def _weight_bias(self):
if self.dtype == torch.qint8:
return torch.ops.quantized.linear_unpack(self._packed_params)
elif self.dtype == torch.float16:
return torch.ops.quantized.linear_unpack_fp16(self._packed_params)
else:
raise RuntimeError("Unsupported dtype on dynamic quantized linear!")
def forward(self, x):
return x
# Version 1
# self
# |--- weight : Tensor
# |--- bias : Tensor
#
# Version 2
# self
# |--- weight : Tensor
# |--- bias : Tensor
# |--- dtype : torch.dtype
#
# Version 3
# self
# |--- _packed_params : (Tensor, Tensor) representing (weight, bias)
# of LinearPackedParams
# |--- dtype : torch.dtype
def _save_to_state_dict(self, destination, prefix, keep_vars):
super()._save_to_state_dict(destination, prefix, keep_vars)
destination[prefix + "dtype"] = self.dtype
destination[prefix + "_packed_params"] = self._weight_bias()
def _load_from_state_dict(
self,
state_dict,
prefix,
local_metadata,
strict,
missing_keys,
unexpected_keys,
error_msgs,
):
version = local_metadata.get("version", None)
if version is None or version < 2:
self.dtype = torch.qint8
else:
self.dtype = state_dict[prefix + "dtype"]
state_dict.pop(prefix + "dtype")
if version is None or version < 3:
self.set_weight_bias(
state_dict[prefix + "weight"], state_dict[prefix + "bias"]
)
state_dict.pop(prefix + "weight")
state_dict.pop(prefix + "bias")
if version == 3:
weight, bias = state_dict[prefix + "_packed_params"]
state_dict.pop(prefix + "_packed_params")
self.set_weight_bias(weight, bias)
super()._load_from_state_dict(
state_dict,
prefix,
local_metadata,
False,
missing_keys,
unexpected_keys,
error_msgs,
)
def __repr__(self):
return self._weight_bias().__repr__()
class Linear(WeightedQuantizedModule):
r"""
A quantized linear module with quantized tensor as inputs and outputs.
We adopt the same interface as `torch.nn.Linear`, please see
https://pytorch.org/docs/stable/nn.html#torch.nn.Linear for documentation.
Similar to :class:`~torch.nn.Linear`, attributes will be randomly
initialized at module creation time and will be overwritten later
Attributes:
weight (Tensor): the non-learnable quantized weights of the module of
shape :math:`(\text{out\_features}, \text{in\_features})`.
bias (Tensor): the non-learnable bias of the module of shape :math:`(\text{out\_features})`.
If :attr:`bias` is ``True``, the values are initialized to zero.
scale: `scale` parameter of output Quantized Tensor, type: double
zero_point: `zero_point` parameter for output Quantized Tensor, type: long
Examples::
>>> # xdoctest: +REQUIRES(env:TORCH_DOCTEST_QENGINE)
>>> m = nn.quantized.Linear(20, 30)
>>> input = torch.randn(128, 20)
>>> # xdoctest: +SKIP
>>> input = torch.quantize_per_tensor(input, 1.0, 0, torch.quint8)
>>> output = m(input)
>>> print(output.size())
torch.Size([128, 30])
"""
_version = 3
_FLOAT_MODULE = (nn.Linear, nn.modules.linear.NonDynamicallyQuantizableLinear)
def __init__(self, in_features, out_features, bias_=True, dtype=torch.qint8):
super().__init__()
# We don't muck around with buffers or attributes or anything here
# to keep the module simple. *everything* is simply a Python attribute.
# Serialization logic is explicitly handled in the below serialization and
# deserialization modules
self.in_features = in_features
self.out_features = out_features
bias = None
if bias_:
bias = torch.zeros(out_features, dtype=torch.float)
if dtype == torch.qint8:
qweight = torch._empty_affine_quantized(
[out_features, in_features], scale=1, zero_point=0, dtype=torch.qint8
)
elif dtype == torch.float16:
qweight = torch.zeros([out_features, in_features], dtype=torch.float)
else:
raise RuntimeError("Unsupported dtype specified for quantized Linear!")
self._packed_params = LinearPackedParams(dtype)
self._packed_params.set_weight_bias(qweight, bias)
self.scale = 1.0
self.zero_point = 0
def _get_name(self):
return "QuantizedLinear"
def extra_repr(self):
return (
f"in_features={self.in_features}, out_features={self.out_features}, scale={self.scale}, "
f"zero_point={self.zero_point}, qscheme={self.weight().qscheme()}"
)
def __repr__(self):
return _hide_packed_params_repr(self, LinearPackedParams)
def forward(self, x: torch.Tensor) -> torch.Tensor:
return torch.ops.quantized.linear(
x, self._packed_params._packed_params, self.scale, self.zero_point
)
# ===== Serialization methods =====
# The special consideration here is that we have to unpack the weights into their
# regular QTensor form for serialization. Packed weights should not live
# outside the process in which they were created, rather they should be derived
# from the QTensor weight.
#
# Version 1
# self
# |--- scale : float
# |--- zero_point : int
# |--- weight : Tensor
# |--- bias : Tensor
#
# Version 2
# self
# |--- scale : float
# |--- zero_point : int
# |--- _packed_params : Module
# |--- weight : Tensor
# |--- bias : Tensor
#
# Version 3
# self
# |--- scale : float
# |--- zero_point : int
# |--- _packed_params : Module
# |--- _packed_params : (Tensor, Tensor) representing weight, bias
# of LinearPackedParams C++ struct
#
def _save_to_state_dict(self, destination, prefix, keep_vars):
super()._save_to_state_dict(destination, prefix, keep_vars)
destination[prefix + "scale"] = torch.tensor(self.scale)
destination[prefix + "zero_point"] = torch.tensor(self.zero_point)
# ===== Deserialization methods =====
# Counterpart to the serialization methods, we must pack the serialized QTensor
# weight into its packed format for use by the FBGEMM ops.
def _load_from_state_dict(
self,
state_dict,
prefix,
local_metadata,
strict,
missing_keys,
unexpected_keys,
error_msgs,
):
self.scale = float(state_dict[prefix + "scale"])
state_dict.pop(prefix + "scale")
self.zero_point = int(state_dict[prefix + "zero_point"])
state_dict.pop(prefix + "zero_point")
version = local_metadata.get("version", None)
if version is None or version == 1:
# We moved the parameters into a LinearPackedParameters submodule
weight = state_dict.pop(prefix + "weight")
bias = state_dict.pop(prefix + "bias")
state_dict.update(
{
prefix + "_packed_params.weight": weight,
prefix + "_packed_params.bias": bias,
}
)
super()._load_from_state_dict(
state_dict,
prefix,
local_metadata,
False,
missing_keys,
unexpected_keys,
error_msgs,
)
# Function rather than property to make sure that JIT serialization doesn't
# register this as an attribute
def _weight_bias(self):
return self._packed_params._weight_bias()
def weight(self):
return self._weight_bias()[0]
def bias(self):
return self._weight_bias()[1]
def set_weight_bias(self, w: torch.Tensor, b: torch.Tensor | None) -> None:
self._packed_params.set_weight_bias(w, b)
@classmethod
def from_float(cls, mod, use_precomputed_fake_quant=False):
r"""Create a quantized module from an observed float module
Args:
mod (Module): a float module, either produced by torch.ao.quantization
utilities or provided by the user
use_precomputed_fake_quant (bool): if True, the module will reuse min/max
values from the precomputed fake quant module.
"""
if hasattr(mod, "weight_fake_quant"):
if type_before_parametrizations(mod) == nniqat.LinearBn1d:
mod.weight, mod.bias = fuse_linear_bn_weights(
mod.weight,
mod.bias,
mod.bn.running_mean,
mod.bn.running_var,
mod.bn.eps,
mod.bn.weight,
mod.bn.bias,
)
weight_post_process = mod.weight_fake_quant
activation_post_process = mod.activation_post_process
else:
# This function does not participate in JIT, so it is OK to ignore
# the type mismatch in assignment. Also, mypy has an issue with
# iterables not being implemented, so we are ignoring those too.
if not isinstance(cls._FLOAT_MODULE, Iterable):
# pyrefly: ignore [bad-assignment]
cls._FLOAT_MODULE = [cls._FLOAT_MODULE]
supported_modules = ", ".join(
[float_mod.__name__ for float_mod in cls._FLOAT_MODULE]
)
error_msg = f"nnq.{cls.__name__}.from_float only works for {supported_modules}, but got: {type(mod)}"
if type_before_parametrizations(mod) not in cls._FLOAT_MODULE:
raise AssertionError(error_msg)
if not hasattr(mod, "qconfig"):
raise AssertionError("Input float module must have qconfig defined")
activation_post_process = mod.activation_post_process
if type_before_parametrizations(mod) == nni.LinearReLU:
mod = mod[0]
weight_post_process = (
mod.qconfig.weight()
if not hasattr(mod, "weight_fake_quant")
else mod.weight_fake_quant
)
if not use_precomputed_fake_quant:
# Observer may not have been called yet
# Observer might have been called in the previous stage via PTQ algorithm e.g. AdaRound
weight_post_process(mod.weight)
dtype = weight_post_process.dtype
act_scale, act_zp = activation_post_process.calculate_qparams()
if dtype != torch.qint8:
raise AssertionError(
f"Weight observer must have dtype torch.qint8, got {dtype}"
)
qweight = _quantize_weight(mod.weight.float(), weight_post_process)
qlinear = cls(mod.in_features, mod.out_features, dtype=dtype)
qlinear.set_weight_bias(qweight, mod.bias)
qlinear.scale = float(act_scale)
qlinear.zero_point = int(act_zp)
return qlinear
@classmethod
def from_reference(cls, ref_qlinear, output_scale, output_zero_point):
r"""Create a (fbgemm/qnnpack) quantized module from a reference quantized module
Args:
ref_qlinear (Module): a reference quantized linear module, either produced by torch.ao.quantization
utilities or provided by the user
output_scale (float): scale for output Tensor
output_zero_point (int): zero point for output Tensor
"""
qlinear = cls(ref_qlinear.in_features, ref_qlinear.out_features)
qweight = ref_qlinear.get_quantized_weight()
qlinear.set_weight_bias(qweight, ref_qlinear.bias)
qlinear.scale = float(output_scale)
qlinear.zero_point = int(output_zero_point)
return qlinear
@@ -0,0 +1,347 @@
# mypy: allow-untyped-defs
import torch
__all__ = [
"LayerNorm",
"GroupNorm",
"InstanceNorm1d",
"InstanceNorm2d",
"InstanceNorm3d",
]
class LayerNorm(torch.nn.LayerNorm):
r"""This is the quantized version of :class:`~torch.nn.LayerNorm`.
Additional args:
* **scale** - quantization scale of the output, type: double.
* **zero_point** - quantization zero point of the output, type: long.
"""
def __init__(
self,
normalized_shape,
weight,
bias,
scale,
zero_point,
eps=1e-5,
elementwise_affine=True,
device=None,
dtype=None,
) -> None:
factory_kwargs = {"device": device, "dtype": dtype}
super().__init__(
normalized_shape,
eps=eps,
elementwise_affine=elementwise_affine,
**factory_kwargs,
)
self.weight = weight
self.bias = bias
self.register_buffer("scale", torch.tensor(scale, **factory_kwargs))
self.register_buffer("zero_point", torch.tensor(zero_point, **factory_kwargs))
def forward(self, input):
return torch.ops.quantized.layer_norm(
input,
self.normalized_shape,
weight=self.weight,
bias=self.bias,
eps=self.eps,
output_scale=self.scale,
output_zero_point=self.zero_point,
)
def _get_name(self):
return "QuantizedLayerNorm"
@classmethod
def from_float(cls, mod, use_precomputed_fake_quant=False):
scale, zero_point = mod.activation_post_process.calculate_qparams()
new_mod = cls(
mod.normalized_shape,
mod.weight,
mod.bias,
float(scale),
int(zero_point),
mod.eps,
mod.elementwise_affine,
)
return new_mod
@classmethod
def from_reference(cls, mod, scale, zero_point):
return cls(
mod.normalized_shape,
mod.weight,
mod.bias,
float(scale),
int(zero_point),
mod.eps,
mod.elementwise_affine,
)
class GroupNorm(torch.nn.GroupNorm):
r"""This is the quantized version of :class:`~torch.nn.GroupNorm`.
Additional args:
* **scale** - quantization scale of the output, type: double.
* **zero_point** - quantization zero point of the output, type: long.
"""
__constants__ = ["num_groups", "num_channels", "eps", "affine"]
def __init__(
self,
num_groups,
num_channels,
weight,
bias,
scale,
zero_point,
eps=1e-5,
affine=True,
device=None,
dtype=None,
) -> None:
factory_kwargs = {"device": device, "dtype": dtype}
super().__init__(num_groups, num_channels, eps, affine, **factory_kwargs)
self.weight = weight
self.bias = bias
self.register_buffer("scale", torch.tensor(scale, **factory_kwargs))
self.register_buffer("zero_point", torch.tensor(zero_point, **factory_kwargs))
def forward(self, input):
return torch.ops.quantized.group_norm(
input,
self.num_groups,
self.weight,
self.bias,
self.eps,
self.scale,
self.zero_point,
)
def _get_name(self):
return "QuantizedGroupNorm"
@classmethod
def from_float(cls, mod, use_precomputed_fake_quant=False):
scale, zero_point = mod.activation_post_process.calculate_qparams()
new_mod = cls(
mod.num_groups,
mod.num_channels,
mod.weight,
mod.bias,
float(scale),
int(zero_point),
mod.eps,
mod.affine,
)
return new_mod
class InstanceNorm1d(torch.nn.InstanceNorm1d):
r"""This is the quantized version of :class:`~torch.nn.InstanceNorm1d`.
Additional args:
* **scale** - quantization scale of the output, type: double.
* **zero_point** - quantization zero point of the output, type: long.
"""
def __init__(
self,
num_features,
weight,
bias,
scale,
zero_point,
eps=1e-5,
momentum=0.1,
affine=False,
track_running_stats=False,
device=None,
dtype=None,
) -> None:
factory_kwargs = {"device": device, "dtype": dtype}
super().__init__(
num_features, eps, momentum, affine, track_running_stats, **factory_kwargs
)
self.weight = weight
self.bias = bias
self.register_buffer("scale", torch.tensor(scale, **factory_kwargs))
self.register_buffer("zero_point", torch.tensor(zero_point, **factory_kwargs))
def forward(self, input):
return torch.ops.quantized.instance_norm(
input, self.weight, self.bias, self.eps, self.scale, self.zero_point
)
def _get_name(self):
return "QuantizedInstanceNorm1d"
@classmethod
def from_float(cls, mod, use_precomputed_fake_quant=False):
scale, zero_point = mod.activation_post_process.calculate_qparams()
new_mod = cls(
mod.num_features,
mod.weight,
mod.bias,
float(scale),
int(zero_point),
mod.eps,
mod.affine,
)
return new_mod
@classmethod
def from_reference(cls, mod, scale, zero_point):
return cls(
mod.num_features,
mod.weight,
mod.bias,
float(scale),
int(zero_point),
mod.eps,
mod.affine,
)
class InstanceNorm2d(torch.nn.InstanceNorm2d):
r"""This is the quantized version of :class:`~torch.nn.InstanceNorm2d`.
Additional args:
* **scale** - quantization scale of the output, type: double.
* **zero_point** - quantization zero point of the output, type: long.
"""
def __init__(
self,
num_features,
weight,
bias,
scale,
zero_point,
eps=1e-5,
momentum=0.1,
affine=False,
track_running_stats=False,
device=None,
dtype=None,
) -> None:
factory_kwargs = {"device": device, "dtype": dtype}
super().__init__(
num_features, eps, momentum, affine, track_running_stats, **factory_kwargs
)
self.weight = weight
self.bias = bias
self.register_buffer("scale", torch.tensor(scale, **factory_kwargs))
self.register_buffer("zero_point", torch.tensor(zero_point, **factory_kwargs))
def forward(self, input):
return torch.ops.quantized.instance_norm(
input, self.weight, self.bias, self.eps, self.scale, self.zero_point
)
def _get_name(self):
return "QuantizedInstanceNorm2d"
@classmethod
def from_float(cls, mod, use_precomputed_fake_quant=False):
scale, zero_point = mod.activation_post_process.calculate_qparams()
new_mod = cls(
mod.num_features,
mod.weight,
mod.bias,
float(scale),
int(zero_point),
mod.eps,
mod.affine,
)
return new_mod
@classmethod
def from_reference(cls, mod, scale, zero_point):
return cls(
mod.num_features,
mod.weight,
mod.bias,
float(scale),
int(zero_point),
mod.eps,
mod.affine,
)
class InstanceNorm3d(torch.nn.InstanceNorm3d):
r"""This is the quantized version of :class:`~torch.nn.InstanceNorm3d`.
Additional args:
* **scale** - quantization scale of the output, type: double.
* **zero_point** - quantization zero point of the output, type: long.
"""
def __init__(
self,
num_features,
weight,
bias,
scale,
zero_point,
eps=1e-5,
momentum=0.1,
affine=False,
track_running_stats=False,
device=None,
dtype=None,
) -> None:
factory_kwargs = {"device": device, "dtype": dtype}
super().__init__(
num_features, eps, momentum, affine, track_running_stats, **factory_kwargs
)
self.weight = weight
self.bias = bias
self.register_buffer("scale", torch.tensor(scale, **factory_kwargs))
self.register_buffer("zero_point", torch.tensor(zero_point, **factory_kwargs))
def forward(self, input):
return torch.ops.quantized.instance_norm(
input, self.weight, self.bias, self.eps, self.scale, self.zero_point
)
def _get_name(self):
return "QuantizedInstanceNorm3d"
@classmethod
def from_float(cls, mod, use_precomputed_fake_quant=False):
scale, zero_point = mod.activation_post_process.calculate_qparams()
new_mod = cls(
mod.num_features,
mod.weight,
mod.bias,
float(scale),
int(zero_point),
mod.eps,
mod.affine,
)
return new_mod
@classmethod
def from_reference(cls, mod, scale, zero_point):
return cls(
mod.num_features,
mod.weight,
mod.bias,
float(scale),
int(zero_point),
mod.eps,
mod.affine,
)
@@ -0,0 +1,62 @@
from typing import Any
import torch
__all__ = [
"LSTM",
]
class LSTM(torch.ao.nn.quantizable.LSTM):
r"""A quantized long short-term memory (LSTM).
For the description and the argument types, please, refer to :class:`~torch.nn.LSTM`
Attributes:
layers : instances of the `_LSTMLayer`
.. note::
To access the weights and biases, you need to access them per layer.
See examples in :class:`~torch.ao.nn.quantizable.LSTM`
Examples::
>>> # xdoctest: +SKIP
>>> custom_module_config = {
... 'float_to_observed_custom_module_class': {
... nn.LSTM: nn.quantizable.LSTM,
... },
... 'observed_to_quantized_custom_module_class': {
... nn.quantizable.LSTM: nn.quantized.LSTM,
... }
... }
>>> tq.prepare(model, prepare_custom_module_class=custom_module_config)
>>> tq.convert(model, convert_custom_module_class=custom_module_config)
"""
_FLOAT_MODULE = torch.ao.nn.quantizable.LSTM # type: ignore[assignment]
def _get_name(self) -> str:
return "QuantizedLSTM"
@classmethod
def from_float(cls, *args: Any, **kwargs: Any) -> None:
# The whole flow is float -> observed -> quantized
# This class does observed -> quantized only
raise NotImplementedError(
"It looks like you are trying to convert a "
"non-observed LSTM module. Please, see "
"the examples on quantizable LSTMs."
)
@classmethod
def from_observed(cls: type["LSTM"], other: torch.ao.nn.quantizable.LSTM) -> "LSTM":
if not isinstance(other, cls._FLOAT_MODULE): # type: ignore[has-type]
raise AssertionError(
f"Expected module type {cls._FLOAT_MODULE}, got {type(other)}"
)
converted = torch.ao.quantization.convert(
other, inplace=False, remove_qconfig=True
)
converted.__class__ = cls
return converted
@@ -0,0 +1,144 @@
# mypy: allow-untyped-defs
import abc
import collections
import itertools
import torch
from torch.nn.modules.module import _addindent
__all__ = [
"WeightedQuantizedModule",
]
class WeightedQuantizedModule(torch.nn.Module, metaclass=abc.ABCMeta):
"""Wrapper for quantized modules than can be lowered from reference modules."""
@classmethod
@abc.abstractmethod
def from_reference(cls, ref_module, output_scale, output_zero_point):
raise NotImplementedError
def _get_weight_observer(observer):
# FakeQuantize observer
if hasattr(observer, "activation_post_process"):
observer = observer.activation_post_process
# UniformQuantizationObserverBase observer
return observer
def _needs_weight_clamping(observer, dtype):
observer = _get_weight_observer(observer)
if dtype in [torch.qint8, torch.quint8, torch.qint32]:
info = torch.iinfo(dtype)
return observer.quant_min > info.min or observer.quant_max < info.max
return False
def _clamp_weights(qweight, observer, scale, zp):
if not _needs_weight_clamping(observer, qweight.dtype):
return qweight
observer = _get_weight_observer(observer)
min_, max_ = observer.quant_min, observer.quant_max
# Doing this because can't use torch.ops.quantized.clamp() with per_channel qscheme yet.
qw_int_max = torch.clone(qweight.int_repr()).fill_(max_)
qw_int_min = torch.clone(qweight.int_repr()).fill_(min_)
qw_int = torch.minimum(torch.maximum(qweight.int_repr(), qw_int_min), qw_int_max)
if observer.qscheme in [torch.per_tensor_symmetric, torch.per_tensor_affine]:
qweight = torch._make_per_tensor_quantized_tensor(
qw_int, scale.item(), zp.item()
)
elif observer.qscheme in [
torch.per_channel_symmetric,
torch.per_channel_affine,
torch.per_channel_affine_float_qparams,
]:
qweight = torch._make_per_channel_quantized_tensor(
qw_int, scale, zp, axis=observer.ch_axis
)
else:
raise ValueError("Unexpected qscheme " + observer.qscheme)
return qweight
def _quantize_weight(float_wt, observer):
wt_scale, wt_zp = observer.calculate_qparams()
if observer.qscheme in [torch.per_tensor_symmetric, torch.per_tensor_affine]:
qweight = torch.quantize_per_tensor(
float_wt, float(wt_scale), int(wt_zp), torch.qint8
)
qweight = _clamp_weights(qweight, observer, wt_scale, wt_zp)
elif observer.qscheme in [torch.per_channel_symmetric, torch.per_channel_affine]:
wt_axis = observer.ch_axis
qweight = torch.quantize_per_channel(
float_wt,
wt_scale.to(torch.double),
wt_zp.to(torch.int64),
wt_axis,
torch.qint8,
)
qweight = _clamp_weights(qweight, observer, wt_scale, wt_zp)
elif observer.qscheme == torch.per_channel_affine_float_qparams:
qweight = torch.quantize_per_channel(
float_wt,
wt_scale.to(torch.float),
wt_zp.to(torch.float),
observer.ch_axis,
observer.dtype,
)
qweight = _clamp_weights(qweight, observer, wt_scale, wt_zp)
else:
raise ValueError("Unexpected qscheme " + observer.qscheme)
return qweight
def _ntuple_from_first(n):
"""Converts the argument to a tuple of size n
with the first element repeated."""
def parse(x):
while isinstance(x, collections.abc.Sequence):
if len(x) == n:
break
x = x[0]
return tuple(itertools.repeat(x, n))
return parse
def _hide_packed_params_repr(self, params):
# We don't want to show `PackedParams` children, hence custom
# `__repr__`. This is the same as nn.Module.__repr__, except the check
# for the `params module`.
extra_lines = []
extra_repr = self.extra_repr()
# empty string will be split into list ['']
if extra_repr:
extra_lines = extra_repr.split("\n")
child_lines = []
for key, module in self._modules.items():
if isinstance(module, params):
continue
mod_str = repr(module)
mod_str = _addindent(mod_str, 2)
child_lines.append("(" + key + "): " + mod_str)
lines = extra_lines + child_lines
main_str = self._get_name() + "("
if lines:
# simple one-liner info, which most builtin Modules will use
if len(extra_lines) == 1 and not child_lines:
main_str += extra_lines[0]
else:
main_str += "\n " + "\n ".join(lines) + "\n"
main_str += ")"
return main_str
_pair_from_first = _ntuple_from_first(2)
@@ -0,0 +1,19 @@
from .modules import * # noqa: F403
__all__ = [
"Linear",
"Conv1d",
"Conv2d",
"Conv3d",
"ConvTranspose1d",
"ConvTranspose2d",
"ConvTranspose3d",
"RNNCell",
"LSTMCell",
"GRUCell",
"LSTM",
"GRU",
"Embedding",
"EmbeddingBag",
]
@@ -0,0 +1,29 @@
from .conv import (
Conv1d,
Conv2d,
Conv3d,
ConvTranspose1d,
ConvTranspose2d,
ConvTranspose3d,
)
from .linear import Linear
from .rnn import GRU, GRUCell, LSTM, LSTMCell, RNNCell
from .sparse import Embedding, EmbeddingBag
__all__ = [
"Linear",
"Conv1d",
"Conv2d",
"Conv3d",
"ConvTranspose1d",
"ConvTranspose2d",
"ConvTranspose3d",
"RNNCell",
"LSTMCell",
"GRUCell",
"LSTM",
"GRU",
"Embedding",
"EmbeddingBag",
]
@@ -0,0 +1,527 @@
# mypy: allow-untyped-defs
from typing import Any, Literal
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.nn.common_types import _size_1_t
from .utils import ReferenceQuantizedModule
__all__ = [
"Conv1d",
"Conv2d",
"Conv3d",
"ConvTranspose1d",
"ConvTranspose2d",
"ConvTranspose3d",
]
class _ConvNd(torch.nn.modules.conv._ConvNd, ReferenceQuantizedModule):
"""A reference version of nn.quantized.Conv2d
we will not pack the parameters in this module, since weight packing is an
optimization for quantized backends supported in PyTorch (fbgemm/qnnpack),
this is useful when user want to use this module in other backends like Glow.
"""
__annotations__ = {"bias": torch.Tensor | None}
_IS_REFERENCE = True
@staticmethod
def from_float(cls, float_conv, weight_qparams):
qref_conv = cls(
float_conv.in_channels,
float_conv.out_channels,
float_conv.kernel_size, # type: ignore[arg-type]
float_conv.stride, # type: ignore[arg-type]
float_conv.padding, # type: ignore[arg-type]
float_conv.dilation, # type: ignore[arg-type]
float_conv.groups,
float_conv.bias is not None, # type: ignore[arg-type]
float_conv.padding_mode,
device=float_conv.weight.device,
dtype=float_conv.weight.dtype,
weight_qparams=weight_qparams,
)
qref_conv.weight = torch.nn.Parameter(float_conv.weight.detach())
if float_conv.bias is not None:
qref_conv.bias = torch.nn.Parameter(float_conv.bias.detach())
return qref_conv
class Conv1d(_ConvNd, nn.Conv1d):
def __init__(
self,
in_channels: int,
out_channels: int,
kernel_size: _size_1_t,
stride: _size_1_t = 1,
padding: _size_1_t = 0,
dilation: _size_1_t = 1,
groups: int = 1,
bias: bool = True,
padding_mode: Literal["zeros", "reflect", "replicate", "circular"] = "zeros",
device=None,
dtype=None,
weight_qparams: dict[str, Any] | None = None,
):
nn.Conv1d.__init__(
self,
in_channels,
out_channels,
kernel_size,
stride,
padding,
dilation,
groups,
bias,
padding_mode,
device,
dtype,
)
self._init_weight_qparams(weight_qparams, device)
def forward(self, x: torch.Tensor) -> torch.Tensor:
"""
we have:
w(float) -- quant - dequant \
x(float) ------------- F.conv1d ---
In the full model, we will see
w(float) -- quant - *dequant \
x -- quant --- *dequant -- *F.conv1d --- *quant - dequant
and the backend should be able to fuse the ops with `*` into a quantized conv1d
"""
weight_quant_dequant = self.get_weight()
result = F.conv1d(
x,
weight_quant_dequant,
self.bias,
self.stride,
self.padding,
self.dilation,
self.groups,
)
return result
def _get_name(self):
return "QuantizedConv1d(Reference)"
@classmethod
def from_float(cls, float_conv, weight_qparams): # type: ignore[override]
return _ConvNd.from_float(cls, float_conv, weight_qparams)
class Conv2d(_ConvNd, nn.Conv2d):
def __init__(
self,
in_channels,
out_channels,
kernel_size,
stride=1,
padding=0,
dilation=1,
groups=1,
bias=True,
padding_mode="zeros",
device=None,
dtype=None,
weight_qparams: dict[str, Any] | None = None,
):
nn.Conv2d.__init__(
self,
in_channels,
out_channels,
kernel_size,
stride,
padding,
dilation,
groups,
bias,
# pyrefly: ignore [bad-argument-type]
padding_mode,
device,
dtype,
)
self._init_weight_qparams(weight_qparams, device)
def forward(self, x: torch.Tensor) -> torch.Tensor:
"""
we have:
w(float) -- quant - dequant \
x(float) ------------- F.conv2d ---
In the full model, we will see
w(float) -- quant - *dequant \
x -- quant --- *dequant -- *F.conv2d --- *quant - dequant
and the backend should be able to fuse the ops with `*` into a quantized conv2d
"""
weight_quant_dequant = self.get_weight()
result = F.conv2d(
x,
weight_quant_dequant,
self.bias,
self.stride,
self.padding,
self.dilation,
self.groups,
)
return result
def _get_name(self):
return "QuantizedConv2d(Reference)"
@classmethod
def from_float(cls, float_conv, weight_qparams): # type: ignore[override]
return _ConvNd.from_float(cls, float_conv, weight_qparams)
class Conv3d(_ConvNd, nn.Conv3d):
def __init__(
self,
in_channels,
out_channels,
kernel_size,
stride=1,
padding=0,
dilation=1,
groups=1,
bias=True,
padding_mode="zeros",
device=None,
dtype=None,
weight_qparams: dict[str, Any] | None = None,
):
nn.Conv3d.__init__(
self,
in_channels,
out_channels,
kernel_size,
stride,
padding,
dilation,
groups,
bias,
# pyrefly: ignore [bad-argument-type]
padding_mode,
device,
dtype,
)
self._init_weight_qparams(weight_qparams, device)
def forward(self, x: torch.Tensor) -> torch.Tensor:
"""
we have:
w(float) -- quant - dequant \
x(float) ------------- F.conv3d ---
In the full model, we will see
w(float) -- quant - *dequant \
x -- quant --- *dequant -- *F.conv3d --- *quant - dequant
and the backend should be able to fuse the ops with `*` into a quantized conv3d
"""
weight_quant_dequant = self.get_weight()
result = F.conv3d(
x,
weight_quant_dequant,
self.bias,
self.stride,
self.padding,
self.dilation,
self.groups,
)
return result
def _get_name(self):
return "QuantizedConv3d(Reference)"
@classmethod
def from_float(cls, float_conv, weight_qparams): # type: ignore[override]
return _ConvNd.from_float(cls, float_conv, weight_qparams)
class _ConvTransposeNd(_ConvNd, torch.nn.modules.conv._ConvTransposeNd):
"""A reference version of nn.quantized.ConvTranspose2d
we will not pack the parameters in this module, since weight packing is an
optimization for quantized backends supported in PyTorch (fbgemm/qnnpack),
this is useful when user want to use this module in other backends like Glow.
"""
@staticmethod
def from_float(cls, float_conv, weight_qparams):
qref_conv = cls(
float_conv.in_channels,
float_conv.out_channels,
float_conv.kernel_size, # type: ignore[arg-type]
float_conv.stride, # type: ignore[arg-type]
float_conv.padding, # type: ignore[arg-type]
float_conv.output_padding, # type: ignore[arg-type]
float_conv.groups,
float_conv.bias is not None, # type: ignore[arg-type]
float_conv.dilation, # type: ignore[arg-type]
float_conv.padding_mode,
device=float_conv.weight.device,
dtype=float_conv.weight.dtype,
weight_qparams=weight_qparams,
)
qref_conv.weight = torch.nn.Parameter(float_conv.weight.detach())
if float_conv.bias is not None:
qref_conv.bias = torch.nn.Parameter(float_conv.bias.detach())
return qref_conv
class ConvTranspose1d(_ConvTransposeNd, nn.ConvTranspose1d):
def __init__(
self,
in_channels: int,
out_channels: int,
kernel_size: _size_1_t,
stride: _size_1_t = 1,
padding: _size_1_t = 0,
output_padding: _size_1_t = 0,
groups: int = 1,
bias: bool = True,
dilation: _size_1_t = 1,
padding_mode: Literal["zeros", "reflect", "replicate", "circular"] = "zeros",
device=None,
dtype=None,
weight_qparams: dict[str, Any] | None = None,
):
nn.ConvTranspose1d.__init__(
self,
in_channels,
out_channels,
kernel_size,
stride,
padding,
output_padding,
groups,
bias,
dilation,
padding_mode,
device,
dtype,
)
self._init_weight_qparams(weight_qparams, device)
def forward(
self, x: torch.Tensor, output_size: list[int] | None = None
) -> torch.Tensor:
"""
we have:
w(float) -- quant - dequant \
x(float) ------------- F.convTranspose1d ---
In the full model, we will see
w(float) -- quant - *dequant \
x -- quant --- *dequant -- *F.convTranspose1d --- *quant - dequant
and the backend should be able to fuse the ops with `*` into a quantized conv1d
"""
if not isinstance(self.padding, tuple):
raise AssertionError(
f"Expected self.padding to be a tuple, got {type(self.padding)}"
)
# One cannot replace List by Tuple or Sequence in "_output_padding" because
# TorchScript does not support `Sequence[T]` or `Tuple[T, ...]`.
output_padding = self._output_padding(
input, # type: ignore[arg-type]
output_size,
self.stride, # type: ignore[arg-type]
self.padding, # type: ignore[arg-type]
self.kernel_size, # type: ignore[arg-type]
self.dilation, # type: ignore[arg-type]
)
weight_quant_dequant = self.get_weight()
result = F.conv_transpose1d(
x,
weight_quant_dequant,
self.bias,
self.stride,
self.padding,
output_padding,
self.groups,
self.dilation,
)
return result
def _get_name(self):
return "QuantizedConvTranspose1d(Reference)"
@classmethod
def from_float(cls, float_conv, weight_qparams): # type: ignore[override]
return _ConvTransposeNd.from_float(cls, float_conv, weight_qparams)
class ConvTranspose2d(_ConvTransposeNd, nn.ConvTranspose2d):
def __init__(
self,
in_channels,
out_channels,
kernel_size,
stride=1,
padding=0,
output_padding=0,
groups=1,
bias=True,
dilation=1,
padding_mode="zeros",
device=None,
dtype=None,
weight_qparams: dict[str, Any] | None = None,
):
nn.ConvTranspose2d.__init__(
self,
in_channels,
out_channels,
kernel_size,
stride,
padding,
output_padding,
groups,
bias,
dilation,
# pyrefly: ignore [bad-argument-type]
padding_mode,
device,
dtype,
)
self._init_weight_qparams(weight_qparams, device)
def forward(
self, x: torch.Tensor, output_size: list[int] | None = None
) -> torch.Tensor:
"""
we have:
w(float) -- quant - dequant \
x(float) ------------- F.convTranspose2d ---
In the full model, we will see
w(float) -- quant - *dequant \
x -- quant --- *dequant -- *F.convTranspose2d --- *quant - dequant
and the backend should be able to fuse the ops with `*` into a quantized conv2d
"""
if not isinstance(self.padding, tuple):
raise AssertionError(
f"Expected self.padding to be a tuple, got {type(self.padding)}"
)
# One cannot replace List by Tuple or Sequence in "_output_padding" because
# TorchScript does not support `Sequence[T]` or `Tuple[T, ...]`.
output_padding = self._output_padding(
input, # type: ignore[arg-type]
output_size,
self.stride, # type: ignore[arg-type]
self.padding, # type: ignore[arg-type]
self.kernel_size, # type: ignore[arg-type]
self.dilation, # type: ignore[arg-type]
)
weight_quant_dequant = self.get_weight()
result = F.conv_transpose2d(
x,
weight_quant_dequant,
self.bias,
self.stride,
self.padding,
output_padding,
self.groups,
self.dilation,
)
return result
def _get_name(self):
return "QuantizedConvTranspose2d(Reference)"
@classmethod
def from_float(cls, float_conv, weight_qparams): # type: ignore[override]
return _ConvTransposeNd.from_float(cls, float_conv, weight_qparams)
class ConvTranspose3d(_ConvTransposeNd, nn.ConvTranspose3d):
def __init__(
self,
in_channels,
out_channels,
kernel_size,
stride=1,
padding=0,
output_padding=0,
groups=1,
bias=True,
dilation=1,
padding_mode="zeros",
device=None,
dtype=None,
weight_qparams: dict[str, Any] | None = None,
):
nn.ConvTranspose3d.__init__(
self,
in_channels,
out_channels,
kernel_size,
stride,
padding,
output_padding,
groups,
bias,
dilation,
# pyrefly: ignore [bad-argument-type]
padding_mode,
device,
dtype,
)
self._init_weight_qparams(weight_qparams, device)
def forward(
self, x: torch.Tensor, output_size: list[int] | None = None
) -> torch.Tensor:
"""
we have:
w(float) -- quant - dequant \
x(float) ------------- F.convTranspose3d ---
In the full model, we will see
w(float) -- quant - *dequant \
x -- quant --- *dequant -- *F.convTranspose3d --- *quant - dequant
and the backend should be able to fuse the ops with `*` into a quantized conv3d
"""
if not isinstance(self.padding, tuple):
raise AssertionError(
f"Expected self.padding to be a tuple, got {type(self.padding)}"
)
# One cannot replace List by Tuple or Sequence in "_output_padding" because
# TorchScript does not support `Sequence[T]` or `Tuple[T, ...]`.
output_padding = self._output_padding(
input, # type: ignore[arg-type]
output_size,
self.stride, # type: ignore[arg-type]
self.padding, # type: ignore[arg-type]
self.kernel_size, # type: ignore[arg-type]
self.dilation, # type: ignore[arg-type]
)
weight_quant_dequant = self.get_weight()
result = F.conv_transpose3d(
x,
weight_quant_dequant,
self.bias,
self.stride,
self.padding,
output_padding,
self.groups,
self.dilation,
)
return result
def _get_name(self):
return "QuantizedConvTranspose3d(Reference)"
@classmethod
def from_float(cls, float_conv, weight_qparams): # type: ignore[override]
return _ConvTransposeNd.from_float(cls, float_conv, weight_qparams)
@@ -0,0 +1,69 @@
from typing import Any
import torch
import torch.nn as nn
import torch.nn.functional as F
from .utils import ReferenceQuantizedModule
__all__ = ["Linear"]
class Linear(nn.Linear, ReferenceQuantizedModule):
"""A reference quantized linear module that fits into the FX
Graph Mode Quantization workflow
activation will be floating point Tensor, we will store floating
point weight as well in the module, but in forward we'll quantize
and dequantize the weight before running the floating point functional
linear operator.
"""
_IS_REFERENCE = True
def __init__(
self,
in_features: int,
out_features: int,
bias_: bool = True,
device: torch.device | None = None,
dtype: torch.dtype | None = None,
weight_qparams: dict[str, Any] | None = None,
) -> None:
super().__init__(in_features, out_features, bias_, device, dtype)
self._init_weight_qparams(weight_qparams, device)
def _get_name(self) -> str:
return "QuantizedLinear(Reference)"
def forward(self, x: torch.Tensor) -> torch.Tensor:
"""
we have:
w(float) -- quant - dequant \
x(float) ------------- F.linear ---
In the full model, we will see
w(float) -- quant - *dequant \
x -- quant --- *dequant -- *F.linear --- *quant - dequant
and the backend should be able to fuse the ops with `*` into a quantized linear
"""
weight_quant_dequant = self.get_weight()
result = F.linear(x, weight_quant_dequant, self.bias)
return result
@classmethod
def from_float(
cls, float_linear: nn.Linear, weight_qparams: dict[str, Any]
) -> "Linear":
qref_linear = Linear(
float_linear.in_features,
float_linear.out_features,
float_linear.bias is not None,
device=float_linear.weight.device,
dtype=float_linear.weight.dtype,
weight_qparams=weight_qparams,
)
qref_linear.weight = torch.nn.Parameter(float_linear.weight.detach())
if float_linear.bias is not None:
qref_linear.bias = torch.nn.Parameter(float_linear.bias.detach())
return qref_linear
@@ -0,0 +1,856 @@
# mypy: allow-untyped-defs
from typing import Any
import torch
import torch.nn as nn
from torch import _VF, Tensor
from torch.nn.utils.rnn import PackedSequence
from .utils import _quantize_and_dequantize_weight, _quantize_weight
__all__ = [
"RNNCellBase",
"RNNCell",
"LSTMCell",
"GRUCell",
"RNNBase",
"LSTM",
"GRU",
"get_quantized_weight",
]
def _apply_permutation(tensor: Tensor, permutation: Tensor, dim: int = 1) -> Tensor:
return tensor.index_select(dim, permutation)
def _get_weight_and_quantization_params(module, wn):
weight = getattr(module, wn)
params = [weight]
for param_name in [
wn + n for n in ["_qscheme", "_dtype", "_scale", "_zero_point", "_axis_int"]
]:
if hasattr(module, param_name):
param = getattr(module, param_name)
else:
param = None
params.append(param)
return params
def get_quantized_weight(module, wn):
if not hasattr(module, wn):
return None
params = _get_weight_and_quantization_params(module, wn)
weight = _quantize_weight(*params)
return weight
def _get_quantize_and_dequantized_weight(module, wn):
if not hasattr(module, wn):
return None
params = _get_weight_and_quantization_params(module, wn)
weight = _quantize_and_dequantize_weight(*params)
return weight
class RNNCellBase(nn.RNNCellBase):
def __init__(
self,
input_size: int,
hidden_size: int,
bias: bool,
num_chunks: int,
device=None,
dtype=None,
weight_qparams_dict=None,
) -> None:
super().__init__(
input_size, hidden_size, bias, num_chunks, device=device, dtype=dtype
)
# TODO(jerryzh168): maybe make this arg a required arg
if weight_qparams_dict is None:
weight_qparams = {
"qscheme": torch.per_tensor_affine,
"dtype": torch.quint8,
"scale": 1.0,
"zero_point": 0,
}
weight_qparams_dict = {
"weight_ih": weight_qparams,
"weight_hh": weight_qparams,
"is_decomposed": False,
}
if len(weight_qparams_dict) != 3:
raise AssertionError(
f"Expected length for weight_qparams_dict to be 3 for QuantizedRNNCellBase(Reference), "
f"got {len(weight_qparams_dict)}"
)
self._init_weight_qparams_dict(weight_qparams_dict, device)
def _init_weight_qparams_dict(self, weight_qparams_dict, device):
if weight_qparams_dict is None:
raise AssertionError("weight_qparams_dict must not be None")
self.is_decomposed = weight_qparams_dict["is_decomposed"]
for key, weight_qparams in weight_qparams_dict.items():
if key == "is_decomposed":
continue
# TODO: refactor the duplicated code to utils.py
weight_qscheme = weight_qparams["qscheme"]
weight_dtype = weight_qparams["dtype"]
setattr(self, key + "_qscheme", weight_qscheme)
setattr(self, key + "_dtype", weight_dtype)
if weight_qscheme not in [
None,
torch.per_tensor_affine,
torch.per_channel_affine,
]:
raise AssertionError(
f"qscheme: {weight_qscheme} is not supported in {self._get_name()}"
)
if weight_qscheme is not None:
scale = weight_qparams["scale"]
scale_tensor = (
scale.detach().clone()
if isinstance(scale, torch.Tensor)
else torch.tensor(scale, dtype=torch.float, device=device)
)
self.register_buffer(key + "_scale", scale_tensor)
zp = weight_qparams["zero_point"]
zp_tensor = (
zp.detach().clone()
if isinstance(zp, torch.Tensor)
else torch.tensor(zp, dtype=torch.int, device=device)
)
self.register_buffer(key + "_zero_point", zp_tensor)
if weight_qscheme == torch.per_channel_affine:
axis = weight_qparams["axis"]
axis_tensor = (
axis.detach().clone()
if isinstance(axis, torch.Tensor)
else torch.tensor(axis, dtype=torch.int, device=device)
)
self.register_buffer(key + "_axis", axis_tensor)
else:
# added for TorchScriptability, not used
self.register_buffer(
key + "_axis", torch.tensor(0, dtype=torch.int, device=device)
)
setattr(self, key + "_axis_int", getattr(self, key + "_axis").item())
def _get_name(self):
return "QuantizedRNNCellBase(Reference)"
def get_quantized_weight_ih(self):
return get_quantized_weight(self, "weight_ih")
def get_quantized_weight_hh(self):
return get_quantized_weight(self, "weight_hh")
def get_weight_ih(self):
return _get_quantize_and_dequantized_weight(self, "weight_ih")
def get_weight_hh(self):
return _get_quantize_and_dequantized_weight(self, "weight_hh")
class RNNCell(RNNCellBase):
"""
We'll store weight_qparams for all the weights (weight_ih and weight_hh),
we need to pass in a `weight_qparams_dict` that maps from weight name,
e.g. weight_ih, to the weight_qparams for that weight
"""
def __init__(
self,
input_size: int,
hidden_size: int,
bias: bool = True,
nonlinearity: str = "tanh",
device=None,
dtype=None,
weight_qparams_dict: dict[str, Any] | None = None,
) -> None:
factory_kwargs = {
"device": device,
"dtype": dtype,
"weight_qparams_dict": weight_qparams_dict,
}
super().__init__(input_size, hidden_size, bias, num_chunks=1, **factory_kwargs)
self.nonlinearity = nonlinearity
def _get_name(self):
return "QuantizedRNNCell(Reference)"
# TODO: refactor nn.RNNCell to have a _forward that takes weight_ih and weight_hh as input
# and remove duplicated code, same for the other two Cell modules
def forward(self, input: Tensor, hx: Tensor | None = None) -> Tensor:
if input.dim() not in (1, 2):
raise AssertionError(
f"RNNCell: Expected input to be 1-D or 2-D but received {input.dim()}-D tensor"
)
is_batched = input.dim() == 2
if not is_batched:
input = input.unsqueeze(0)
if hx is None:
hx = torch.zeros(
input.size(0), self.hidden_size, dtype=input.dtype, device=input.device
)
else:
hx = hx.unsqueeze(0) if not is_batched else hx
if self.nonlinearity == "tanh":
ret = _VF.rnn_tanh_cell(
input,
hx,
self.get_weight_ih(),
self.get_weight_hh(),
self.bias_ih,
self.bias_hh,
)
elif self.nonlinearity == "relu":
ret = _VF.rnn_relu_cell(
input,
hx,
self.get_weight_ih(),
self.get_weight_hh(),
self.bias_ih,
self.bias_hh,
)
else:
ret = input # TODO: remove when jit supports exception flow
raise RuntimeError(f"Unknown nonlinearity: {self.nonlinearity}")
if not is_batched:
ret = ret.squeeze(0)
return ret
@classmethod
def from_float(cls, mod, weight_qparams_dict):
ref_mod = cls(
mod.input_size,
mod.hidden_size,
mod.bias,
mod.nonlinearity,
mod.weight_ih.device,
mod.weight_ih.dtype,
weight_qparams_dict,
)
ref_mod.weight_ih = mod.weight_ih
ref_mod.weight_hh = mod.weight_hh
ref_mod.bias_ih = mod.bias_ih
ref_mod.bias_hh = mod.bias_hh
return ref_mod
class LSTMCell(RNNCellBase):
"""
We'll store weight_qparams for all the weights (weight_ih and weight_hh),
we need to pass in a `weight_qparams_dict` that maps from weight name,
e.g. weight_ih, to the weight_qparams for that weight
"""
def __init__(
self,
input_size: int,
hidden_size: int,
bias: bool = True,
device=None,
dtype=None,
weight_qparams_dict: dict[str, Any] | None = None,
) -> None:
factory_kwargs = {
"device": device,
"dtype": dtype,
"weight_qparams_dict": weight_qparams_dict,
}
super().__init__(input_size, hidden_size, bias, num_chunks=4, **factory_kwargs)
def _get_name(self):
return "QuantizedLSTMCell(Reference)"
def forward(
self, input: Tensor, hx: tuple[Tensor, Tensor] | None = None
) -> tuple[Tensor, Tensor]:
if input.dim() not in (1, 2):
raise AssertionError(
f"LSTMCell: Expected input to be 1-D or 2-D but received {input.dim()}-D tensor"
)
is_batched = input.dim() == 2
if not is_batched:
input = input.unsqueeze(0)
if hx is None:
zeros = torch.zeros(
input.size(0), self.hidden_size, dtype=input.dtype, device=input.device
)
hx = (zeros, zeros)
else:
hx = (hx[0].unsqueeze(0), hx[1].unsqueeze(0)) if not is_batched else hx
ret = _VF.lstm_cell(
input,
hx,
self.get_weight_ih(),
self.get_weight_hh(),
self.bias_ih,
self.bias_hh,
)
if not is_batched:
ret = (ret[0].squeeze(0), ret[1].squeeze(0))
return ret
@classmethod
def from_float(cls, mod, weight_qparams_dict, use_precomputed_fake_quant=False):
ref_mod = cls(
mod.input_size,
mod.hidden_size,
mod.bias,
mod.weight_ih.device,
mod.weight_ih.dtype,
weight_qparams_dict,
)
ref_mod.weight_ih = mod.weight_ih
ref_mod.weight_hh = mod.weight_hh
ref_mod.bias_ih = mod.bias_ih
ref_mod.bias_hh = mod.bias_hh
return ref_mod
class GRUCell(RNNCellBase):
"""
We'll store weight_qparams for all the weights (weight_ih and weight_hh),
we need to pass in a `weight_qparams_dict` that maps from weight name,
e.g. weight_ih, to the weight_qparams for that weight
"""
def __init__(
self,
input_size: int,
hidden_size: int,
bias: bool = True,
device=None,
dtype=None,
weight_qparams_dict: dict[str, Any] | None = None,
) -> None:
factory_kwargs = {
"device": device,
"dtype": dtype,
"weight_qparams_dict": weight_qparams_dict,
}
super().__init__(input_size, hidden_size, bias, num_chunks=3, **factory_kwargs)
def _get_name(self):
return "QuantizedGRUCell(Reference)"
def forward(self, input: Tensor, hx: Tensor | None = None) -> Tensor:
if input.dim() not in (1, 2):
raise AssertionError(
f"GRUCell: Expected input to be 1-D or 2-D but received {input.dim()}-D tensor"
)
is_batched = input.dim() == 2
if not is_batched:
input = input.unsqueeze(0)
if hx is None:
hx = torch.zeros(
input.size(0), self.hidden_size, dtype=input.dtype, device=input.device
)
else:
hx = hx.unsqueeze(0) if not is_batched else hx
ret = _VF.gru_cell(
input,
hx,
self.get_weight_ih(),
self.get_weight_hh(),
self.bias_ih,
self.bias_hh,
)
if not is_batched:
ret = ret.squeeze(0)
return ret
@classmethod
def from_float(cls, mod, weight_qparams_dict):
ref_mod = cls(
mod.input_size,
mod.hidden_size,
mod.bias,
mod.weight_ih.device,
mod.weight_ih.dtype,
weight_qparams_dict,
)
ref_mod.weight_ih = mod.weight_ih
ref_mod.weight_hh = mod.weight_hh
ref_mod.bias_ih = mod.bias_ih
ref_mod.bias_hh = mod.bias_hh
return ref_mod
class RNNBase(nn.RNNBase):
def __init__(
self,
mode: str,
input_size: int,
hidden_size: int,
num_layers: int = 1,
bias: bool = True,
batch_first: bool = False,
dropout: float = 0.0,
bidirectional: bool = False,
proj_size: int = 0,
device=None,
dtype=None,
weight_qparams_dict: dict[str, Any] | None = None,
) -> None:
super().__init__(
mode,
input_size,
hidden_size,
num_layers,
bias,
batch_first,
dropout,
bidirectional,
proj_size,
device,
dtype,
)
# TODO(jerryzh168): maybe make this arg a required arg
if weight_qparams_dict is None:
weight_qparams = {
"qscheme": torch.per_tensor_affine,
"dtype": torch.quint8,
"scale": 1.0,
"zero_point": 0,
}
weight_qparams_dict = {"is_decomposed": False} # type: ignore[dict-item]
for wn in self._flat_weights_names:
if wn.startswith("weight"):
weight_qparams_dict[wn] = weight_qparams
self._init_weight_qparams_dict(weight_qparams_dict, device)
def _init_weight_qparams_dict(self, weight_qparams_dict, device):
self.is_decomposed = weight_qparams_dict["is_decomposed"]
for key, weight_qparams in weight_qparams_dict.items():
if key == "is_decomposed":
continue
weight_qscheme = weight_qparams["qscheme"]
weight_dtype = weight_qparams["dtype"]
setattr(self, key + "_qscheme", weight_qscheme)
setattr(self, key + "_dtype", weight_dtype)
if weight_qscheme not in [
None,
torch.per_tensor_affine,
torch.per_channel_affine,
]:
raise AssertionError(
f"qscheme: {weight_qscheme} is not supported in {self._get_name()}"
)
if weight_qscheme is not None:
self.register_buffer(
key + "_scale",
torch.tensor(
weight_qparams["scale"], dtype=torch.float, device=device
),
)
self.register_buffer(
key + "_zero_point",
torch.tensor(
weight_qparams["zero_point"], dtype=torch.int, device=device
),
)
if weight_qscheme == torch.per_channel_affine:
self.register_buffer(
key + "_axis",
torch.tensor(
weight_qparams["axis"], dtype=torch.int, device=device
),
)
else:
# added for TorchScriptability, not used
self.register_buffer(
key + "_axis", torch.tensor(0, dtype=torch.int, device=device)
)
setattr(self, key + "_axis_int", getattr(self, key + "_axis").item())
class LSTM(RNNBase):
"""Reference Quantized LSTM Module
We'll store weight_qparams for all the weights in _flat_weights, we need to pass in
a `weight_qparams_dict` that maps from weight name, e.g. weight_ih_l0,
to the weight_qparams for that weight
"""
def __init__(self, *args, **kwargs):
super().__init__("LSTM", *args, **kwargs)
# Same as above, see torch/nn/modules/module.py::_forward_unimplemented
def permute_hidden( # type: ignore[override]
self,
hx: tuple[Tensor, Tensor],
permutation: Tensor | None,
) -> tuple[Tensor, Tensor]:
if permutation is None:
return hx
return _apply_permutation(hx[0], permutation), _apply_permutation(
hx[1], permutation
)
def get_expected_cell_size(
self, input: Tensor, batch_sizes: Tensor | None
) -> tuple[int, int, int]:
if batch_sizes is not None:
mini_batch = int(batch_sizes[0])
else:
mini_batch = input.size(0) if self.batch_first else input.size(1)
num_directions = 2 if self.bidirectional else 1
expected_hidden_size = (
self.num_layers * num_directions,
mini_batch,
self.hidden_size,
)
return expected_hidden_size
# In the future, we should prevent mypy from applying contravariance rules here.
# See torch/nn/modules/module.py::_forward_unimplemented
def check_forward_args( # type: ignore[override]
self,
input: Tensor,
hidden: tuple[Tensor, Tensor],
batch_sizes: Tensor | None,
):
self.check_input(input, batch_sizes)
self.check_hidden_size(
hidden[0],
self.get_expected_hidden_size(input, batch_sizes),
"Expected hidden[0] size {}, got {}",
)
self.check_hidden_size(
hidden[1],
self.get_expected_cell_size(input, batch_sizes),
"Expected hidden[1] size {}, got {}",
)
def get_quantized_weight_bias_dict(self):
"""dictionary from flat_weight_name to quantized weight or (unquantized) bias
e.g.
{
"weight_ih_l0": quantized_weight,
"bias_ih_l0": unquantized_bias,
...
}
"""
quantized_weight_bias_dict = {}
for wn in self._flat_weights_names:
if hasattr(self, wn):
if wn.startswith("weight"):
weight_or_bias = get_quantized_weight(self, wn)
else:
weight_or_bias = getattr(self, wn)
else:
weight_or_bias = None
quantized_weight_bias_dict[wn] = weight_or_bias
return quantized_weight_bias_dict
def get_flat_weights(self):
flat_weights = []
for wn in self._flat_weights_names:
if hasattr(self, wn):
weight = getattr(self, wn)
if wn.startswith("weight"):
params = _get_weight_and_quantization_params(self, wn)
weight = _quantize_and_dequantize_weight(*params)
else:
weight = None
flat_weights.append(weight)
return flat_weights
def forward(self, input, hx=None): # noqa: F811
orig_input = input
# xxx: isinstance check needs to be in conditional for TorchScript to compile
batch_sizes = None
if isinstance(orig_input, PackedSequence):
input, batch_sizes, sorted_indices, unsorted_indices = input
max_batch_size = int(batch_sizes[0])
else:
batch_sizes = None
is_batched = input.dim() == 3
batch_dim = 0 if self.batch_first else 1
if not is_batched:
input = input.unsqueeze(batch_dim)
max_batch_size = input.size(0) if self.batch_first else input.size(1)
sorted_indices = None
unsorted_indices = None
if hx is None:
num_directions = 2 if self.bidirectional else 1
real_hidden_size = (
self.proj_size if self.proj_size > 0 else self.hidden_size
)
h_zeros = torch.zeros(
self.num_layers * num_directions,
max_batch_size,
real_hidden_size,
dtype=input.dtype,
device=input.device,
)
c_zeros = torch.zeros(
self.num_layers * num_directions,
max_batch_size,
self.hidden_size,
dtype=input.dtype,
device=input.device,
)
hx = (h_zeros, c_zeros)
else:
if batch_sizes is None: # If not PackedSequence input.
if is_batched: # type: ignore[possibly-undefined]
if hx[0].dim() != 3 or hx[1].dim() != 3:
msg = (
"For batched 3-D input, hx and cx should "
f"also be 3-D but got ({hx[0].dim()}-D, {hx[1].dim()}-D) tensors"
)
raise RuntimeError(msg)
else:
if hx[0].dim() != 2 or hx[1].dim() != 2:
msg = (
"For unbatched 2-D input, hx and cx should "
f"also be 2-D but got ({hx[0].dim()}-D, {hx[1].dim()}-D) tensors"
)
raise RuntimeError(msg)
hx = (hx[0].unsqueeze(1), hx[1].unsqueeze(1))
# Each batch of the hidden state should match the input sequence that
# the user believes he/she is passing in.
hx = self.permute_hidden(hx, sorted_indices)
self.check_forward_args(input, hx, batch_sizes)
if batch_sizes is None:
result = _VF.lstm(
input,
hx,
self.get_flat_weights(),
self.bias,
self.num_layers,
self.dropout,
self.training,
self.bidirectional,
self.batch_first,
)
else:
result = _VF.lstm(
input,
batch_sizes,
hx,
self.get_flat_weights(),
self.bias,
self.num_layers,
self.dropout,
self.training,
self.bidirectional,
)
output = result[0]
hidden = result[1:]
# xxx: isinstance check needs to be in conditional for TorchScript to compile
if isinstance(orig_input, PackedSequence):
output_packed = PackedSequence(
output,
batch_sizes,
sorted_indices,
unsorted_indices,
)
return output_packed, self.permute_hidden(hidden, unsorted_indices)
else:
if not is_batched: # type: ignore[possibly-undefined]
output = output.squeeze(batch_dim) # type: ignore[possibly-undefined]
hidden = (hidden[0].squeeze(1), hidden[1].squeeze(1))
return output, self.permute_hidden(hidden, unsorted_indices)
def _get_name(self):
return "QuantizedLSTM(Reference)"
@classmethod
def from_float(cls, mod, weight_qparams_dict):
ref_mod = cls(
mod.input_size,
mod.hidden_size,
mod.num_layers,
mod.bias,
mod.batch_first,
mod.dropout,
mod.bidirectional,
weight_qparams_dict=weight_qparams_dict,
)
for wn in mod._flat_weights_names:
setattr(ref_mod, wn, getattr(mod, wn))
return ref_mod
class GRU(RNNBase):
"""Reference Quantized GRU Module
We'll store weight_qparams for all the weights in _flat_weights, we need to pass in
a `weight_qparams_dict` that maps from weight name, e.g. weight_ih_l0,
to the weight_qparams for that weight
"""
def __init__(self, *args, **kwargs):
if "proj_size" in kwargs:
raise ValueError(
"proj_size argument is only supported for LSTM, not RNN or GRU"
)
super().__init__("GRU", *args, **kwargs)
def get_quantized_weight_bias_dict(self):
"""dictionary from flat_weight_name to quantized weight or (unquantized) bias
e.g.
{
"weight_ih_l0": quantized_weight,
"bias_ih_l0": unquantized_bias,
...
}
"""
quantized_weight_bias_dict = {}
for wn in self._flat_weights_names:
if hasattr(self, wn):
if wn.startswith("weight"):
weight_or_bias = get_quantized_weight(self, wn)
else:
weight_or_bias = getattr(self, wn)
else:
weight_or_bias = None
quantized_weight_bias_dict[wn] = weight_or_bias
return quantized_weight_bias_dict
def get_flat_weights(self):
flat_weights = []
for wn in self._flat_weights_names:
if hasattr(self, wn):
weight = getattr(self, wn)
if wn.startswith("weight"):
params = _get_weight_and_quantization_params(self, wn)
weight = _quantize_and_dequantize_weight(*params)
else:
weight = None
flat_weights.append(weight)
return flat_weights
def forward(self, input, hx=None): # noqa: F811
# Note: this is copied from the forward of GRU in https://github.com/pytorch/pytorch/blob/master/torch/nn/modules/rnn.py
# only changed self._flat_weights to self.get_flat_weights()
# TODO: maybe we can try inheriting from that class and define get_flat_weights
# as a @property? this might interfere with TorchScript, if we remove that
# requirement in the future we should be able to do this
orig_input = input
# xxx: isinstance check needs to be in conditional for TorchScript to compile
if isinstance(orig_input, PackedSequence):
input, batch_sizes, sorted_indices, unsorted_indices = input
max_batch_size = int(batch_sizes[0])
else:
batch_sizes = None
if input.dim() not in (2, 3):
raise AssertionError(
f"GRU: Expected input to be 2-D or 3-D but received {input.dim()}-D tensor"
)
is_batched = input.dim() == 3
batch_dim = 0 if self.batch_first else 1
if not is_batched:
input = input.unsqueeze(batch_dim)
if hx is not None:
if hx.dim() != 2:
raise RuntimeError(
f"For unbatched 2-D input, hx should also be 2-D but got {hx.dim()}-D tensor"
)
hx = hx.unsqueeze(1)
else:
if hx is not None and hx.dim() != 3:
raise RuntimeError(
f"For batched 3-D input, hx should also be 3-D but got {hx.dim()}-D tensor"
)
max_batch_size = input.size(0) if self.batch_first else input.size(1)
sorted_indices = None
unsorted_indices = None
if hx is None:
num_directions = 2 if self.bidirectional else 1
hx = torch.zeros(
self.num_layers * num_directions,
max_batch_size,
self.hidden_size,
dtype=input.dtype,
device=input.device,
)
else:
# Each batch of the hidden state should match the input sequence that
# the user believes he/she is passing in.
hx = self.permute_hidden(hx, sorted_indices)
self.check_forward_args(input, hx, batch_sizes)
if batch_sizes is None:
result = _VF.gru(
input,
hx,
self.get_flat_weights(),
self.bias,
self.num_layers,
self.dropout,
self.training,
self.bidirectional,
self.batch_first,
)
else:
result = _VF.gru(
input,
batch_sizes,
hx,
self.get_flat_weights(),
self.bias,
self.num_layers,
self.dropout,
self.training,
self.bidirectional,
)
output = result[0]
hidden = result[1]
# xxx: isinstance check needs to be in conditional for TorchScript to compile
if isinstance(orig_input, PackedSequence):
output_packed = PackedSequence(
output,
batch_sizes,
sorted_indices,
unsorted_indices,
)
return output_packed, self.permute_hidden(hidden, unsorted_indices)
else:
if not is_batched: # type: ignore[possibly-undefined]
output = output.squeeze(batch_dim) # type: ignore[possibly-undefined]
hidden = hidden.squeeze(1)
return output, self.permute_hidden(hidden, unsorted_indices)
def _get_name(self):
return "QuantizedGRU(Reference)"
@classmethod
def from_float(cls, mod, weight_qparams_dict):
ref_mod = cls(
mod.input_size,
mod.hidden_size,
mod.num_layers,
mod.bias,
mod.batch_first,
mod.dropout,
mod.bidirectional,
weight_qparams_dict=weight_qparams_dict,
)
for wn in mod._flat_weights_names:
setattr(ref_mod, wn, getattr(mod, wn))
return ref_mod
@@ -0,0 +1,163 @@
# mypy: allow-untyped-defs
from typing import Any
import torch.nn as nn
import torch.nn.functional as F
from torch import Tensor
from .utils import ReferenceQuantizedModule
__all__ = ["Embedding", "EmbeddingBag"]
class Embedding(nn.Embedding, ReferenceQuantizedModule):
"""A reference quantized Embedding module that fits into the
FX Graph Mode Quantization workflow, activation will be floating point Tensor,
we will store floating point weight as well in the module, but in forward we'll
quantize and dequantize the weight before running the floating point functional
embedding operator.
"""
def __init__(
self,
num_embeddings: int,
embedding_dim: int,
padding_idx: int | None = None,
max_norm: float | None = None,
norm_type: float = 2.0,
scale_grad_by_freq: bool = False,
sparse: bool = False,
_weight: Tensor | None = None,
device=None,
dtype=None,
weight_qparams: dict[str, Any] | None = None,
) -> None:
super().__init__(
num_embeddings,
embedding_dim,
padding_idx,
max_norm,
norm_type,
scale_grad_by_freq,
sparse,
_weight,
# pyrefly: ignore [bad-argument-type]
device,
dtype,
)
self._init_weight_qparams(weight_qparams, device)
def _get_name(self):
return "QuantizedEmbedding(Reference)"
def forward(self, input: Tensor) -> Tensor:
weight_quant_dequant = self.get_weight()
return F.embedding(
input,
weight_quant_dequant,
self.padding_idx,
self.max_norm,
self.norm_type,
self.scale_grad_by_freq,
self.sparse,
)
@classmethod
def from_float(cls, mod, weight_qparams):
return cls(
mod.num_embeddings,
mod.embedding_dim,
mod.padding_idx,
mod.max_norm,
mod.norm_type,
mod.scale_grad_by_freq,
mod.sparse,
mod.weight,
mod.weight.device,
mod.weight.dtype,
weight_qparams,
)
class EmbeddingBag(nn.EmbeddingBag, ReferenceQuantizedModule):
"""A reference quantized EmbeddingBag module that fits into the
FX Graph Mode Quantization workflow, activation will be floating point Tensor,
we will store floating point weight as well in the module, but in forward we'll
quantize and dequantize the weight before running the floating point functional
embedding operator.
"""
def __init__(
self,
num_embeddings: int,
embedding_dim: int,
max_norm: float | None = None,
norm_type: float = 2.0,
scale_grad_by_freq: bool = False,
mode: str = "mean",
sparse: bool = False,
_weight: Tensor | None = None,
include_last_offset: bool = False,
padding_idx: int | None = None,
device=None,
dtype=None,
weight_qparams: dict[str, Any] | None = None,
) -> None:
super().__init__(
num_embeddings,
embedding_dim,
max_norm,
norm_type,
scale_grad_by_freq,
mode,
sparse,
_weight,
include_last_offset,
padding_idx,
device,
dtype,
)
self._init_weight_qparams(weight_qparams, device)
def _get_name(self):
return "QuantizedEmbedding(Reference)"
def forward(
self,
input: Tensor,
offsets: Tensor | None = None,
per_sample_weights: Tensor | None = None,
) -> Tensor:
weight_quant_dequant = self.get_weight()
return F.embedding_bag(
input,
weight_quant_dequant,
offsets,
self.max_norm,
self.norm_type,
self.scale_grad_by_freq,
self.mode,
self.sparse,
per_sample_weights,
self.include_last_offset,
self.padding_idx,
)
@classmethod
def from_float(cls, mod, weight_qparams, use_precomputed_fake_quant=False):
return cls(
mod.num_embeddings,
mod.embedding_dim,
mod.max_norm,
mod.norm_type,
mod.scale_grad_by_freq,
mod.mode,
mod.sparse,
mod.weight,
mod.include_last_offset,
mod.padding_idx,
mod.weight.device,
mod.weight.dtype,
weight_qparams,
)
@@ -0,0 +1,439 @@
# mypy: allow-untyped-defs
import typing
import torch
__all__ = [
"ReferenceQuantizedModule",
]
class ReferenceQuantizedModule(torch.nn.Module):
def _init_weight_qparams(self, weight_qparams, device):
if weight_qparams is None:
weight_qparams = {
"qscheme": torch.per_tensor_affine,
"dtype": torch.quint8,
"scale": 1.0,
"zero_point": 0,
}
self.weight_qscheme: torch.qscheme = weight_qparams["qscheme"]
self.weight_dtype = weight_qparams["dtype"]
if self.weight_qscheme not in [
None,
torch.per_tensor_affine,
torch.per_channel_affine,
torch.per_channel_affine_float_qparams,
]:
raise AssertionError(
f"qscheme: {self.weight_qscheme} is not supported in reference quantized {self._get_name()}"
)
if self.weight_dtype in [
torch.quint8,
torch.qint8,
torch.quint4x2,
torch.qint32,
]:
zero_point_dtype = (
weight_qparams["zero_point"].dtype
if isinstance(weight_qparams["zero_point"], torch.Tensor)
else torch.int
)
w_scale = weight_qparams["scale"]
w_scale_tensor = (
w_scale.detach().clone()
if isinstance(w_scale, torch.Tensor)
else torch.tensor(w_scale, dtype=torch.float, device=device)
)
self.register_buffer("weight_scale", w_scale_tensor)
w_zp = weight_qparams["zero_point"]
w_zp_tensor = (
w_zp.detach().clone()
if isinstance(w_zp, torch.Tensor)
else torch.tensor(w_zp, dtype=zero_point_dtype, device=device)
)
self.register_buffer("weight_zero_point", w_zp_tensor)
if self.weight_qscheme in [
torch.per_channel_affine,
torch.per_channel_affine_float_qparams,
]:
w_axis = weight_qparams["axis"]
w_axis_tensor = (
w_axis.detach().clone()
if isinstance(w_axis, torch.Tensor)
else torch.tensor(w_axis, dtype=torch.int, device=device)
)
self.register_buffer("weight_axis", w_axis_tensor)
else:
# added for TorchScriptability, not used
self.register_buffer(
"weight_axis", torch.tensor(0, dtype=torch.int, device=device)
)
else:
# added for TorchScriptability, and for torch.float
self.register_buffer(
"weight_scale", torch.tensor(1.0, dtype=torch.float, device=device)
)
self.register_buffer(
"weight_zero_point", torch.tensor(0, dtype=torch.int, device=device)
)
self.register_buffer(
"weight_axis", torch.tensor(0, dtype=torch.int, device=device)
)
self.is_decomposed: bool = weight_qparams.get("is_decomposed", False)
# store weight_axis as weight_axis_int due to some constraints of torchdynamo.export
# for capturing `.item` operations
self.weight_axis_int: int = self.weight_axis.item() # type: ignore[operator, assignment]
self.weight_quant_min: int | None = weight_qparams.get("quant_min")
self.weight_quant_max: int | None = weight_qparams.get("quant_max")
def get_weight(self):
"""
Fake quantize (quantize and dequantize) the weight with
the quantization parameters for weight, this is used to
simulate the numerics for the quantized weight in a quantized
model
"""
# suppress mypy warning
if not isinstance(self.weight_scale, torch.Tensor):
raise AssertionError("weight_scale must be a Tensor")
if not isinstance(self.weight_zero_point, torch.Tensor):
raise AssertionError("weight_zero_point must be a Tensor")
if self.is_decomposed:
return _quantize_and_dequantize_weight_decomposed(
self.weight, # type: ignore[arg-type]
self.weight_qscheme,
self.weight_dtype,
self.weight_scale,
self.weight_zero_point,
self.weight_axis_int,
self.weight_quant_min,
self.weight_quant_max,
)
else:
return _quantize_and_dequantize_weight(
self.weight, # type: ignore[arg-type]
self.weight_qscheme,
self.weight_dtype,
self.weight_scale,
self.weight_zero_point,
self.weight_axis_int,
)
def get_quantized_weight(self):
# suppress mypy warning
if not isinstance(self.weight_scale, torch.Tensor):
raise AssertionError("weight_scale must be a Tensor")
if not isinstance(self.weight_zero_point, torch.Tensor):
raise AssertionError("weight_zero_point must be a Tensor")
# assert isinstance(self.weight_axis, torch.Tensor)
if self.is_decomposed:
return _quantize_weight_decomposed(
self.weight, # type: ignore[arg-type]
self.weight_qscheme,
self.weight_dtype,
self.weight_scale,
self.weight_zero_point,
self.weight_axis_int,
self.weight_quant_min,
self.weight_quant_max,
)
else:
return _quantize_weight(
self.weight, # type: ignore[arg-type]
self.weight_qscheme,
self.weight_dtype,
self.weight_scale,
self.weight_zero_point,
self.weight_axis_int,
)
def _save_to_state_dict(self, destination, prefix, keep_vars):
super()._save_to_state_dict(destination, prefix, keep_vars)
_save_weight_qparams(
destination,
prefix,
self.weight_qscheme,
self.weight_dtype,
self.weight_scale,
self.weight_zero_point,
self.weight_axis,
)
def _load_from_state_dict(
self,
state_dict,
prefix,
local_metadata,
strict,
missing_keys,
unexpected_keys,
error_msgs,
):
for key in _get_weight_qparam_keys(state_dict, prefix):
setattr(self, key, state_dict[prefix + key])
state_dict.pop(prefix + key)
super()._load_from_state_dict(
state_dict,
prefix,
local_metadata,
False,
missing_keys,
unexpected_keys,
error_msgs,
)
def _quantize_weight_decomposed(
weight: torch.Tensor,
weight_qscheme: torch.qscheme,
weight_dtype: torch.dtype,
weight_scale: torch.Tensor,
weight_zero_point: torch.Tensor,
weight_axis: int,
weight_quant_min: int | None,
weight_quant_max: int | None,
) -> torch.Tensor:
_DTYPE_TO_QVALUE_BOUNDS: dict[torch.dtype, tuple[int, int]] = {
torch.uint8: (0, 255),
torch.int8: (-128, 127),
torch.int32: (-2147483648, 2147483647), # torch.jit interprets 2**31 as a float
}
# TODO: add an util function for converting qdtype to dtype
_QDTYPE_TO_UNDERLYING_INT_REPR_DTYPE = {
torch.quint8: torch.uint8,
torch.qint8: torch.int8,
torch.qint32: torch.int32,
}
if weight_qscheme == torch.per_tensor_affine:
if weight_dtype in [torch.quint8, torch.qint8, torch.qint32]:
weight_dtype_ = _QDTYPE_TO_UNDERLYING_INT_REPR_DTYPE[weight_dtype]
if weight_quant_min is None or weight_quant_max is None:
weight_quant_min, weight_quant_max = _DTYPE_TO_QVALUE_BOUNDS[
weight_dtype_
]
weight = torch.ops.quantized_decomposed.quantize_per_tensor(
weight,
weight_scale,
weight_zero_point,
weight_quant_min,
weight_quant_max,
weight_dtype_,
)
return weight
elif weight_qscheme in [
torch.per_channel_affine,
torch.per_channel_affine_float_qparams,
]:
# TODO: torch.quint4x2 is not supported
if weight_dtype in [torch.quint8, torch.qint8, torch.qint32]:
weight_dtype_ = _QDTYPE_TO_UNDERLYING_INT_REPR_DTYPE[weight_dtype]
if weight_quant_min is None or weight_quant_max is None:
weight_quant_min, weight_quant_max = _DTYPE_TO_QVALUE_BOUNDS[
weight_dtype_
]
weight = torch.ops.quantized_decomposed.quantize_per_channel(
weight,
weight_scale,
weight_zero_point,
weight_axis,
weight_quant_min,
weight_quant_max,
weight_dtype_,
) # type: ignore[arg-type]
return weight
raise ValueError(f"Unsupported dtype and qscheme: {weight_dtype}, {weight_qscheme}")
def _dequantize_weight_decomposed(
weight: torch.Tensor,
weight_qscheme: torch.qscheme,
weight_dtype: torch.dtype,
weight_scale: torch.Tensor,
weight_zero_point: torch.Tensor,
weight_axis: int,
weight_quant_min: int | None,
weight_quant_max: int | None,
) -> torch.Tensor:
# TODO: get the quant_min and quant_max from activation_post_process
_DTYPE_TO_QVALUE_BOUNDS: dict[torch.dtype, tuple[int, int]] = {
torch.uint8: (0, 255),
torch.int8: (-128, 127),
torch.int32: (-2147483648, 2147483647), # torch.jit interprets 2**31 as a float
}
# TODO: add an util function for converting qdtype to dtype
_QDTYPE_TO_UNDERLYING_INT_REPR_DTYPE = {
torch.quint8: torch.uint8,
torch.qint8: torch.int8,
torch.qint32: torch.int32,
}
weight_dtype_ = _QDTYPE_TO_UNDERLYING_INT_REPR_DTYPE[weight_dtype]
if weight_quant_min is None or weight_quant_max is None:
weight_quant_min, weight_quant_max = _DTYPE_TO_QVALUE_BOUNDS[weight_dtype_]
if weight_qscheme == torch.per_tensor_affine:
if weight_dtype in [torch.quint8, torch.qint8, torch.qint32]:
weight = torch.ops.quantized_decomposed.dequantize_per_tensor(
weight,
weight_scale,
weight_zero_point,
weight_quant_min,
weight_quant_max,
weight_dtype_,
)
return weight
elif weight_qscheme in [
torch.per_channel_affine,
torch.per_channel_affine_float_qparams,
]:
# TODO: torch.quint4x2 is not supported
if weight_dtype in [torch.quint8, torch.qint8, torch.qint32]:
weight = torch.ops.quantized_decomposed.dequantize_per_channel(
weight,
weight_scale,
weight_zero_point,
weight_axis,
weight_quant_min,
weight_quant_max,
weight_dtype_,
) # type: ignore[arg-type]
return weight
raise ValueError(f"Unsupported dtype and qscheme: {weight_dtype}, {weight_qscheme}")
def _quantize_weight(
weight: torch.Tensor,
weight_qscheme: torch.qscheme,
weight_dtype: torch.dtype,
weight_scale: torch.Tensor,
weight_zero_point: torch.Tensor,
weight_axis_int: int,
) -> torch.Tensor:
if weight_dtype == torch.float16:
weight = weight.to(weight_dtype)
return weight
if weight_qscheme == torch.per_tensor_affine:
if weight_dtype in [torch.quint8, torch.qint8, torch.qint32]:
weight = torch.quantize_per_tensor(
weight, weight_scale, weight_zero_point, weight_dtype
)
return weight
elif weight_qscheme in [
torch.per_channel_affine,
torch.per_channel_affine_float_qparams,
]:
if weight_dtype in [torch.quint8, torch.qint8, torch.quint4x2, torch.qint32]:
weight = torch.quantize_per_channel(
weight, weight_scale, weight_zero_point, weight_axis_int, weight_dtype
) # type: ignore[arg-type]
return weight
raise ValueError(f"Unsupported dtype and qscheme: {weight_dtype}, {weight_qscheme}")
def _quantize_and_dequantize_weight_decomposed(
weight: torch.Tensor,
weight_qscheme: torch.qscheme,
weight_dtype: torch.dtype,
weight_scale: torch.Tensor,
weight_zero_point: torch.Tensor,
weight_axis_int: int,
weight_quant_min: int | None,
weight_quant_max: int | None,
) -> torch.Tensor:
"""Quantize and then dequantize the weight based on
the quantization parameters
"""
if weight_qscheme in [
torch.per_tensor_affine,
torch.per_channel_affine,
torch.per_channel_affine_float_qparams,
]:
weight_quant = _quantize_weight_decomposed(
weight,
weight_qscheme,
weight_dtype,
weight_scale,
weight_zero_point,
weight_axis_int,
weight_quant_min,
weight_quant_max,
)
weight_dequant = _dequantize_weight_decomposed(
weight_quant,
weight_qscheme,
weight_dtype,
weight_scale,
weight_zero_point,
weight_axis_int,
weight_quant_min,
weight_quant_max,
)
else:
weight_dequant = weight
return weight_dequant
def _quantize_and_dequantize_weight(
weight: torch.Tensor,
weight_qscheme: torch.qscheme,
weight_dtype: torch.dtype,
weight_scale: torch.Tensor,
weight_zero_point: torch.Tensor,
weight_axis_int: int,
) -> torch.Tensor:
"""Quantize and then dequantize the weight based on
the quantization parameters
"""
if weight_qscheme in [
torch.per_tensor_affine,
torch.per_channel_affine,
torch.per_channel_affine_float_qparams,
]:
weight_quant = _quantize_weight(
weight,
weight_qscheme,
weight_dtype,
weight_scale,
weight_zero_point,
weight_axis_int,
)
weight_dequant = weight_quant.dequantize()
else:
weight_dequant = weight
return weight_dequant
def _save_weight_qparams(
destination,
prefix,
weight_qscheme,
weight_dtype,
weight_scale,
weight_zero_point,
weight_axis,
):
destination[prefix + "weight_qscheme"] = weight_qscheme
destination[prefix + "weight_dtype"] = weight_dtype
if weight_qscheme is not None:
destination[prefix + "weight_scale"] = weight_scale
destination[prefix + "weight_zero_point"] = weight_zero_point
if weight_qscheme == torch.per_channel_affine:
destination[prefix + "weight_axis"] = weight_axis
def _get_weight_qparam_keys(state_dict: dict[str, typing.Any], prefix: str):
keys = ["weight_qscheme", "weight_dtype"]
weight_qscheme = state_dict[prefix + "weight_qscheme"]
if weight_qscheme is not None:
keys.append("weight_scale")
keys.append("weight_zero_point")
if weight_qscheme == torch.quantize_per_channel:
keys.append("weight_axis")
return keys
@@ -0,0 +1 @@
from . import quantized
@@ -0,0 +1,10 @@
from torch.ao.nn.sparse.quantized import dynamic
from .linear import Linear, LinearPackedParams
__all__ = [
"dynamic",
"Linear",
"LinearPackedParams",
]
@@ -0,0 +1,6 @@
from .linear import Linear
__all__ = [
"Linear",
]
@@ -0,0 +1,203 @@
# mypy: allow-untyped-defs
import torch
import torch.ao.nn.intrinsic as nni
from torch.ao.nn.quantized.modules.utils import (
_hide_packed_params_repr,
_quantize_weight,
)
from torch.ao.nn.sparse.quantized import linear
from torch.ao.nn.sparse.quantized.utils import LinearBlockSparsePattern
__all__ = ["Linear"]
class Linear(torch.nn.Module):
r"""
A dynamically quantized sparse linear module with float tensor as inputs and outputs.
"""
_version = 1
_op_type = "sparse_dynamic"
_FLOAT_MODULE = torch.nn.Linear
def __init__(
self,
in_features,
out_features,
row_block_size,
col_block_size,
bias=True,
dtype=torch.qint8,
):
super().__init__()
if dtype != torch.qint8:
raise NotImplementedError(
"Only QINT8 is supported for Sparse Quantized Linear Dynamic"
)
self.in_features = in_features
self.out_features = out_features
if bias:
bias = torch.zeros(self.out_features, dtype=torch.float)
else:
bias = None
qweight = torch._empty_affine_quantized(
[out_features, in_features], scale=1, zero_point=0, dtype=torch.qint8
)
self._packed_params = linear.LinearPackedParams(
row_block_size=row_block_size, col_block_size=col_block_size, dtype=dtype
)
self._packed_params.set_weight_bias(
qweight, bias, row_block_size, col_block_size
)
def _get_name(self):
return "SparseQuantizedDynamicLinear"
def extra_repr(self):
return f"in_features={self.in_features}, out_features={self.out_features}, qscheme={self.weight().qscheme()}"
def __repr__(self):
return _hide_packed_params_repr(self, linear.LinearPackedParams)
def forward(self, x: torch.Tensor) -> torch.Tensor:
return torch.ops.sparse.qlinear_dynamic(x, self._packed_params._packed_params)
def _save_to_state_dict(self, destination, prefix, keep_vars):
super()._save_to_state_dict(destination, prefix, keep_vars)
destination[prefix + "op_type"] = self._op_type
def _load_from_state_dict(
self,
state_dict,
prefix,
local_metadata,
strict,
missing_keys,
unexpected_keys,
error_msgs,
):
op_type = int(state_dict[prefix + "op_type"])
if op_type != "sparse":
raise AssertionError(
f"Cannot load from op_type [{op_type}], expecting [{self._op_type}]"
)
state_dict.pop(prefix + "op_type")
version = local_metadata.get("version", None)
if version is not None and version > self._version:
raise AssertionError(f"version {version} > self._version {self._version}")
# Is this code valid? In old quantization it seemed to be used to load
# older model
weight = state_dict.pop(prefix + "weight")
bias = state_dict.pop(prefix + "bias")
state_dict.update(
{
prefix + "_packed_params.weight": weight,
prefix + "_packed_params.bias": bias,
}
)
super()._load_from_state_dict(
state_dict,
prefix,
local_metadata,
False,
missing_keys,
unexpected_keys,
error_msgs,
)
def _weight_bias(self):
return self._packed_params._weight_bias()
def weight(self):
return self._weight_bias()[0]
def bias(self):
return self._weight_bias()[1]
def set_weight_bias(
self,
w: torch.Tensor,
b: torch.Tensor | None,
row_block_size: int | None,
col_block_size: int | None,
) -> None:
if row_block_size is None or col_block_size is None:
raise AssertionError(
f"row_block_size and col_block_size must not be None, got {row_block_size=}, {col_block_size=}"
)
self.out_features = w.shape[0]
self.in_features = w.shape[1]
self._packed_params.set_weight_bias(w, b, row_block_size, col_block_size)
@classmethod
def from_float(cls, mod, use_precomputed_fake_quant=False):
r"""Create a quantized sparse dynamic module from a float module.
We only care about the convert at this stage, no need for observers just yet.
"""
if type(mod) is not cls._FLOAT_MODULE:
raise AssertionError(
" nnq."
+ cls.__name__
+ ".from_float only works for "
+ cls._FLOAT_MODULE.__name__
)
# TODO: Need to add options to qconfig to avoid the calibration.
# TODO: Add calibration for the sparsity
if not hasattr(mod, "qconfig"):
raise AssertionError("Input float module must have qconfig defined")
if type(mod) is nni.LinearReLU:
mod = mod[0]
# pyrefly: ignore [missing-attribute]
if mod.qconfig is not None and mod.qconfig.weight is not None:
# pyrefly: ignore [not-callable]
weight_observer = mod.qconfig.weight()
else:
# We have the circular import issues if we import the qconfig in the beginning of this file:
# https://github.com/pytorch/pytorch/pull/24231. The current workaround is to postpone the
# import until we need it.
from torch.ao.quantization.qconfig import default_dynamic_qconfig
weight_observer = default_dynamic_qconfig.weight()
# It is important to multiply by the mask BEFORE calling the `weight_observer`
# TODO (zaf): Mask might not be part of the qconfig (T83295194)
weight = mod.weight
if getattr(mod.qconfig, "mask", False):
weight = mod.qconfig.mask * mod.weight
weight_observer(weight)
dtype = weight_observer.dtype
if dtype != torch.qint8:
raise AssertionError(
f"Weight observer must have dtype torch.qint8, got {dtype}"
)
_w_sc, w_zp = weight_observer.calculate_qparams()
if isinstance(w_zp, torch.Tensor):
if torch.any(w_zp.bool()):
raise AssertionError("All weight zero points must map to 0")
else:
if w_zp != 0:
raise AssertionError(f"Weight zero point must map to 0, got {w_zp}")
qweight = _quantize_weight(weight.float(), weight_observer)
row_block_size, col_block_size = LinearBlockSparsePattern.block_size()
qlinear = cls(
mod.in_features,
mod.out_features,
row_block_size,
col_block_size,
dtype=dtype,
)
# pyrefly: ignore [bad-argument-type]
qlinear.set_weight_bias(qweight, mod.bias, row_block_size, col_block_size)
return qlinear
@@ -0,0 +1,300 @@
# mypy: allow-untyped-defs
import torch
from torch.ao.nn.quantized.modules.utils import (
_hide_packed_params_repr,
_quantize_weight,
)
__all__ = ["LinearPackedParams", "Linear"]
# TODO (zaf): Inherit from `quantized.LinearPackedParams` (T83294430)
class LinearPackedParams(torch.nn.Module):
_version = 1
def __init__(self, row_block_size=1, col_block_size=4, dtype=torch.qint8):
super().__init__()
if dtype != torch.qint8:
raise NotImplementedError("Linear prepacking only supports QINT8")
self.dtype = dtype
wq = torch._empty_affine_quantized(
[1, 1], scale=1.0, zero_point=0, dtype=torch.qint8
)
self.set_weight_bias(wq, None, row_block_size, col_block_size)
def _get_name(self):
return "SparseQuantizedLinearPackedParams"
@torch.jit.export
def set_weight_bias(
self,
weight: torch.Tensor,
bias: torch.Tensor | None,
row_block_size: int | None,
col_block_size: int | None,
) -> None:
if row_block_size is None or col_block_size is None:
raise AssertionError(
"row_block_size and col_block_size must not be None, got "
f"row_block_size={row_block_size}, col_block_size={col_block_size}"
)
self._packed_params = torch.ops.sparse.qlinear_prepack(
weight, bias, row_block_size, col_block_size
)
@torch.jit.export
def _weight_bias(self):
(weight, bias, block_sizes) = torch.ops.sparse.qlinear_unpack(
self._packed_params
)
return (weight, bias, block_sizes[0], block_sizes[1])
def forward(self, x):
return x
def _save_to_state_dict(self, destination, prefix, keep_vars):
super()._save_to_state_dict(destination, prefix, keep_vars)
destination[prefix + "dtype"] = self.dtype
destination[prefix + "_packed_params"] = self._weight_bias()
def _load_from_state_dict(
self,
state_dict,
prefix,
local_metadata,
strict,
missing_keys,
unexpected_keys,
error_msgs,
):
version = local_metadata.get("version", None)
if version is not None and version > self._version:
raise AssertionError(f"version {version} > self._version {self._version}")
self.dtype = state_dict.pop(prefix + "dtype")
weight, bias, row_block_size, col_block_size = state_dict.pop(
prefix + "_packed_params"
)
self.set_weight_bias(weight, bias, row_block_size, col_block_size)
super()._load_from_state_dict(
state_dict,
prefix,
local_metadata,
False,
missing_keys,
unexpected_keys,
error_msgs,
)
@torch.jit.export
def __getstate__(self):
return self._packed_params, self.training, self.dtype
@torch.jit.export
def __setstate__(self, state):
(self._packed_params, self.training, self.dtype) = state
def __repr__(self):
return self._weight_bias().__repr__()
# TODO (zaf): Inherit from `quantized.Linear` (T83294430)
class Linear(torch.nn.Module):
r"""
A quantized sparse linear module with quantized tensor as inputs and outputs.
"""
_version = 1
_FLOAT_MODULE = torch.nn.Linear
def __init__(
self,
in_features,
out_features,
row_block_size,
col_block_size,
bias=True,
dtype=torch.qint8,
):
super().__init__()
if dtype != torch.qint8:
raise NotImplementedError(
"Only QINT8 is supported for Sparse Quantized Linear"
)
self.in_features = in_features
self.out_features = out_features
if bias:
bias = torch.zeros(self.out_features, dtype=torch.float)
else:
bias = None
qweight = torch._empty_affine_quantized(
[out_features, in_features], scale=1, zero_point=0, dtype=torch.qint8
)
self._packed_params = LinearPackedParams(
row_block_size=row_block_size, col_block_size=col_block_size, dtype=dtype
)
self._packed_params.set_weight_bias(
qweight, bias, row_block_size, col_block_size
)
self.scale = 1.0
self.zero_point = 0
@classmethod
def _get_name(cls):
return "SparseQuantizedLinear"
def extra_repr(self):
return (
f"in_features={self.in_features}, out_features={self.out_features}, scale={self.scale}, "
f"zero_point={self.zero_point}, qscheme={self.weight().qscheme()}"
)
def __repr__(self):
return _hide_packed_params_repr(self, LinearPackedParams)
def forward(self, x: torch.Tensor) -> torch.Tensor:
return torch.ops.sparse.qlinear(
x, self._packed_params._packed_params, self.scale, self.zero_point
)
def _save_to_state_dict(self, destination, prefix, keep_vars):
super()._save_to_state_dict(destination, prefix, keep_vars)
destination[prefix + "scale"] = torch.tensor(self.scale)
destination[prefix + "zero_point"] = torch.tensor(self.zero_point)
def _load_from_state_dict(
self,
state_dict,
prefix,
local_metadata,
strict,
missing_keys,
unexpected_keys,
error_msgs,
):
self.scale = float(state_dict[prefix + "scale"])
state_dict.pop(prefix + "scale")
self.zero_point = int(state_dict[prefix + "zero_point"])
state_dict.pop(prefix + "zero_point")
state_dict.pop(prefix + "op_type")
version = local_metadata.get("version", None)
if version is not None and version > self._version:
raise AssertionError(f"version {version} > self._version {self._version}")
super()._load_from_state_dict(
state_dict,
prefix,
local_metadata,
False,
missing_keys,
unexpected_keys,
error_msgs,
)
def _weight_bias(self):
return self._packed_params._weight_bias()
def weight(self):
return self._weight_bias()[0]
def bias(self):
return self._weight_bias()[1]
def set_weight_bias(
self,
w: torch.Tensor,
b: torch.Tensor | None,
row_block_size: int | None,
col_block_size: int | None,
) -> None:
if row_block_size is None or col_block_size is None:
raise AssertionError(
"row_block_size and col_block_size must not be None, "
f"got row_block_size={row_block_size}, col_block_size={col_block_size}"
)
self._packed_params.set_weight_bias(w, b, row_block_size, col_block_size)
@classmethod
def from_float(cls, mod, use_precomputed_fake_quant=False):
r"""Create a quantized sparse module from a float module.
We only care about the convert at this stage, no need for observers just yet.
TODO(zaf): Need to add the sparse params to the qconfig
"""
if type(mod) is not cls._FLOAT_MODULE:
raise AssertionError(
cls._get_name()
+ ".from_float only works for "
+ cls._FLOAT_MODULE.__name__
)
if not hasattr(mod, "sparse_params"):
raise AssertionError(
"Expecting the Linear to have `sparse_params`. Make sure you have provided arguments "
'in the `sparsifier.squash_mask(params_to_save=("sparse_block_shape",))` method.'
)
sparse_block_shape = mod.sparse_params.get("sparse_block_shape", None) # type: ignore[operator, union-attr]
if not isinstance(sparse_block_shape, (tuple, list)):
raise AssertionError(
f"sparse_block_shape must be tuple or list, got {type(sparse_block_shape)}"
)
if len(sparse_block_shape) != 2:
raise AssertionError(
f"sparse_block_shape must have length 2, got {len(sparse_block_shape)}"
)
# TODO: Need to add options to qconfig to avoid the calibration.
# TODO: Add calibration for the sparsity
if not hasattr(mod, "qconfig"):
raise AssertionError("Input float module must have qconfig defined")
activation_post_process = mod.activation_post_process
weight_post_process = mod.qconfig.weight() # type: ignore[operator, union-attr]
# Assumption is that the weight is already sparsified by the
# `sparsifier.convert`
weight = mod.weight
weight_post_process(weight)
dtype = weight_post_process.dtype
act_scale, act_zp = activation_post_process.calculate_qparams() # type: ignore[operator, union-attr]
if dtype != torch.qint8:
raise AssertionError(
f"Weight observer must have dtype torch.qint8, got {dtype}"
)
w_sc, w_zp = weight_post_process.calculate_qparams()
if isinstance(w_zp, torch.Tensor):
if torch.any(w_zp.bool()):
raise AssertionError("All weight zero points must map to 0")
else:
if w_zp != 0:
raise AssertionError(f"Weight zero point must map to 0, got {w_zp}")
qweight = _quantize_weight(weight.float(), weight_post_process)
row_block_size = mod.sparse_params["sparse_block_shape"][0] # type: ignore[index]
col_block_size = mod.sparse_params["sparse_block_shape"][1] # type: ignore[index]
qlinear = cls(
mod.in_features,
mod.out_features,
row_block_size,
col_block_size,
dtype=dtype,
)
qlinear.set_weight_bias(
qweight,
mod.bias,
row_block_size, # type: ignore[arg-type]
col_block_size, # type: ignore[arg-type]
)
qlinear.scale = float(act_scale)
qlinear.zero_point = int(act_zp)
return qlinear
@@ -0,0 +1,66 @@
import threading
__all__ = ["LinearBlockSparsePattern"]
def _is_valid_linear_block_sparse_pattern(
row_block_size: int, col_block_size: int
) -> bool:
return (row_block_size == 1 and col_block_size == 4) or (
row_block_size == 8 and col_block_size == 1
)
# This is a stop-gap measure as current flow does not allow module
# specific block sparse pattern.
# In fact there is no way to convey sparse pattern via module config
# of quantization flow. Thus using the global context to convey
# sparsity pattern.
# Once the flow supports it, this should be removed.
class LinearBlockSparsePattern:
rlock = threading.RLock()
row_block_size: int = 1
col_block_size: int = 4
prev_row_block_size: int = 1
prev_col_block_size: int = 4
def __init__(self, row_block_size: int = 1, col_block_size: int = 4):
if not _is_valid_linear_block_sparse_pattern(row_block_size, col_block_size):
raise AssertionError(
f"Invalid linear block sparse pattern: "
f"row_block_size={row_block_size}, col_block_size={col_block_size}"
)
LinearBlockSparsePattern.rlock.acquire()
LinearBlockSparsePattern.prev_row_block_size = (
LinearBlockSparsePattern.row_block_size
)
LinearBlockSparsePattern.prev_col_block_size = (
LinearBlockSparsePattern.col_block_size
)
LinearBlockSparsePattern.row_block_size = row_block_size
LinearBlockSparsePattern.col_block_size = col_block_size
def __enter__(self) -> None:
pass
def __exit__(
self,
exc_type: type[BaseException] | None,
exc_value: BaseException | None,
backtrace: object | None,
) -> None:
LinearBlockSparsePattern.row_block_size = (
LinearBlockSparsePattern.prev_row_block_size
)
LinearBlockSparsePattern.col_block_size = (
LinearBlockSparsePattern.prev_col_block_size
)
LinearBlockSparsePattern.rlock.release()
@staticmethod
def block_size() -> tuple[int, int]:
return (
LinearBlockSparsePattern.row_block_size,
LinearBlockSparsePattern.col_block_size,
)
@@ -0,0 +1,568 @@
# mypy: allow-untyped-defs
from collections.abc import Callable
from typing import Any
import torch
import torch.ao.nn.quantized as nnq
import torch.ao.nn.quantized.dynamic as nnqd
import torch.nn as nn
from torch.ao.quantization import prepare
from torch.ao.quantization.quantization_mappings import (
get_default_compare_output_module_list,
)
NON_LEAF_MODULE_TO_ADD_OBSERVER_ALLOW_LIST = {
nnqd.Linear,
nnq.Linear,
nnqd.LSTM,
nn.LSTM,
}
def _find_match(
str_list: dict[str, Any] | list[str],
key_str: str,
postfix: str,
) -> str | None:
split_str = key_str.split(".")
if split_str[-1] == postfix:
match_string = "".join(key_str.split(".")[0:-1])
for s2 in str_list:
pattern1 = "".join(s2.split(".")[0:-1])
pattern2 = "".join(s2.split(".")[0:-2])
if match_string == pattern1:
return s2
if match_string == pattern2:
return s2
# For matching "fc.weight" and "fc._packed_params._packed_params"
if postfix == "_packed_params":
match_string = "".join(key_str.split(".")[0:-2])
if len(match_string) == 0:
return None
for s2 in str_list:
pattern1 = "".join(s2.split(".")[0:-1])
pattern2 = "".join(s2.split(".")[0:-2])
if match_string == pattern1:
return s2
if match_string == pattern2:
return s2
return None
else:
return None
def compare_weights(
float_dict: dict[str, Any], quantized_dict: dict[str, Any]
) -> dict[str, dict[str, torch.Tensor]]:
r"""Compare the weights of the float module with its corresponding quantized
module. Return a dict with key corresponding to module names and each entry being
a dictionary with two keys 'float' and 'quantized', containing the float and
quantized weights. This dict can be used to compare and compute the quantization
error of the weights of float and quantized models.
Example usage::
wt_compare_dict = compare_weights(float_model.state_dict(), qmodel.state_dict())
for key in wt_compare_dict:
print(
key,
compute_error(
wt_compare_dict[key]["float"],
wt_compare_dict[key]["quantized"].dequantize(),
),
)
Args:
float_dict: state dict of the float model
quantized_dict: state dict of the quantized model
Return:
weight_dict: dict with key corresponding to module names and each entry being
a dictionary with two keys 'float' and 'quantized', containing the float and
quantized weights
"""
torch._C._log_api_usage_once("quantization_api._numeric_suite.compare_weights")
weight_dict: dict[str, dict] = {}
for key in quantized_dict:
match_key = _find_match(float_dict, key, "weight")
if match_key is not None:
weight_dict[key] = {}
weight_dict[key]["float"] = float_dict[match_key]
weight_dict[key]["quantized"] = quantized_dict[key]
continue
# For matching "fc.weight" and "fc._packed_params._packed_params"
match_key = _find_match(float_dict, key, "_packed_params")
if match_key is not None:
weight_dict[key] = {}
weight_dict[key]["float"] = float_dict[match_key]
weight_dict[key]["quantized"] = quantized_dict[key][0]
# For LSTM
split_str = key.split(".")
if split_str[-1] == "param" and split_str[-3] == "_all_weight_values":
layer = split_str[-2]
module_name = ".".join(split_str[:-3])
float_weight_ih_key = module_name + ".weight_ih_l" + layer
float_weight_hh_key = module_name + ".weight_hh_l" + layer
if float_weight_ih_key in float_dict and float_weight_hh_key in float_dict:
weight_dict[key] = {}
weight_dict[key]["float"] = float_dict[float_weight_ih_key]
weight_dict[key]["quantized"] = (
quantized_dict[key].__getstate__()[0][4][0].__getstate__()[0][0]
)
weight_dict[key]["float"] = float_dict[float_weight_hh_key]
weight_dict[key]["quantized"] = (
quantized_dict[key].__getstate__()[0][4][1].__getstate__()[0][0]
)
return weight_dict
def _get_logger_dict_helper(
mod: nn.Module,
target_dict: dict[str, Any],
prefix: str = "",
) -> None:
r"""This is the helper function for get_logger_dict
Args:
mod: module we want to save all logger stats
prefix: prefix for the current module
target_dict: the dictionary used to save all logger stats
"""
def get_prefix(prefix):
return prefix if prefix == "" else prefix + "."
for child in mod.children():
if isinstance(child, Logger):
target_dict[get_prefix(prefix) + "stats"] = child.stats
break
for name, child in mod.named_children():
module_prefix = get_prefix(prefix) + name if prefix else name
_get_logger_dict_helper(child, target_dict, module_prefix)
def get_logger_dict(mod: nn.Module, prefix: str = "") -> dict[str, dict]:
r"""Traverse the modules and save all logger stats into target dict.
This is mainly used for quantization accuracy debug.
Type of loggers supported:
ShadowLogger: used to log the outputs of the quantized module and its matching float shadow module,
OutputLogger: used to log the outputs of the modules
Args:
mod: module we want to save all logger stats
prefix: prefix for the current module
Return:
target_dict: the dictionary used to save all logger stats
"""
torch._C._log_api_usage_once("quantization_api._numeric_suite.get_logger_dict")
target_dict: dict[str, dict] = {}
_get_logger_dict_helper(mod, target_dict, prefix)
return target_dict
class Logger(nn.Module):
r"""Base class for stats logging"""
def __init__(self):
super().__init__()
self.stats = {}
# We only insert observer if the op is quantized with static quantization,
# which is identified by activation_observer.dtype == quint8. This is needed
# when attaching Logger as observer for FX mode
self.dtype = torch.quint8
def forward(self, x):
# fmt: off
"""
""" # blank docblock to make autodoc happy
# fmt: on
class ShadowLogger(Logger):
r"""Class used in Shadow module to record the outputs of the original and
shadow modules.
"""
def __init__(self):
super().__init__()
self.stats["float"] = []
self.stats["quantized"] = []
def forward(self, x, y): # type: ignore[override]
# fmt: off
"""
""" # blank docblock to make autodoc happy
# fmt: on
if len(x) > 1:
x = x[0]
if len(y) > 1:
y = y[0]
self.stats["quantized"].append(x.detach())
self.stats["float"].append(y.detach())
class OutputLogger(Logger):
r"""Class used to log the outputs of the module"""
def __init__(self):
super().__init__()
self.stats["tensor_val"] = []
def forward(self, x):
# fmt: off
"""
""" # blank docblock to make autodoc happy
# fmt: on
self.stats["tensor_val"].append(x)
return x
def _convert_tuple_to_list(t: Any) -> Any:
return [_convert_tuple_to_list(x) for x in t] if type(t) is tuple else t
def _dequantize_tensor_list(t: Any) -> Any:
return (
[_dequantize_tensor_list(x) for x in t]
if type(t) is list
else t.dequantize()
if t.is_quantized
else t
)
class Shadow(nn.Module):
r"""Shadow module attaches the float module to its matching quantized module
as the shadow. Then it uses Logger module to process the outputs of both
modules.
Args:
q_module: module quantized from float_module that we want to shadow
float_module: float module used to shadow q_module
logger_cls: type of logger used to process the outputs of q_module and
float_module. ShadowLogger or custom loggers can be used.
"""
def __init__(self, q_module, float_module, logger_cls):
super().__init__()
self.orig_module = q_module
self.shadow_module = float_module
self.dequant = nnq.DeQuantize()
self.logger = logger_cls()
def forward(self, *x) -> torch.Tensor:
# fmt: off
"""
""" # blank docblock to make autodoc happy
# fmt: on
xl = _convert_tuple_to_list(x)
output = self.orig_module(*xl)
xl_float = _dequantize_tensor_list(xl)
shadow_output = self.shadow_module(*xl_float)
self.logger(output, shadow_output)
return output
def add(self, x: torch.Tensor, y: torch.Tensor) -> torch.Tensor:
# fmt: off
"""
""" # blank docblock to make autodoc happy
# fmt: on
output = self.orig_module.add(x, y)
x = x.dequantize()
y = y.dequantize()
shadow_output = self.shadow_module.add(x, y)
self.logger(output, shadow_output)
return output
def add_scalar(self, x: torch.Tensor, y: float) -> torch.Tensor:
# fmt: off
"""
""" # blank docblock to make autodoc happy
# fmt: on
output = self.orig_module.add_scalar(x, y)
x = x.dequantize()
shadow_output = self.shadow_module.add_scalar(x, y)
self.logger(output, shadow_output)
return output
def mul(self, x: torch.Tensor, y: torch.Tensor) -> torch.Tensor:
# fmt: off
"""
""" # blank docblock to make autodoc happy
# fmt: on
output = self.orig_module.mul(x, y)
x = x.dequantize()
y = y.dequantize()
shadow_output = self.shadow_module.mul(x, y)
self.logger(output, shadow_output)
return output
def mul_scalar(self, x: torch.Tensor, y: float) -> torch.Tensor:
# fmt: off
"""
""" # blank docblock to make autodoc happy
# fmt: on
output = self.orig_module.mul_scalar(x, y)
x = x.dequantize()
shadow_output = self.shadow_module.mul_scalar(x, y)
self.logger(output, shadow_output)
return output
def cat(self, x: list[torch.Tensor], dim: int = 0) -> torch.Tensor:
# fmt: off
"""
""" # blank docblock to make autodoc happy
# fmt: on
output = self.orig_module.cat(x, dim)
x = [y.dequantize() for y in x]
shadow_output = self.shadow_module.cat(x, dim)
self.logger(output, shadow_output)
return output
def add_relu(self, x: torch.Tensor, y: torch.Tensor) -> torch.Tensor:
# fmt: off
"""
""" # blank docblock to make autodoc happy
# fmt: on
output = self.orig_module.add_relu(x, y)
x = x.dequantize()
y = y.dequantize()
shadow_output = self.shadow_module.add_relu(x, y)
self.logger(output, shadow_output)
return output
def prepare_model_with_stubs(
float_module: nn.Module,
q_module: nn.Module,
module_swap_list: set[type],
logger_cls: Callable,
) -> None:
r"""Prepare the model by attaching the float module to its matching quantized
module as the shadow if the float module type is in module_swap_list.
Example usage::
prepare_model_with_stubs(float_model, q_model, module_swap_list, Logger)
q_model(data)
ob_dict = get_logger_dict(q_model)
Args:
float_module: float module used to generate the q_module
q_module: module quantized from float_module
module_swap_list: list of float module types to attach the shadow
logger_cls: type of logger to be used in shadow module to process the outputs of
quantized module and its float shadow module
"""
torch._C._log_api_usage_once(
"quantization_api._numeric_suite.prepare_model_with_stubs"
)
float_module_children = dict(float_module.named_children())
reassign = {}
for name, mod in q_module.named_children():
if name not in float_module_children:
continue
float_mod = float_module_children[name]
if type(float_mod) not in module_swap_list:
prepare_model_with_stubs(float_mod, mod, module_swap_list, logger_cls)
# Insert shadow module only if the module is not of the same type as
# the floating point module
if type(float_mod) in module_swap_list and not _is_identical_module_type(
mod, float_mod
):
reassign[name] = Shadow(mod, float_mod, logger_cls)
for key, value in reassign.items():
q_module._modules[key] = value
def _is_identical_module_type(mod1, mod2):
# Compare if two modules have the same dtype
mod1_module_types = [type(mod) for mod in mod1.modules()]
mod2_module_types = [type(mod) for mod in mod2.modules()]
return mod1_module_types == mod2_module_types
def compare_model_stub(
float_model: nn.Module,
q_model: nn.Module,
module_swap_list: set[type],
*data,
logger_cls=ShadowLogger,
) -> dict[str, dict]:
r"""Compare quantized module in a model with its floating point counterpart,
feeding both of them the same input. Return a dict with key corresponding to
module names and each entry being a dictionary with two keys 'float' and
'quantized', containing the output tensors of quantized and its matching
float shadow module. This dict can be used to compare and compute the module
level quantization error.
This function first call prepare_model_with_stubs() to swap the quantized
module that we want to compare with the Shadow module, which takes quantized
module, corresponding float module and logger as input, and creates a forward
path inside to make the float module to shadow quantized module sharing the
same input. The logger can be customizable, default logger is ShadowLogger
and it will save the outputs of the quantized module and float module that
can be used to compute the module level quantization error.
Example usage::
module_swap_list = [
torchvision.models.quantization.resnet.QuantizableBasicBlock
]
ob_dict = compare_model_stub(float_model, qmodel, module_swap_list, data)
for key in ob_dict:
print(
key,
compute_error(
ob_dict[key]["float"], ob_dict[key]["quantized"].dequantize()
),
)
Args:
float_model: float model used to generate the q_model
q_model: model quantized from float_model
module_swap_list: list of float module types at which shadow modules will
be attached.
data: input data used to run the prepared q_model
logger_cls: type of logger to be used in shadow module to process the outputs of
quantized module and its float shadow module
"""
torch._C._log_api_usage_once("quantization_api._numeric_suite.compare_model_stub")
prepare_model_with_stubs(float_model, q_model, module_swap_list, logger_cls)
q_model(*data)
ob_dict = get_logger_dict(q_model)
return ob_dict
def get_matching_activations(
float_module: nn.Module,
q_module: nn.Module,
) -> dict[str, dict[str, torch.Tensor]]:
r"""Find the matching activation between float and quantized modules.
Args:
float_module: float module used to generate the q_module
q_module: module quantized from float_module
Return:
act_dict: dict with key corresponding to quantized module names and each
entry being a dictionary with two keys 'float' and 'quantized', containing
the matching float and quantized activations
"""
torch._C._log_api_usage_once(
"quantization_api._numeric_suite.get_matching_activations"
)
float_dict = get_logger_dict(float_module)
quantized_dict = get_logger_dict(q_module)
act_dict: dict[str, dict] = {}
for key in quantized_dict:
if len(quantized_dict[key]["tensor_val"]) == 0:
continue
match_key = _find_match(sorted(float_dict, reverse=True), key, "stats")
if match_key is not None:
act_dict[key] = {}
act_dict[key]["float"] = float_dict[match_key]["tensor_val"]
act_dict[key]["quantized"] = quantized_dict[key]["tensor_val"]
return act_dict
def prepare_model_outputs(
float_module: nn.Module,
q_module: nn.Module,
logger_cls=OutputLogger,
allow_list=None,
) -> None:
r"""Prepare the model by attaching the logger to both float module
and quantized module if they are in the allow_list.
Args:
float_module: float module used to generate the q_module
q_module: module quantized from float_module
logger_cls: type of logger to be attached to float_module and q_module
allow_list: list of module types to attach logger
"""
torch._C._log_api_usage_once(
"quantization_api._numeric_suite.prepare_model_outputs"
)
if allow_list is None:
allow_list = get_default_compare_output_module_list()
qconfig_debug = torch.ao.quantization.QConfig(activation=logger_cls, weight=None)
float_module.qconfig = qconfig_debug # type: ignore[assignment]
prepare(
float_module, inplace=True, allow_list=allow_list, prepare_custom_config_dict={}
)
q_module.qconfig = qconfig_debug # type: ignore[assignment]
prepare(
q_module,
inplace=True,
allow_list=allow_list,
observer_non_leaf_module_list=NON_LEAF_MODULE_TO_ADD_OBSERVER_ALLOW_LIST,
prepare_custom_config_dict={},
)
def compare_model_outputs(
float_model: nn.Module,
q_model: nn.Module,
*data,
logger_cls=OutputLogger,
allow_list=None,
) -> dict[str, dict[str, torch.Tensor]]:
r"""Compare output activations between float and quantized models at
corresponding locations for the same input. Return a dict with key corresponding
to quantized module names and each entry being a dictionary with two keys
'float' and 'quantized', containing the activations of quantized model and
float model at matching locations. This dict can be used to compare and
compute the propagation quantization error.
Example usage::
act_compare_dict = compare_model_outputs(float_model, qmodel, data)
for key in act_compare_dict:
print(
key,
compute_error(
act_compare_dict[key]["float"],
act_compare_dict[key]["quantized"].dequantize(),
),
)
Args:
float_model: float model used to generate the q_model
q_model: model quantized from float_model
data: input data used to run the prepared float_model and q_model
logger_cls: type of logger to be attached to float_module and q_module
allow_list: list of module types to attach logger
Return:
act_compare_dict: dict with key corresponding to quantized module names
and each entry being a dictionary with two keys 'float' and 'quantized',
containing the matching float and quantized activations
"""
torch._C._log_api_usage_once(
"quantization_api._numeric_suite.compare_model_outputs"
)
if allow_list is None:
allow_list = get_default_compare_output_module_list()
prepare_model_outputs(float_model, q_model, logger_cls, allow_list)
float_model(*data)
q_model(*data)
act_compare_dict = get_matching_activations(float_model, q_model)
return act_compare_dict
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,485 @@
# mypy: allow-untyped-defs
import collections
import enum
from typing import Any
import torch
from torch.ao.quantization import FakeQuantizeBase, ObserverBase
from torch.ao.quantization.utils import getattr_from_fqn
from torch.fx import GraphModule
from torch.fx.graph import Graph, Node
from .mappings import get_base_name_to_sets_of_related_ops, get_unmatchable_types_map
from .ns_types import NSNodeTargetType, NSSubgraph
from .pattern_utils import (
end_node_matches_reversed_fusion,
get_reversed_fusions,
get_type_a_related_to_b,
)
toq = torch.ops.quantized
def _get_output_nodes(g: Graph) -> list[Node]:
return [n for n in g.nodes if n.op == "output"]
class _NSGraphMatchableSubgraphsIterator:
"""
Iterates through the graph of gm, starting with the output nodes
and continuing backwards.
1. Returns matchable subgraphs, in order. A subgraph is defined by
(start_node, end_node).
2. Skips over non-matchable subgraphs
"""
def __init__(
self,
gm: GraphModule,
non_matchable_functions: set[NSNodeTargetType],
non_matchable_modules: set[NSNodeTargetType],
non_matchable_methods: set[NSNodeTargetType],
):
self.gm: GraphModule = gm
self.non_matchable_functions: set[NSNodeTargetType] = non_matchable_functions
self.non_matchable_modules: set[NSNodeTargetType] = non_matchable_modules
self.non_matchable_methods: set[NSNodeTargetType] = non_matchable_methods
self.seen_nodes: set[Node] = set()
self.stack: list[Node] = []
for start_node in _get_output_nodes(self.gm.graph):
self.stack.append(start_node)
def __iter__(self):
return self
def __next__(self) -> NSSubgraph:
"""
Returns the next matchable subgraph.
"""
while len(self.stack) > 0:
cur_end_node = self.stack.pop()
if cur_end_node in self.seen_nodes:
continue
# for subgraphs which are single nodes, start_node == end_node
# for subgraphs with more than one node, start node != end_node
cur_start_node = cur_end_node
# Subgraphs like linear-relu have the base node as the start node.
# Subgraphs like dequantize-linear-relu-to(torch.float16) have the
# base node as the second node.
# The cur_base_op_node var will move to the actual node during
# the fusion matching later in this code block.
cur_base_op_node = cur_end_node
# Check for potential fusions. For now, we are greedy
# and always skip all non-base nodes of a fusion. For example,
# if we match linear-relu backwards, we will always skip the
# relu node and attempt to match the linear node. This can
# be made configurable later if needed.
for _reverse_fusion_ops, base_op_idx in get_reversed_fusions():
is_match = end_node_matches_reversed_fusion(
cur_end_node, _reverse_fusion_ops, self.gm, self.seen_nodes
)
if is_match:
# navigate to the base node
# pyrefly: ignore [bad-assignment, non-convergent-recursion]
for rev_fusion_idx in range(len(_reverse_fusion_ops) - 1):
# pyrefly: ignore [bad-argument-type]
self.seen_nodes.add(cur_start_node)
# for now, assume that there are no other nodes
# which need to be added to the stack
cur_start_node = cur_start_node.args[0] # type: ignore[assignment]
# if the base op index matches the current node, set it
rev_base_op_idx = len(_reverse_fusion_ops) - 2 - base_op_idx
if rev_fusion_idx == rev_base_op_idx:
cur_base_op_node = cur_start_node
break
# pyrefly: ignore [bad-argument-type]
self.seen_nodes.add(cur_start_node)
# add args of previous nodes to stack
# pyrefly: ignore [missing-attribute]
for arg in cur_start_node.all_input_nodes:
self._recursively_add_node_arg_to_stack(arg)
# skip unmatchable nodes
# note: this check is done on the start_node, i.e.
# if we are matching linear-relu in reverse, this would do the matchable
# check on the linear
# pyrefly: ignore [bad-argument-type]
if not self._is_matchable(cur_base_op_node):
continue
# If an observer or a fake_quant was not matched as a part of
# a pattern of multiple nodes, ignore it. One case where this is
# relevant is an observer on a graph input, which was added because
# it is necessary for the next node.
if cur_end_node.op == "call_module" and cur_start_node is cur_end_node:
maybe_obs = getattr_from_fqn(self.gm, cur_end_node.target) # type: ignore[arg-type]
if isinstance(maybe_obs, (ObserverBase, FakeQuantizeBase)):
continue
return NSSubgraph(
# pyrefly: ignore [bad-argument-type]
start_node=cur_start_node,
end_node=cur_end_node,
# pyrefly: ignore [bad-argument-type]
base_op_node=cur_base_op_node,
)
raise StopIteration
def _recursively_add_node_arg_to_stack(self, arg: Any) -> None:
"""
Adds all of the nodes in this arg to the stack, properly navigating
through list, dicts and tuples.
"""
if isinstance(arg, Node):
self.stack.append(arg)
elif (
isinstance(arg, torch.fx.immutable_collections.immutable_list)
or type(arg) is tuple
):
for inner_arg in arg:
self._recursively_add_node_arg_to_stack(inner_arg)
elif isinstance(arg, torch.fx.immutable_collections.immutable_dict):
for value in arg.values():
self._recursively_add_node_arg_to_stack(value)
def _is_matchable(self, node: Node) -> bool:
if node.op == "call_function":
return node.target not in self.non_matchable_functions
elif node.op == "call_module":
if not isinstance(node.target, str):
raise AssertionError(f"Expected str, got {type(node.target)}")
target_mod = getattr_from_fqn(self.gm, node.target)
return not any(
isinstance(target_mod, t) # type: ignore[arg-type]
for t in self.non_matchable_modules
)
elif node.op == "call_method":
return node.target not in self.non_matchable_methods
else:
return False
class GraphMatchingException(Exception):
"""
Exception raised when two graphs cannot be matched.
"""
class SubgraphTypeRelationship(enum.Enum):
# same type, known
# example: F.linear and F.linear, or nn.Conv2d and nn.Conv2d
EQUAL = enum.auto()
# same type, but the type is not known to Numerical Suite
# (user defined type, etc).
EQUAL_BUT_UKNOWN = enum.auto()
# known, same subgraph_relationship set, but not the same type
# example: F.linear and toq.linear
RELATED_BUT_NOT_EQUAL = enum.auto()
# not related
NOT_RELATED = enum.auto()
def _get_subgraph_relationship_type(
subgraph_a: NSSubgraph,
subgraph_b: NSSubgraph,
gm_a: GraphModule,
gm_b: GraphModule,
type_a_related_to_b: set[tuple[NSNodeTargetType, NSNodeTargetType]],
) -> SubgraphTypeRelationship:
node_a = subgraph_a.base_op_node
node_b = subgraph_b.base_op_node
# TODO(next): make this code handle matching by what is before the base op
if node_a.op != node_b.op:
if not (
node_a.op in ("call_function", "call_method")
and node_b.op in ("call_function", "call_method")
):
return SubgraphTypeRelationship.NOT_RELATED
if node_a.op in ("call_function", "call_method"):
key = (node_a.target, node_b.target)
if key not in type_a_related_to_b:
if node_a.target == node_b.target:
return SubgraphTypeRelationship.EQUAL_BUT_UKNOWN
else:
return SubgraphTypeRelationship.NOT_RELATED
# after this point, we are dealing with known types
if node_a.target == node_b.target:
node_a_has_prev = subgraph_a.base_op_node == subgraph_a.start_node
node_b_has_prev = subgraph_b.base_op_node == subgraph_b.start_node
if node_a_has_prev and (not node_b_has_prev):
return SubgraphTypeRelationship.RELATED_BUT_NOT_EQUAL
elif (not node_a_has_prev) and node_b_has_prev:
return SubgraphTypeRelationship.RELATED_BUT_NOT_EQUAL
elif (not node_a_has_prev) and (not node_b_has_prev):
return SubgraphTypeRelationship.EQUAL
else:
# TODO(future PR): check for matches start_op_node and base_op_node
return SubgraphTypeRelationship.EQUAL
if key in type_a_related_to_b:
return SubgraphTypeRelationship.RELATED_BUT_NOT_EQUAL
else:
return SubgraphTypeRelationship.NOT_RELATED
elif node_a.op == "call_module":
if (
subgraph_a.base_op_node != subgraph_a.start_node
or subgraph_b.base_op_node != subgraph_b.start_node
):
raise AssertionError(
"Matching call_module patterns where base_op_node != start_node is not supported yet"
)
# for call_module, we need to look up the modules to do the type check
if not isinstance(node_a.target, str):
raise AssertionError(f"Expected str, got {type(node_a.target)}")
mod_a = getattr_from_fqn(gm_a, node_a.target)
if not isinstance(node_b.target, str):
raise AssertionError(f"Expected str, got {type(node_b.target)}")
mod_b = getattr_from_fqn(gm_b, node_b.target)
key = (type(mod_a), type(mod_b))
if key not in type_a_related_to_b:
if type(mod_a) is type(mod_b):
return SubgraphTypeRelationship.EQUAL_BUT_UKNOWN
else:
return SubgraphTypeRelationship.NOT_RELATED
elif type(mod_a) is type(mod_b):
return SubgraphTypeRelationship.EQUAL
else:
return SubgraphTypeRelationship.RELATED_BUT_NOT_EQUAL
return SubgraphTypeRelationship.NOT_RELATED
def _get_name_for_subgraph(
subgraph_a: NSSubgraph,
gm_a: GraphModule,
base_name_to_sets_of_related_ops: dict[str, set[NSNodeTargetType]],
existing_names: set[str],
) -> str:
"""
Returns a unique name for a subgraph. This name is based on two things:
1. the name of the set containing the underlying type of the base op in the
subgraph (i.e. 'torch.nn.functional.linear' if this is related to a linear op)
2. the number of previous subgraphs with related underlying type of the base op
For example, in the graph
linear0 -> relu0 -> linear1 -> relu1
The subgraphs are (linear0, relu0) and (linear1, relu1). If we iterate
from the output node backwards, the name given to (linear1, relu1) will be
`base_op_torch.nn.functional.linear_0`, and the name given to (linear0, relu0)
will be `base_op_torch.nn.functional.linear_1`.
Why are we not just using the node name? Answer: because of two requirements:
A. fusions must be supported
B. some Numeric Suite APIs can be called without having all of the models in memory
For example, let's say we need to match nodes of
(1) ... -> linear0 -> relu0 -> ...
And
(2) ... -> linear_relu0 -> ...
Without being able to inspect them together. With the current naming scheme, if
we iterate through both of these graphs in the same order, and assuming the rest
of the graphs match, both of these subgraphs will get the same name without
(1) and (2) knowing anything about each other.
"""
target_type = _get_node_target_type(subgraph_a.base_op_node, gm_a)
target_base_type = None
for base_name, sets_of_related_ops in base_name_to_sets_of_related_ops.items():
if target_type in sets_of_related_ops:
target_base_type = base_name
target_base_name = "base_op_" + str(target_base_type)
counter = 0
proposed_name = target_base_name + "_" + str(counter)
while proposed_name in existing_names:
counter += 1
proposed_name = target_base_name + "_" + str(counter)
existing_names.add(proposed_name)
return proposed_name
def _get_node_target_type(node: Node, gm: GraphModule) -> NSNodeTargetType | None:
if node.op in ("call_function", "call_method"):
return node.target
elif node.op == "call_module":
if not isinstance(node.target, str):
raise AssertionError(f"Expected str, got {type(node.target)}")
mod = getattr_from_fqn(gm, node.target)
return type(mod)
return None
def get_matching_subgraph_pairs(
gm_a: GraphModule,
gm_b: GraphModule,
base_name_to_sets_of_related_ops: dict[str, set[NSNodeTargetType]] | None = None,
unmatchable_types_map: dict[str, set[NSNodeTargetType]] | None = None,
) -> dict[str, tuple[NSSubgraph, NSSubgraph]]:
"""
Matches matchable subgraphs of graph_a to graph_b.
For a node, "matchable" is defined as a node which is not an observer,
fake_quants, quant or dequant.
A subgraph can contain one or more nodes. A subgraph is matchable if
at least one node inside of it is matchable. Currently, all nodes in
a subgraph must be matchable (because we assume no observers will be
inserted in the middle of a fusion).
A subgraph is defined by (start_node, end_node). We assume that only
start_node and end_node are linked with the surrounding graph, all other
nodes in a subgraph are self-contained.
A pair of nodes is "related" if both nodes represent the same mathematical
operation across different quantization flavors. For example,
`F.linear` and `torch.ops.quantized.linear` are related, and
`F.linear` and `torch.nn.Conv` are not related.
For each matchable pair of nodes node_a and node_b, they will match
if node_a and node_b are related.
For graphs A and B, they will match iff:
1. the number of matchable subgraphs in A and B is equivalent
2. when iterating through the matchable subgraphs of A and B in the same order, each
corresponding pair of base nodes is related.
This enables us to find the corresponding subgraphs between
graphs of related models. For example, if we had two graphs such as:
graph_a: x0 -> conv_0 (type: nn.Conv2d) -> obs_0 -> x1
w -/
b -/
graph_b: x0 -> quant_0 -> qconv_0 (type: nnq.Conv2d) -> dequant_0 -> x1
packed_params_0 -/
This function will return the following result:
{
'conv_0': ( # the name of the node in graph_b
(conv_0, conv_0), # (start_node_a, end_node_a)
(qconv_0, qconv_0), # (start_node_b, end_node_b)
),
}
Or, if we have a fusion pattern,
graph_a: x0 -> linear_0 -> relu_0 -> obs_0 -> x1
w -/
b -/
graph_b: x0 -> quant_0 -> linear_relu_0 -> dequant_0 -> x1
packed_params_0 -/
This function will return the following result:
{
'linear_relu_0': ( # the name of the node in graph_b
(linear_0, relu_0), # (start_node_a, end_node_a)
(linear_relu_0, linear_relu_0), # (start_node_b, end_node_b)
),
}
"""
if unmatchable_types_map is None:
unmatchable_types_map = get_unmatchable_types_map()
non_matchable_functions = unmatchable_types_map["funs_unmatchable"]
non_matchable_modules = unmatchable_types_map["mods_unmatchable"]
non_matchable_methods = unmatchable_types_map["meths_unmatchable"]
graph_a_iterator = _NSGraphMatchableSubgraphsIterator(
gm_a, non_matchable_functions, non_matchable_modules, non_matchable_methods
)
graph_b_iterator = _NSGraphMatchableSubgraphsIterator(
gm_b, non_matchable_functions, non_matchable_modules, non_matchable_methods
)
results = collections.OrderedDict()
if base_name_to_sets_of_related_ops is None:
base_name_to_sets_of_related_ops = get_base_name_to_sets_of_related_ops()
type_a_related_to_b = get_type_a_related_to_b(base_name_to_sets_of_related_ops)
existing_names_a: set[str] = set()
existing_names_b: set[str] = set()
while True:
# fetch the next subgraphs from a and b
cur_subgraph_a, cur_subgraph_b = None, None
try:
cur_subgraph_a = next(graph_a_iterator)
except StopIteration:
pass
try:
cur_subgraph_b = next(graph_b_iterator)
except StopIteration:
pass
# look up types of a and b for useful error messages
type_start_a, type_start_b = None, None
if cur_subgraph_a is not None:
type_start_a = _get_node_target_type(cur_subgraph_a.start_node, gm_a)
if cur_subgraph_b is not None:
type_start_b = _get_node_target_type(cur_subgraph_b.start_node, gm_b)
# check for results and determine what to do next
if cur_subgraph_a is not None and cur_subgraph_b is not None:
# both nodes were fetched, check for subgraph_relationship
# note: subgraph_relationship is checked on the start node, i.e.
# if a linear-relu pattern is checked, we would check for subgraph_relationship
# of the linear
subgraph_relationship = _get_subgraph_relationship_type(
cur_subgraph_a, cur_subgraph_b, gm_a, gm_b, type_a_related_to_b
)
if subgraph_relationship == SubgraphTypeRelationship.NOT_RELATED:
msg = f"""
The subgraphs
({cur_subgraph_a}, {type_start_a}) and
({cur_subgraph_b}, {type_start_b})
are not related. Please ensure that the two models you pass in have the same number
of subgraphs, and each pair of subgraphs is related to each other."""
raise GraphMatchingException(msg)
elif subgraph_relationship == SubgraphTypeRelationship.EQUAL_BUT_UKNOWN:
# skip matching but unknown types
continue
key_name_a = _get_name_for_subgraph(
cur_subgraph_a, gm_a, base_name_to_sets_of_related_ops, existing_names_a
)
key_name_b = _get_name_for_subgraph(
cur_subgraph_b, gm_b, base_name_to_sets_of_related_ops, existing_names_b
)
if key_name_a != key_name_b:
raise AssertionError(
f"Subgraph names {key_name_a} and {key_name_b} do not match"
)
results[key_name_a] = (cur_subgraph_a, cur_subgraph_b)
continue
elif cur_subgraph_a is None and cur_subgraph_b is None:
# we reached the end of both graphs
break
else:
# only one node was fetched, no match possible, throw error
msg = f"""
Attempting to match
({cur_subgraph_a}, {type_start_a}) and
({cur_subgraph_b}, {type_start_b}),
one of which is empty. Please ensure that the two models you pass in have the same number
of subgraphs."""
raise GraphMatchingException(msg)
# The subgraph pairs are originally created by traversing the two graphs
# from the outputs to the inputs. Reverse the results to return the
# subgraphs in their order of execution.
results = collections.OrderedDict(reversed(results.items()))
return results
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,763 @@
import operator
from typing import TYPE_CHECKING
import torch
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.quantization.fx._lower_to_native_backend as _lower_to_native_backend
import torch.ao.quantization.quantization_mappings as quantization_mappings
import torch.nn as nn
import torch.nn.functional as F
from torch.ao.quantization.backend_config import get_native_backend_config
from .ns_types import NSNodeTargetType
if TYPE_CHECKING:
from collections.abc import Callable
toq = torch.ops.quantized
def get_base_name_to_sets_of_related_ops() -> dict[str, set[NSNodeTargetType]]:
# note: this set is modified below by items from backend_config
sets_of_related_ops: list[set[NSNodeTargetType]] = [
# conv modules
{
nn.Conv1d,
},
{
nn.Conv2d,
},
{
nn.Conv3d,
},
# conv functionals
{
F.conv1d,
},
{
F.conv2d,
},
{
F.conv3d,
},
# linear modules
{
nn.Linear,
},
# linear functionals
{
F.linear,
},
# average pool
{
nn.AvgPool1d,
torch.avg_pool1d,
},
{
nn.AvgPool2d,
torch._C._nn.avg_pool2d,
},
{
nn.AvgPool3d,
torch._C._nn.avg_pool3d,
},
# adaptive average pool
{
nn.AdaptiveAvgPool1d,
F.adaptive_avg_pool1d,
},
{
nn.AdaptiveAvgPool2d,
F.adaptive_avg_pool2d,
},
{
nn.AdaptiveAvgPool3d,
F.adaptive_avg_pool3d,
},
# LSTM
{
nn.LSTM,
},
# add
{
torch.add,
operator.add, # x + y
},
# cat
{
torch.cat,
},
# mul
{
torch.mul,
operator.mul,
},
# relu
{
F.relu,
nn.ReLU,
"relu",
"relu_",
torch.relu,
},
# maxpool
{
nn.MaxPool1d,
F.max_pool1d,
},
{
nn.MaxPool2d,
F.max_pool2d,
},
{
nn.MaxPool3d,
F.max_pool3d,
},
# sigmoid
{
torch.sigmoid,
"sigmoid",
"sigmoid_",
nn.Sigmoid,
F.sigmoid,
},
# BatchNorm
{
nn.BatchNorm2d,
},
{
nn.BatchNorm3d,
},
# ConvTranspose
{
nn.ConvTranspose1d,
},
{
nn.ConvTranspose2d,
},
{
nn.ConvTranspose3d,
},
# functional transposed conv
{
F.conv_transpose1d,
},
{
F.conv_transpose2d,
},
{
F.conv_transpose3d,
},
# ELU
{
nn.ELU,
},
# Embedding
{
nn.Embedding,
},
# EmbeddingBag
{
nn.EmbeddingBag,
},
# GroupNorm
{
nn.GroupNorm,
},
# Hardswish
{
nn.Hardswish,
},
# InstanceNorm
{
nn.InstanceNorm1d,
},
{
nn.InstanceNorm2d,
},
{
nn.InstanceNorm3d,
},
# LayerNorm
{
nn.LayerNorm,
},
# LeakyReLU
{
nn.LeakyReLU,
},
# ReLU6
{
nn.ReLU6,
F.relu6,
},
# F.elu
{
F.elu,
},
# F.hardswish
{
F.hardswish,
},
# F.group_norm
{
F.group_norm,
},
# F.instance_norm
{
F.instance_norm,
},
# F.layer_norm
{
F.layer_norm,
},
# F.leaky_relu
{
F.leaky_relu,
},
# F.silu
{
nn.SiLU,
F.silu,
},
# F.mish
{
nn.Mish,
F.mish,
},
# F.tanh
{
nn.Tanh,
F.tanh,
torch.tanh,
"tanh_",
"tanh",
},
# F.hardsigmoid
{
"hardsigmoid_",
"hardsigmoid",
F.hardsigmoid,
nn.Hardsigmoid,
},
# F.hardtanh
{
nn.Hardtanh,
F.hardtanh,
F.hardtanh_,
},
# floordiv
{
operator.floordiv,
},
# unsqueeze
{
torch.unsqueeze,
},
# stack
{
torch.stack,
},
# squeeze
{
torch.squeeze,
},
# sort
{
torch.sort,
},
# repeat_interleave
{
torch.repeat_interleave,
},
# min
{
torch.min,
},
# mean
{
torch.mean,
},
# max
{
torch.max,
},
# transpose
{
torch.transpose,
},
# flatten
{
torch.flatten,
},
# clamp
{
torch.clamp,
},
# chunk
{
torch.chunk,
},
# interpolate
{
torch.nn.functional.interpolate,
},
# dropout
{
nn.Dropout,
},
# F.dropout
{
F.dropout,
},
# matmul
{
torch.matmul,
},
# Softmax
{
nn.Softmax,
},
# PReLU
{
nn.PReLU,
nnq.PReLU,
},
# F.prelu
{
F.prelu,
toq.prelu,
},
# pixel shuffle
{
nn.PixelShuffle,
},
{
F.pixel_shuffle,
},
# pixel unshuffle
{
nn.PixelUnshuffle,
},
{
F.pixel_unshuffle,
},
# narrow
{
torch.narrow,
},
]
# for each floating point op, add versions of the op added by
# backend_config
backend_config = get_native_backend_config()
new_connections: list[tuple[Callable, Callable]] = [
# technical debt edge case
(nn.Linear, nn.modules.linear.NonDynamicallyQuantizableLinear),
]
for pattern, config in backend_config._pattern_complex_format_to_config.items():
# pattern format: (c, (b, a))
first_element = pattern
# look from the end, because pattern is in reverse order
while isinstance(first_element, (list, tuple)):
first_element = first_element[-1]
if config.fused_module is not None:
# case 1: pattern fuses a pattern of ops into an op
# example: nn.Conv1d, nn.ReLU fused into nni.ConvReLU1d
new_connections.append((first_element, config.fused_module))
if config.qat_module is not None:
# case 2: pattern swaps a module into a QAT module
# example: nni.ConvReLU1d swapped into nniqat.ConvReLU1d
new_connections.append((first_element, config.qat_module))
if config.reference_quantized_module is not None:
# case 3: reference version of floating point module, such as
# nn.Conv2d and nnqr.Conv2d
new_connections.append((first_element, config.reference_quantized_module))
#
# Add reference module swaps from default lowering path
#
for source_to_target in (
_lower_to_native_backend.STATIC_LOWER_MODULE_MAP,
_lower_to_native_backend.DYNAMIC_LOWER_MODULE_MAP,
_lower_to_native_backend.WEIGHT_ONLY_LOWER_MODULE_MAP,
_lower_to_native_backend.SPECIAL_PATTERN_LOWER_MODULE_MAP,
):
for source, target in source_to_target.items(): # type: ignore[attr-defined]
new_connections.append((source, target))
for source_to_double_target in (
_lower_to_native_backend.STATIC_LOWER_FUSED_MODULE_MAP,
_lower_to_native_backend.STATIC_LOWER_FUSED_MODULE_TWO_INPUTS_MAP,
_lower_to_native_backend.DYNAMIC_LOWER_FUSED_MODULE_MAP,
):
for source, (target1, target2) in source_to_double_target.items(): # type: ignore[attr-defined]
new_connections.append((source, target1))
new_connections.append((source, target2))
#
# Add function swaps from default lowering path
#
for source, ( # type:ignore[assignment]
target1,
target2,
) in _lower_to_native_backend.STATIC_LOWER_FUNCTIONAL_MAP.items():
new_connections.append((source, target1))
# pyrefly: ignore [bad-argument-type]
new_connections.append((source, target2))
for source_to_target in (
_lower_to_native_backend.QBIN_OP_MAPPING,
_lower_to_native_backend.QBIN_RELU_OP_MAPPING,
quantization_mappings.DEFAULT_FLOAT_TO_QUANTIZED_OPERATOR_MAPPINGS,
):
for source, target in source_to_target.items(): # type:ignore[assignment]
# pyrefly: ignore [bad-argument-type]
new_connections.append((source, target))
#
# Add other swaps, ideally in the future this could be removed
# after the lowering code stops using these.
#
for source_to_target in (
quantization_mappings.DEFAULT_DYNAMIC_QUANT_MODULE_MAPPINGS,
):
for source, target in source_to_target.items(): # type:ignore[assignment]
new_connections.append((source, target))
# add the new connections from backend_config
for item1, item2 in new_connections:
for set_of_related_ops in sets_of_related_ops:
if item1 in set_of_related_ops or item2 in set_of_related_ops:
set_of_related_ops.add(item1)
set_of_related_ops.add(item2)
break
base_name_to_sets_of_related_ops: dict[str, set[NSNodeTargetType]] = {}
for counter, set_of_related_ops in enumerate(sets_of_related_ops):
base_name = str(counter)
base_name_to_sets_of_related_ops[base_name] = set_of_related_ops
return base_name_to_sets_of_related_ops
def get_base_name_for_op(
base_name_to_sets_of_related_ops: dict[str, set[NSNodeTargetType]],
op: NSNodeTargetType,
) -> str | None:
for base_name, set_of_related_ops in base_name_to_sets_of_related_ops.items():
if op in set_of_related_ops:
return base_name
return None
def add_op_to_sets_of_related_ops(
base_name_to_sets_of_related_ops: dict[str, set[NSNodeTargetType]],
op: NSNodeTargetType,
related_op: NSNodeTargetType | None,
) -> None:
if related_op is not None:
for set_of_related_ops in base_name_to_sets_of_related_ops.values():
if related_op in set_of_related_ops:
set_of_related_ops.add(op)
return
# if we got here, related_op was not found
raise AssertionError(f"{related_op} was not found")
else:
counter = 0
while str(counter) in base_name_to_sets_of_related_ops:
counter += 1
base_name_to_sets_of_related_ops[str(counter)] = {op}
# TODO(future PR): clean this up
def get_node_type_to_io_type_map() -> dict[str, set[NSNodeTargetType]]:
FUNS_IO_TYPE_FP32: set[NSNodeTargetType] = {
F.linear,
F.conv1d,
F.conv2d,
F.conv3d,
torch.cat,
F.elu,
F.hardswish,
F.instance_norm,
F.layer_norm,
F.leaky_relu,
F.dropout,
F.silu,
F.mish,
operator.add,
torch.add,
operator.mul,
torch.mul,
torch.sum,
F.prelu,
}
FUNS_IO_TYPE_FP16: set[NSNodeTargetType] = set()
FUNS_IO_TYPE_INT8: set[NSNodeTargetType] = {
toq.linear,
toq.linear_relu,
toq.conv1d,
toq.conv1d_relu,
toq.conv2d,
toq.conv2d_relu,
toq.conv3d,
toq.conv3d_relu,
toq.cat,
toq.elu,
toq.hardswish,
toq.instance_norm,
toq.layer_norm,
toq.leaky_relu,
toq.dropout,
toq.prelu,
# TODO(future PR): implement shadowing for binary ops and
# uncomment below
# toq.add,
# toq.mul,
}
FUNS_IO_TYPE_FP32_OR_INT8: set[NSNodeTargetType] = {
F.relu,
F.tanh,
torch.tanh,
F.sigmoid,
torch.sigmoid,
F.hardsigmoid,
operator.floordiv,
torch.adaptive_avg_pool1d,
F.adaptive_avg_pool2d,
F.adaptive_avg_pool3d,
F.dropout,
F.hardtanh,
F.hardtanh_,
F.interpolate,
F.max_pool1d,
F.max_pool2d,
F.max_pool3d,
F.relu6,
F.pixel_shuffle,
F.pixel_unshuffle,
torch.avg_pool1d,
torch._C._nn.avg_pool2d,
torch._C._nn.avg_pool3d,
torch.cat,
torch.chunk,
torch.clamp,
torch.flatten,
torch.transpose,
torch.max,
torch.mean,
torch.min,
torch.narrow,
torch.repeat_interleave,
torch.sort,
torch.squeeze,
torch.stack,
torch.unsqueeze,
operator.add,
}
MODS_IO_TYPE_FP32: set[NSNodeTargetType] = {
nn.Linear,
nnqat.Linear,
nnqatd.Linear,
nnqd.Linear,
torch.nn.modules.linear.NonDynamicallyQuantizableLinear,
nn.Conv1d,
nn.Conv2d,
nn.Conv3d,
nnqat.Conv1d,
nnqat.Conv2d,
nnqat.Conv3d,
nnqat.Embedding,
nnqat.EmbeddingBag,
nn.LSTM,
# note: nnqd.Linear is an instance of nnq.Linear, so this
# check has to happen before the int8 module check
nnqd.LSTM,
nn.BatchNorm2d,
nn.BatchNorm3d,
nn.Dropout,
nn.ConvTranspose1d,
nn.ConvTranspose2d,
nn.ConvTranspose3d,
nn.ELU,
nn.GroupNorm,
nn.InstanceNorm1d,
nn.InstanceNorm2d,
nn.InstanceNorm3d,
nn.LayerNorm,
nn.Hardswish,
nn.LeakyReLU,
nn.ReLU6,
nn.SiLU,
nn.Mish,
nn.Softmax,
nn.PReLU,
nni.BNReLU2d,
nni.BNReLU3d,
nni.ConvReLU1d,
nni.ConvReLU2d,
nni.ConvReLU3d,
nni.LinearReLU,
nni.LinearBn1d,
nni.ConvBn1d,
nni.ConvBn2d,
nni.ConvBn3d,
nniqat.ConvBn1d,
nniqat.ConvBn2d,
nniqat.ConvBn3d,
nniqat.ConvBnReLU1d,
nniqat.ConvBnReLU2d,
nniqat.ConvBnReLU3d,
nniqat.ConvReLU1d,
nniqat.ConvReLU2d,
nniqat.ConvReLU3d,
nniqat.LinearReLU,
nniqat.LinearBn1d,
nniqd.LinearReLU,
nni.LinearLeakyReLU,
nni.LinearTanh,
nni.ConvAdd2d,
nni.ConvAddReLU2d,
}
MODS_IO_TYPE_INT8: set[NSNodeTargetType] = {
nnq.Linear,
nnq.Conv1d,
nnq.Conv2d,
nnq.Conv3d,
nnq.BatchNorm2d,
nnq.BatchNorm3d,
nnq.Dropout,
nnq.ConvTranspose1d,
nnq.ConvTranspose2d,
nnq.ELU,
nnq.InstanceNorm1d,
nnq.InstanceNorm2d,
nnq.InstanceNorm3d,
nnq.LayerNorm,
nnq.Hardswish,
nnq.LeakyReLU,
nnq.Embedding,
nnq.EmbeddingBag,
nnq.Dropout,
nnq.Softmax,
nnq.PReLU,
nniq.BNReLU2d,
nniq.BNReLU3d,
nniq.ConvReLU1d,
nniq.ConvReLU2d,
nniq.ConvReLU3d,
nniq.LinearReLU,
nniq.LinearLeakyReLU,
nniq.LinearTanh,
nniq.ConvAdd2d,
nniq.ConvAddReLU2d,
}
MODS_IO_TYPE_FP32_OR_INT8: set[NSNodeTargetType] = {
nn.ReLU,
nn.Tanh,
nn.Sigmoid,
nn.Hardsigmoid,
nn.AdaptiveAvgPool1d,
nn.AdaptiveAvgPool2d,
nn.AdaptiveAvgPool3d,
nn.AvgPool1d,
nn.AvgPool2d,
nn.AvgPool3d,
nn.Dropout,
nn.Hardtanh,
nn.Identity,
nn.MaxPool1d,
nn.MaxPool2d,
nn.MaxPool3d,
nn.PixelShuffle,
nn.PixelUnshuffle,
nn.ReLU6,
}
METHS_IO_TYPE_FP32_OR_INT8: set[NSNodeTargetType] = {
"sigmoid_",
"sigmoid",
"tanh_",
"tanh",
"hardsigmoid_",
"hardsigmoid",
"relu_",
"relu",
}
return {
"funs_io_type_fp32": FUNS_IO_TYPE_FP32,
"funs_io_type_fp16": FUNS_IO_TYPE_FP16,
"funs_io_type_int8": FUNS_IO_TYPE_INT8,
"funs_io_type_fp32_or_int8": FUNS_IO_TYPE_FP32_OR_INT8,
"mods_io_type_fp32": MODS_IO_TYPE_FP32,
"mods_io_type_int8": MODS_IO_TYPE_INT8,
"mods_io_type_fp32_or_int8": MODS_IO_TYPE_FP32_OR_INT8,
"meths_io_type_fp32_or_int8": METHS_IO_TYPE_FP32_OR_INT8,
}
def get_unmatchable_types_map() -> dict[str, set[NSNodeTargetType]]:
FUNS_UNMATCHABLE: set[NSNodeTargetType] = {
torch.quantize_per_tensor,
operator.getitem,
}
MODS_UNMATCHABLE: set[NSNodeTargetType] = {
nn.Identity,
}
METHS_UNMATCHABLE: set[NSNodeTargetType] = {
"to",
"dequantize",
"reshape",
"view",
"unsqueeze_",
"unsqueeze",
"transpose",
"squeeze_",
"squeeze",
"size",
"shape",
"resize_",
"repeat_interleave",
"repeat",
"permute",
"numel",
"mean",
"detach_",
"detach",
"contiguous",
"clamp",
"chunk",
}
return {
"funs_unmatchable": FUNS_UNMATCHABLE,
"mods_unmatchable": MODS_UNMATCHABLE,
"meths_unmatchable": METHS_UNMATCHABLE,
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,66 @@
import enum
from collections.abc import Callable
from typing import Any, NamedTuple
from torch.fx.graph import Node
class NSSingleResultValuesType(str, enum.Enum):
WEIGHT = "weight"
NODE_OUTPUT = "node_output"
NODE_INPUT = "node_input"
class NSSubgraph(NamedTuple):
start_node: Node
end_node: Node
base_op_node: Node
# TODO(future PR): see if we can use typing_extensions's TypedDict instead
# to properly type the various keys
# {
# # one of NSSingleResultValuesType
# 'type': 'weight',
# # the values of type specified above
# 'values': [torch.tensor(...), ...],
# # name of the node directly before the logger
# 'prev_node_name': 'linear1',
# # type of the underlying function or module
# 'prev_node_target_type': torch.nn.functional.linear # or torch.nn.Linear, etc
# # name of the node responsible for adding this logger
# # Note: this may differ from prev_node_name if we are logging inputs
# 'ref_node_name': 'linear1',
# # index of this node within the arg of the input/output node
# # for example, in cat([x1, x2, x3], dim=0), x2 would have index_within_arg == 1
# 'index_within_arg': 0,
# # index of this node within the args of the input/output node
# # for example, in add(x1, x2), x2 would have index_of_arg == 1
# 'index_of_arg': 0,
# # precomputed comparisons of logger values to reference values
# 'comparisons': [torch.tensor(...), ...]
# # name of function used for precomputed comparisons
# 'comparison_fn_name': 'sqnr',
# # string representation of qconfig responsible for creating this logger
# 'qconfig_str': 'QConfig(...)',
# }
NSSingleResultType = dict[str, Any]
# {
# 'layer_name_1': { # subgraph name
# 'node_output': { # results type (node_output, node_input, weight)
# 'model_name_a': # model name
# [NSSingleResultType, ...], # results, ordered by index_within_arg
# 'model_name_b':
# [NSSingleResultType, ...],
# },
# },
# }
#
NSResultsType = dict[str, dict[str, dict[str, list[NSSingleResultType]]]]
# Defines the underlying target type of a node, for example:
# `F.conv1d` for a `call_function` conv node
# `nn.Conv1d` for a `call_module` node calling the forward of a `nn.Conv1d` module
# `'sigmoid'` for a `call_method` node calling `x.sigmoid()`
NSNodeTargetType = Callable | str
@@ -0,0 +1,215 @@
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 import FakeQuantizeBase, ObserverBase
from torch.ao.quantization.backend_config import get_native_backend_config
from torch.ao.quantization.fx.quantize_handler import _get_pattern_to_quantize_handlers
from torch.ao.quantization.utils import getattr_from_fqn
from torch.fx import GraphModule
from torch.fx.graph import Node
from .ns_types import NSNodeTargetType
toq = torch.ops.quantized
def get_type_a_related_to_b(
base_name_to_sets_of_related_ops: dict[str, set[NSNodeTargetType]],
) -> set[tuple[NSNodeTargetType, NSNodeTargetType]]:
# TODO(future PR): allow customizations
# TODO(future PR): reuse existing quantization mappings
# TODO(future PR): add the rest of modules and ops here
type_a_related_to_b: set[tuple[NSNodeTargetType, NSNodeTargetType]] = set()
for s in base_name_to_sets_of_related_ops.values():
s_list = list(s)
# add every bidirectional pair
for idx_0 in range(len(s_list)):
for idx_1 in range(idx_0, len(s_list)):
type_a_related_to_b.add((s_list[idx_0], s_list[idx_1]))
type_a_related_to_b.add((s_list[idx_1], s_list[idx_0]))
return type_a_related_to_b
NSFusionElType = (
Callable # call_function or call_module type, example: F.linear or nn.Conv2d
| str # call_method name, example: "dequantize"
| tuple[
str, Any
] # call_method name and first argument, example: ("to", torch.float16)
)
NSFusionType = (
tuple[NSFusionElType, NSFusionElType]
| tuple[NSFusionElType, NSFusionElType, NSFusionElType, NSFusionElType]
)
def get_reversed_fusions() -> list[tuple[NSFusionType, int]]:
"""
Set of potential fusions, in reverse order. The order is reversed
to match how fusion patterns are defined in quantization code.
Fusion format:
((fusion_op_0, fusion_op_1), base_op_idx)
Where base_op_idx is the idx of the op we should use to match other related
ops. Note: base_op_idx is specified in non-reverse order, i.e. a base_op_idx
of 0 represents the first op in regular (non-reverse) order, 1 represents the
second op, etc.
"""
results: list[tuple[NSFusionType, int]] = []
# Possible syntaxes:
# * single op: torch.nn.Conv2d
# * multiple ops: (torch.nn.ReLU, torch.nn.Conv2d)
# For fusions, we only care about patterns composed of multiple ops.
# TODO(future PR): allow customizations from default patterns.
all_quant_patterns = _get_pattern_to_quantize_handlers(get_native_backend_config())
default_base_op_idx = 0
for quant_pattern in all_quant_patterns:
# TODO: this is a temporary hack to flatten the patterns from quantization so
# that it works with the ns matcher function, maybe we should use `_is_match`
# in torch.ao.quantization.fx.match_utils to match the patterns
if (
isinstance(quant_pattern, tuple)
and len(quant_pattern) == 2
and isinstance(quant_pattern[1], tuple)
and len(quant_pattern[1]) == 2
):
# flatten the pattern with form (nn.ReLU, (nn.BatchNorm2d, nn.Conv2d))
quant_pattern = (quant_pattern[0], quant_pattern[1][0], quant_pattern[1][1])
# Only patterns of multiple ops are fusions, ignore
# patterns which contain a single ops (they get matched
# without caring about fusions).
if isinstance(quant_pattern, tuple):
results.append((quant_pattern, default_base_op_idx)) # type: ignore[arg-type]
# For each pattern, add additional patterns with observers and
# fake quants at the end.
# TODO(future PR): if needed, implement matching for a node
# having multiple output observers.
for cls in (ObserverBase, FakeQuantizeBase):
if isinstance(quant_pattern, tuple):
# pyrefly: ignore [not-iterable]
new_pattern = (cls, *quant_pattern)
else:
new_pattern = (cls, quant_pattern)
results.append((new_pattern, default_base_op_idx)) # type: ignore[arg-type]
# After this point, results contains values such as
# [..., ((torch.nn.Relu, torch.nn.Conv2d), 0), ...]
# Patterns for matching fp16 emulation are not specified in the quantization
# fusion mappings. For now, define them here.
fp16_em_base_op_idx = 1
patterns_to_add = [
# linear-relu fp16 emulation:
# fp16_to_fp32 -> linear -> relu -> fp32_to_fp16
(
(("to", torch.float16), F.relu, F.linear, "dequantize"),
fp16_em_base_op_idx,
),
# Conv-BN fusion (this happens outside of quantization patterns,
# which is why it is defined separately here).
((nn.BatchNorm1d, nn.Conv1d), default_base_op_idx),
((nn.BatchNorm2d, nn.Conv2d), default_base_op_idx),
((nn.BatchNorm3d, nn.Conv3d), default_base_op_idx),
((nn.ReLU, nn.BatchNorm1d, nn.Conv1d), default_base_op_idx),
((nn.ReLU, nn.BatchNorm2d, nn.Conv2d), default_base_op_idx),
((nn.ReLU, nn.BatchNorm3d, nn.Conv3d), default_base_op_idx),
]
for p in patterns_to_add:
results.append(p) # type: ignore[arg-type]
results.append(((ObserverBase, *p[0]), p[1])) # type: ignore[arg-type]
results.append(((FakeQuantizeBase, *p[0]), p[1])) # type: ignore[arg-type]
return results
def end_node_matches_reversed_fusion(
end_node: Node,
reversed_fusion: NSFusionType,
gm: GraphModule,
seen_nodes: set[Node],
) -> bool:
"""
Returns true if a pattern ending with `end_node` matches
the fusion pattern.
"""
cur_node = end_node
for fusion_idx in range(len(reversed_fusion)):
# each node can only belong to one matched pattern
if cur_node in seen_nodes:
return False
cur_fusion_el = reversed_fusion[fusion_idx]
if cur_node.op == "call_function":
fusion_el_is_fun = (not isinstance(cur_fusion_el, str)) and (
not isinstance(cur_fusion_el, type)
)
if fusion_el_is_fun:
if cur_node.target != cur_fusion_el:
return False
if len(cur_node.args) > 0 and isinstance(cur_node.args[0], Node):
cur_node = cur_node.args[0]
else:
return False
else:
return False
elif cur_node.op == "call_module":
fusion_el_is_mod = isinstance(cur_fusion_el, type)
if fusion_el_is_mod:
if not isinstance(cur_node.target, str):
raise AssertionError(f"Expected str, got {type(cur_node.target)}")
target_mod = getattr_from_fqn(gm, cur_node.target)
if not isinstance(cur_fusion_el, type):
return False
if not isinstance(target_mod, cur_fusion_el):
return False
if len(cur_node.args) > 0 and isinstance(cur_node.args[0], Node):
cur_node = cur_node.args[0]
else:
return False
else:
return False
elif cur_node.op == "call_method":
fusion_el_is_meth_with_second_arg = (
isinstance(cur_fusion_el, tuple) and len(cur_fusion_el) == 2
)
fusion_el_is_meth_without_args = isinstance(cur_fusion_el, str)
if fusion_el_is_meth_without_args or fusion_el_is_meth_with_second_arg:
if fusion_el_is_meth_without_args:
if cur_node.target != cur_fusion_el:
return False
else:
if not isinstance(cur_fusion_el, tuple):
raise AssertionError(
f"Expected tuple, got {type(cur_fusion_el)}"
)
if cur_node.target != cur_fusion_el[0]:
return False
elif len(cur_node.args) < 2:
return False
elif cur_node.args[1] != cur_fusion_el[1]:
return False
if len(cur_node.args) > 0 and isinstance(cur_node.args[0], Node):
cur_node = cur_node.args[0]
else:
return False
else:
return False
else:
return False
return True
@@ -0,0 +1,251 @@
# mypy: allow-untyped-defs
from __future__ import annotations
import copy
from typing import Any, TYPE_CHECKING
import torch
from torch.ao.quantization import QConfigMapping
from torch.ao.quantization.qconfig_mapping import _QCONFIG_STYLE_ORDER
if TYPE_CHECKING:
from collections.abc import Callable
from torch.ao.quantization.qconfig import QConfigAny
__all__ = ["QConfigMultiMapping"]
_QCONFIG_STYLE_TO_METHOD: dict[str, str] = {
"global_qconfig": "set_global",
"object_type_qconfigs": "set_object_type",
"module_name_regex_qconfigs": "set_module_name_regex",
"module_name_qconfigs": "set_module_name",
"module_name_object_type_order_qconfigs": "set_module_name_object_type_order",
}
def _remove_duplicates_and_none(qconfig_list: list[QConfigAny]) -> None:
to_remove = []
for index, cur_qconfig in enumerate(qconfig_list):
if cur_qconfig is None:
to_remove.append(index)
break
for checked_qconfig in qconfig_list[:index]:
if torch.ao.quantization.qconfig_equals(cur_qconfig, checked_qconfig):
to_remove.append(index)
break
for index in to_remove[::-1]:
qconfig_list.pop(index)
class QConfigMultiMapping:
"""
This class, used with the prepare_n_shadows_model API, stores a list of :class:`torch.ao.quantization.QConfigMapping`s
so that multiple QConfigs can be specified for each QConfig matching style.
The user can specify QConfigs using the following methods (in increasing match priority):
``set_global`` : sets the global (default) QConfigs
``set_object_type`` : sets the QConfigs for a given module type, function, or method name
``set_module_name_regex`` : sets the QConfigs for modules matching the given regex string
``set_module_name`` : sets the QConfigs for modules matching the given module name
``set_module_name_object_type_order`` : sets the QConfigs for modules matching a combination
of the given module name, object type, and the index at which the module appears
Note: Usage of set methods is the same as in QConfigMapping except with a passed in list of QConfigs rather than a
single QConfig.
Example usage::
qconfig_mapping = QConfigMultiMapping()
.set_global([qconfig1, qconfig2])
.set_object_type(torch.nn.Linear, [qconfig2, qconfig3])
.set_object_type(torch.nn.ReLU, [qconfig1])
.set_module_name_regex("foo.*bar.*conv[0-9]+", [qconfig2])
.set_module_name_regex("foo.*", [qconfig1, qconfig2, qconfig3])
.set_module_name("module1", [None])
.set_module_name("module2", [qconfig2])
.set_module_name_object_type_order("foo.bar", torch.nn.functional.linear, 0, [qconfig3])
"""
def __init__(self) -> None:
# initialize this with 1 QConfigMapping to avoid corner cases
self.qconfig_mappings_list: list[QConfigMapping] = [QConfigMapping()]
def _handle_list_size_mismatch(
self, qconfig_list: list[QConfigAny], style: str
) -> None:
# this method handles cases where the size of qconfig_list does not match
# the size of qconfig_mappings_list.
# Issue: Consider a user inserting global_qconfig A and B first, then inserting
# qconfig C as an object_type_qconfig for conv ops. If we internally store
# 1 QConfigMapping with A and C and another with just B, then the
# second QConfigMapping will match B to conv ops (which is not wanted), since B is global.
# we avoid this by maintaining the invariant that if any QConfigMapping
# has a qconfig style+key with a qconfig in it, all QConfigMappings must
# have either a qconfig or None for that same style+key. In the above
# example, a None qconfig would prevent the unwanted match in the
# second QConfigMapping
if len(qconfig_list) > len(self.qconfig_mappings_list):
# Case: we have more qconfigs (in qconfig_list) than QConfigMappings
# Add new QConfigMappings (initialized so we maintain the `invariant`)
new_qconfig_mapping = QConfigMapping()
# searches other QConfigMappings for qconfig style+keys
# that need to be inserted as `None` into the new QConfigMapping
for qconfig_mapping in self.qconfig_mappings_list:
# global_qconfig has None by default
for check_style in _QCONFIG_STYLE_ORDER[1:]:
qconfigs_dict = getattr(qconfig_mapping, check_style)
target_qconfigs_dict = getattr(new_qconfig_mapping, check_style)
for key in qconfigs_dict:
target_qconfigs_dict[key] = None
break
# insert copies of this new QConfigMapping until all entries
# in qconfig_list can fit among the QConfigMappings
while len(qconfig_list) > len(self.qconfig_mappings_list):
self.qconfig_mappings_list.append(copy.deepcopy(new_qconfig_mapping))
else:
# Case: we have fewer qconfigs in qconfig_list than QConfigMappings
# pad qconfig_list with `None` until length is same
while len(qconfig_list) < len(self.qconfig_mappings_list):
qconfig_list.append(None)
# this function applies the insertion method across each QConfigMapping
def _insert_qconfig_list(
self,
style: str,
args: list[str | int | Callable],
qconfig_list: list[QConfigAny],
) -> None:
# we remove duplicates and None to make the ordering of qconfigs
# deterministic upon insertion.
_remove_duplicates_and_none(qconfig_list)
self._handle_list_size_mismatch(qconfig_list, style)
method_name = _QCONFIG_STYLE_TO_METHOD[style]
for qconfig_mapping, qconfig in zip(self.qconfig_mappings_list, qconfig_list):
# uses QConfigMapping set method to insert qconfig
set_method = getattr(qconfig_mapping, method_name)
set_method(*args, qconfig)
def set_global(self, global_qconfig_list: list[QConfigAny]) -> QConfigMultiMapping:
"""
Set global QConfigs
see :func:`~torch.ao.quantization.QConfigMapping.set_global()` for more info
"""
self._insert_qconfig_list("global_qconfig", [], global_qconfig_list)
return self
def set_object_type(
self, object_type: Callable | str, qconfig_list: list[QConfigAny]
) -> QConfigMultiMapping:
"""
Set object type QConfigs
see :func:`~torch.ao.quantization.QConfigMapping.set_object_type()` for more info
"""
self._insert_qconfig_list("object_type_qconfigs", [object_type], qconfig_list)
return self
def set_module_name_regex(
self, module_name_regex: str, qconfig_list: list[QConfigAny]
) -> QConfigMultiMapping:
"""
Set module_name_regex QConfigs
see :func:`~torch.ao.quantization.QConfigMapping.set_module_name_regex()` for more info
"""
self._insert_qconfig_list(
"module_name_regex_qconfigs", [module_name_regex], qconfig_list
)
return self
def set_module_name(
self, module_name: str, qconfig_list: list[QConfigAny]
) -> QConfigMultiMapping:
"""
Set module_name QConfigs
see :func:`~torch.ao.quantization.QConfigMapping.set_module_name()` for more info
"""
self._insert_qconfig_list("module_name_qconfigs", [module_name], qconfig_list)
return self
def set_module_name_object_type_order(
self,
module_name: str,
object_type: Callable,
index: int,
qconfig_list: list[QConfigAny],
) -> QConfigMultiMapping:
"""
Set module_name QConfigs
see :func:`~torch.ao.quantization.QConfigMapping.set_module_name_object_type_order()` for more info
"""
self._insert_qconfig_list(
"module_name_object_type_order_qconfigs",
[module_name, object_type, index],
qconfig_list,
)
return self
def __repr__(self):
return (
self.__class__.__name__
+ " ["
+ "".join(
f"\n{qconfig_mapping.__repr__()},"
for qconfig_mapping in self.qconfig_mappings_list
)
+ "\n]"
)
@classmethod
def from_list_qconfig_mapping(
cls, qconfig_mapping_list: list[QConfigMapping]
) -> QConfigMultiMapping:
"""
Creates a QConfigMultiMapping from a list of QConfigMappings
"""
new_qconfig_multi_mapping = cls()
new_qconfig_multi_mapping.qconfig_mappings_list = copy.deepcopy(
qconfig_mapping_list
)
# we need to avoid the issue described in _handle_list_size_mismatch,
# so we reinsert all the qconfigs using the QConfigMultiMapping
# set methods
# go through all qconfig styles
# note: global can be ignored since it is None by default
for style in _QCONFIG_STYLE_ORDER[1:]:
# gather all key+qconfigs for current style
# into qconfig_dict_list
qconfig_dict_list: dict[Any, list[QConfigAny]] = {}
for qconfig_mapping in qconfig_mapping_list:
qconfig_dict = getattr(qconfig_mapping, style)
for key, qconfig in qconfig_dict.items():
if key not in qconfig_dict_list:
qconfig_dict_list[key] = []
qconfig_dict_list[key].append(qconfig)
# reinsert all gathered key+qconfigs
set_method_name = _QCONFIG_STYLE_TO_METHOD[style]
set_method = getattr(new_qconfig_multi_mapping, set_method_name)
for key, qconfig_list in qconfig_dict_list.items():
if isinstance(key, tuple):
set_method(*key, qconfig_list)
else:
set_method(key, qconfig_list)
return new_qconfig_multi_mapping
@@ -0,0 +1,579 @@
# mypy: allow-untyped-decorators
# mypy: allow-untyped-defs
import enum
import operator
from collections.abc import Callable
import torch
import torch.ao.nn.intrinsic.quantized as nniq
import torch.ao.nn.quantized as nnq
import torch.nn as nn
from torch.ao.quantization import FakeQuantizeBase, ObserverBase
from torch.ao.quantization.observer import _is_activation_post_process
from torch.ao.quantization.utils import getattr_from_fqn
from torch.fx import GraphModule
from torch.fx.graph import Node
from .ns_types import NSNodeTargetType, NSResultsType
toq = torch.ops.quantized
# TODO(future PR): consider deleting this enum and using the torch types
# directly. This might be tricky because it is not a one to one mapping.
class NodeInputOrOutputType(enum.Enum):
FP32 = enum.auto() # torch.float
INT8 = enum.auto() # torch.qint8 or torch.quint8
FP16 = enum.auto() # torch.float16
UNKNOWN = enum.auto() # we cannot determine input/output dtype
# TODO(future PR): while these functions can support multiple dtypes,
# for the purposes of numerical debugging we want to get the actual
# dtype used in the model. We will likely need some kind of dtype
# propagation to estimate this.
FP32_OR_INT8 = enum.auto() # either torch.float or torch.quint8 or torch.qint8
# TODO(future PRs): dynamic quant, fake quant, etc
def get_node_first_input_and_output_type(
node: Node,
gm: GraphModule,
logger_cls: Callable,
node_type_to_io_type_map: dict[str, set[NSNodeTargetType]],
) -> tuple[NodeInputOrOutputType, NodeInputOrOutputType]:
# TODO(future PR): clean this up
FUNS_IO_TYPE_FP32 = node_type_to_io_type_map["funs_io_type_fp32"]
FUNS_IO_TYPE_FP16 = node_type_to_io_type_map["funs_io_type_fp16"]
FUNS_IO_TYPE_INT8 = node_type_to_io_type_map["funs_io_type_int8"]
FUNS_IO_TYPE_FP32_OR_INT8 = node_type_to_io_type_map["funs_io_type_fp32_or_int8"]
MODS_IO_TYPE_FP32 = node_type_to_io_type_map["mods_io_type_fp32"]
MODS_IO_TYPE_INT8 = node_type_to_io_type_map["mods_io_type_int8"]
MODS_IO_TYPE_FP32_OR_INT8 = node_type_to_io_type_map["mods_io_type_fp32_or_int8"]
METHS_IO_TYPE_FP32_OR_INT8 = node_type_to_io_type_map["meths_io_type_fp32_or_int8"]
if node.op == "call_function":
if node.target in FUNS_IO_TYPE_FP32:
return (NodeInputOrOutputType.FP32, NodeInputOrOutputType.FP32)
if node.target in FUNS_IO_TYPE_FP16:
return (NodeInputOrOutputType.FP16, NodeInputOrOutputType.FP16)
elif node.target in FUNS_IO_TYPE_INT8:
return (NodeInputOrOutputType.INT8, NodeInputOrOutputType.INT8)
elif node.target in FUNS_IO_TYPE_FP32_OR_INT8:
first_arg = get_normalized_nth_input(node, gm, 0)
if not isinstance(first_arg, Node):
raise AssertionError(f"Expected Node, got {type(first_arg)}")
(
_prev_node_input_type,
prev_node_output_type,
) = get_node_first_input_and_output_type(
first_arg, gm, logger_cls, node_type_to_io_type_map
)
return (prev_node_output_type, prev_node_output_type)
else:
return (NodeInputOrOutputType.UNKNOWN, NodeInputOrOutputType.UNKNOWN)
elif node.op == "call_module":
if node.op != "call_module":
raise AssertionError(f"Expected call_module, got '{node.op}'")
if not isinstance(node.target, str):
raise AssertionError(f"Expected str, but got {type(node.target)}")
mod = getattr_from_fqn(gm, node.target)
is_known_fp32_or_int8_input_module = any(
isinstance(mod, target_type) # type: ignore[arg-type]
for target_type in MODS_IO_TYPE_FP32_OR_INT8
)
if (
isinstance(mod, (logger_cls, ObserverBase, FakeQuantizeBase)) # type: ignore[arg-type]
or is_known_fp32_or_int8_input_module
):
# A logger or observer's input and output type is the output
# type of the preceding node.
first_arg = get_normalized_nth_input(node, gm, 0)
if not isinstance(first_arg, Node):
raise AssertionError(f"Expected Node, got {type(first_arg)}")
(
_prev_node_input_type,
prev_node_output_type,
) = get_node_first_input_and_output_type(
first_arg, gm, logger_cls, node_type_to_io_type_map
)
return (prev_node_output_type, prev_node_output_type)
is_known_fp32_input_module = any(
isinstance(mod, target_type) # type: ignore[arg-type]
for target_type in MODS_IO_TYPE_FP32
)
is_known_int8_input_module = any(
isinstance(mod, target_type) # type: ignore[arg-type]
for target_type in MODS_IO_TYPE_INT8
)
if is_known_fp32_input_module:
return (NodeInputOrOutputType.FP32, NodeInputOrOutputType.FP32)
elif is_known_int8_input_module:
return (NodeInputOrOutputType.INT8, NodeInputOrOutputType.INT8)
else:
return (NodeInputOrOutputType.UNKNOWN, NodeInputOrOutputType.UNKNOWN)
elif node.op == "call_method":
if node.target == "dequantize":
# Dequantize is a special node because it allows multiple input types.
# So, we look up the output type of the previous node and return that
# as the input type of this node instance.
prev_node = get_normalized_nth_input(node, gm, 0)
if not isinstance(prev_node, Node):
raise AssertionError(f"Expected Node, got {type(prev_node)}")
(
_prev_node_input_type,
prev_node_output_type,
) = get_node_first_input_and_output_type(
prev_node, gm, logger_cls, node_type_to_io_type_map
)
return (prev_node_output_type, NodeInputOrOutputType.FP32)
elif node.target == "to":
# to is a special node because it allows multiple input types.
# So, we look up the output type of the previous node and return that
# as the input type of this node instance. We also look up the target
# of to and return the correct output type.
prev_node = get_normalized_nth_input(node, gm, 0)
if not isinstance(prev_node, Node):
raise AssertionError(f"Expected Node, got {type(prev_node)}")
(
_prev_node_input_type,
prev_node_output_type,
) = get_node_first_input_and_output_type(
prev_node, gm, logger_cls, node_type_to_io_type_map
)
cur_node_dtype_target = get_normalized_nth_input(node, gm, 1)
if cur_node_dtype_target is not torch.float16:
raise AssertionError(
f"{cur_node_dtype_target} handling needs to be added"
)
return (prev_node_output_type, NodeInputOrOutputType.FP16)
elif node.target in METHS_IO_TYPE_FP32_OR_INT8:
first_arg = get_normalized_nth_input(node, gm, 0)
if not isinstance(first_arg, Node):
raise AssertionError(f"Expected Node, got {type(first_arg)}")
(
_prev_node_input_type,
prev_node_output_type,
) = get_node_first_input_and_output_type(
first_arg, gm, logger_cls, node_type_to_io_type_map
)
return (prev_node_output_type, prev_node_output_type)
return (NodeInputOrOutputType.UNKNOWN, NodeInputOrOutputType.UNKNOWN)
else:
return (NodeInputOrOutputType.UNKNOWN, NodeInputOrOutputType.UNKNOWN)
def get_node_input_qparams(
node: Node,
gm: GraphModule,
node_type_to_io_type_map: dict[str, set[NSNodeTargetType]],
) -> tuple[torch.Tensor | float, torch.Tensor | int] | None:
"""
Returns the qparams (scale, zero_point) of the first input to `node`,
if they can be inferred from the graph.
"""
prev_node = get_normalized_nth_input(node, gm, 0)
if not isinstance(prev_node, Node):
return None
MODS_IO_TYPE_FP32_OR_INT8 = node_type_to_io_type_map["mods_io_type_fp32_or_int8"]
def _get_scale_zp_from_function_args(node, gm, scale_arg_idx, zp_arg_idx):
scale_node = get_normalized_nth_input(node, gm, scale_arg_idx)
zp_node = get_normalized_nth_input(node, gm, zp_arg_idx)
if not isinstance(scale_node, Node):
raise AssertionError(f"Expected Node, got {type(scale_node)}")
if not isinstance(scale_node.target, str):
raise AssertionError(f"Expected str, got {type(scale_node.target)}")
if not isinstance(zp_node, Node):
raise AssertionError(f"Expected Node, got {type(zp_node)}")
if not isinstance(zp_node.target, str):
raise AssertionError(f"Expected str, got {type(zp_node.target)}")
scale_obj = getattr_from_fqn(gm, scale_node.target)
zp_obj = getattr_from_fqn(gm, zp_node.target)
return (scale_obj, zp_obj)
if prev_node.op == "call_function":
# quantize - read the args directly
if prev_node.target is torch.quantize_per_tensor:
return _get_scale_zp_from_function_args(prev_node, gm, 1, 2)
elif prev_node.target in (toq.add, toq.add_relu, toq.mul, toq.mul_relu):
return _get_scale_zp_from_function_args(prev_node, gm, 2, 3)
return None
# TODO(future PR): handle more functionals
# TODO(future PR): handle functional ops which inherit qparams from input
elif prev_node.op == "call_module":
# get type of the module
if not isinstance(prev_node.target, str):
raise AssertionError(f"Expected str, got {type(prev_node.target)}")
module_obj = getattr_from_fqn(gm, prev_node.target)
if isinstance(
module_obj,
(
nnq.Linear,
nnq.Conv1d,
nnq.Conv2d,
nniq.ConvReLU2d,
nnq.Conv3d,
nnq.BatchNorm2d,
nnq.BatchNorm3d,
nnq.ConvTranspose1d,
nnq.ConvTranspose2d,
nnq.ELU,
nnq.GroupNorm,
nnq.InstanceNorm1d,
nnq.InstanceNorm2d,
nnq.InstanceNorm3d,
nnq.LayerNorm,
nnq.Hardswish,
nnq.LeakyReLU,
nnq.ReLU6,
nniq.BNReLU2d,
nniq.BNReLU3d,
nniq.ConvReLU1d,
nniq.ConvReLU2d,
nniq.ConvReLU3d,
nniq.LinearReLU,
),
):
return (module_obj.scale, module_obj.zero_point) # type: ignore[return-value]
is_known_fp32_or_int8_input_module = any(
isinstance(module_obj, target_type) # type: ignore[arg-type]
for target_type in MODS_IO_TYPE_FP32_OR_INT8
)
if is_known_fp32_or_int8_input_module:
return get_node_input_qparams(prev_node, gm, node_type_to_io_type_map)
return None
def return_first_non_observer_node(
node: Node,
gm: GraphModule,
) -> Node:
"""
If node is not an observer, returns it. If node is an observer,
navigates up the graph and returns the first parent which is not an
observer. For example,
graph: (node_non_obs), node = node_non_obs : returns node_non_obs
graph: (node_non_obs -> obs0), node = obs0 : returns node_non_obs
graph: (node_non_obs -> obs0 -> fq0), node = fq0 : returns node_non_obs
"""
if node.op == "call_module":
node_obj = getattr_from_fqn(gm, node.target) # type: ignore[arg-type]
if _is_activation_post_process(node_obj):
if len(node.args) != 1:
raise AssertionError(
f"Expected node.args to have length 1, got {len(node.args)}"
)
if not isinstance(node.args[0], Node):
raise AssertionError(f"Expected Node, got {type(node.args[0])}")
node = node.args[0]
# code duplication intended, not worth refactoring
if not isinstance(node.target, str):
raise AssertionError(f"Expected str, got {type(node.target)}")
node_obj = getattr_from_fqn(gm, node.target)
if _is_activation_post_process(node_obj):
if len(node.args) != 1:
raise AssertionError(
f"Expected node.args to have length 1, got {len(node.args)}"
)
if not isinstance(node.args[0], Node):
raise AssertionError(f"Expected Node, got {type(node.args[0])}")
node = node.args[0]
return node
def get_number_of_non_param_args(
node: Node,
gm: GraphModule,
) -> int:
"""
Assumes that all non-param args occur first. Returns the number of
non-param args expected for a node. For example, for
F.linear(x, weight, bias)
Returns 1, because x is a non-param arg and weight and bias are params.
For
lstm_mod(x, hid)
Returns 2, because both x and hid are non-param args.
"""
if node.op == "call_module":
node_obj = getattr_from_fqn(gm, node.target) # type: ignore[arg-type]
if isinstance(node_obj, nn.LSTM):
return 2
# default is 1
return 1
def get_arg_indices_of_inputs_to_log(node: Node) -> list[int]:
"""
Returns the indices of args of the node which we should attach
loggers to, if input logging is enabled.
For example,
* for (x + y), returns [0, 1]
* for (1 + y), returns [1]
* for (x + 1), returns [0]
* for (linear(x, w, b)) returns [0]
* by default, returns [0]
"""
if len(node.args) == 0:
return []
if node.op == "call_function" and (
# TODO(future PR): use relationship map instead of hardcoding
node.target in (torch.add, torch.ops.quantized.add, operator.add)
or node.target in (torch.mul, torch.ops.quantized.mul, operator.mul)
):
result = [i for i in range(2) if type(node.args[i]) is Node]
return result
return [0]
def get_target_type_str(node: Node, gm: GraphModule) -> str:
"""
Returns a string representation of the type of the function or module
pointed to by this node, or '' for other node types.
"""
target_type = ""
if node.op in ("call_function", "call_method"):
target_type = torch.typename(node.target)
elif node.op == "call_module":
if not isinstance(node.target, str):
raise AssertionError(f"Expected str, got {type(node.target)}")
target_mod = getattr_from_fqn(gm, node.target)
target_type = torch.typename(target_mod)
return target_type
def rekey_logger_info_on_node_name_of_model(
results: NSResultsType,
model_name: str,
) -> NSResultsType:
"""
Rekeys the layer name of a results dictionary to use node names
from `model_name`.
For example, transforms
{'base_op_1_0': {'node_output': {'model_a':
[{'ref_node_name': 'linear1', ...}]}}}
into
{'linear1': {'node_output': {'model_a':
[{'ref_node_name': 'linear1', ...}]}}}
Note: we cannot use these node names directly because they are not
guaranteed to be consistent across models. This is why we extract
the results first and rekey afterwards.
"""
new_results = {}
for old_layer_name, result_type_to_results in results.items():
new_layer_name = None
for model_name_to_results in result_type_to_results.values():
for cur_model_name, list_of_results in model_name_to_results.items():
if cur_model_name == model_name:
if len(list_of_results) == 0:
raise AssertionError("Expected list_of_results to be not empty")
new_layer_name = list_of_results[0]["ref_node_name"]
else:
continue
if new_layer_name is not None:
new_results[new_layer_name] = result_type_to_results
else:
new_results[old_layer_name] = result_type_to_results
return new_results
def maybe_add_missing_fqns(results: NSResultsType) -> None:
"""
If `fqn` entries are filled in for one of the models in `results`, copies
them over to any models which do not have them filled out.
A common use case benefitting from this is comparing a model prepared by
quantization to a quantized model. In this case, the model prepared by
quantization would have `fqn` entries, and the quantized model would not.
"""
# Check in the first result to find any model with fqn entries defined.
model_name_with_fqns = None
for result_type_to_results in results.values():
for model_name_to_results in result_type_to_results.values():
for model_name, model_results in model_name_to_results.items():
if len(model_results) > 0:
if model_results[0]["fqn"] is not None:
model_name_with_fqns = model_name
break
break
break
if model_name_with_fqns:
for result_type_to_results in results.values():
for model_name_to_results in result_type_to_results.values():
ref_model_results = model_name_to_results[model_name_with_fqns]
for model_name, model_results in model_name_to_results.items():
if model_name == model_name_with_fqns:
continue
for i in range(len(model_results)):
fqn = ref_model_results[i]["fqn"]
model_results[i]["fqn"] = fqn
def maybe_dequantize_first_two_tensor_args_and_handle_tuples(f):
def inner(*args, **kwargs):
a0, a1, *a_other = args
if (isinstance(a0, tuple) and isinstance(a1, tuple)) or (
isinstance(a0, list) and isinstance(a1, list)
):
results = []
for el0, el1 in zip(a0, a1):
new_args = (el0, el1, *a_other)
results.append(inner(*new_args, **kwargs))
return results
elif isinstance(a0, torch.Tensor) and isinstance(a1, torch.Tensor):
if a0.is_quantized:
a0 = a0.dequantize()
if a1.is_quantized:
a1 = a1.dequantize()
# for the purposes of this util, only handle floats
if a0.dtype != torch.float or a1.dtype != torch.float:
return None
new_args = (a0, a1, *a_other)
return f(*new_args, **kwargs)
return inner
@maybe_dequantize_first_two_tensor_args_and_handle_tuples
def compute_sqnr(x: torch.Tensor, y: torch.Tensor) -> torch.Tensor:
"""
Computes the SQNR between `x` and `y`.
Args:
x: Tensor or tuple of tensors
y: Tensor or tuple of tensors
Return:
float or tuple of floats
"""
Ps = torch.norm(x)
Pn = torch.norm(x - y)
return 20 * torch.log10(Ps / Pn)
@maybe_dequantize_first_two_tensor_args_and_handle_tuples
def compute_normalized_l2_error(x: torch.Tensor, y: torch.Tensor) -> torch.Tensor:
"""
Computes the normalized L2 error between `x` and `y`.
Args:
x: Tensor or tuple of tensors
y: Tensor or tuple of tensors
Return:
float or tuple of floats
"""
return torch.sqrt(((x - y) ** 2).sum() / (x**2).sum())
@maybe_dequantize_first_two_tensor_args_and_handle_tuples
def compute_cosine_similarity(x: torch.Tensor, y: torch.Tensor) -> torch.Tensor:
"""
Computes the cosine similarity between `x` and `y`.
Args:
x: Tensor or tuple of tensors
y: Tensor or tuple of tensors
Return:
float or tuple of floats
"""
# For convolutions, the shape of the quantized weight has one additional
# dimension compared to the shape of the fp32 weight. Match the shapes
# to enable cosine similarity comparison.
x = x.reshape(1, -1)
y = y.reshape(1, -1)
return torch.nn.functional.cosine_similarity(x, y)
def op_type_supports_shadowing(node: Node) -> bool:
if node.op == "call_function":
if node.target in (
torch.add,
torch.mul,
operator.add,
operator.mul,
torch.cat,
torch.stack,
):
# shadowing for ops with multiple tensor inputs is not implemented yet
return False
return True
def get_normalized_nth_input(node: Node, gm: GraphModule, idx: int) -> Node:
"""
Given a node, gets the n'th input to that node, normalizing
args and kwargs to the best of its ability.
"""
try:
norm_args_and_kwargs = node.normalized_arguments(
gm, normalize_to_only_use_kwargs=True
)
if norm_args_and_kwargs is not None:
norm_args, norm_kwargs = norm_args_and_kwargs
if len(norm_args) + len(norm_kwargs) <= idx:
raise AssertionError(
f"Index {idx} out of range: total = {len(norm_args) + len(norm_kwargs)}"
)
if idx < len(norm_args):
return norm_args[idx]
else:
# note: in Python 3.7+ dicts are ordered
return list(norm_kwargs.values())[idx]
else:
if len(node.args) + len(node.kwargs) <= idx:
raise AssertionError(
f"Index {idx} out of range: total = {len(node.args) + len(node.kwargs)}"
)
if idx < len(node.args):
return node.args[idx] # type: ignore[return-value]
else:
kwargs_idx = idx + len(node.args)
return list(node.kwargs.values())[kwargs_idx] # type: ignore[return-value]
except RuntimeError:
# this RuntimeError happens when node argument normalization
# requires typehints to proceed, such as for torch.add where
# either the first, second or both arguments could be tensors
if len(node.args) + len(node.kwargs) <= idx:
raise AssertionError(
f"Index {idx} out of range: total = {len(node.args) + len(node.kwargs)}"
) from None
if idx < len(node.args):
return node.args[idx] # type: ignore[return-value]
else:
kwargs_idx = idx + len(node.args)
return list(node.kwargs.values())[kwargs_idx] # type: ignore[return-value]
@@ -0,0 +1,302 @@
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.intrinsic.quantized as nniq
import torch.ao.nn.qat as nnqat
import torch.ao.nn.quantized as nnq
import torch.ao.nn.quantized.dynamic as nnqd
import torch.nn as nn
import torch.nn.functional as F
from torch.fx import GraphModule
from torch.fx.graph import Node
from .ns_types import NSSingleResultType, NSSingleResultValuesType
from .utils import get_target_type_str, getattr_from_fqn, return_first_non_observer_node
toq = torch.ops.quantized
def mod_weight_detach(mod: nn.Module) -> torch.Tensor:
return mod.weight.detach() # type: ignore[operator]
def mod_0_weight_detach(mod: nn.Module) -> torch.Tensor:
return mod[0].weight.detach() # type: ignore[index]
def mod_weight_bias_0(mod: nn.Module) -> torch.Tensor:
return mod._weight_bias()[0] # type: ignore[operator]
def get_lstm_weight(mod: nn.Module) -> list[torch.Tensor]:
res = []
for idx, param_name in enumerate(mod._flat_weights_names): # type: ignore[arg-type]
if "weight_ih_l" in param_name or "weight_hh_l" in param_name:
param_value = mod._flat_weights[idx].detach() # type: ignore[index,union-attr]
res.append(param_value)
return res
def get_qlstm_weight(mod: nn.Module) -> list[torch.Tensor]:
res = []
for weight_value in mod._all_weight_values: # type: ignore[union-attr]
res.append(weight_value.param.__getstate__()[0][4][0].__getstate__()[0][0])
res.append(weight_value.param.__getstate__()[0][4][1].__getstate__()[0][0])
return res
def get_conv_mod_weight(mod: nn.Module) -> torch.Tensor:
if isinstance(mod, (nn.Conv1d, nn.Conv2d, nn.Conv3d)):
return mod.weight.detach()
elif isinstance(mod, (nni.ConvReLU1d, nni.ConvReLU2d, nni.ConvReLU3d)):
return mod[0].weight.detach() # type: ignore[operator]
else:
return mod._weight_bias()[0] # type: ignore[operator]
def get_linear_mod_weight(mod: nn.Module) -> torch.Tensor:
if isinstance(mod, nn.Linear):
return mod.weight.detach()
elif isinstance(mod, nni.LinearReLU):
return mod[0].weight.detach() # type: ignore[operator]
else:
return mod._weight_bias()[0] # type: ignore[operator]
def get_lstm_mod_weights(mod: nn.Module) -> list[torch.Tensor]:
# TODO(future PR): make more generic, handle everything
if isinstance(mod, nn.LSTM):
res = []
for idx, param_name in enumerate(mod._flat_weights_names):
if "weight_ih_l" in param_name or "weight_hh_l" in param_name:
param_value = mod._flat_weights[idx].detach() # type: ignore[index,union-attr]
res.append(param_value)
return res
else:
if not isinstance(mod, nnqd.LSTM):
raise AssertionError(f"type {type(mod)} not handled yet")
res = []
for weight_value in mod._all_weight_values:
res.append(
weight_value.param.__getstate__()[0][4][0].__getstate__()[0][0] # type: ignore[index]
)
res.append(
weight_value.param.__getstate__()[0][4][1].__getstate__()[0][0] # type: ignore[index]
)
return res
def get_conv_fun_weight(node: Node, gm: GraphModule) -> torch.Tensor:
# traverse backwards from the weight arg, accounting for any observers
weight_arg_node = node.args[1]
if not isinstance(weight_arg_node, Node):
raise AssertionError(f"Expected Node, got {type(weight_arg_node)}")
weight_node = return_first_non_observer_node(weight_arg_node, gm)
if not isinstance(weight_node, Node):
raise AssertionError(f"Expected Node, got {type(weight_node)}")
if weight_node.op != "get_attr":
raise AssertionError(f"Expected get_attr, got {weight_node.op}")
weight = getattr_from_fqn(gm, weight_node.target) # type: ignore[arg-type]
return weight.detach()
def get_qconv_fun_weight(node: Node, gm: GraphModule) -> torch.Tensor:
# qconv state is arg 1
qconv_state_node = node.args[1]
if not isinstance(qconv_state_node, Node):
raise AssertionError(f"Expected Node, got {type(qconv_state_node)}")
if qconv_state_node.op != "get_attr":
raise AssertionError(f"Expected get_attr, got {qconv_state_node.op}")
qconv_state_obj = getattr_from_fqn(gm, qconv_state_node.target) # type: ignore[arg-type]
return qconv_state_obj.weight()
def get_linear_fun_weight(node: Node, gm: GraphModule) -> torch.Tensor:
# traverse backwards from the weight arg, accounting for any observers
# supported patterns:
# weight -> obs -> linear
# weight -> to(torch.float16) -> dequantize -> linear
linear_second_arg = node.args[1]
if not isinstance(linear_second_arg, Node):
raise AssertionError(f"Expected Node, got {type(linear_second_arg)}")
if linear_second_arg.op == "call_module":
# weight -> obs -> linear
weight_arg_node = node.args[1]
if not isinstance(weight_arg_node, Node):
raise AssertionError(f"Expected Node, got {type(weight_arg_node)}")
weight_node = weight_arg_node.args[0]
if not isinstance(weight_node, Node):
raise AssertionError(f"Expected Node, got {type(weight_node)}")
if weight_node.op != "get_attr":
raise AssertionError(f"Expected get_attr, got {weight_node.op}")
weight = getattr_from_fqn(gm, weight_node.target) # type: ignore[arg-type]
return weight.detach()
elif linear_second_arg.op == "call_method":
# weight -> to(torch.float16) -> dequantize -> linear
if linear_second_arg.op != "call_method":
raise AssertionError(f"Expected call_method, got {linear_second_arg.op}")
dequant_node = node.args[1]
if not isinstance(dequant_node, Node):
raise AssertionError(f"Expected Node, got {type(dequant_node)}")
to_fp16_node = dequant_node.args[0]
if not isinstance(to_fp16_node, Node):
raise AssertionError(f"Expected Node, got {type(to_fp16_node)}")
# extract the dtype, so we can cast to it before returning
target_dtype = to_fp16_node.args[1]
weight_node = to_fp16_node.args[0]
if not isinstance(weight_node, Node):
raise AssertionError(f"Expected Node, got {type(weight_node)}")
if weight_node.op != "get_attr":
raise AssertionError(f"Expected get_attr, got {weight_node.op}")
weight = getattr_from_fqn(gm, weight_node.target) # type: ignore[arg-type]
# return the weight with fp16 cast
return weight.detach().to(target_dtype)
else:
if linear_second_arg.op != "get_attr":
raise AssertionError(f"Expected get_attr, got {linear_second_arg.op}")
weight = getattr_from_fqn(gm, linear_second_arg.target) # type: ignore[arg-type]
return weight.detach()
def get_qlinear_fun_weight(node: Node, gm: GraphModule) -> torch.Tensor:
# packed weight is arg 1
packed_weight_node = node.args[1]
if not isinstance(packed_weight_node, Node):
raise AssertionError(f"Expected Node, got {type(packed_weight_node)}")
if packed_weight_node.op != "get_attr":
raise AssertionError(f"Expected get_attr, got {packed_weight_node.op}")
packed_weight = getattr_from_fqn(gm, packed_weight_node.target) # type: ignore[arg-type]
# TODO(future PR): why does packed_weight.unpack() not work?
(weight, _bias), _name = packed_weight.__getstate__()
return weight
def get_op_to_type_to_weight_extraction_fn() -> dict[str, dict[Callable, Callable]]:
op_to_type_to_weight_extraction_fn: dict[str, dict[Callable, Callable]] = {
"call_module": {
# Conv1d
nn.Conv1d: mod_weight_detach,
nni.ConvReLU1d: mod_0_weight_detach,
nnq.Conv1d: mod_weight_bias_0,
nnqat.Conv1d: mod_weight_detach,
nniqat.ConvBn1d: mod_weight_detach,
nniqat.ConvBnReLU1d: mod_weight_detach,
nniqat.ConvReLU1d: mod_weight_detach,
nniq.ConvReLU1d: mod_weight_bias_0,
# Conv2d
nn.Conv2d: mod_weight_detach,
nni.ConvReLU2d: mod_0_weight_detach,
nnq.Conv2d: mod_weight_bias_0,
nnqat.Conv2d: mod_weight_detach,
nniqat.ConvBn2d: mod_weight_detach,
nniqat.ConvBnReLU2d: mod_weight_detach,
nniqat.ConvReLU2d: mod_weight_detach,
nniq.ConvReLU2d: mod_weight_bias_0,
# Conv3d
nn.Conv3d: mod_weight_detach,
nni.ConvReLU3d: mod_0_weight_detach,
nnq.Conv3d: mod_weight_bias_0,
nnqat.Conv3d: mod_weight_detach,
nniqat.ConvBn3d: mod_weight_detach,
nniqat.ConvBnReLU3d: mod_weight_detach,
nniqat.ConvReLU3d: mod_weight_detach,
nniq.ConvReLU3d: mod_weight_bias_0,
# Linear
nn.Linear: mod_weight_detach,
nnq.Linear: mod_weight_bias_0,
nni.LinearReLU: mod_0_weight_detach,
nniq.LinearReLU: mod_weight_bias_0,
nnqat.Linear: mod_weight_detach,
nnqd.Linear: mod_weight_bias_0,
nniqat.LinearReLU: mod_weight_detach,
nniqat.LinearBn1d: mod_weight_detach,
nn.modules.linear.NonDynamicallyQuantizableLinear: mod_weight_detach,
# LSTM
nn.LSTM: get_lstm_weight,
nnqd.LSTM: get_qlstm_weight,
},
"call_function": {
# Conv
F.conv1d: get_conv_fun_weight,
F.conv2d: get_conv_fun_weight,
F.conv3d: get_conv_fun_weight,
toq.conv1d: get_qconv_fun_weight,
toq.conv2d: get_qconv_fun_weight,
toq.conv3d: get_qconv_fun_weight,
toq.conv1d_relu: get_qconv_fun_weight,
toq.conv2d_relu: get_qconv_fun_weight,
toq.conv3d_relu: get_qconv_fun_weight,
# Linear
F.linear: get_linear_fun_weight,
toq.linear: get_qlinear_fun_weight,
toq.linear_relu: get_qlinear_fun_weight,
},
}
return op_to_type_to_weight_extraction_fn
def extract_weight_from_node(
node: Node,
gm: GraphModule,
op_to_type_to_weight_extraction_fn: dict[str, dict[Callable, Callable]]
| None = None,
) -> NSSingleResultType | None:
res_type = NSSingleResultValuesType.WEIGHT.value
# Not all graphmodules have _node_name_to_scope, so only fill it
# out if it exists.
fqn = None
if hasattr(gm, "_node_name_to_scope"):
fqn = gm._node_name_to_scope[node.name][0] # type: ignore[index]
if op_to_type_to_weight_extraction_fn is None:
op_to_type_to_weight_extraction_fn = get_op_to_type_to_weight_extraction_fn()
ref_node_type = get_target_type_str(node, gm)
# for extracting weights, these are always the same
prev_node_type = ref_node_type
if node.op == "call_function":
function_mapping = op_to_type_to_weight_extraction_fn["call_function"]
for target_fn_type, weight_extraction_fn in function_mapping.items():
if node.target == target_fn_type:
weight = weight_extraction_fn(node, gm)
return {
"type": res_type,
"values": [weight],
"prev_node_name": node.name,
"prev_node_target_type": prev_node_type,
"ref_node_name": node.name,
"ref_node_target_type": ref_node_type,
"index_within_arg": 0,
"index_of_arg": 0,
"fqn": fqn,
}
elif node.op == "call_module":
# for call_module, we need to look up the modules to do the type check
if not isinstance(node.target, str):
raise AssertionError(f"Expected str, got {type(node.target)}")
mod = getattr_from_fqn(gm, node.target)
module_mapping = op_to_type_to_weight_extraction_fn["call_module"]
for target_mod_type, weight_extraction_fn in module_mapping.items():
if type(mod) is target_mod_type:
weight = weight_extraction_fn(mod)
return {
"type": res_type,
"values": [weight],
"prev_node_name": node.name,
"prev_node_target_type": prev_node_type,
"ref_node_name": node.name,
"ref_node_target_type": ref_node_type,
"index_within_arg": 0,
"index_of_arg": 0,
"fqn": fqn,
}
return None
@@ -0,0 +1,23 @@
# Variables
from ._mappings import (
get_dynamic_sparse_quantized_mapping,
get_static_sparse_quantized_mapping,
)
# Scheduler
from .scheduler.base_scheduler import BaseScheduler
from .scheduler.cubic_scheduler import CubicSL
from .scheduler.lambda_scheduler import LambdaSL
# Sparsifier
from .sparsifier.base_sparsifier import BaseSparsifier
from .sparsifier.nearly_diagonal_sparsifier import NearlyDiagonalSparsifier
# Parametrizations
from .sparsifier.utils import (
FakeSparsity,
fqn_to_module,
get_arg_info_from_tensor_fqn,
module_to_fqn,
)
from .sparsifier.weight_norm_sparsifier import WeightNormSparsifier
@@ -0,0 +1,482 @@
# mypy: allow-untyped-defs
import copy
import warnings
from collections import defaultdict
from typing import Any
import torch
from torch import nn
from torch.ao.pruning.sparsifier.utils import fqn_to_module, module_to_fqn
__all__ = ["ActivationSparsifier"]
class ActivationSparsifier:
r"""
The Activation sparsifier class aims to sparsify/prune activations in a neural
network. The idea is to attach the sparsifier to a layer (or layers) and it
zeroes out the activations based on the mask_fn (or sparsification function)
input by the user.
The mask_fn is applied once all the inputs are aggregated and reduced i.e.
mask = mask_fn(reduce_fn(aggregate_fn(activations)))
Note::
The sparsification mask is computed on the input **before it goes through the attached layer**.
Args:
model (nn.Module):
The model whose layers will be sparsified. The layers that needs to be
sparsified should be added separately using the register_layer() function
aggregate_fn (Optional, Callable):
default aggregate_fn that is used if not specified while registering the layer.
specifies how inputs should be aggregated over time.
The aggregate_fn should usually take 2 torch tensors and return the aggregated tensor.
Example
def add_agg_fn(tensor1, tensor2): return tensor1 + tensor2
reduce_fn (Optional, Callable):
default reduce_fn that is used if not specified while registering the layer.
reduce_fn will be called on the aggregated tensor i.e. the tensor obtained after
calling agg_fn() on all inputs.
Example
def mean_reduce_fn(agg_tensor): return agg_tensor.mean(dim=0)
mask_fn (Optional, Callable):
default mask_fn that is used to create the sparsification mask using the tensor obtained after
calling the reduce_fn(). This is used by default if a custom one is passed in the
register_layer().
Note that the mask_fn() definition should contain the sparse arguments that is passed in sparse_config
arguments.
features (Optional, list):
default selected features to sparsify.
If this is non-empty, then the mask_fn will be applied for each feature of the input.
For example,
mask = [mask_fn(reduce_fn(aggregated_fn(input[feature])) for feature in features]
feature_dim (Optional, int):
default dimension of input features. Again, features along this dim will be chosen
for sparsification.
sparse_config (Dict):
Default configuration for the mask_fn. This config will be passed
with the mask_fn()
Example:
>>> # xdoctest: +SKIP
>>> model = SomeModel()
>>> act_sparsifier = ActivationSparsifier(...) # init activation sparsifier
>>> # Initialize aggregate_fn
>>> def agg_fn(x, y):
>>> return x + y
>>>
>>> # Initialize reduce_fn
>>> def reduce_fn(x):
>>> return torch.mean(x, dim=0)
>>>
>>> # Initialize mask_fn
>>> def mask_fn(data):
>>> return torch.eye(data.shape).to(data.device)
>>>
>>>
>>> act_sparsifier.register_layer(
... model.some_layer,
... aggregate_fn=agg_fn,
... reduce_fn=reduce_fn,
... mask_fn=mask_fn,
... )
>>>
>>> # start training process
>>> for _ in [...]:
>>> # epoch starts
>>> # model.forward(), compute_loss() and model.backwards()
>>> # epoch ends
>>> act_sparsifier.step()
>>> # end training process
>>> sparsifier.squash_mask()
"""
def __init__(
self,
model: nn.Module,
aggregate_fn=None,
reduce_fn=None,
mask_fn=None,
features=None,
feature_dim=None,
**sparse_config,
):
self.model = model
self.defaults: dict[str, Any] = defaultdict()
self.defaults["sparse_config"] = sparse_config
# functions
self.defaults["aggregate_fn"] = aggregate_fn
self.defaults["reduce_fn"] = reduce_fn
self.defaults["mask_fn"] = mask_fn
# default feature and feature_dim
self.defaults["features"] = features
self.defaults["feature_dim"] = feature_dim
self.data_groups: dict[str, dict] = defaultdict(
dict
) # contains all relevant info w.r.t each registered layer
self.state: dict[str, Any] = defaultdict(dict) # layer name -> mask
@staticmethod
def _safe_rail_checks(args):
"""Makes sure that some of the functions and attributes are not passed incorrectly"""
# if features are not None, then feature_dim must not be None
features, feature_dim = args["features"], args["feature_dim"]
if features is not None:
if feature_dim is None:
raise AssertionError("need feature dim to select features")
# all the *_fns should be callable
fn_keys = ["aggregate_fn", "reduce_fn", "mask_fn"]
for key in fn_keys:
fn = args[key]
if not callable(fn):
raise AssertionError(f"{fn} must be callable")
def _aggregate_hook(self, name):
"""Returns hook that computes aggregate of activations passing through."""
# gather some data
feature_dim = self.data_groups[name]["feature_dim"]
features = self.data_groups[name]["features"]
agg_fn = self.data_groups[name]["aggregate_fn"]
def hook(module, input) -> None:
input_data = input[0]
data = self.data_groups[name].get("data") # aggregated data
if features is None:
# no features associated, data should not be a list
if data is None:
data = torch.zeros_like(input_data)
self.state[name]["mask"] = torch.ones_like(input_data)
out_data = agg_fn(data, input_data)
else:
# data should be a list [aggregated over each feature only]
if data is None:
out_data = [
0 for _ in range(len(features))
] # create one in case of 1st forward
self.state[name]["mask"] = [0 for _ in range(len(features))]
else:
out_data = data # a list
# compute aggregate over each feature
for feature_idx in range(len(features)):
# each feature is either a list or scalar, convert it to torch tensor
feature_tensor = (
torch.Tensor([features[feature_idx]])
.long()
.to(input_data.device)
)
data_feature = torch.index_select(
input_data, feature_dim, feature_tensor
)
if data is None:
curr_data = torch.zeros_like(data_feature)
self.state[name]["mask"][feature_idx] = torch.ones_like(
data_feature
)
else:
curr_data = data[feature_idx]
out_data[feature_idx] = agg_fn(curr_data, data_feature)
self.data_groups[name]["data"] = out_data
return hook
def register_layer(
self,
layer: nn.Module,
aggregate_fn=None,
reduce_fn=None,
mask_fn=None,
features=None,
feature_dim=None,
**sparse_config,
):
r"""
Registers a layer for sparsification. The layer should be part of self.model.
Specifically, registers a pre-forward hook to the layer. The hook will apply the aggregate_fn
and store the aggregated activations that is input over each step.
Note::
- There is no need to pass in the name of the layer as it is automatically computed as per
the fqn convention.
- All the functions (fn) passed as argument will be called at a dim, feature level.
"""
name = module_to_fqn(self.model, layer)
if name is None:
raise AssertionError("layer not found in the model")
if name in self.data_groups: # unregister layer if already present
warnings.warn(
"layer already attached to the sparsifier, deregistering the layer and registering with new config",
stacklevel=2,
)
self.unregister_layer(name=name)
local_args = copy.deepcopy(self.defaults)
update_dict = {
"aggregate_fn": aggregate_fn,
"reduce_fn": reduce_fn,
"mask_fn": mask_fn,
"features": features,
"feature_dim": feature_dim,
"layer": layer,
}
local_args.update(
(arg, val) for arg, val in update_dict.items() if val is not None
)
local_args["sparse_config"].update(sparse_config)
self._safe_rail_checks(local_args)
self.data_groups[name] = local_args
agg_hook = layer.register_forward_pre_hook(self._aggregate_hook(name=name))
self.state[name]["mask"] = (
None # mask will be created when model forward is called.
)
# attach agg hook
self.data_groups[name]["hook"] = agg_hook
# for serialization purposes, we know whether aggregate_hook is attached
# or sparsify_hook()
self.data_groups[name]["hook_state"] = "aggregate" # aggregate hook is attached
def get_mask(self, name: str | None = None, layer: nn.Module | None = None):
"""
Returns mask associated to the layer.
The mask is
- a torch tensor is features for that layer is None.
- a list of torch tensors for each feature, otherwise
Note::
The shape of the mask is unknown until model.forward() is applied.
Hence, if get_mask() is called before model.forward(), an
error will be raised.
"""
if name is None and layer is None:
raise AssertionError("Need at least name or layer obj to retrieve mask")
if name is None:
if layer is None:
raise AssertionError("layer must be provided when name is None")
name = module_to_fqn(self.model, layer)
if name is None:
raise AssertionError("layer not found in the specified model")
if name not in self.state:
raise ValueError("Error: layer with the given name not found")
mask = self.state[name].get("mask", None)
if mask is None:
raise ValueError(
"Error: shape unknown, call layer() routine at least once to infer mask"
)
return mask
def unregister_layer(self, name):
"""Detaches the sparsifier from the layer"""
# detach any hooks attached
self.data_groups[name]["hook"].remove()
# pop from the state dict
self.state.pop(name)
# pop from the data groups
self.data_groups.pop(name)
def step(self):
"""Internally calls the update_mask() function for each layer"""
with torch.no_grad():
for name, configs in self.data_groups.items():
data = configs["data"]
self.update_mask(name, data, configs)
self.data_groups[name].pop("data") # reset the accumulated data
def update_mask(self, name, data, configs):
"""
Called for each registered layer and does the following-
1. apply reduce_fn on the aggregated activations
2. use mask_fn to compute the sparsification mask
Note:
the reduce_fn and mask_fn is called for each feature, dim over the data
"""
mask = self.get_mask(name)
sparse_config = configs["sparse_config"]
features = configs["features"]
reduce_fn = configs["reduce_fn"]
mask_fn = configs["mask_fn"]
if features is None:
data = reduce_fn(data)
mask.data = mask_fn(data, **sparse_config)
else:
for feature_idx in range(len(features)):
data_feature = reduce_fn(data[feature_idx])
mask[feature_idx].data = mask_fn(data_feature, **sparse_config)
def _sparsify_hook(self, name):
"""Returns hook that applies sparsification mask to input entering the attached layer"""
mask = self.get_mask(name)
features = self.data_groups[name]["features"]
feature_dim = self.data_groups[name]["feature_dim"]
def hook(module, input):
input_data = input[0]
if features is None:
# apply to all the features
return input_data * mask
else:
# apply per feature, feature_dim
for feature_idx in range(len(features)):
feature = (
torch.Tensor([features[feature_idx]])
.long()
.to(input_data.device)
)
sparsified = (
torch.index_select(input_data, feature_dim, feature)
* mask[feature_idx]
)
input_data.index_copy_(feature_dim, feature, sparsified)
return input_data
return hook
def squash_mask(self, attach_sparsify_hook=True, **kwargs):
"""
Unregisters aggregate hook that was applied earlier and registers sparsification hooks if
attach_sparsify_hook = True.
"""
for name, configs in self.data_groups.items():
# unhook agg hook
configs["hook"].remove()
configs.pop("hook")
self.data_groups[name]["hook_state"] = "None"
if attach_sparsify_hook:
configs["hook"] = configs["layer"].register_forward_pre_hook(
self._sparsify_hook(name)
)
configs["hook_state"] = (
"sparsify" # signals that sparsify hook is now attached
)
def _get_serializable_data_groups(self):
"""Exclude hook and layer from the config keys before serializing
TODO: Might have to treat functions (reduce_fn, mask_fn etc) in a different manner while serializing.
For time-being, functions are treated the same way as other attributes
"""
data_groups: dict[str, Any] = defaultdict()
for name, config in self.data_groups.items():
new_config = {
key: value
for key, value in config.items()
if key not in ["hook", "layer"]
}
data_groups[name] = new_config
return data_groups
def _convert_mask(self, states_dict, sparse_coo=True):
r"""Converts the mask to sparse coo or dense depending on the `sparse_coo` argument.
If `sparse_coo=True`, then the mask is stored as sparse coo else dense tensor
"""
states = copy.deepcopy(states_dict)
for state in states.values():
if state["mask"] is not None:
if isinstance(state["mask"], list):
for idx in range(len(state["mask"])):
if sparse_coo:
state["mask"][idx] = state["mask"][idx].to_sparse_coo()
else:
state["mask"][idx] = state["mask"][idx].to_dense()
else:
if sparse_coo:
state["mask"] = state["mask"].to_sparse_coo()
else:
state["mask"] = state["mask"].to_dense()
return states
def state_dict(self) -> dict[str, Any]:
r"""Returns the state of the sparsifier as a :class:`dict`.
It contains:
* state - contains name -> mask mapping.
* data_groups - a dictionary containing all config information for each
layer
* defaults - the default config while creating the constructor
"""
data_groups = self._get_serializable_data_groups()
state = self._convert_mask(self.state)
return {"state": state, "data_groups": data_groups, "defaults": self.defaults}
def load_state_dict(self, state_dict: dict[str, Any]) -> None:
r"""The load_state_dict() restores the state of the sparsifier based on the state_dict
Args:
* state_dict - the dictionary that to which the current sparsifier needs to be restored to
"""
state = state_dict["state"]
data_groups, defaults = state_dict["data_groups"], state_dict["defaults"]
self.__set_state__(
{"state": state, "data_groups": data_groups, "defaults": defaults}
)
def __get_state__(self) -> dict[str, Any]:
data_groups = self._get_serializable_data_groups()
state = self._convert_mask(self.state)
return {
"defaults": self.defaults,
"state": state,
"data_groups": data_groups,
}
def __set_state__(self, state: dict[str, Any]) -> None:
state["state"] = self._convert_mask(
state["state"], sparse_coo=False
) # convert mask to dense tensor
self.__dict__.update(state)
# need to attach layer and hook info into the data_groups
for name, config in self.data_groups.items():
# fetch layer
layer = fqn_to_module(self.model, name)
if layer is None:
raise AssertionError(f"layer {name} not found in the model")
# if agg_mode is True, then layer in aggregate mode
if "hook_state" in config and config["hook_state"] == "aggregate":
hook = layer.register_forward_pre_hook(self._aggregate_hook(name))
elif "hook_state" in config and config["hook_state"] == "sparsify":
hook = layer.register_forward_pre_hook(self._sparsify_hook(name))
config["layer"] = layer
config["hook"] = hook # type: ignore[possibly-undefined]
def __repr__(self):
format_string = self.__class__.__name__ + " ("
for name, config in self.data_groups.items():
format_string += "\n"
format_string += "\tData Group\n"
format_string += f"\t name: {name}\n"
for key in sorted(config.keys()):
if key in ["data", "hook", "reduce_fn", "mask_fn", "aggregate_fn"]:
continue
format_string += f"\t {key}: {config[key]}\n"
format_string += ")"
return format_string
@@ -0,0 +1,6 @@
from .base_data_scheduler import BaseDataScheduler
__all__ = [
"BaseDataScheduler",
]
@@ -0,0 +1,199 @@
# mypy: allow-untyped-defs
import abc
import warnings
import weakref
from functools import wraps
from torch.ao.pruning._experimental.data_sparsifier import BaseDataSparsifier
__all__ = ["BaseDataScheduler"]
class BaseDataScheduler:
r"""
The BaseDataScheduler is the abstract scheduler class specifically for the
BaseDataSparsifier class. This class controls a specific hyperparameter of
the sparsifier class and varies it across the training process (or across time).
Args:
data_sparsifier (instance of BaseDataSparsifier)
Implemented class data sparsifier class wherein the update_mask is implemented
schedule_param (str)
A specific hyperparameter of the passed sparsifier that needs to be scheduled/varied
last_epoch (int, default=-1)
This is specifically is passed when training needs to be resumed from a particular
point.
verbose (bool, default=False)
Verbosity of the BaseDataScheduler
The *get_hyperparam()* function needs to be implemented by the user.
"""
def __init__(
self, data_sparsifier, schedule_param: str, last_epoch=-1, verbose=False
):
# Attach sparsifier
if not isinstance(data_sparsifier, BaseDataSparsifier):
raise TypeError(
f"{type(data_sparsifier).__name__} is not an instance of torch.ao.pruning.BaseDataSparsifier"
)
self.data_sparsifier = data_sparsifier
self.schedule_param = schedule_param
# Initialize epoch and base hyper-params
self.base_param = {
name: config.get(schedule_param, None)
for name, config in self.data_sparsifier.data_groups.items()
}
self.last_epoch = last_epoch
# Following https://github.com/pytorch/pytorch/issues/20124
# We would like to ensure that `scheduler.step()` is called after
# `sparsifier.step()`
def with_counter(method):
if getattr(method, "_with_counter", False):
# `sparsifier.step()` has already been replaced, return.
return method
# Keep a weak reference to the sparsifier instance to prevent
# cyclic references.
instance_ref = weakref.ref(method.__self__)
# Get the unbound method for the same purpose.
func = method.__func__
cls = instance_ref().__class__
del method
@wraps(func)
def wrapper(*args, **kwargs):
instance = instance_ref()
instance._step_count += 1 # type: ignore[union-attr]
wrapped = func.__get__(instance, cls)
return wrapped(*args, **kwargs)
# Note that the returned function here is no longer a bound method,
# so attributes like `__func__` and `__self__` no longer exist.
wrapper._with_counter = True # type: ignore[attr-defined]
return wrapper
self.data_sparsifier.step = with_counter(self.data_sparsifier.step) # type: ignore[assignment]
self.data_sparsifier._step_count = 0 # type: ignore[attr-defined]
self._step_count: int = 0
self.verbose = verbose
# Housekeeping
self._get_sp_called_within_step: bool = False # sp -> schedule parameter
self.step()
@abc.abstractmethod
def get_schedule_param(self):
r"""
Abstract method that needs to be implemented by the child class.
The expected return type should is a dictionary of name to schedule_param value
The returned values will be updated in sparsifier when the scheduler step() function
is called.
Example:
>>> def get_schedule_param(self):
... new_param = {}
... for name in self.sparsifier.data_groups.keys():
... new_param[name] = (
... self.sparsifier.data_groups[name][self.schedule_param] * 0.5
... )
... return new_param
When the step() function is called, the value in self.sparsifier.data_groups[name][self.schedule_param]
would be halved
"""
raise NotImplementedError
def __repr__(self):
format_string = self.__class__.__name__ + " ("
format_string += "\n"
format_string += f"Data Sparsifier {self.data_sparsifier}\n"
format_string += f" {self.schedule_param}: {self.base_param}\n"
format_string += ")"
return format_string
def state_dict(self):
"""Returns the state of the scheduler as a :class:`dict`.
It contains an entry for every variable in self.__dict__ which
is not the sparsifier.
Note:
The scheduler class does not track the state of the data_sparsifier.
Make sure to store the state of the sparsifier before storing the
state of the scheduler
"""
return {
key: value
for key, value in self.__dict__.items()
if key != "data_sparsifier"
}
def load_state_dict(self, state_dict):
"""Loads the schedulers state.
Note:
Remember to restore the state of the data_sparsifier before the scheduler.
Args:
state_dict (dict): scheduler state. Should be an object returned
from a call to :meth:`state_dict`.
"""
self.__dict__.update(state_dict)
def get_last_param(self):
return self._last_param
def step(self):
# Raise warning if trying to call scheduler step before the sparsifier.
# https://github.com/pytorch/pytorch/issues/20124
if self._step_count == 1:
if not hasattr(self.data_sparsifier.step, "_with_counter"):
warnings.warn(
"Seems like `data_sparsifier.step()` has been overridden after sparsity scheduler "
"initialization. Please, make sure to call `data_sparsifier.step()` before "
"`scheduler.step()`.",
UserWarning,
stacklevel=2,
)
# Just check if there were two first scheduler.step() calls before sparsifier.step()
elif self.data_sparsifier._step_count < 1: # type: ignore[attr-defined]
warnings.warn(
"Detected call of `scheduler.step()` before `data_sparsifier.step()`. "
"You have to make sure you run the data_sparsifier.step() BEFORE any "
"calls to the scheduler.step().",
UserWarning,
stacklevel=2,
)
self._step_count += 1
class _enable_get_sp_call:
def __init__(self, o):
self.o = o
def __enter__(self):
self.o._get_sp_called_within_step = True
return self
def __exit__(self, type, value, traceback):
self.o._get_sp_called_within_step = False
with _enable_get_sp_call(self):
self.last_epoch += 1
updated_scheduler_params = self.get_schedule_param()
for name, param in updated_scheduler_params.items():
self.data_sparsifier.data_groups[name][self.schedule_param] = param
if self.verbose:
print(f"Adjusting {self.schedule_param} for group {name} to {param}")
self._last_param = {
name: config.get(self.schedule_param, None)
for name, config in self.data_sparsifier.data_groups.items()
}
self.data_sparsifier.enable_mask_update = True
@@ -0,0 +1,8 @@
from .base_data_sparsifier import BaseDataSparsifier
from .data_norm_sparsifier import DataNormSparsifier
__all__ = [
"BaseDataSparsifier",
"DataNormSparsifier",
]
@@ -0,0 +1,334 @@
# mypy: allow-untyped-defs
import abc
import copy
import sys
import warnings
from collections import defaultdict
from typing import Any
import torch
from torch import nn
from torch.ao.pruning.sparsifier import base_sparsifier, utils
from torch.nn.utils import parametrize
if not sys.warnoptions:
# to suppress repeated warnings when being used in a training loop.
warnings.simplefilter("once")
__all__ = ["BaseDataSparsifier"]
EMBEDDING_TYPES = {
nn.Embedding,
nn.EmbeddingBag,
}
SUPPORTED_TYPES = {
torch.Tensor,
nn.Parameter,
*EMBEDDING_TYPES,
}
class _Container(nn.Module):
pass
class BaseDataSparsifier(base_sparsifier.BaseSparsifier):
r"""
Base Data Sparsifier class for all Data sparsifiers.
The abstract class accepts raw torch tensors / embedding / embedding bags (refer to SUPPORTED_TYPES above)
to prepare for sparsification.
In this case, mask (and parametrizations) is owned by the class and not by the user.
Specifically, the container object inside the class maintains the mask and parametrizations of the input data
Args:
data_list (list of tuples)
list of (name, data) tuples to sparsify. Lookup SUPPORTED_TYPES
for type of data. Internally, a container module handles the data sparsification.
defaults (dict)
default configurations will be attached to the
configuration. Only the keys that don't exist in the `config` will
be updated.
Example::
>>> # xdoctest: +SKIP
>>> data_list = [('tensor_1', torch.randn(3,3)), ('tensor_2', torch.randn(4,4))]
>>> defaults = {'sparsity_level': 0.7}
>>> sparsifier = DerivedDataSparsifier(data_list = data_list, **defaults) # Some sparsifier that inherits BaseDataSparsifier
>>> new_tensor_to_add = {'name': 'tensor_3', 'data': torch.randn(5,5), 'sparsity_level': 0.3}
>>> sparsifier.add_data(**new_tensor_to_add)
>>> # tensor_1 and tensor_2 will have sparsity_level of 0.7 but tensor_3 will have sparsity_level=0.3
"""
def __init__(self, data_list: list[tuple[str, Any]] | None = None, **defaults):
super().__init__(defaults=defaults)
self._container = _Container()
self.data_groups: dict[str, dict] = defaultdict(dict) # name -> {**config}
if data_list is not None:
# add data with default config here
[self.add_data(name, data, **self.defaults) for name, data in data_list]
def prepare(self, model, config):
raise NotImplementedError("this function is undefined for this class")
def _extract_weight(self, data):
# extract the weight parameter instead of underlying data
if type(data) in [torch.Tensor, nn.Parameter]:
return data
elif type(data) in EMBEDDING_TYPES:
return data.weight
def add_data(self, name: str, data, reuse_mask=True, **config):
r"""Configures and parametrizes the internal container model with name and data.
**Note**:
1. If the data with name already exists, it replaces the data.
2. While replacing, the old mask is reused when `reuse_mask=True`
3. If `reuse_mask=True`, then the replacing data needs to have the same shape as that of old data.
4. By default, the config of the replaced data is used as config for the replacing data, unless something
is specified in the config dictionary.
"""
if type(data) not in SUPPORTED_TYPES:
raise AssertionError(
f"specified data type:{type(data)} not supported at the moment"
)
local_args = copy.deepcopy(self.defaults)
local_args.update(config)
weight = self._extract_weight(data)
# Bookkeeping in the container class
mask = local_args.get("mask", torch.ones_like(weight))
param_class = local_args.get("parametrization", utils.FakeSparsity)
if name in self.state:
# If the named data already exists - replace
warnings.warn(
"Replacing existing data of the same name. - Did you mean a different name?",
stacklevel=2,
)
# reuse old config
old_args = self.data_groups[name]
local_args = copy.deepcopy(old_args)
local_args.update(config)
if reuse_mask:
current_data = self.get_data(name=name)
if weight.shape != current_data.shape:
raise AssertionError(
"to retain the old mask, the shape of the new data must be the same as the previous one"
)
mask = self.get_mask(
name=name
) # reuse mask instead of creating a new one
self._delete_data(name=name)
# parameter creates a deepcopy of the weight inside, so create a buffer
self._container.register_buffer(name=name, tensor=weight)
parametrize.register_parametrization(self._container, name, param_class(mask))
self.state[name]["mask"] = mask
self.data_groups[name] = local_args
return getattr(self._container, name)
def get_data(self, name: str, return_original: bool = True):
r"""Returns weight tensor (or data)
Args:
- name: name of the data to be returned
- return_original returns weight tensor without applying parametrization if True
else - returns the sparsified version (parametrized)
"""
if name not in self.data_groups:
raise ValueError("data with specified name does not exist")
if return_original:
if not parametrize.is_parametrized(self._container, name):
raise ValueError("mask squashed - original mask value does not exist")
data = getattr(self._container.parametrizations, name).original
return data
else:
return getattr(self._container, name)
def _convert_mask(self, states, sparse_coo=True):
r"""Converts the mask to sparse coo or dense tensors depending on the `sparse_coo` argument."""
states = copy.deepcopy(states)
for state in states.values():
if sparse_coo:
state["mask"] = state["mask"].to_sparse_coo()
else:
state["mask"] = state["mask"].to_dense()
return states
def state_dict(self):
r"""Returns the state of the optimizer as a :class:`dict`.
It contains:
* state - contains name -> mask mapping.
* data_groups - a list containing all sparsity configuration groups
with the key name specifying the name of the data
* container_state_dict - the state dictionary of the internal
container model used for sparsification
"""
state = self._convert_mask(self.state)
return {
"state": state,
"data_groups": self.data_groups,
"_container": self._container.state_dict(),
}
def _load_container_from_state(self, states, data_groups, container_state_dict):
r"""This restores the state of the container specifically based on the data present in state and data_groups
If the data was parametrized, then the data would be added to the container and then parametrized,
else it would just add the attribute the container.
"""
for name, state in states.items():
config_name = data_groups.get(name, None)
if config_name is None:
raise RuntimeError(f"Error loading {name}")
# check if the data with such a name was parametrized, if so parametrize
# otherwise just set the attribute and continue
parametrized_name = f"parametrizations.{name}.original"
parametrized = False
data = container_state_dict.get(name, None)
if name in container_state_dict:
# the parametrization was probably removed for this
data = container_state_dict.get(name)
elif parametrized_name in container_state_dict:
# so the weight was parametrized
data = container_state_dict.get(parametrized_name)
parametrized = True
else:
raise RuntimeError(f"Error loading {name}")
self._container.register_buffer(name=name, tensor=data)
if parametrized:
# register parameter if parametrized
mask = state.get("mask", torch.ones_like(data))
param_class = data_groups.get(
"parametrization", utils.FakeSparsity
) # change once public_api for utils is fixed!
parametrize.register_parametrization(
self._container, name, param_class(mask)
)
def load_state_dict(self, state_dict, strict=True):
r"""The load_state_dict() restores the state of the sparsifier based on the state_dict
Args:
* state_dict - the dictionary that to which the current sparsifier needs to be restored to
* strict - If True - the sparsifier is reset and is restored exactly to the state in state_dict.
If False - the current sparsifier is not reset before loading the state_dict i.e. data added
before loading the state_dict is not erased.
"""
states = copy.deepcopy(state_dict["state"])
data_groups = copy.deepcopy(state_dict["data_groups"])
container_state_dict = copy.deepcopy(state_dict["_container"])
states = self._convert_mask(
states, sparse_coo=False
) # convert sparse coo mask to dense
if strict:
# if strict load -> then reset container
self._container = _Container()
self._load_container_from_state(states, data_groups, container_state_dict)
if not strict:
states.update(self.state)
data_groups.update(self.data_groups)
self.__setstate__({"state": states, "data_groups": data_groups})
def __setstate__(self, state):
if "_container" in state: # If container object is in state then load model
container_dict = state.pop("_container")
self._container = _Container()
state["state"] = self._convert_mask(
state["state"], sparse_coo=False
) # convert sparse coo mask to dense
self._load_container_from_state(
state["state"], state["data_groups"], container_dict
)
self.__dict__.update(state)
def __getstate__(self):
state = self._convert_mask(self.state)
return {
"defaults": self.defaults,
"state": state,
"data_groups": self.data_groups,
"_container": self._container.state_dict(),
}
def __repr__(self): # type:ignore[override]
format_string = self.__class__.__name__ + " ("
for name, sparse_args in self.data_groups.items():
format_string += "\n"
format_string += "\tData Group\n"
format_string += f"\t name: {name}\n"
for key in sorted(sparse_args.keys()):
if key == "data":
continue
format_string += f"\t {key}: {sparse_args[key]}\n"
format_string += ")"
return format_string
def get_mask(self, name: str):
if name not in self.state:
raise ValueError("data with specified name does not exist")
return self.state[name]["mask"]
def squash_mask(self, *args, leave_parametrized=True, names=None, **kwargs):
r"""Squashes the sparse masks into the appropriate tensors. Also, accepts list of strings
to squash mask for. If none, squashes mask for all the keys
kwargs:
* names: list of strings to squash mask for
* sparsified: if true - applies the mask before squashing
if false - does not apply the mask before squashing
"""
if names is None:
names = list(self.data_groups.keys())
for name in names:
parametrize.remove_parametrizations(
self._container, name, leave_parametrized=leave_parametrized
)
def step(self): # type:ignore[override]
if not self.enable_mask_update:
return
with torch.no_grad():
for name, config in self.data_groups.items():
# get non-sparsified data
data = self.get_data(name)
# need name for the mask otherwise can directly pass mask?
self.update_mask(name, data, **config)
@abc.abstractmethod
def update_mask(self, name, data, **kwargs): # type: ignore[override]
pass
def _delete_data(self, name):
"""Detaches some data from the sparsifier.
Args:
name (str)
Name of the data to be removed from the sparsifier
Note:
Currently private. Kind of used as a helper function when replacing data of the same name
"""
self.squash_mask(
names=[name], leave_parametrized=False
) # do not apply the mask while deleting
delattr(self._container, name)
self.state.pop(name)
self.data_groups.pop(name)
@@ -0,0 +1,204 @@
# mypy: allow-untyped-defs
import operator
from functools import reduce
from typing import Any
import torch
from torch.nn import functional as F
from .base_data_sparsifier import BaseDataSparsifier
__all__ = ["DataNormSparsifier"]
class DataNormSparsifier(BaseDataSparsifier):
r"""L1-Norm Sparsifier
This sparsifier computes the *L1-norm* of every sparse block and "zeroes-out" the
ones with the lowest norm. The level of sparsity defines how many of the
blocks is removed.
This sparsifier is controlled by three variables:
1. `sparsity_level` defines the number of *sparse blocks* that are zeroed-out
2. `sparse_block_shape` defines the shape of the sparse blocks. Note that
the sparse blocks originate at the zero-index of the tensor.
3. `zeros_per_block` is the number of zeros that we are expecting in each
sparse block. By default we assume that all elements within a block are
zeroed-out. However, setting this variable sets the target number of
zeros per block. The zeros within each block are chosen as the *smallest
absolute values*.
Args:
sparsity_level: The target level of sparsity
sparse_block_shape: The shape of a sparse block
zeros_per_block: Number of zeros in a sparse block
Note::
All arguments to the DataNormSparsifier constructor are "default"
arguments and could be overridden by the configuration provided in the
`add_data` step.
"""
def __init__(
self,
data_list: list[tuple[str, Any]] | None = None,
sparsity_level: float = 0.5,
sparse_block_shape: tuple[int, int] = (1, 4),
zeros_per_block: int | None = None,
norm: str = "L1",
):
if zeros_per_block is None:
zeros_per_block = reduce(operator.mul, sparse_block_shape)
if norm not in ["L1", "L2"]:
raise AssertionError("only L1 and L2 norm supported at the moment")
defaults = {
"sparsity_level": sparsity_level,
"sparse_block_shape": sparse_block_shape,
"zeros_per_block": zeros_per_block,
}
self.norm = norm
super().__init__(data_list=data_list, **defaults)
def __get_scatter_folded_mask(
self, data, dim, indices, output_size, sparse_block_shape
):
mask = torch.ones_like(data)
mask.scatter_(dim=dim, index=indices, value=0) # zeroing out
mask = F.fold(
mask,
output_size=output_size,
kernel_size=sparse_block_shape,
stride=sparse_block_shape,
)
mask = mask.to(torch.int8)
return mask
def __get_block_level_mask(self, data, sparse_block_shape, zeros_per_block):
# Assume data is a squeezed tensor
height, width = data.shape[-2], data.shape[-1]
block_height, block_width = sparse_block_shape
values_per_block = block_height * block_width
# just return zeros if zeroing all elements in block
if values_per_block == zeros_per_block:
return torch.zeros_like(data, dtype=torch.int8)
# creating additional height and width to support padding
dh = (block_height - height % block_height) % block_height
dw = (block_width - width % block_width) % block_width
# create a new padded tensor like data (to match the block_shape)
padded_data = torch.ones(
height + dh, width + dw, dtype=data.dtype, device=data.device
)
padded_data = (
padded_data * torch.nan
) # can also be replaced with 0 to stop the removal of edge data
padded_data[0:height, 0:width] = data
unfolded_data = F.unfold(
padded_data[None, None, :],
kernel_size=sparse_block_shape,
stride=sparse_block_shape,
)
_, sorted_idx = torch.sort(unfolded_data, dim=1)
sorted_idx = sorted_idx[
:, :zeros_per_block, :
] # zero out zeros_per_block number of elements
mask = self.__get_scatter_folded_mask(
data=unfolded_data,
dim=1,
indices=sorted_idx,
output_size=padded_data.shape,
sparse_block_shape=sparse_block_shape,
)
mask = (
mask.squeeze(0).squeeze(0)[:height, :width].contiguous()
) # remove padding and make contiguous
return mask
def __get_data_level_mask(self, data, sparsity_level, sparse_block_shape):
height, width = data.shape[-2], data.shape[-1]
block_height, block_width = sparse_block_shape
dh = (block_height - height % block_height) % block_height
dw = (block_width - width % block_width) % block_width
data_norm = F.avg_pool2d(
data[None, None, :],
kernel_size=sparse_block_shape,
stride=sparse_block_shape,
ceil_mode=True,
)
values_per_block = reduce(operator.mul, sparse_block_shape)
data_norm = data_norm.flatten()
num_blocks = len(data_norm)
data_norm = data_norm.repeat(
1, values_per_block, 1
) # get similar shape after unfold
_, sorted_idx = torch.sort(data_norm, dim=2)
threshold_idx = round(sparsity_level * num_blocks) # number of blocks to remove
sorted_idx = sorted_idx[:, :, :threshold_idx]
mask = self.__get_scatter_folded_mask(
data=data_norm,
dim=2,
indices=sorted_idx,
output_size=(height + dh, width + dw),
sparse_block_shape=sparse_block_shape,
)
mask = mask.squeeze(0).squeeze(0)[
:height, :width
] # squeeze only the first 2 dimension
return mask
def update_mask( # type: ignore[override]
self, name, data, sparsity_level, sparse_block_shape, zeros_per_block, **kwargs
):
values_per_block = reduce(operator.mul, sparse_block_shape)
if zeros_per_block > values_per_block:
raise ValueError(
"Number of zeros per block cannot be more than "
"the total number of elements in that block."
)
if zeros_per_block < 0:
raise ValueError("Number of zeros per block should be positive.")
if self.norm == "L1":
data_norm = torch.abs(data).squeeze() # absolute value based (L1)
else:
data_norm = (data * data).squeeze() # square every element for L2
if len(data_norm.shape) > 2: # only supports 2 dimensional data at the moment
raise ValueError("only supports 2-D at the moment")
elif len(data_norm.shape) == 1: # in case the data is bias (or 1D)
data_norm = data_norm[None, :]
mask = self.get_mask(name)
if sparsity_level <= 0 or zeros_per_block == 0:
mask.data = torch.ones_like(mask)
elif sparsity_level >= 1.0 and (zeros_per_block == values_per_block):
mask.data = torch.zeros_like(mask)
# Fetch the high level mask that zeros out entire blocks
data_lvl_mask = self.__get_data_level_mask(
data=data_norm,
sparsity_level=sparsity_level,
sparse_block_shape=sparse_block_shape,
)
# Fetch block level mask that zeros out 'zeros_per_block' number of elements in every block
block_lvl_mask = self.__get_block_level_mask(
data=data_norm,
sparse_block_shape=sparse_block_shape,
zeros_per_block=zeros_per_block,
)
# zero out the entries inside those blocks whose block is sparsified
mask.data = torch.where(data_lvl_mask == 1, data_lvl_mask, block_lvl_mask)
@@ -0,0 +1,44 @@
# mypy: allow-untyped-defs
import logging
from torch.ao.pruning._experimental.data_sparsifier.base_data_sparsifier import (
SUPPORTED_TYPES,
)
logger: logging.Logger = logging.getLogger(__name__)
def _attach_model_to_data_sparsifier(module, data_sparsifier, config=None):
"""Attaches a data sparsifier to all the layers of the module.
Essentially, loop over all the weight parameters in the module and
attach it to the data sparsifier.
Note::
The '.' in the layer names are replaced with '_' (refer to _get_valid_name() below)
before attaching to the sparsifier. This is because, the data
sparsifier uses a dummy model inside to store the weight parameters.
"""
if config is None:
config = {}
for name, parameter in module.named_parameters():
if type(parameter) in SUPPORTED_TYPES:
valid_name = _get_valid_name(name)
# will be defaulted to default configs
data_sparsifier.add_data(
name=valid_name, data=parameter, **config.get(valid_name, {})
)
def _get_valid_name(name):
return name.replace(".", "_") # . is not allowed as a name
def _log_sparsified_level(model, data_sparsifier) -> None:
# Show the level of sparsity AFTER step:
for name, parameter in model.named_parameters():
if type(parameter) not in SUPPORTED_TYPES:
continue
valid_name = _get_valid_name(name)
mask = data_sparsifier.get_mask(name=valid_name)
sparsity_level = 1.0 - mask.float().mean()
logger.info("Sparsity in layer %s = % .2%", name, sparsity_level)
@@ -0,0 +1,181 @@
# mypy: allow-untyped-defs
from collections import defaultdict
from copy import deepcopy
from typing import Any, TYPE_CHECKING
import pytorch_lightning as pl # type: ignore[import]
from ._data_sparstity_utils import (
_attach_model_to_data_sparsifier,
_get_valid_name,
_log_sparsified_level,
)
if TYPE_CHECKING:
import torch
class PostTrainingDataSparsity(pl.callbacks.Callback):
"""Lightning callback that enables post-training sparsity.
This callback aims to sparsify the model inside lightning module after training.
**Note that the model is copied and then sparsified, so the existing model is not modified**
The sparsified model can be used for comparison and can be accessed using
<callback_obj>.sparsified
Args:
data_sparsifier_class (some implemented class of BaseDataSparsifier)
The data sparsifier object of this class is created when the
training starts.
Note: Objects should not be passed in here as they are created
once the training completes.
data_sparsifier_args (Dict)
Dictionary of args to be passed to the data sparsifier.
Note: data_list arg should be ignored
Hooks implemented:
on_fit_end()
1. copies the model and attaches it to the sparsifier
2. sparsier step() is called
3. squashes the mask()
"""
def __init__(self, data_sparsifier_class, data_sparsifier_args):
super().__init__()
self.data_sparsifier_class = data_sparsifier_class
self.data_sparsifier_args = data_sparsifier_args
self.data_sparsifier: Any = None
self.sparsified: torch.nn.Module | None = None
def on_fit_end(self, trainer, pl_module) -> None:
self.sparsified = deepcopy(pl_module.model).eval()
self.data_sparsifier = self.data_sparsifier_class(**self.data_sparsifier_args)
_attach_model_to_data_sparsifier(self.sparsified, self.data_sparsifier)
self.data_sparsifier.step()
self.data_sparsifier.squash_mask() # currently squashes params for all mask
_log_sparsified_level(self.sparsified, self.data_sparsifier)
class TrainingAwareDataSparsity(pl.callbacks.Callback):
"""Lightning callback that enables in-training sparsity.
This callback aims to sparsify the model inside lightning module during training.
**Note that the model is copied and then sparsified, so the existing model is not modified**
The sparsified model can be used for comparison and can be accessed using
<callback_obj>.sparsified
Args:
data_sparsifier_class (some implemented class of BaseDataSparsifier)
The data sparsifier object of this class is created when the
training starts.
Note: Objects should not be passed in here as they are created
when the training starts.
data_sparsifier_args (Dict)
Dictionary of args to be passed to the data sparsifier.
Note: data_list arg should be ignored
data_scheduler_class (some implemented class of BaseDataScheduler)
The data scheduler of this class is created when the training starts
Note: Objects should not be passed in here as they are created
when the training starts.
data_scheduler_args(Dict)
Dictionary of args to be passed to the data scheduler.
**Note: data_sparsifier arg should be ignored as the recipe
creates and pass sparsifier object into the class**
Hooks implemented:
on_train_start()
Data sparsifier and scheduler objects are created.
Pytorch model attached to the sparsifier
on_train_epoch_start()
Loads the state_dict of the data sparsifier
on_train_epoch_end()
1. Copies the model and attaches it to the sparsifier
2. sparsifier step() and scheduler step()
3. Dump state_dict of the current sparsifier
on_train_end()
squash mask
"""
def __init__(
self,
data_sparsifier_class,
data_sparsifier_args,
data_scheduler_class,
data_scheduler_args,
):
super().__init__()
# data sparsifier objects
self.data_sparsifier_class = data_sparsifier_class
self.data_sparsifier_args = data_sparsifier_args
# scheduler objects
self.data_scheduler_class = data_scheduler_class
self.data_scheduler_args = data_scheduler_args
# fields
self.data_sparsifier: Any = None
self.data_scheduler: Any = None
self.sparsified: torch.nn.Module | None = None
self.data_sparsifier_state_dict: Any = None
def on_train_start(self, trainer, pl_module) -> None:
# create sparsifier
self.data_sparsifier = self.data_sparsifier_class(**self.data_sparsifier_args)
self.sparsified = deepcopy(pl_module.model)
_attach_model_to_data_sparsifier(
self.sparsified, self.data_sparsifier
) # just to populate the base_sl in the scheduler
# create scheduler
args = deepcopy(self.data_scheduler_args)
args["data_sparsifier"] = self.data_sparsifier
self.data_scheduler = self.data_scheduler_class(**args)
def on_train_epoch_start(self, trainer, pl_module):
if self.data_sparsifier_state_dict is None:
return # probably first epoch
# load the existing config for each data
self.data_sparsifier.load_state_dict(self.data_sparsifier_state_dict)
def __create_config_based_on_state(self, pl_module):
config: dict = defaultdict()
if self.data_sparsifier_state_dict is None:
return config
for name, _ in pl_module.model.named_parameters():
valid_name = _get_valid_name(name)
config[valid_name] = self.data_sparsifier.data_groups[valid_name]
return config
def on_train_epoch_end(self, trainer, pl_module):
self.sparsified = deepcopy(pl_module.model)
config = self.__create_config_based_on_state(pl_module)
# attach model to the data sparsifier
_attach_model_to_data_sparsifier(
self.sparsified, self.data_sparsifier, config=config
)
self.data_sparsifier.step()
self.data_scheduler.step()
self.data_sparsifier_state_dict = self.data_sparsifier.state_dict()
def on_train_end(self, trainer, pl_module):
self.data_sparsifier.squash_mask()
@@ -0,0 +1,156 @@
# mypy: allow-untyped-defs
import torch
import torch.nn as nn
from torch.ao.pruning.sparsifier.utils import fqn_to_module, module_to_fqn
SUPPORTED_MODULES = {nn.Embedding, nn.EmbeddingBag}
def _fetch_all_embeddings(model):
"""Fetches Embedding and EmbeddingBag modules from the model"""
embedding_modules = []
stack = [model]
while stack:
module = stack.pop()
for _, child in module.named_children():
fqn_name = module_to_fqn(model, child)
if type(child) in SUPPORTED_MODULES:
embedding_modules.append((fqn_name, child))
else:
stack.append(child)
return embedding_modules
def post_training_sparse_quantize(
model,
data_sparsifier_class,
sparsify_first=True,
select_embeddings: list[nn.Module] | None = None,
**sparse_config,
):
"""Takes in a model and applies sparsification and quantization to only embeddings & embeddingbags.
The quantization step can happen before or after sparsification depending on the `sparsify_first` argument.
Args:
- model (nn.Module)
model whose embeddings needs to be sparsified
- data_sparsifier_class (type of data sparsifier)
Type of sparsification that needs to be applied to model
- sparsify_first (bool)
if true, sparsifies first and then quantizes
otherwise, quantizes first and then sparsifies.
- select_embeddings (List of Embedding modules)
List of embedding modules to in the model to be sparsified & quantized.
If None, all embedding modules with be sparsified
- sparse_config (Dict)
config that will be passed to the constructor of data sparsifier object.
Note:
1. When `sparsify_first=False`, quantization occurs first followed by sparsification.
- before sparsifying, the embedding layers are dequantized.
- scales and zero-points are saved
- embedding layers are sparsified and `squash_mask` is applied
- embedding weights are requantized using the saved scales and zero-points
2. When `sparsify_first=True`, sparsification occurs first followed by quantization.
- embeddings are sparsified first
- quantization is applied on the sparsified embeddings
"""
data_sparsifier = data_sparsifier_class(**sparse_config)
# if select_embeddings is None, perform it on all embeddings
if select_embeddings is None:
embedding_modules = _fetch_all_embeddings(model)
else:
embedding_modules = []
if not isinstance(select_embeddings, list):
raise AssertionError(
"the embedding_modules must be a list of embedding modules"
)
for emb in select_embeddings:
if type(emb) not in SUPPORTED_MODULES:
raise AssertionError(
"the embedding_modules list must be an embedding or embedding bags"
)
fqn_name = module_to_fqn(model, emb)
if fqn_name is None:
raise AssertionError(
"the embedding modules must be part of input model"
)
embedding_modules.append((fqn_name, emb))
if sparsify_first:
# sparsify
for name, emb_module in embedding_modules:
valid_name = name.replace(".", "_")
data_sparsifier.add_data(name=valid_name, data=emb_module)
data_sparsifier.step()
data_sparsifier.squash_mask()
# quantize
for _, emb_module in embedding_modules:
# pyrefly: ignore [bad-argument-type]
emb_module.qconfig = torch.ao.quantization.float_qparams_weight_only_qconfig
torch.ao.quantization.prepare(model, inplace=True)
torch.ao.quantization.convert(model, inplace=True)
else:
# quantize
for _, emb_module in embedding_modules:
# pyrefly: ignore [bad-argument-type]
emb_module.qconfig = torch.ao.quantization.float_qparams_weight_only_qconfig
torch.ao.quantization.prepare(model, inplace=True)
torch.ao.quantization.convert(model, inplace=True)
# retrieve scale & zero_points
quantize_params: dict[str, dict] = {
"scales": {},
"zero_points": {},
"dequant_weights": {},
"axis": {},
"dtype": {},
}
for name, _ in embedding_modules:
quantized_emb = fqn_to_module(model, name)
if quantized_emb is None:
raise AssertionError(f"quantized embedding {name} not found in model")
quantized_weight = quantized_emb.weight() # type: ignore[operator]
quantize_params["scales"][name] = quantized_weight.q_per_channel_scales()
quantize_params["zero_points"][name] = (
quantized_weight.q_per_channel_zero_points()
)
quantize_params["dequant_weights"][name] = torch.dequantize(
quantized_weight
)
quantize_params["axis"][name] = quantized_weight.q_per_channel_axis()
quantize_params["dtype"][name] = quantized_weight.dtype
# attach data to sparsifier
data_sparsifier.add_data(
name=name.replace(".", "_"),
data=quantize_params["dequant_weights"][name],
)
data_sparsifier.step()
data_sparsifier.squash_mask()
for name, _ in embedding_modules:
quantized_emb = fqn_to_module(model, name)
if quantized_emb is None:
raise AssertionError(f"quantized embedding {name} not found in model")
requantized_vector = torch.quantize_per_channel(
quantize_params["dequant_weights"][name],
scales=quantize_params["scales"][name],
zero_points=quantize_params["zero_points"][name],
dtype=quantize_params["dtype"][name],
axis=quantize_params["axis"][name],
)
quantized_emb.set_weight(requantized_vector) # type: ignore[operator]
@@ -0,0 +1,95 @@
# mypy: allow-untyped-defs
from collections.abc import Callable
import torch
from .base_structured_sparsifier import BaseStructuredSparsifier
__all__ = ["FPGMPruner"]
class FPGMPruner(BaseStructuredSparsifier):
r"""Filter Pruning via Geometric Median (FPGM) Structured Pruner
This sparsifier prune filter (row) in a tensor according to distances among filters according to
`Filter Pruning via Geometric Median for Deep Convolutional Neural Networks Acceleration <https://arxiv.org/abs/1811.00250>`_.
This sparsifier is controlled by three variables:
1. `sparsity_level` defines the number of filters (rows) that are zeroed-out.
2. `dist` defines the distance measurement type. Default: 3 (L2 distance).
Available options are: [1, 2, (custom callable distance function)].
Note::
Inputs should be a 4D convolutional tensor of shape (N, C, H, W).
- N: output channels size
- C: input channels size
- H: height of kernel
- W: width of kernel
"""
def __init__(self, sparsity_level: float = 0.5, dist: Callable | int | None = None):
defaults = {
"sparsity_level": sparsity_level,
}
if dist is None:
dist = 2
if callable(dist):
self.dist_fn = dist
elif dist == 1:
self.dist_fn = lambda x: torch.cdist(x, x, p=1)
elif dist == 2:
self.dist_fn = lambda x: torch.cdist(x, x, p=2)
else:
raise NotImplementedError("Distance function is not yet implemented.")
super().__init__(defaults=defaults)
def _compute_distance(self, t):
r"""Compute distance across all entries in tensor `t` along all dimension
except for the one identified by dim.
Args:
t (torch.Tensor): tensor representing the parameter to prune
Returns:
distance (torch.Tensor): distance computed across filtters
"""
dim = 0 # prune filter (row)
size = t.size(dim)
slc = [slice(None)] * t.dim()
# flatten the tensor along the dimension
t_flatten = [
t[tuple(slc[:dim] + [slice(i, i + 1)] + slc[dim + 1 :])].reshape(-1)
for i in range(size)
]
t_flatten = torch.stack(t_flatten)
# distance measurement
dist_matrix = self.dist_fn(t_flatten)
# more similar with other filter indicates large in the sum of row
distance = torch.sum(torch.abs(dist_matrix), 1)
return distance
def update_mask( # type: ignore[override]
self, module, tensor_name, sparsity_level, **kwargs
):
tensor_weight = getattr(module, tensor_name)
mask = getattr(module.parametrizations, tensor_name)[0].mask
if sparsity_level <= 0:
mask.data = torch.ones_like(mask).bool()
elif sparsity_level >= 1.0:
mask.data = torch.zeros_like(mask).bool()
else:
distance = self._compute_distance(tensor_weight)
tensor_size = tensor_weight.shape[0] # prune filter (row)
nparams_toprune = round(sparsity_level * tensor_size)
nparams_toprune = min(
max(nparams_toprune, 0), tensor_size
) # clamp to [0, tensor_size]
topk = torch.topk(distance, k=nparams_toprune, largest=False)
mask[topk.indices] = False
@@ -0,0 +1,5 @@
from .base_structured_sparsifier import BaseStructuredSparsifier
from .FPGM_pruner import FPGMPruner
from .lstm_saliency_pruner import LSTMSaliencyPruner
from .parametrization import BiasHook, FakeStructuredSparsity
from .saliency_pruner import SaliencyPruner
@@ -0,0 +1,314 @@
# mypy: allow-untyped-defs
from collections.abc import Callable
from itertools import chain
from operator import getitem
import torch
import torch.nn.functional as F
from torch import nn
from torch.ao.pruning.sparsifier.base_sparsifier import BaseSparsifier
from torch.fx import symbolic_trace
from torch.nn.utils import parametrize
from .match_utils import apply_match, MatchAllNode
from .parametrization import BiasHook, FakeStructuredSparsity, module_contains_param
from .prune_functions import (
prune_conv2d,
prune_conv2d_activation_conv2d,
prune_conv2d_activation_pool_conv2d,
prune_conv2d_conv2d,
prune_conv2d_pool_activation_conv2d,
prune_conv2d_pool_flatten_linear,
prune_linear,
prune_linear_activation_linear,
prune_linear_linear,
prune_lstm_output_layernorm_linear,
prune_lstm_output_linear,
)
def _get_supported_structured_pruning_modules():
SUPPORTED_STRUCTURED_PRUNING_MODULES = { # added to config if None given
nn.Linear,
nn.Conv2d,
nn.LSTM,
}
return SUPPORTED_STRUCTURED_PRUNING_MODULES
def _get_supported_activation_functions():
SUPPORTED_ACTIVATION_FUNCTIONS = {
F.relu,
F.rrelu,
F.hardtanh,
F.relu6,
F.sigmoid,
F.hardsigmoid,
F.tanh,
F.silu,
F.mish,
F.hardswish,
F.elu,
F.celu,
F.selu,
F.hardshrink,
F.leaky_relu,
F.logsigmoid,
F.softplus,
F.prelu,
F.softsign,
F.tanhshrink,
F.gelu,
}
return SUPPORTED_ACTIVATION_FUNCTIONS
def _get_supported_activation_modules():
SUPPORTED_ACTIVATION_MODULES = {
nn.ReLU,
nn.RReLU,
nn.Hardtanh,
nn.ReLU6,
nn.Sigmoid,
nn.Hardsigmoid,
nn.Tanh,
nn.SiLU,
nn.Mish,
nn.Hardswish,
nn.ELU,
nn.CELU,
nn.SELU,
nn.Hardshrink,
nn.LeakyReLU,
nn.LogSigmoid,
nn.Softplus,
nn.PReLU,
nn.Softsign,
nn.Tanhshrink,
nn.GELU,
}
return SUPPORTED_ACTIVATION_MODULES
def _get_default_structured_pruning_patterns() -> dict[
tuple[type[nn.Module] | Callable | MatchAllNode | str, ...],
Callable[..., None],
]:
"""
Returns the patterns for conv2d / linear conversion for each element in the activation functions/modules defined above.
"""
patterns: dict[
tuple[type[nn.Module] | Callable | MatchAllNode | str, ...],
Callable[..., None],
] = {
# linear -> linear
(nn.Linear, "output"): prune_linear,
(nn.Linear, nn.Linear): prune_linear_linear,
# conv2d -> conv2d
(nn.Conv2d, "output"): prune_conv2d,
(nn.Conv2d, nn.Conv2d): prune_conv2d_conv2d,
# TODO LSTM Structured pruning does not support returned state currently.
# Should find a way to explicitly match getitem(0) instead of getitem.
# This will also require changing the pruning function.
# lstm -> getitem(0) -> linear
(nn.LSTM, getitem, nn.Linear): prune_lstm_output_linear,
# lstm -> getitem(0) -> layernorm -> linear
(nn.LSTM, getitem, nn.LayerNorm, nn.Linear): prune_lstm_output_layernorm_linear,
}
for activation in chain(
_get_supported_activation_functions(), _get_supported_activation_modules()
):
patterns.update(
{
# linear -> activation -> linear
(nn.Linear, activation, nn.Linear): prune_linear_activation_linear,
# conv2d -> activation -> conv2d
(nn.Conv2d, activation, nn.Conv2d): prune_conv2d_activation_conv2d,
# conv2d -> activation -> pool -> conv2d
(
nn.Conv2d,
activation,
nn.AvgPool2d,
nn.Conv2d,
): prune_conv2d_activation_pool_conv2d,
(
nn.Conv2d,
activation,
F.avg_pool2d,
nn.Conv2d,
): prune_conv2d_activation_pool_conv2d,
(
nn.Conv2d,
activation,
nn.MaxPool2d,
nn.Conv2d,
): prune_conv2d_activation_pool_conv2d,
(
nn.Conv2d,
activation,
F.max_pool2d,
nn.Conv2d,
): prune_conv2d_activation_pool_conv2d,
# conv2d -> pool -> activation -> conv2d
(
nn.Conv2d,
nn.AvgPool2d,
activation,
nn.Conv2d,
): prune_conv2d_pool_activation_conv2d,
(
nn.Conv2d,
F.avg_pool2d,
activation,
nn.Conv2d,
): prune_conv2d_pool_activation_conv2d,
(
nn.Conv2d,
nn.MaxPool2d,
activation,
nn.Conv2d,
): prune_conv2d_pool_activation_conv2d,
(
nn.Conv2d,
F.max_pool2d,
activation,
nn.Conv2d,
): prune_conv2d_pool_activation_conv2d,
# conv2d -> adaptive pool -> flatten -> linear
(
nn.Conv2d,
nn.AdaptiveAvgPool2d,
nn.Flatten,
nn.Linear,
): prune_conv2d_pool_flatten_linear,
(
nn.Conv2d,
nn.AdaptiveAvgPool2d,
torch.flatten,
nn.Linear,
): prune_conv2d_pool_flatten_linear,
(
nn.Conv2d,
nn.AdaptiveMaxPool2d,
nn.Flatten,
nn.Linear,
): prune_conv2d_pool_flatten_linear,
(
nn.Conv2d,
nn.AdaptiveMaxPool2d,
torch.flatten,
nn.Linear,
): prune_conv2d_pool_flatten_linear,
}
)
return patterns
class BaseStructuredSparsifier(BaseSparsifier):
r"""Base class for structured pruning.
Abstract methods that need to be implemented:
- update_mask: Function to compute a new mask for all keys in the
`groups` attribute.
Args:
- defaults [dict]: default configurations will be attached to the
configuration. Only the keys that don't exist in the `config` will
be updated.
"""
def __init__(self, defaults, patterns=None):
super().__init__(defaults)
if patterns is None:
patterns = _get_default_structured_pruning_patterns()
self.patterns = patterns
def make_config_from_model(
self,
model: nn.Module,
SUPPORTED_MODULES: set[type] | None = None,
) -> None:
if SUPPORTED_MODULES is None:
SUPPORTED_MODULES = _get_supported_structured_pruning_modules()
super().make_config_from_model(model, SUPPORTED_MODULES=SUPPORTED_MODULES)
def _prepare(self, *args, **kwargs) -> None:
r"""This function will attach the FakeStructuredSparsity parameterizations
and BiasHooks at the appropriate points in the model.
"""
for config in self.groups:
module = config["module"]
tensor_name = config["tensor_name"]
parametrization = config.get("parametrization", FakeStructuredSparsity)
tensor = getattr(module, tensor_name)
mask = config.get(
"mask",
torch.ones(tensor.shape[0], dtype=torch.bool, device=tensor.device),
)
self.state[config["tensor_fqn"]]["mask"] = mask
parametrize.register_parametrization(
module, tensor_name, parametrization(mask)
)
# if linear / conv, we add in bias hooks
if isinstance(module, (nn.Linear, nn.Conv2d)):
prune_bias = config.get("prune_bias", True)
if module.bias is not None:
module.register_parameter(
"_bias", nn.Parameter(module.bias.detach())
)
# pyrefly: ignore [bad-assignment]
module.bias = None
module.prune_bias = prune_bias
module.register_forward_hook(
BiasHook(module.parametrizations.weight[0], prune_bias) # type: ignore[union-attr, index]
)
def prune(self) -> None:
r"""
This function will FX symbolically trace the model and then find instances of the patterns
defined in self.patterns (by default SUPPORTED_STRUCTURED_PRUNING_PATTERNS ).
For each pattern, it will apply to corresponding conversion function, which will modify the output
and input size expected by the modules within the pattern
"""
self.traced = symbolic_trace(self.model)
modules = dict(self.traced.named_modules())
# Right now we check for matches simply by iterating across all the patterns
# if this is slow we can store patterns in a trie-structure and modify this code for faster lookup
for node in self.traced.graph.nodes:
for pattern, convert_fn in self.patterns.items():
matched = apply_match(modules, pattern, node, [])
if matched is None:
continue
# pyrefly: ignore [no-matching-overload]
first_module = modules.get(node.target)
# check if first module exists and has appropriate parameterization, otherwise skip
if (
first_module is not None
and parametrize.is_parametrized(first_module)
and module_contains_param(first_module, FakeStructuredSparsity)
):
convert_block = []
for node in matched:
if node.op == "call_module":
convert_block.append(modules.get(node.target))
elif node.op == "call_function":
convert_block.append(node.target)
convert_fn(*convert_block)
for module in self.traced.modules():
if module_contains_param(module, FakeStructuredSparsity):
raise Exception( # noqa: TRY002
f"Error: {module} still contains FakeStructuredSparsity parametrizations!"
)
self.traced.graph.lint()
self.traced.recompile()
return self.traced # type: ignore[return-value]
@@ -0,0 +1,54 @@
from typing import Any, cast
import torch
from torch import nn
from .base_structured_sparsifier import BaseStructuredSparsifier
from .parametrization import FakeStructuredSparsity
class LSTMSaliencyPruner(BaseStructuredSparsifier):
"""
Prune packed LSTM weights based on saliency.
For each layer {k} inside a LSTM, we have two packed weight matrices
- weight_ih_l{k}
- weight_hh_l{k}
These tensors pack the weights for the 4 linear layers together for efficiency.
[W_ii | W_if | W_ig | W_io]
Pruning this tensor directly will lead to weights being misassigned when unpacked.
To ensure that each packed linear layer is pruned the same amount:
1. We split the packed weight into the 4 constituent linear parts
2. Update the mask for each individual piece using saliency individually
This applies to both weight_ih_l{k} and weight_hh_l{k}.
"""
def update_mask(self, module: nn.Module, tensor_name: str, **kwargs: Any) -> None:
weights = getattr(module, tensor_name)
for p in getattr(module.parametrizations, tensor_name):
if isinstance(p, FakeStructuredSparsity):
mask = cast(torch.Tensor, p.mask)
# select weights based on magnitude
if weights.dim() <= 1:
raise Exception( # noqa: TRY002
"Structured pruning can only be applied to a 2+dim weight tensor!"
)
# take norm over all but first dim
dims = tuple(range(1, weights.dim()))
saliency = weights.norm(dim=dims, p=1)
# handle weights in 4 groups
split_size = len(mask) // 4
masks = torch.split(mask, split_size)
saliencies = torch.split(saliency, split_size)
for keep_mask, sal in zip(masks, saliencies):
# mask smallest k values to be removed
k = int(len(keep_mask) * kwargs["sparsity_level"])
prune = sal.topk(k, largest=False, sorted=False).indices
keep_mask.data[prune] = False # modifies underlying p.mask directly
@@ -0,0 +1,65 @@
"""
Contains utility functions to check if a pattern is in the graph and return the matching nodes
"""
from typing import Any
import torch
from torch import nn
from torch.ao.quantization.utils import MatchAllNode
from torch.fx import Node
from torch.nn.utils import parametrize
def _match(
modules: dict[str, nn.ModuleDict],
node: Node,
current: nn.Module | Any,
) -> bool:
r"""
checks to see if a single node of a pattern matches
"""
if isinstance(current, type) and issubclass(current, MatchAllNode):
return True
if not isinstance(node, Node):
return False
if isinstance(current, type) and issubclass(current, torch.nn.Module):
return (
node.op == "call_module"
and parametrize.type_before_parametrizations(modules[node.target]) # type: ignore[index]
== current
)
elif callable(current):
return node.op == "call_function" and node.target is current
elif isinstance(current, str):
return node.target == current
return False
def apply_match(
modules: dict[str, nn.ModuleDict],
pattern: tuple[Any] | Any,
node: Node,
matched_node_pattern: list[Node],
) -> list[Node] | None:
r"""
This function will return the matched nodes if the pattern matches the node given
If there is no match, it will return None
"""
if isinstance(pattern, tuple):
if len(pattern) == 1:
if _match(modules, node, pattern[0]):
return matched_node_pattern + [node]
first, *rest = pattern
if _match(modules, node, first):
if rest is None:
return matched_node_pattern + [node]
for user in node.users:
return apply_match(
modules, tuple(rest), user, matched_node_pattern + [node]
)
elif _match(modules, node, pattern):
return [node]
return None
@@ -0,0 +1,63 @@
# mypy: allow-untyped-defs
import torch
from torch import nn
from torch.nn.utils.parametrize import is_parametrized
def module_contains_param(module, parametrization):
if is_parametrized(module):
# see if any of the module tensors have a parametriztion attached that matches the one passed in
return any(
any(isinstance(param, parametrization) for param in param_list)
for key, param_list in module.parametrizations.items()
)
return False
# Structured Pruning Parameterizations
class FakeStructuredSparsity(nn.Module):
r"""
Parametrization for Structured Pruning. Like FakeSparsity, this should be attached to
the 'weight' or any other parameter that requires a mask.
Instead of an element-wise bool mask, this parameterization uses a row-wise bool mask.
"""
def __init__(self, mask):
super().__init__()
self.register_buffer("mask", mask)
def forward(self, x):
if not isinstance(self.mask, torch.Tensor):
raise AssertionError("mask must be a torch.Tensor")
if self.mask.shape[0] != x.shape[0]:
raise AssertionError(
f"mask shape[0] ({self.mask.shape[0]}) must match x shape[0] ({x.shape[0]})"
)
shape = [1] * len(x.shape)
shape[0] = -1
return self.mask.reshape(shape) * x
def state_dict(self, *args, **kwargs):
# avoid double saving masks
return {}
class BiasHook:
def __init__(self, parametrization, prune_bias):
self.param = parametrization
self.prune_bias = prune_bias
def __call__(self, module, input, output):
if getattr(module, "_bias", None) is not None:
bias = module._bias.data
if self.prune_bias:
bias[~self.param.mask] = 0
# reshape bias to broadcast over output dimensions
idx = [1] * len(output.shape)
idx[1] = -1
bias = bias.reshape(idx)
output += bias
return output
@@ -0,0 +1,485 @@
# mypy: allow-untyped-defs
"""
Collection of conversion functions for linear / conv2d structured pruning
Also contains utilities for bias propagation
"""
from collections.abc import Callable
from typing import cast
import torch
from torch import nn, Tensor
from torch.nn.utils import parametrize
from torch.nn.utils.parametrize import ParametrizationList
from .parametrization import BiasHook, FakeStructuredSparsity
# BIAS PROPAGATION
def _remove_bias_handles(module: nn.Module) -> None:
if hasattr(module, "_forward_hooks"):
bias_hooks: list[int] = []
for key, hook in module._forward_hooks.items():
if isinstance(hook, BiasHook):
bias_hooks.append(key)
for key in bias_hooks:
del module._forward_hooks[key]
def _get_adjusted_next_layer_bias(
next_layer: nn.Module, pruned_biases: Tensor, mask: Tensor
) -> nn.Parameter:
r"""Returns new adjusted bias for the second supported module"""
if parametrize.is_parametrized(next_layer):
# need to access original weight
parametrization_dict = cast(nn.ModuleDict, next_layer.parametrizations)
weight_parameterizations = cast(
ParametrizationList, parametrization_dict.weight
)
next_weight = weight_parameterizations.original
else:
next_weight = cast(Tensor, next_layer.weight)
scaling_weight = next_weight[:, ~mask]
if isinstance(next_layer, nn.Conv2d): # checking for Conv2d
# Propagating first layer pruned biases and calculating the new second layer bias
# involves more steps since the Conv2d scaling weight has extra dimensions,
# so adding bias involves broadcasting, logically:
# for each channel k in range(oC):
# scaled_biases = sum(first_bias[pruned_idx] @ next_weight[k, pruned_idx, :, :].T)
# new_next_bias[k] = old_next_bias[k] + scaled_biases
scaling_product = torch.matmul(
pruned_biases.reshape(1, -1), torch.transpose(scaling_weight, 1, 2)
)
sum_range = list(range(len(scaling_product.shape)))[
1:
] # all but the first dimension
scaled_biases = torch.sum(scaling_product, sum_range)
elif isinstance(next_layer, nn.Linear): # Linear
scaled_biases = torch.matmul(
pruned_biases, torch.transpose(scaling_weight, 0, 1)
) # recall b2_new = b1 @ w2.T + b2
else:
raise NotImplementedError(f"Type {type(next_layer)} not supported yet.")
if (
parametrize.is_parametrized(next_layer)
and getattr(next_layer, "_bias", None) is not None
): # next_layer is parametrized & has original bias ._bias
adjusted_bias = nn.Parameter(scaled_biases + next_layer._bias) # type: ignore[operator]
elif (
not parametrize.is_parametrized(next_layer) and next_layer.bias is not None
): # next_layer not parametrized & has .bias
adjusted_bias = nn.Parameter(scaled_biases + next_layer.bias) # type: ignore[operator]
else: # next_layer has no bias
adjusted_bias = nn.Parameter(scaled_biases)
return adjusted_bias
def _prune_module_bias(module: nn.Module, mask: Tensor) -> None:
r"""Applies mask to given modules bias"""
# prune bias along with weights, discard pruned indices of bias
original_bias = cast(Tensor, getattr(module, "_bias", module.bias))
if original_bias is not None:
module.bias = nn.Parameter(original_bias[mask])
# remove _bias parameter
if hasattr(module, "_bias"):
delattr(module, "_bias")
def _propagate_module_bias(module: nn.Module, mask: Tensor) -> Tensor | None:
r"""
In the case that we need to propagate biases, this function will return the biases we need
"""
# set current module bias
if module.bias is not None:
module.bias = nn.Parameter(cast(Tensor, module.bias)[mask])
elif getattr(module, "_bias", None) is not None:
# pyrefly: ignore [bad-assignment]
module.bias = nn.Parameter(cast(Tensor, module._bias)[mask])
# get pruned biases to propagate to subsequent layer
if getattr(module, "_bias", None) is not None:
pruned_biases = cast(Tensor, module._bias)[~mask]
else:
pruned_biases = None
if hasattr(module, "_bias"):
delattr(module, "_bias")
return pruned_biases
# LINEAR
def _prune_linear_helper(linear: nn.Linear) -> Tensor:
# expects linear to be a parameterized linear module
parametrization_dict = cast(nn.ModuleDict, linear.parametrizations)
weight_parameterizations = cast(ParametrizationList, parametrization_dict.weight)
for p in weight_parameterizations:
if isinstance(p, FakeStructuredSparsity):
mask = cast(Tensor, p.mask)
with torch.no_grad():
parametrize.remove_parametrizations(linear, "weight", leave_parametrized=True)
linear.weight = nn.Parameter(linear.weight[mask]) # type: ignore[possibly-undefined]
linear.out_features = linear.weight.shape[0]
_remove_bias_handles(linear)
# pyrefly: ignore [unbound-name]
return mask
def prune_linear(linear: nn.Linear) -> None:
mask = _prune_linear_helper(linear)
if getattr(linear, "prune_bias", False):
_prune_module_bias(linear, mask)
def prune_linear_linear(linear1: nn.Linear, linear2: nn.Linear) -> None:
prune_linear_activation_linear(linear1, None, linear2)
def prune_linear_activation_linear(
linear1: nn.Linear,
activation: Callable[[Tensor], Tensor] | None,
linear2: nn.Linear,
):
mask = _prune_linear_helper(linear1)
if getattr(linear1, "prune_bias", False):
_prune_module_bias(linear1, mask)
else:
pruned_biases = _propagate_module_bias(linear1, mask)
if pruned_biases is not None:
if activation:
pruned_biases = activation(pruned_biases)
linear2.bias = _get_adjusted_next_layer_bias(linear2, pruned_biases, mask)
with torch.no_grad():
if parametrize.is_parametrized(linear2):
parametrization_dict = cast(nn.ModuleDict, linear2.parametrizations)
weight_parameterizations = cast(
ParametrizationList, parametrization_dict.weight
)
weight_parameterizations.original = nn.Parameter(
weight_parameterizations.original[:, mask]
)
linear2.in_features = weight_parameterizations.original.shape[1]
else:
linear2.weight = nn.Parameter(linear2.weight[:, mask])
linear2.in_features = linear2.weight.shape[1]
# CONV2D
def _prune_conv2d_helper(conv2d: nn.Conv2d) -> Tensor:
parametrization_dict = cast(nn.ModuleDict, conv2d.parametrizations)
weight_parameterizations = cast(ParametrizationList, parametrization_dict.weight)
for p in weight_parameterizations:
if isinstance(p, FakeStructuredSparsity):
mask = cast(Tensor, p.mask)
with torch.no_grad():
parametrize.remove_parametrizations(conv2d, "weight", leave_parametrized=True)
conv2d.weight = nn.Parameter(conv2d.weight[mask]) # type: ignore[possibly-undefined]
conv2d.out_channels = conv2d.weight.shape[0]
_remove_bias_handles(conv2d)
# pyrefly: ignore [unbound-name]
return mask
def prune_conv2d_padded(conv2d_1: nn.Conv2d) -> None:
parametrization_dict = cast(nn.ModuleDict, conv2d_1.parametrizations)
weight_parameterizations = cast(ParametrizationList, parametrization_dict.weight)
for p in weight_parameterizations:
if isinstance(p, FakeStructuredSparsity):
mask = cast(Tensor, p.mask)
with torch.no_grad():
parametrize.remove_parametrizations(conv2d_1, "weight", leave_parametrized=True)
if getattr(conv2d_1, "_bias", None) is not None:
if (
conv2d_1.bias is not None
): # conv2d_1 has original bias and bias propagated from previous layer
new_bias = torch.zeros(conv2d_1.bias.shape)
new_bias[mask] = conv2d_1.bias[mask] # type: ignore[possibly-undefined]
# adjusted bias that to keep in conv2d_1
# pyrefly: ignore [unbound-name]
new_bias[~mask] = cast(Tensor, conv2d_1._bias)[~mask]
# pruned biases that are kept instead of propagated
conv2d_1.bias = nn.Parameter(new_bias)
else: # conv2d_1 has only original bias
conv2d_1.bias = nn.Parameter(cast(Tensor, conv2d_1._bias))
else:
# no original bias, only propagated bias
if (
conv2d_1.bias is not None
): # conv2d_1 has bias propagated from previous layer
conv2d_1.bias.data[~mask] = 0 # type: ignore[possibly-undefined]
if hasattr(conv2d_1, "_bias"):
delattr(conv2d_1, "_bias")
def prune_conv2d(conv2d: nn.Conv2d) -> None:
mask = _prune_conv2d_helper(conv2d)
if getattr(conv2d, "prune_bias", False):
_prune_module_bias(conv2d, mask)
def prune_conv2d_conv2d(conv2d_1: nn.Conv2d, conv2d_2: nn.Conv2d) -> None:
prune_conv2d_activation_conv2d(conv2d_1, None, conv2d_2)
def prune_conv2d_activation_conv2d(
conv2d_1: nn.Conv2d,
activation: Callable[[Tensor], Tensor] | None,
conv2d_2: nn.Conv2d,
):
r"""
Fusion Pattern for conv2d -> some activation module / function -> conv2d layers
"""
parametrization_dict = cast(nn.ModuleDict, conv2d_1.parametrizations)
weight_parameterizations = cast(ParametrizationList, parametrization_dict.weight)
for p in weight_parameterizations:
if isinstance(p, FakeStructuredSparsity):
mask = cast(Tensor, p.mask)
prune_bias = getattr(conv2d_1, "prune_bias", False)
if (
hasattr(conv2d_2, "padding")
and cast(tuple[int], conv2d_2.padding) > (0, 0)
and (conv2d_1.bias is not None or getattr(conv2d_1, "_bias", None) is not None)
):
prune_conv2d_padded(conv2d_1)
else:
mask = _prune_conv2d_helper(conv2d_1)
if prune_bias:
_prune_module_bias(conv2d_1, mask)
else:
pruned_biases = _propagate_module_bias(conv2d_1, mask)
if pruned_biases is not None:
if activation:
pruned_biases = activation(pruned_biases)
conv2d_2.bias = _get_adjusted_next_layer_bias(
conv2d_2, pruned_biases, mask
)
if (
not (
hasattr(conv2d_2, "padding")
and cast(tuple[int], conv2d_2.padding) > (0, 0)
)
or conv2d_1.bias is None
):
with torch.no_grad():
if parametrize.is_parametrized(conv2d_2):
parametrization_dict = cast(
nn.ModuleDict, conv2d_2.parametrizations
)
weight_parameterizations = cast(
ParametrizationList, parametrization_dict.weight
)
weight_parameterizations.original = nn.Parameter(
weight_parameterizations.original[:, mask]
)
conv2d_2.in_channels = weight_parameterizations.original.shape[1]
else:
conv2d_2.weight = nn.Parameter(conv2d_2.weight[:, mask])
conv2d_2.in_channels = conv2d_2.weight.shape[1]
def prune_conv2d_pool_activation_conv2d(
c1: nn.Conv2d,
pool: nn.Module,
activation: Callable[[Tensor], Tensor] | None,
c2: nn.Conv2d,
) -> None:
prune_conv2d_activation_conv2d(c1, activation, c2)
def prune_conv2d_activation_pool_conv2d(
c1: nn.Conv2d,
activation: Callable[[Tensor], Tensor] | None,
pool: nn.Module,
c2: nn.Conv2d,
) -> None:
prune_conv2d_activation_conv2d(c1, activation, c2)
def prune_conv2d_pool_flatten_linear(
conv2d: nn.Conv2d,
pool: nn.Module,
flatten: Callable[[Tensor], Tensor] | None,
linear: nn.Linear,
) -> None:
mask = _prune_conv2d_helper(conv2d)
# We map the pruned indices of the Conv2d output to the flattened indices of the Linear following the Flatten layer.
# we determine the flattening scale (h * w), and readjust `first_pruned_indices`
# (each idx maps to range idx * h * w to (idx+1) * h * w), `first_valid_indices`,
# and `pruned_biases` (repeat each bias by h * w).
if parametrize.is_parametrized(linear):
parametrization_dict = cast(nn.ModuleDict, linear.parametrizations)
weight_parameterizations = cast(
ParametrizationList, parametrization_dict.weight
)
linear_ic = weight_parameterizations.original.shape[1]
else:
linear_ic = linear.weight.shape[1]
conv2d_oc = len(mask)
if linear_ic % conv2d_oc != 0:
raise AssertionError(
f"Flattening from dimensions {conv2d_oc} to {linear_ic} not supported"
)
flatten_scale = linear_ic // conv2d_oc
flattened_mask = torch.tensor(
[[val] * flatten_scale for val in mask], dtype=torch.bool, device=mask.device
).flatten()
if getattr(conv2d, "prune_bias", False):
_prune_module_bias(conv2d, mask)
else:
pruned_biases = cast(Tensor, _propagate_module_bias(conv2d, mask))
flattened_pruned_biases = torch.tensor(
[[bias] * flatten_scale for bias in pruned_biases], device=mask.device
).flatten()
linear.bias = _get_adjusted_next_layer_bias(
linear, flattened_pruned_biases, flattened_mask
)
with torch.no_grad():
if parametrize.is_parametrized(linear):
parametrization_dict = cast(nn.ModuleDict, linear.parametrizations)
weight_parameterizations = cast(
ParametrizationList, parametrization_dict.weight
)
weight_parameterizations.original = nn.Parameter(
weight_parameterizations.original[:, flattened_mask]
)
linear.in_features = weight_parameterizations.original.shape[1]
else:
linear.weight = nn.Parameter(linear.weight[:, flattened_mask])
linear.in_features = linear.weight.shape[1]
def prune_lstm_output_linear(
lstm: nn.LSTM, getitem: Callable, linear: nn.Linear
) -> None:
prune_lstm_output_layernorm_linear(lstm, getitem, None, linear)
def prune_lstm_output_layernorm_linear(
lstm: nn.LSTM,
getitem: Callable,
layernorm: nn.LayerNorm | None,
linear: nn.Linear,
) -> None:
for i in range(lstm.num_layers):
if parametrize.is_parametrized(lstm, f"weight_ih_l{i}"):
parametrization_dict = cast(nn.ModuleDict, lstm.parametrizations)
weight_parameterizations = cast(
ParametrizationList, parametrization_dict[f"weight_ih_l{i}"]
)
mask = weight_parameterizations[0].mask
with torch.no_grad():
parametrize.remove_parametrizations(
lstm, f"weight_ih_l{i}", leave_parametrized=True
)
setattr(
lstm,
f"weight_ih_l{i}",
nn.Parameter(getattr(lstm, f"weight_ih_l{i}")[mask]),
)
setattr(
lstm,
f"bias_ih_l{i}",
nn.Parameter(getattr(lstm, f"bias_ih_l{i}")[mask]),
)
if parametrize.is_parametrized(lstm, f"weight_hh_l{i}"):
parametrization_dict = cast(nn.ModuleDict, lstm.parametrizations)
weight_parameterizations = cast(
ParametrizationList, parametrization_dict[f"weight_hh_l{i}"]
)
mask = weight_parameterizations[0].mask
with torch.no_grad():
parametrize.remove_parametrizations(
lstm, f"weight_hh_l{i}", leave_parametrized=True
)
# splitting out hidden-hidden masks
W_hi, W_hf, W_hg, W_ho = torch.split(
getattr(lstm, f"weight_hh_l{i}"), lstm.hidden_size
)
M_hi, M_hf, M_hg, M_ho = torch.split(mask, lstm.hidden_size) # type: ignore[arg-type]
# resize each individual weight separately
W_hi = W_hi[M_hi][:, M_hi]
W_hf = W_hf[M_hf][:, M_hf]
W_hg = W_hg[M_hg][:, M_hg]
W_ho = W_ho[M_ho][:, M_ho]
# concat, use this as new weight
new_weight = torch.cat((W_hi, W_hf, W_hg, W_ho))
setattr(lstm, f"weight_hh_l{i}", nn.Parameter(new_weight))
setattr(
lstm,
f"bias_hh_l{i}",
nn.Parameter(getattr(lstm, f"bias_hh_l{i}")[mask]),
)
# If this is the final layer, then we need to prune linear layer columns
if i + 1 == lstm.num_layers:
lstm.hidden_size = int(M_hi.sum())
with torch.no_grad():
if parametrize.is_parametrized(linear):
parametrization_dict = cast(
nn.ModuleDict, linear.parametrizations
)
weight_parameterizations = cast(
ParametrizationList, parametrization_dict.weight
)
weight_parameterizations.original = nn.Parameter(
weight_parameterizations.original[:, M_ho]
)
linear.in_features = weight_parameterizations.original.shape[1]
else:
linear.weight = nn.Parameter(linear.weight[:, M_ho])
linear.in_features = linear.weight.shape[1]
# if layernorm module, prune weight and bias
if layernorm is not None:
layernorm.normalized_shape = (linear.in_features,)
layernorm.weight = nn.Parameter(layernorm.weight[M_ho])
layernorm.bias = nn.Parameter(layernorm.bias[M_ho])
# otherwise need to prune the columns of the input of the next LSTM layer
else:
with torch.no_grad():
if parametrize.is_parametrized(lstm, f"weight_ih_l{i + 1}"):
parametrization_dict = cast(
nn.ModuleDict, lstm.parametrizations
)
weight_parameterizations = cast(
ParametrizationList,
getattr(parametrization_dict, f"weight_ih_l{i + 1}"),
)
weight_parameterizations.original = nn.Parameter(
weight_parameterizations.original[:, M_ho]
)
else:
next_layer_weight = getattr(lstm, f"weight_ih_l{i + 1}")
setattr(
lstm,
f"weight_ih_l{i + 1}",
nn.Parameter(next_layer_weight[:, M_ho]),
)
@@ -0,0 +1,35 @@
# mypy: allow-untyped-defs
from .base_structured_sparsifier import BaseStructuredSparsifier
class SaliencyPruner(BaseStructuredSparsifier):
"""
Prune rows based on the saliency (L1 norm) of each row.
This pruner works on N-Dimensional weight tensors.
For each row, we will calculate the saliency, which is the sum the L1 norm of all weights in that row.
We expect that the resulting saliency vector has the same shape as our mask.
We then pick elements to remove until we reach the target sparsity_level.
"""
def update_mask(self, module, tensor_name, **kwargs):
# tensor_name will give you the FQN, all other entries in sparse config is present in kwargs
weights = getattr(module, tensor_name)
mask = getattr(module.parametrizations, tensor_name)[0].mask
# use negative weights so we can use topk (we prune out the smallest)
if weights.dim() <= 1:
raise Exception( # noqa: TRY002
"Structured pruning can only be applied to a 2+dim weight tensor!"
)
saliency = -weights.norm(dim=tuple(range(1, weights.dim())), p=1)
if saliency.shape != mask.shape:
raise AssertionError(
f"saliency shape ({saliency.shape}) must match mask shape ({mask.shape})"
)
num_to_pick = int(len(mask) * kwargs["sparsity_level"])
prune = saliency.topk(num_to_pick).indices
# Set the mask to be false for the rows we want to prune
mask.data[prune] = False
@@ -0,0 +1,23 @@
# mypy: allow-untyped-defs
__all__ = [
"get_static_sparse_quantized_mapping",
"get_dynamic_sparse_quantized_mapping",
]
def get_static_sparse_quantized_mapping():
import torch.ao.nn.sparse
_static_sparse_quantized_mapping = {
torch.nn.Linear: torch.ao.nn.sparse.quantized.Linear,
}
return _static_sparse_quantized_mapping
def get_dynamic_sparse_quantized_mapping():
import torch.ao.nn.sparse
_dynamic_sparse_quantized_mapping = {
torch.nn.Linear: torch.ao.nn.sparse.quantized.dynamic.Linear,
}
return _dynamic_sparse_quantized_mapping
@@ -0,0 +1,173 @@
# mypy: allow-untyped-defs
import warnings
import weakref
from functools import wraps
from torch.ao.pruning.sparsifier.base_sparsifier import BaseSparsifier
__all__ = ["BaseScheduler"]
class BaseScheduler:
def __init__(self, sparsifier, last_epoch=-1, verbose=False):
# Attach sparsifier
if not isinstance(sparsifier, BaseSparsifier):
raise TypeError(
f"{type(sparsifier).__name__} is not an instance of torch.ao.pruning.BaseSparsifier"
)
self.sparsifier = sparsifier
# Initialize epoch and base sparsity levels
self.base_sl = [group["sparsity_level"] for group in sparsifier.groups]
self.last_epoch = last_epoch
# Following https://github.com/pytorch/pytorch/issues/20124
# We would like to ensure that `scheduler.step()` is called after
# `sparsifier.step()`
def with_counter(method):
if getattr(method, "_with_counter", False):
# `sparsifier.step()` has already been replaced, return.
return method
# Keep a weak reference to the sparsifier instance to prevent
# cyclic references.
instance_ref = weakref.ref(method.__self__)
# Get the unbound method for the same purpose.
func = method.__func__
cls = instance_ref().__class__
del method
@wraps(func)
def wrapper(*args, **kwargs):
instance = instance_ref()
instance._step_count += 1 # type: ignore[union-attr]
wrapped = func.__get__(instance, cls)
return wrapped(*args, **kwargs)
# Note that the returned function here is no longer a bound method,
# so attributes like `__func__` and `__self__` no longer exist.
wrapper._with_counter = True # type: ignore[attr-defined]
return wrapper
self.sparsifier.step = with_counter(self.sparsifier.step) # type: ignore[assignment]
self.sparsifier._step_count = 0 # type: ignore[attr-defined]
self._step_count: int = 0
self.verbose = verbose
# Housekeeping
self._get_sl_called_within_step: bool = False
self.step()
def state_dict(self):
"""Returns the state of the scheduler as a :class:`dict`.
It contains an entry for every variable in self.__dict__ which
is not the sparsifier.
"""
return {
key: value for key, value in self.__dict__.items() if key != "sparsifier"
}
def load_state_dict(self, state_dict):
"""Loads the schedulers state.
Args:
state_dict (dict): scheduler state. Should be an object returned
from a call to :meth:`state_dict`.
"""
self.__dict__.update(state_dict)
def get_last_sl(self):
"""Return last computed sparsity level by current scheduler."""
return self._last_sl
def get_sl(self):
# Compute sparsity level using chainable form of the scheduler
# Note: This method is not intended to be called directly, and is only
# used by the ".step" method. Use .get_last_sl() instead.
if not self._get_sl_called_within_step:
warnings.warn(
"To get the last sparsity level computed by the scheduler, "
"please use `get_last_sl()`.",
stacklevel=2,
)
raise NotImplementedError
def print_sl(self, is_verbose, group, sl, epoch=None):
"""Display the current sparsity level."""
if is_verbose:
if epoch is None:
print(f"Adjusting sparsity level of group {group} to {sl:.4e}.")
else:
print(
f"Epoch {epoch:5d}: adjusting sparsity level of group {group} to {sl:.4e}."
)
def __repr__(self):
format_string = self.__class__.__name__ + " ("
format_string += "\n"
format_string += f"Sparsifier {self.sparsifier}\n"
format_string += f" base_sl: {self.base_sl}\n"
format_string += ")"
return format_string
def step(self, epoch=None):
# Raise warning if trying to call scheduler step before the sparsifier.
# https://github.com/pytorch/pytorch/issues/20124
if self._step_count == 1:
if not hasattr(self.sparsifier.step, "_with_counter"):
warnings.warn(
"Seems like `sparsifier.step()` has been overridden after sparsity scheduler "
"initialization. Please, make sure to call `sparsifier.step()` before "
"`scheduler.step()`.",
UserWarning,
stacklevel=2,
)
# Just check if there were two first scheduler.step() calls before sparsifier.step()
elif self.sparsifier._step_count < 1: # type: ignore[attr-defined]
warnings.warn(
"Detected call of `scheduler.step()` before `sparsifier.step()`. "
"You have to make sure you run the sparsifier.step() BEFORE any "
"calls to the scheduler.step().",
UserWarning,
stacklevel=2,
)
self._step_count += 1
class _enable_get_sl_call:
def __init__(self, o):
self.o = o
def __enter__(self):
self.o._get_sl_called_within_step = True
return self
def __exit__(self, type, value, traceback):
self.o._get_sl_called_within_step = False
with _enable_get_sl_call(self):
self.last_epoch += 1
values = self.get_sl()
for i, data in enumerate(zip(self.sparsifier.groups, values)):
param_group, sl = data
param_group["sparsity_level"] = sl
self.print_sl(self.verbose, i, sl, epoch)
self._last_sl = [group["sparsity_level"] for group in self.sparsifier.groups]
self.sparsifier.enable_mask_update = True
def _make_sure_a_list(self, var):
r"""Utility that extends it to the same length as the .groups, ensuring it is a list"""
n = len(self.sparsifier.groups)
if not isinstance(var, (list, tuple)):
return [var] * n
else:
if len(var) != n:
raise ValueError(f"Expected variable of length {n}, but got {len(var)}")
return list(var) # We want the result to be in a list, not tuple

Some files were not shown because too many files have changed in this diff Show More