chore: import upstream snapshot with attribution
Docker Image CI / build-ubuntu2004 (push) Waiting to run

This commit is contained in:
wehub-resource-sync
2026-07-13 13:36:55 +08:00
commit c8a779b1bb
1887 changed files with 3245738 additions and 0 deletions
@@ -0,0 +1,22 @@
#
# SPDX-FileCopyrightText: Copyright (c) 1993-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
from absl import logging
from .version import __version__
from .quant_modules import *
logging.use_absl_handler()
@@ -0,0 +1,24 @@
#
# SPDX-FileCopyrightText: Copyright (c) 1993-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
"""``pytorch_quantization.calib`` provides Calibrator classes that
collect data statistics and determine pytorch_quantization parameters.
"""
from .max import MaxCalibrator
from .histogram import *
@@ -0,0 +1,61 @@
#
# SPDX-FileCopyrightText: Copyright (c) 1993-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
"""Abstract base class for calibrators"""
class _Calibrator():
"""Abstract base class of calibrators
Args:
num_bits: An integer. Number of bits of quantization.
axis: A tuple. see QuantDescriptor.
unsigned: A boolean. using unsigned quantization.
Readonly Properties:
axis:
"""
def __init__(self, num_bits, axis, unsigned):
self._num_bits = num_bits
self._axis = axis
self._unsigned = unsigned
def collect(self, x):
"""Abstract method: collect tensor statistics used to compute amax
Args:
x: A tensor
"""
raise NotImplementedError
def reset(self):
"""Abstract method: reset calibrator to initial state"""
raise NotImplementedError
def compute_amax(self, *args, **kwargs):
"""Abstract method: compute the amax from the collected data
Returns:
amax: a tensor
"""
raise NotImplementedError
def __repr__(self):
s = "num_bits={_num_bits}"
s += " axis={_axis}"
s += " unsigned={_unsigned}"
return s.format(**self.__dict__)
@@ -0,0 +1,386 @@
#
# SPDX-FileCopyrightText: Copyright (c) 1993-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
"""Histogram based calibrators"""
from collections import Counter
import numpy as np
from scipy.stats import entropy
from absl import logging
import torch
from pytorch_quantization.calib.calibrator import _Calibrator
from pytorch_quantization.tensor_quant import fake_tensor_quant, scaled_e4m3
from pytorch_quantization import nn as quant_nn
from pytorch_quantization import utils as quant_utils
__all__ = ["HistogramCalibrator", "calibrate_weights"]
class HistogramCalibrator(_Calibrator):
"""Unified histogram calibrator
Histogram will be only collected once. compute_amax() performs entropy, percentile, or mse
calibration based on arguments
Args:
num_bits: An integer. Number of bits of quantization.
axis: A tuple. see QuantDescriptor.
unsigned: A boolean. using unsigned quantization.
num_bins: An integer. Number of histograms bins. Default 2048.
grow_method: A string. DEPRECATED. default None.
skip_zeros: A boolean. If True, skips zeros when collecting data for histogram. Default False.
torch_hist: A boolean. If True, collect histogram by torch.histc instead of np.histogram. If input tensor
is on GPU, histc will also be running on GPU. Default True.
"""
def __init__(self, num_bits, axis, unsigned, num_bins=2048, grow_method=None, skip_zeros=False, torch_hist=True):
super(HistogramCalibrator, self).__init__(num_bits, axis, unsigned)
self._num_bins = num_bins
self._skip_zeros = skip_zeros
self._calib_bin_edges = None
self._calib_hist = None
self._torch_hist = torch_hist
if axis is not None:
raise NotImplementedError("Calibrator histogram collection only supports per tensor scaling")
if grow_method is not None:
logging.warning("grow_method is deprecated. Got %s, ingored!", grow_method)
def collect(self, x):
"""Collect histogram"""
if torch.min(x) < 0.:
logging.log_first_n(logging.INFO,
("Calibrator encountered negative values. It shouldn't happen after ReLU. "
"Make sure this is the right tensor to calibrate."), 1)
x = x.abs()
x = x.float()
if not self._torch_hist:
x_np = x.cpu().detach().numpy()
if self._skip_zeros:
x_np = x_np[np.where(x_np != 0)]
if self._calib_bin_edges is None and self._calib_hist is None:
# first time it uses num_bins to compute histogram.
self._calib_hist, self._calib_bin_edges = np.histogram(x_np, bins=self._num_bins)
else:
temp_amax = np.max(x_np)
if temp_amax > self._calib_bin_edges[-1]:
# increase the number of bins
width = self._calib_bin_edges[1] - self._calib_bin_edges[0]
# NOTE: np.arange may create an extra bin after the one containing temp_amax
new_bin_edges = np.arange(self._calib_bin_edges[-1] + width, temp_amax + width, width)
self._calib_bin_edges = np.hstack((self._calib_bin_edges, new_bin_edges))
hist, self._calib_bin_edges = np.histogram(x_np, bins=self._calib_bin_edges)
hist[:len(self._calib_hist)] += self._calib_hist
self._calib_hist = hist
else:
# This branch of code is designed to match numpy version as close as possible
with torch.no_grad():
if self._skip_zeros:
x = x[torch.where(x != 0)]
# Because we collect histogram on absolute value, setting min=0 simplifying the rare case where
# minimum value is not exactly 0 and first batch collected has larger min value than later batches
x_max = x.max()
if self._calib_bin_edges is None and self._calib_hist is None:
self._calib_hist = torch.histc(x, bins=self._num_bins, min=0, max=x_max)
self._calib_bin_edges = torch.linspace(0, x_max, self._num_bins + 1)
else:
if x_max > self._calib_bin_edges[-1]:
width = self._calib_bin_edges[1] - self._calib_bin_edges[0]
self._num_bins = int((x_max / width).ceil().item())
self._calib_bin_edges = torch.arange(0, x_max + width, width, device=x.device)
hist = torch.histc(x, bins=self._num_bins, min=0, max=self._calib_bin_edges[-1])
hist[:self._calib_hist.numel()] += self._calib_hist
self._calib_hist = hist
def reset(self):
"""Reset the collected histogram"""
self._calib_bin_edges = None
self._calib_hist = None
def compute_amax(self, method: str, *, stride: int = 1, start_bin: int = 128, percentile: float = 99.99):
"""Compute the amax from the collected histogram
Args:
method: A string. One of ['entropy', 'mse', 'percentile']
Keyword Arguments:
stride: An integer. Default 1
start_bin: An integer. Default 128
percentils: A float number between [0, 100]. Default 99.99.
Returns:
amax: a tensor
"""
if isinstance(self._calib_hist, torch.Tensor):
calib_hist = self._calib_hist.to(torch.int64).cpu().numpy()
calib_bin_edges = self._calib_bin_edges.cpu().numpy()
else:
calib_hist = self._calib_hist
calib_bin_edges = self._calib_bin_edges
if method == 'entropy':
calib_amax = _compute_amax_entropy(calib_hist, calib_bin_edges, self._num_bits, self._unsigned, stride,
start_bin)
elif method == 'mse':
calib_amax = _compute_amax_mse(calib_hist, calib_bin_edges, self._num_bits, self._unsigned, stride,
start_bin)
elif method == 'percentile':
calib_amax = _compute_amax_percentile(calib_hist, calib_bin_edges, percentile)
else:
raise TypeError("Unknown calibration method {}".format(method))
return calib_amax
# pylint:disable=missing-docstring
def __str__(self):
s = "HistogramCalibrator("
if self._calib_bin_edges is None:
bin_edge_str = "None"
else:
bin_edge_str = "[{:.3f}, ..., {:.3f}]({})".format(self._calib_bin_edges[0], self._calib_bin_edges[-1],
len(self._calib_bin_edges))
s += "calib_bin_edges={})".format(bin_edge_str)
return s
def __repr__(self):
s = "HistogramCalibrator("
s += super(HistogramCalibrator, self).__repr__()
s += " calib_bin_edges={_calib_bin_edges}"
s += " calib_hist={_calib_hist})"
return s.format(**self.__dict__)
# pylint:enable=missing-docstring
# Ideally, we want to decouple collector (collect histogram) and calibrator (compute amax) as opposed to
# the current calibrator design. The following compute amax functions are broken out from the calibrator
# as first step towards there.
def _compute_amax_entropy(calib_hist, calib_bin_edges, num_bits, unsigned, stride=1, start_bin=128):
"""Returns amax that minimizes KL-Divergence of the collected histogram"""
# If calibrator hasn't collected any data, return none
if calib_bin_edges is None and calib_hist is None:
return None
def _normalize_distr(distr):
summ = np.sum(distr)
if summ != 0:
distr = distr / summ
bins = calib_hist[:]
bins[0] = bins[1]
total_data = np.sum(bins)
divergences = []
arguments = []
# we are quantizing to 128 values + sign if num_bits=8
nbins = 1 << (num_bits - 1 + int(unsigned))
starting = start_bin
stop = len(bins)
new_density_counts = np.zeros(nbins, dtype=np.float64)
for i in range(starting, stop + 1, stride):
new_density_counts.fill(0)
space = np.linspace(0, i, num=nbins + 1)
digitized_space = np.digitize(range(i), space) - 1
digitized_space[bins[:i] == 0] = -1
for idx, digitized in enumerate(digitized_space):
if digitized != -1:
new_density_counts[digitized] += bins[idx]
counter = Counter(digitized_space)
for key, val in counter.items():
if key != -1:
new_density_counts[key] = new_density_counts[key] / val
new_density = np.zeros(i, dtype=np.float64)
for idx, digitized in enumerate(digitized_space):
if digitized != -1:
new_density[idx] = new_density_counts[digitized]
total_counts_new = np.sum(new_density) + np.sum(bins[i:])
_normalize_distr(new_density)
reference_density = np.array(bins[:len(digitized_space)])
reference_density[-1] += np.sum(bins[i:])
total_counts_old = np.sum(reference_density)
if round(total_counts_new) != total_data or round(total_counts_old) != total_data:
raise RuntimeError("Count mismatch! total_counts_new={}, total_counts_old={}, total_data={}".format(
total_counts_new, total_counts_old, total_data))
_normalize_distr(reference_density)
ent = entropy(reference_density, new_density)
divergences.append(ent)
arguments.append(i)
divergences = np.array(divergences)
logging.debug("divergences={}".format(divergences))
last_argmin = len(divergences) - 1 - np.argmin(divergences[::-1])
calib_amax = calib_bin_edges[last_argmin * stride + starting]
calib_amax = torch.tensor(calib_amax.item()) #pylint: disable=not-callable
return calib_amax
def _compute_amax_mse(calib_hist, calib_bin_edges, num_bits, unsigned, stride=1, start_bin=128):
"""Returns amax that minimizes MSE of the collected histogram"""
# If calibrator hasn't collected any data, return none
if calib_bin_edges is None and calib_hist is None:
return None
counts = torch.from_numpy(calib_hist[:]).float().cuda()
edges = torch.from_numpy(calib_bin_edges[:]).float().cuda()
centers = (edges[1:] + edges[:-1]) / 2
mses = []
arguments = []
for i in range(start_bin, len(centers), stride):
amax = centers[i]
if isinstance(num_bits, int) and num_bits >= 0:
if num_bits == 0:
logging.error("num_bits is 0. This will result in the tensor being quantized to all zeros."
" This mode should only be used for debugging purposes.")
quant_centers = fake_tensor_quant(centers, amax, num_bits, unsigned)
elif num_bits == (4, 3):
quant_centers = scaled_e4m3(centers, amax, num_bits[0], num_bits[1])
else:
raise TypeError("Invalid num_bits. num_bits must be a postivie integer or tuple (4,3).")
mse = ((quant_centers - centers)**2 * counts).mean()
mses.append(mse.cpu())
arguments.append(i)
logging.debug("mses={}".format(mses))
argmin = np.argmin(mses)
calib_amax = centers[arguments[argmin]]
return calib_amax
def _compute_amax_percentile(calib_hist, calib_bin_edges, percentile):
"""Returns amax that clips the percentile fraction of collected data"""
if percentile < 0 or percentile > 100:
raise ValueError("Invalid percentile. Must be in range 0 <= percentile <= 100.")
# If calibrator hasn't collected any data, return none
if calib_bin_edges is None and calib_hist is None:
return None
total = calib_hist.sum()
cdf = np.cumsum(calib_hist / total)
idx = np.searchsorted(cdf, percentile / 100)
calib_amax = calib_bin_edges[idx]
calib_amax = torch.tensor(calib_amax.item()) #pylint: disable=not-callable
return calib_amax
def calibrate_weights(model, method="percentile", perchannel=True, percentile=99.99, num_bins=2048):
"""Calibrate weights of all child quantized modules
Ideally, we would split calibration functionality to histogram collector and calibrator which
takes histogram and compute amax. But since we haven't decoupled collector and calibrator, it
is easier to create a separate function to calibrate weight.
.. note::
This function uses `method` specified by the argument to decide which method to use, NOT the one
specified by the calibrator embedded in weight_quantizer.
We haven't moved calibration to GPU, so everything is transfered to CPU
Args:
model: A torch.nn.Module.
method: A string of calibration method. Supports "mse" and "percentile". Default "percentile"
perchannel: A bool. Set channel/neuron axis if True. Default True.
percentile: A float. Default 99.99
num_bins: A integer. Number of bins of histogram. Default 2048.
"""
for name, module in model.named_modules():
if hasattr(module, "weight") and hasattr(module, "weight_quantizer"):
logging.info("Calibrate weight of %s", name)
num_bits = module.weight_quantizer.num_bits
unsigned = module.weight_quantizer.unsigned
channel_second_modules = (quant_nn.QuantConvTranspose1d, quant_nn.QuantConvTranspose2d,
quant_nn.QuantConvTranspose3d)
if perchannel:
axis = 1 if isinstance(module, channel_second_modules) else 0
else:
axis = None
axis_size = module.weight.shape[axis] if axis is not None else 1
# Histogram is always collected even if method is "max". Although "max" is supported here
# but it is not the primary usage of this function
if axis is None:
input_weights = module.weight.abs().cpu().detach().numpy()
calib_hist, calib_bin_edges = np.histogram(input_weights, bins=2048, range=(0, input_weights.max()))
calib_hist = [calib_hist]
calib_bin_edges = [calib_bin_edges]
else:
calib_hist = []
calib_bin_edges = []
for i in range(axis_size):
input_weights = module.weight.index_select(axis, torch.tensor(
i, device=module.weight.device)).abs().cpu().detach().numpy()
hist, bin_edges = np.histogram(input_weights, bins=num_bins, range=(0, input_weights.max()))
calib_hist.append(hist)
calib_bin_edges.append(bin_edges)
calib_amax = []
if method == "max":
reduce_axis = list(range(module.weight.dim()))
reduce_axis.remove(axis)
calib_amax.append(quant_utils.reduce_amax(module.weight, axis=reduce_axis))
elif method == 'mse':
for i in range(axis_size):
calib_amax.append(_compute_amax_mse(calib_hist[i], calib_bin_edges[i], num_bits, unsigned))
elif method == 'percentile':
for i in range(axis_size):
calib_amax.append(_compute_amax_percentile(calib_hist[i], calib_bin_edges[i], percentile))
else:
raise TypeError("Unsupported calibration method {}".format(method))
if axis is None:
calib_amax = calib_amax[0]
else:
calib_amax_shape = [1] * module.weight.dim()
calib_amax_shape[axis] = module.weight.shape[axis]
calib_amax = torch.stack(calib_amax).reshape(calib_amax_shape)
module.weight_quantizer.amax = calib_amax.detach().cpu().numpy()
@@ -0,0 +1,111 @@
#
# SPDX-FileCopyrightText: Copyright (c) 1993-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
"""Calibrator that returns the absolute max of all collected tensors"""
from absl import logging
import torch
from pytorch_quantization.calib.calibrator import _Calibrator
from pytorch_quantization import utils as quant_utils
class MaxCalibrator(_Calibrator):
"""Max calibrator, tracks the maximum value globally
Args:
calib_desc: A MaxCalibDescriptor.
num_bits: An integer. Number of bits of quantization.
axis: A tuple. see QuantDescriptor.
unsigned: A boolean. using unsigned quantization.
Readonly Properties:
amaxs: A list of amax. Numpy array is saved as it is likely to be used for some plot.
"""
def __init__(self, num_bits, axis, unsigned, track_amax=False):
super(MaxCalibrator, self).__init__(num_bits, axis, unsigned)
self._track_amax = track_amax
if self._track_amax:
self._amaxs = [] # shall we have a better name?
self._calib_amax = None
# pylint:disable=missing-docstring
@property
def amaxs(self):
return self._amaxs
# pylint:enable=missing-docstring
def collect(self, x):
"""Tracks the absolute max of all tensors
Args:
x: A tensor
Raises:
RuntimeError: If amax shape changes
"""
if torch.min(x) < 0.:
logging.log_first_n(
logging.INFO,
("Calibrator encountered negative values. It shouldn't happen after ReLU. "
"Make sure this is the right tensor to calibrate."),
1)
x = x.abs()
# Swap axis to reduce.
axis = self._axis if isinstance(self._axis, (list, tuple)) else [self._axis]
# Handle negative axis.
axis = [x.dim() + i if isinstance(i, int) and i < 0 else i for i in axis]
reduce_axis = []
for i in range(x.dim()):
if not i in axis:
reduce_axis.append(i)
local_amax = quant_utils.reduce_amax(x, axis=reduce_axis).detach()
if self._calib_amax is None:
self._calib_amax = local_amax
else:
if local_amax.shape != self._calib_amax.shape:
raise RuntimeError("amax shape changed!")
self._calib_amax.copy_(torch.max(self._calib_amax, local_amax).data)
if self._track_amax:
self._amaxs.append(local_amax.cpu().numpy())
def reset(self):
"""Reset the collected absolute max"""
self._calib_amax = None
def compute_amax(self):
"""Return the absolute max of all tensors collected"""
return self._calib_amax
# pylint:disable=missing-docstring
def __str__(self):
s = "MaxCalibrator("
s += "track_amax={_track_amax}"
s += ")"
return s.format(**self.__dict__)
def __repr__(self):
s = "MaxCalibrator("
s += super(MaxCalibrator, self).__repr__()
s += " calib_amax={_calib_amax}"
s += " track_amax={_track_amax}"
if self._track_amax:
s += " amaxs={_amaxs}"
s += ")"
return s.format(**self.__dict__)
# pylint:enable=missing-docstring
@@ -0,0 +1,25 @@
#
# SPDX-FileCopyrightText: Copyright (c) 1993-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
from pytorch_quantization.nn.modules.tensor_quantizer import *
from pytorch_quantization.nn.modules.quant_conv import *
from pytorch_quantization.nn.modules.quant_linear import *
from pytorch_quantization.nn.modules.quant_pooling import *
from pytorch_quantization.nn.modules.clip import *
from pytorch_quantization.nn.modules.quant_rnn import *
from pytorch_quantization.nn.modules.quant_instancenorm import *
@@ -0,0 +1,286 @@
#
# SPDX-FileCopyrightText: Copyright (c) 1993-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
"""RNN implementation in python
Originally copied from https://github.com/pytorch/pytorch/blob/v0.4.1/torch/nn/_functions/rnn.py
with following modification
fusedBackend is removed
CudnnRNN is removed
Hack for ONNX in RNN() is removed
Only LSTM is quantized. Other paths are excluded in __all__
"""
import warnings
from torch.autograd import NestedIOFunction
from torch.nn import functional as F
import torch
import itertools
from functools import partial
__all__ = ["LSTMCell", "RNN"]
def RNNReLUCell(input, hidden, w_ih, w_hh, b_ih=None, b_hh=None):
hy = F.relu(F.linear(input, w_ih, b_ih) + F.linear(hidden, w_hh, b_hh))
return hy
def RNNTanhCell(input, hidden, w_ih, w_hh, b_ih=None, b_hh=None):
hy = torch.tanh(F.linear(input, w_ih, b_ih) + F.linear(hidden, w_hh, b_hh))
return hy
def LSTMCell(input, hidden, w_ih, w_hh, b_ih=None, b_hh=None, input_quantizer=None, weight_quantizer=None):
"""Quantized LSTM Cell
The assumption is at inference time, only one fused gemm will be launched for one time step Weights of 4 gates
are fused together, and activation from layer and recurrent paths are fused togather. ``input_quantizer`` will be
applied on the fused activation tensor. And ``weight_quantizer`` will be applied on the fused weight tensor.
"""
hx, cx = hidden
if input_quantizer is not None:
input, hx = input_quantizer(torch.cat([input, hx], 1)).split([input.size()[1], hx.size()[1]], 1)
if weight_quantizer is not None:
w_ih, w_hh = weight_quantizer(torch.cat([w_ih, w_hh], 1)).split([w_ih.size()[1], w_hh.size()[1]], 1)
gates = F.linear(input, w_ih, b_ih) + F.linear(hx, w_hh, b_hh)
ingate, forgetgate, cellgate, outgate = gates.chunk(4, 1)
ingate = torch.sigmoid(ingate)
forgetgate = torch.sigmoid(forgetgate)
cellgate = torch.tanh(cellgate)
outgate = torch.sigmoid(outgate)
cy = (forgetgate * cx) + (ingate * cellgate)
hy = outgate * torch.tanh(cy)
return hy, cy
def GRUCell(input, hidden, w_ih, w_hh, b_ih=None, b_hh=None):
gi = F.linear(input, w_ih, b_ih)
gh = F.linear(hidden, w_hh, b_hh)
i_r, i_i, i_n = gi.chunk(3, 1)
h_r, h_i, h_n = gh.chunk(3, 1)
resetgate = torch.sigmoid(i_r + h_r)
inputgate = torch.sigmoid(i_i + h_i)
newgate = torch.tanh(i_n + resetgate * h_n)
hy = newgate + inputgate * (hidden - newgate)
return hy
def StackedRNN(inners, num_layers, lstm=False, dropout=0, train=True):
num_directions = len(inners)
total_layers = num_layers * num_directions
def forward(input, hidden, weight, batch_sizes, input_quantizers, weight_quantizers):
assert(len(weight) == total_layers)
next_hidden = []
if lstm:
hidden = list(zip(*hidden))
for i in range(num_layers):
all_output = []
for j, inner in enumerate(inners):
l = i * num_directions + j
hy, output = inner(input, hidden[l], weight[l], batch_sizes,
input_quantizer=input_quantizers[l], weight_quantizer=weight_quantizers[l])
next_hidden.append(hy)
all_output.append(output)
input = torch.cat(all_output, input.dim() - 1)
if dropout != 0 and i < num_layers - 1:
input = F.dropout(input, p=dropout, training=train, inplace=False)
if lstm:
next_h, next_c = zip(*next_hidden)
next_hidden = (
torch.cat(next_h, 0).view(total_layers, *next_h[0].size()),
torch.cat(next_c, 0).view(total_layers, *next_c[0].size())
)
else:
next_hidden = torch.cat(next_hidden, 0).view(
total_layers, *next_hidden[0].size())
return next_hidden, input
return forward
def Recurrent(inner, reverse=False):
def forward(input, hidden, weight, batch_sizes, input_quantizer, weight_quantizer):
output = []
steps = range(input.size(0) - 1, -1, -1) if reverse else range(input.size(0))
for i in steps:
hidden = inner(input[i], hidden, *weight,
input_quantizer=input_quantizer, weight_quantizer=weight_quantizer)
# hack to handle LSTM
output.append(hidden[0] if isinstance(hidden, tuple) else hidden)
if reverse:
output.reverse()
output = torch.cat(output, 0).view(input.size(0), *output[0].size())
return hidden, output
return forward
def variable_recurrent_factory(inner, reverse=False):
if reverse:
return VariableRecurrentReverse(inner)
else:
return VariableRecurrent(inner)
def VariableRecurrent(inner):
def forward(input, hidden, weight, batch_sizes, input_quantizer, weight_quantizer):
output = []
input_offset = 0
last_batch_size = batch_sizes[0]
hiddens = []
flat_hidden = not isinstance(hidden, tuple)
if flat_hidden:
hidden = (hidden,)
for batch_size in batch_sizes:
step_input = input[input_offset:input_offset + batch_size]
input_offset += batch_size
dec = last_batch_size - batch_size
if dec > 0:
hiddens.append(tuple(h[-dec:] for h in hidden))
hidden = tuple(h[:-dec] for h in hidden)
last_batch_size = batch_size
if flat_hidden:
hidden = (inner(step_input, hidden[0], *weight,
input_quantizer=input_quantizer, weight_quantizer=weight_quantizer),)
else:
hidden = inner(step_input, hidden, *weight,
input_quantizer=input_quantizer, weight_quantizer=weight_quantizer)
output.append(hidden[0])
hiddens.append(hidden)
hiddens.reverse()
hidden = tuple(torch.cat(h, 0) for h in zip(*hiddens))
assert hidden[0].size(0) == batch_sizes[0]
if flat_hidden:
hidden = hidden[0]
output = torch.cat(output, 0)
return hidden, output
return forward
def VariableRecurrentReverse(inner):
def forward(input, hidden, weight, batch_sizes, input_quantizer, weight_quantizer):
output = []
input_offset = input.size(0)
last_batch_size = batch_sizes[-1]
initial_hidden = hidden
flat_hidden = not isinstance(hidden, tuple)
if flat_hidden:
hidden = (hidden,)
initial_hidden = (initial_hidden,)
hidden = tuple(h[:batch_sizes[-1]] for h in hidden)
for i in reversed(range(len(batch_sizes))):
batch_size = batch_sizes[i]
inc = batch_size - last_batch_size
if inc > 0:
hidden = tuple(torch.cat((h, ih[last_batch_size:batch_size]), 0)
for h, ih in zip(hidden, initial_hidden))
last_batch_size = batch_size
step_input = input[input_offset - batch_size:input_offset]
input_offset -= batch_size
if flat_hidden:
hidden = (inner(step_input, hidden[0], *weight,
input_quantizer=input_quantizer, weight_quantizer=weight_quantizer),)
else:
hidden = inner(step_input, hidden, *weight,
input_quantizer=input_quantizer, weight_quantizer=weight_quantizer)
output.append(hidden[0])
output.reverse()
output = torch.cat(output, 0)
if flat_hidden:
hidden = hidden[0]
return hidden, output
return forward
def AutogradRNN(mode, input_size, hidden_size, num_layers=1, batch_first=False,
dropout=0, train=True, bidirectional=False, variable_length=False,
dropout_state=None, flat_weight=None,
input_quantizers=None, weight_quantizers=None):
if mode == 'RNN_RELU':
cell = RNNReLUCell
elif mode == 'RNN_TANH':
cell = RNNTanhCell
elif mode == 'LSTM':
cell = LSTMCell
elif mode == 'GRU':
cell = GRUCell
else:
raise Exception('Unknown mode: {}'.format(mode))
rec_factory = variable_recurrent_factory if variable_length else Recurrent
if bidirectional:
layer = (rec_factory(cell), rec_factory(cell, reverse=True))
else:
layer = (rec_factory(cell),)
func = StackedRNN(layer,
num_layers,
(mode == 'LSTM'),
dropout=dropout,
train=train)
def forward(input, weight, hidden, batch_sizes, input_quantizers, weight_quantizers):
if batch_first and not variable_length:
input = input.transpose(0, 1)
nexth, output = func(input, hidden, weight, batch_sizes, input_quantizers, weight_quantizers)
if batch_first and not variable_length:
output = output.transpose(0, 1)
return output, nexth
return forward
def RNN(*args, **kwargs):
def forward(input, *fargs, **fkwargs):
func = AutogradRNN(*args, **kwargs)
return func(input, *fargs, **fkwargs)
return forward
@@ -0,0 +1,61 @@
#
# SPDX-FileCopyrightText: Copyright (c) 1993-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
"""Some supportive functions"""
from absl import logging
import torch
from torch.autograd import Function
class ClipFunction(Function):
"""An universal tensor clip function
Pytorch's clamp() only supports scalar range and doesn't support broadcast. This implementation uses min/max which
is more genaral. The gradient is defined according to IBM's PACT paper https://arxiv.org/abs/1805.06085, which is
also the behavior of Tensorflow's clip_by_value()
"""
@staticmethod
def forward(ctx, input, clip_value_min, clip_value_max):
output = torch.min(input, clip_value_max)
output = torch.max(output, clip_value_min)
ctx.save_for_backward(input, clip_value_min, clip_value_max)
return output
@staticmethod
def backward(ctx, grad_output):
input, clip_value_min, clip_value_max = ctx.saved_tensors
min_mask = (input > clip_value_min).to(grad_output.dtype)
max_mask = (input < clip_value_max).to(grad_output.dtype)
grad_input = grad_output * min_mask * max_mask
if clip_value_min.requires_grad or clip_value_max.requires_grad:
logging.log_first_n(logging.WARNING, "Learning clip min/max is experimental, use at your own risk :).", 1)
if clip_value_min.numel() != 1 or clip_value_max.numel() != 1:
raise ValueError("Learnable min/max can only be scalar, got size %s and %s." % (clip_value_min.size(),
clip_value_max.size()))
# Ensure the dtypes of min/max grads matches the input dtype
# This might be necessary if running w/ AMP which will cast to fp32 before `sum()`
grad_clip_value_min = (grad_output * (1. - min_mask)).sum().to(clip_value_min.dtype) if clip_value_min.requires_grad else None
grad_clip_value_max = (grad_output * (1. - max_mask)).sum().to(clip_value_min.dtype) if clip_value_max.requires_grad else None
return grad_input, grad_clip_value_min, grad_clip_value_max
clip = ClipFunction.apply
@@ -0,0 +1,165 @@
#
# SPDX-FileCopyrightText: Copyright (c) 1993-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
"""Some helper functions for implementing quantized modules"""
import copy
import inspect
from absl import logging
from torch import nn
from pytorch_quantization.nn import TensorQuantizer
from pytorch_quantization.tensor_quant import QuantDescriptor, QUANT_DESC_8BIT_PER_TENSOR
class QuantMixin():
"""Mixin class for adding basic quantization logic to quantized modules"""
default_quant_desc_input = QUANT_DESC_8BIT_PER_TENSOR
default_quant_desc_weight = QUANT_DESC_8BIT_PER_TENSOR
@classmethod
def set_default_quant_desc_input(cls, value):
"""
Args:
value: An instance of :class:`QuantDescriptor <pytorch_quantization.tensor_quant.QuantDescriptor>`
"""
if not isinstance(value, QuantDescriptor):
raise ValueError("{} is not an instance of QuantDescriptor!")
cls.default_quant_desc_input = copy.deepcopy(value)
@classmethod
def set_default_quant_desc_weight(cls, value):
"""
Args:
value: An instance of :class:`QuantDescriptor <pytorch_quantization.tensor_quant.QuantDescriptor>`
"""
if not isinstance(value, QuantDescriptor):
raise ValueError("{} is not an instance of QuantDescriptor!")
cls.default_quant_desc_weight = copy.deepcopy(value)
def init_quantizer(self, quant_desc_input, quant_desc_weight, num_layers=None):
"""Helper function for __init__ of quantized module
Create input and weight quantizer based on quant_desc passed by kwargs, or default of the class.
Args:
quant_desc_input: An instance of :class:`QuantDescriptor <pytorch_quantization.tensor_quant.QuantDescriptor>`
quant_desc_weight: An instance of :class:`QuantDescriptor <pytorch_quantization.tensor_quant.QuantDescriptor>`
num_layers: An integer. Default None. If not None, create a list of quantizers.
"""
if not inspect.stack()[1].function == "__init__":
raise TypeError("{} should be only called by __init__ of quantized module.".format(__name__))
self._fake_quant = True
if (not quant_desc_input.fake_quant) or (not quant_desc_weight.fake_quant):
raise ValueError("Only fake quantization is supported!")
logging.info("Input is %squantized to %d bits in %s with axis %s!", ""
if not quant_desc_input.fake_quant else "fake ",
quant_desc_input.num_bits, self.__class__.__name__, quant_desc_input.axis)
logging.info("Weight is %squantized to %d bits in %s with axis %s!", ""
if not quant_desc_weight.fake_quant else "fake ",
quant_desc_weight.num_bits, self.__class__.__name__, quant_desc_weight.axis)
if num_layers is None:
self._input_quantizer = TensorQuantizer(quant_desc_input)
self._weight_quantizer = TensorQuantizer(quant_desc_weight)
else:
self._input_quantizers = nn.ModuleList([TensorQuantizer(quant_desc_input) for _ in range(num_layers)])
self._weight_quantizers = nn.ModuleList([TensorQuantizer(quant_desc_weight) for _ in range(num_layers)])
# pylint:disable=missing-docstring
@property
def input_quantizer(self):
return self._input_quantizer
@property
def weight_quantizer(self):
return self._weight_quantizer
# pylint:enable=missing-docstring
class QuantInputMixin():
"""Mixin class for adding basic quantization logic to quantized modules"""
default_quant_desc_input = QUANT_DESC_8BIT_PER_TENSOR
@classmethod
def set_default_quant_desc_input(cls, value):
"""
Args:
value: An instance of :class:`QuantDescriptor <pytorch_quantization.tensor_quant.QuantDescriptor>`
"""
if not isinstance(value, QuantDescriptor):
raise ValueError("{} is not an instance of QuantDescriptor!")
cls.default_quant_desc_input = copy.deepcopy(value)
def init_quantizer(self, quant_desc_input):
"""Helper function for __init__ of simple quantized module
Create input quantizer based on quant_desc passed by kwargs, or default of the class.
Args:
quant_desc_input: An instance of :class:`QuantDescriptor <pytorch_quantization.tensor_quant.QuantDescriptor>`
"""
if not inspect.stack()[1].function == "__init__":
raise TypeError("{} should be only called by __init__ of quantized module.".format(__name__))
self._fake_quant = True
if not quant_desc_input.fake_quant:
raise ValueError("Only fake quantization is supported!")
logging.info("Input is %squantized to %d bits in %s with axis %s!", ""
if not quant_desc_input.fake_quant else "fake ",
quant_desc_input.num_bits, self.__class__.__name__, quant_desc_input.axis)
self._input_quantizer = TensorQuantizer(quant_desc_input)
# pylint:disable=missing-docstring
@property
def input_quantizer(self):
return self._input_quantizer
# pylint:enable=missing-docstring
def pop_quant_desc_in_kwargs(quant_cls, input_only=False, **kwargs):
"""Pop quant descriptors in kwargs
If there is no descriptor in kwargs, the default one in quant_cls will be used
Arguments:
quant_cls: A class that has default quantization descriptors
input_only: A boolean. If True, pop quant_desc_input only, not quant_desc_weight. Default false.
Keyword Arguments:
quant_desc_input: An instance of :class:`QuantDescriptor <pytorch_quantization.tensor_quant.QuantDescriptor>`.
Quantization descriptor of input.
quant_desc_weight: An instance of :class:`QuantDescriptor <pytorch_quantization.tensor_quant.QuantDescriptor>`.
Quantization descriptor of weight.
"""
quant_desc_input = kwargs.pop('quant_desc_input', quant_cls.default_quant_desc_input)
if not input_only:
quant_desc_weight = kwargs.pop('quant_desc_weight', quant_cls.default_quant_desc_weight)
# Check if anything is left in **kwargs
if kwargs:
raise TypeError("Unused keys: {}".format(kwargs.keys()))
if input_only:
return quant_desc_input
return quant_desc_input, quant_desc_weight
@@ -0,0 +1,59 @@
#
# SPDX-FileCopyrightText: Copyright (c) 1993-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
"""Implement a clip module as pytorch only has a simple clamp function """
import torch
from torch import nn
from torch.nn.parameter import Parameter
from pytorch_quantization.nn import functional as QF
__all__ = ['Clip']
class Clip(nn.Module):
"""Clip tensor
Args:
clip_value_min: A number or tensor of lower bound to clip
clip_value_max: A number of tensor of upper bound to clip
learn_min: A boolean. If True, learn min. clip_value_min will be used to initialize. Default False
learn_max: A boolean. Similar as learn_min but for max.
Raises:
ValueError:
"""
def __init__(self, clip_value_min, clip_value_max, learn_min=False, learn_max=False):
super(Clip, self).__init__()
if learn_min:
if not isinstance(clip_value_min, float) and clip_value_min.size != 1:
raise ValueError("clip_value_min/clip_value_max must be scalar for initilizing learnable range.")
self.clip_value_min = Parameter(torch.tensor(clip_value_min)) # pylint: disable=not-callable
else:
self.clip_value_min = clip_value_min
if learn_max:
if not isinstance(clip_value_max, float) and clip_value_max.size != 1:
raise ValueError("clip_value_min/clip_value_max must be scalar for initilizing learnable range.")
self.clip_value_max = Parameter(torch.tensor(clip_value_max)) # pylint: disable=not-callable
else:
self.clip_value_max = clip_value_max
def forward(self, inputs):
outputs = QF.clip(inputs, self.clip_value_min, self.clip_value_max)
return outputs
@@ -0,0 +1,419 @@
#
# SPDX-FileCopyrightText: Copyright (c) 1993-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
"""Quantized convolution
Base code is from nn.Conv, details of Module and original argument can be found there.
Module names are intentionally kept same as unquantized version so that they can be dropped into preexisting model
easily, and load pretrained weight. Aliases with Quant prefix are defined and are encouraged to be used explicitly
when start scratch.
"""
import inspect
import torch
import torch.nn
import torch.nn.functional as F
from torch.nn.modules.utils import _single, _pair, _triple
from torch.nn.modules.conv import _ConvTransposeNd
from pytorch_quantization import tensor_quant
from . import _utils
__all__ = [
"Conv2d", "QuantConv2d", "Conv3d", "QuantConv3d", "Conv1d", "QuantConv1d", "ConvTranspose1d", "ConvTranspose2d",
"ConvTranspose3d", "QuantConvTranspose1d", "QuantConvTranspose2d", "QuantConvTranspose3d"
]
class _QuantConvNd(torch.nn.modules.conv._ConvNd, _utils.QuantMixin):
"""base class of quantized Conv inherited from _ConvNd
Comments of original arguments can be found in torch.nn.modules.conv
Arguments:
quant_desc_input: An instance of :class:`QuantDescriptor <pytorch_quantization.tensor_quant.QuantDescriptor>`.
Quantization descriptor of input.
quant_desc_weight: An instance of :class:`QuantDescriptor <pytorch_quantization.tensor_quant.QuantDescriptor>`.
Quantization descriptor of weight.
Raises:
ValueError: If unsupported arguments are passed in.
Readonly properties:
- input_quantizer:
- weight_quantizer:
Static methods:
- set_default_quant_desc_input: Set default_quant_desc_input
- set_default_quant_desc_weight: Set default_quant_desc_weight
"""
default_quant_desc_input = tensor_quant.QUANT_DESC_8BIT_PER_TENSOR
default_quant_desc_weight = tensor_quant.QUANT_DESC_8BIT_PER_TENSOR
def __init__(self, in_channels, out_channels, kernel_size, stride, padding, dilation, transposed, output_padding,
groups, bias, padding_mode, quant_desc_input, quant_desc_weight):
super(_QuantConvNd, self).__init__(in_channels, out_channels, kernel_size, stride, padding, dilation,
transposed, output_padding, groups, bias, padding_mode)
self.init_quantizer(quant_desc_input, quant_desc_weight)
def _quant(self, input):
"""Apply quantization on input and weight
Function called by the classes lower in the hierarchy, which actually performs the quantization before forward
in the derivate class the particular Function.
Arguments:
input: in_features to quantize
Returns:
A tuple: (quant_in_feature, quant_weight)
"""
quant_input = self._input_quantizer(input)
quant_weight = self._weight_quantizer(self.weight)
return (quant_input, quant_weight)
class QuantConv2d(_QuantConvNd):
"""Quantized 2D conv"""
default_quant_desc_weight = tensor_quant.QUANT_DESC_8BIT_CONV2D_WEIGHT_PER_CHANNEL
def __init__(self,
in_channels,
out_channels,
kernel_size,
stride=1,
padding=0,
dilation=1,
groups=1,
bias=True,
padding_mode='zeros',
**kwargs):
kernel_size = _pair(kernel_size)
stride = _pair(stride)
padding = _pair(padding)
dilation = _pair(dilation)
quant_desc_input, quant_desc_weight = _utils.pop_quant_desc_in_kwargs(self.__class__, **kwargs)
super(QuantConv2d, self).__init__(in_channels, out_channels, kernel_size, stride, padding, dilation, False,
_pair(0), groups, bias, padding_mode,
quant_desc_input=quant_desc_input, quant_desc_weight=quant_desc_weight)
def forward(self, input):
# the actual quantization happens in the next level of the class hierarchy
quant_input, quant_weight = self._quant(input)
if self.padding_mode == 'circular':
expanded_padding = ((self.padding[1] + 1) // 2, self.padding[1] // 2,
(self.padding[0] + 1) // 2, self.padding[0] // 2)
output = F.conv2d(F.pad(quant_input, expanded_padding, mode='circular'),
quant_weight, self.bias, self.stride,
_pair(0), self.dilation, self.groups)
else:
output = F.conv2d(quant_input, quant_weight, self.bias, self.stride, self.padding, self.dilation,
self.groups)
return output
class QuantConv3d(_QuantConvNd):
"""Quantized 3D Conv"""
default_quant_desc_weight = tensor_quant.QUANT_DESC_8BIT_CONV3D_WEIGHT_PER_CHANNEL
def __init__(self,
in_channels,
out_channels,
kernel_size,
stride=1,
padding=0,
dilation=1,
groups=1,
bias=True,
padding_mode='zeros',
**kwargs):
kernel_size = _triple(kernel_size)
stride = _triple(stride)
padding = _triple(padding)
dilation = _triple(dilation)
quant_desc_input, quant_desc_weight = _utils.pop_quant_desc_in_kwargs(self.__class__, **kwargs)
super(QuantConv3d, self).__init__(in_channels, out_channels, kernel_size, stride, padding, dilation, False,
_triple(0), groups, bias, padding_mode,
quant_desc_input=quant_desc_input, quant_desc_weight=quant_desc_weight)
def forward(self, input):
# the actual quantization happens in the next level of the class hierarchy
quant_input, quant_weight = self._quant(input)
if self.padding_mode == 'circular':
expanded_padding = ((self.padding[2] + 1) // 2, self.padding[2] // 2,
(self.padding[1] + 1) // 2, self.padding[1] // 2,
(self.padding[0] + 1) // 2, self.padding[0] // 2)
output = F.conv3d(F.pad(quant_input, expanded_padding, mode='circular'),
quant_weight, self.bias, self.stride, _triple(0),
self.dilation, self.groups)
else:
output = F.conv3d(quant_input, quant_weight, self.bias, self.stride, self.padding, self.dilation,
self.groups)
return output
class QuantConv1d(_QuantConvNd):
"""Quantized 1D Conv"""
default_quant_desc_weight = tensor_quant.QUANT_DESC_8BIT_CONV1D_WEIGHT_PER_CHANNEL
def __init__(self,
in_channels,
out_channels,
kernel_size,
stride=1,
padding=0,
dilation=1,
groups=1,
bias=True,
padding_mode='zeros',
**kwargs):
kernel_size = _single(kernel_size)
stride = _single(stride)
padding = _single(padding)
dilation = _single(dilation)
quant_desc_input, quant_desc_weight = _utils.pop_quant_desc_in_kwargs(self.__class__, **kwargs)
super(QuantConv1d, self).__init__(in_channels, out_channels, kernel_size, stride, padding, dilation, False,
_single(0), groups, bias, padding_mode,
quant_desc_input=quant_desc_input, quant_desc_weight=quant_desc_weight)
def forward(self, input):
# the actual quantization happens in the next level of the class hierarchy
quant_input, quant_weight = self._quant(input)
if self.padding_mode == 'circular':
expanded_padding = ((self.padding[0] + 1) // 2, self.padding[0] // 2)
output = F.conv1d(F.pad(quant_input, expanded_padding, mode='circular'),
quant_weight, self.bias, self.stride,
_single(0), self.dilation, self.groups)
else:
output = F.conv1d(quant_input, quant_weight, self.bias, self.stride,
self.padding, self.dilation, self.groups)
return output
class _QuantConvTransposeNd(torch.nn.modules.conv._ConvTransposeNd, _utils.QuantMixin):
"""base class of quantized Transposed Conv inherited from _ConvTransposeNd
Comments of original arguments can be found in torch.nn.modules.conv
Arguments:
quant_desc_input: An instance of :class:`QuantDescriptor <pytorch_quantization.tensor_quant.QuantDescriptor>`.
Quantization descriptor of input.
quant_desc_weight: An instance of :class:`QuantDescriptor <pytorch_quantization.tensor_quant.QuantDescriptor>`.
Quantization descriptor of weight.
Raises:
ValueError: If unsupported arguments are passed in.
Readonly properties:
- input_quantizer:
- weight_quantizer:
Static methods:
- set_default_quant_desc_input: Set default_quant_desc_input
- set_default_quant_desc_weight: Set default_quant_desc_weight
"""
default_quant_desc_input = tensor_quant.QUANT_DESC_8BIT_PER_TENSOR
default_quant_desc_weight = tensor_quant.QUANT_DESC_8BIT_PER_TENSOR
def __init__(self, in_channels, out_channels, kernel_size, stride,
padding, dilation, transposed, output_padding,
groups, bias, padding_mode, quant_desc_input, quant_desc_weight):
super(_QuantConvTransposeNd, self).__init__(in_channels, out_channels, kernel_size, stride, padding, dilation,
transposed, output_padding, groups, bias, padding_mode)
self.init_quantizer(quant_desc_input, quant_desc_weight)
def _quant(self, input):
"""Apply quantization on input and weight
Function called by the classes lower in the hierarchy, which actually performs the quantization before forward
in the derivate class the particular Function.
Arguments:
input: in_features to quantize
Returns:
A tuple: (quant_in_feature, quant_weight)
"""
quant_input = self._input_quantizer(input)
quant_weight = self._weight_quantizer(self.weight)
return (quant_input, quant_weight)
def _output_padding_nd(self,
input,
output_size,
stride,
padding,
kernel_size,
num_spatial_dims,
dilation=None):
if "num_spatial_dims" in inspect.signature(self._output_padding).parameters:
return self._output_padding(input, output_size, stride, padding, kernel_size, num_spatial_dims)
else:
return self._output_padding(input, output_size, stride, padding, kernel_size)
class QuantConvTranspose1d(_QuantConvTransposeNd):
"""Quantized ConvTranspose1d"""
default_quant_desc_weight = tensor_quant.QUANT_DESC_8BIT_CONVTRANSPOSE1D_WEIGHT_PER_CHANNEL
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',
**kwargs):
kernel_size = _single(kernel_size)
stride = _single(stride)
padding = _single(padding)
dilation = _single(dilation)
output_padding = _single(output_padding)
quant_desc_input, quant_desc_weight = _utils.pop_quant_desc_in_kwargs(self.__class__, **kwargs)
super(QuantConvTranspose1d, self).__init__(
in_channels, out_channels, kernel_size, stride, padding, dilation,
True, output_padding, groups, bias, padding_mode,
quant_desc_input=quant_desc_input, quant_desc_weight=quant_desc_weight)
def forward(self, input, output_size=None):
if self.padding_mode != 'zeros':
raise ValueError('Only `zeros` padding mode is supported for QuantConvTranspose1d')
num_spatial_dims = 1
output_padding = self._output_padding_nd(input, output_size, self.stride, self.padding, self.kernel_size,
num_spatial_dims)
quant_input, quant_weight = self._quant(input)
output = F.conv_transpose1d(quant_input, quant_weight, self.bias, self.stride, self.padding, output_padding,
self.groups, self.dilation)
return output
class QuantConvTranspose2d(_QuantConvTransposeNd):
"""Quantized ConvTranspose2d"""
default_quant_desc_weight = tensor_quant.QUANT_DESC_8BIT_CONVTRANSPOSE2D_WEIGHT_PER_CHANNEL
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',
**kwargs):
kernel_size = _pair(kernel_size)
stride = _pair(stride)
padding = _pair(padding)
dilation = _pair(dilation)
output_padding = _pair(output_padding)
quant_desc_input, quant_desc_weight = _utils.pop_quant_desc_in_kwargs(self.__class__, **kwargs)
super(QuantConvTranspose2d, self).__init__(
in_channels, out_channels, kernel_size, stride, padding, dilation,
True, output_padding, groups, bias, padding_mode,
quant_desc_input=quant_desc_input, quant_desc_weight=quant_desc_weight)
def forward(self, input, output_size=None):
if self.padding_mode != 'zeros':
raise ValueError('Only `zeros` padding mode is supported for QuantConvTranspose2d')
num_spatial_dims = 2
output_padding = self._output_padding_nd(input, output_size, self.stride, self.padding, self.kernel_size,
num_spatial_dims)
quant_input, quant_weight = self._quant(input)
output = F.conv_transpose2d(quant_input, quant_weight, self.bias, self.stride, self.padding, output_padding,
self.groups, self.dilation)
return output
class QuantConvTranspose3d(_QuantConvTransposeNd):
"""Quantized ConvTranspose3d"""
default_quant_desc_weight = tensor_quant.QUANT_DESC_8BIT_CONVTRANSPOSE3D_WEIGHT_PER_CHANNEL
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',
**kwargs):
kernel_size = _triple(kernel_size)
stride = _triple(stride)
padding = _triple(padding)
dilation = _triple(dilation)
output_padding = _triple(output_padding)
quant_desc_input, quant_desc_weight = _utils.pop_quant_desc_in_kwargs(self.__class__, **kwargs)
super(QuantConvTranspose3d, self).__init__(
in_channels, out_channels, kernel_size, stride, padding, dilation,
True, output_padding, groups, bias, padding_mode,
quant_desc_input=quant_desc_input, quant_desc_weight=quant_desc_weight)
def forward(self, input, output_size=None):
if self.padding_mode != 'zeros':
raise ValueError('Only `zeros` padding mode is supported for QuantConvTranspose3d')
num_spatial_dims = 3
output_padding = self._output_padding_nd(input, output_size, self.stride, self.padding, self.kernel_size,
num_spatial_dims)
quant_input, quant_weight = self._quant(input)
output = F.conv_transpose3d(quant_input, quant_weight, self.bias, self.stride, self.padding, output_padding,
self.groups, self.dilation)
return output
# Define alias with Quant prefix
_ConvNd = _QuantConvNd
Conv1d = QuantConv1d
Conv2d = QuantConv2d
Conv3d = QuantConv3d
ConvTranspose1d = QuantConvTranspose1d
ConvTranspose2d = QuantConvTranspose2d
ConvTranspose3d = QuantConvTranspose3d
@@ -0,0 +1,79 @@
#
# SPDX-FileCopyrightText: Copyright (c) 1993-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
"""Quantized instance normalization module
Base code is from nn.InstanceNorm, details of the module can be found from the offical repo.
"""
from torch.nn.modules.batchnorm import _NormBase
import torch.nn.functional as F
from torch.nn.modules import instancenorm
from pytorch_quantization.nn import TensorQuantizer
from pytorch_quantization import tensor_quant
from . import _utils
__all__ = [
"QuantInstanceNorm1d", "QuantInstanceNorm2d", "QuantInstanceNorm3d"
]
class QuantInstanceNorm1d(instancenorm.InstanceNorm1d, _utils.QuantInputMixin):
r"""Applies Quantized Instance Normalization over a 3D input
"""
def __init__(
self, num_features: int, eps: float = 1e-5, momentum: float = 0.1, affine: bool = False,
track_running_stats: bool = False, **kwargs):
super(QuantInstanceNorm1d, self).__init__(
num_features, eps, momentum, affine, track_running_stats)
quant_desc_input = _utils.pop_quant_desc_in_kwargs(self.__class__, input_only=True, **kwargs)
self.init_quantizer(quant_desc_input)
def forward(self, input):
quant_input = self._input_quantizer(input)
return super(QuantInstanceNorm1d, self).forward(quant_input)
class QuantInstanceNorm2d(instancenorm.InstanceNorm2d, _utils.QuantInputMixin):
r"""Applies Quantized Instance Normalization over a 4D input
"""
def __init__(
self, num_features: int, eps: float = 1e-5, momentum: float = 0.1, affine: bool = False,
track_running_stats: bool = False, **kwargs):
super(QuantInstanceNorm2d, self).__init__(
num_features, eps, momentum, affine, track_running_stats)
quant_desc_input = _utils.pop_quant_desc_in_kwargs(self.__class__, input_only=True, **kwargs)
self.init_quantizer(quant_desc_input)
def forward(self, input):
quant_input = self._input_quantizer(input)
return super(QuantInstanceNorm2d, self).forward(quant_input)
class QuantInstanceNorm3d(instancenorm.InstanceNorm3d, _utils.QuantInputMixin):
r"""Applies Quantized Instance Normalization over a 5D input
"""
def __init__(
self, num_features: int, eps: float = 1e-5, momentum: float = 0.1, affine: bool = False,
track_running_stats: bool = False, **kwargs):
super(QuantInstanceNorm3d, self).__init__(
num_features, eps, momentum, affine, track_running_stats)
quant_desc_input = _utils.pop_quant_desc_in_kwargs(self.__class__, input_only=True, **kwargs)
self.init_quantizer(quant_desc_input)
def forward(self, input):
quant_input = self._input_quantizer(input)
return super(QuantInstanceNorm3d, self).forward(quant_input)
@@ -0,0 +1,78 @@
#
# SPDX-FileCopyrightText: Copyright (c) 1993-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
"""Quantized Linear"""
from torch import nn
from torch.nn import functional as F
from pytorch_quantization import tensor_quant
from . import _utils
__all__ = ["Linear", "QuantLinear"]
class QuantLinear(nn.Linear, _utils.QuantMixin):
"""Quantized version of nn.Linear
Apply quantized linear to the incoming data, y = dequant(quant(x)quant(A)^T + b).
Keep Module name "Linear" instead of "QuantLinear" so that it can be easily dropped into preexisting model and load
pretrained weights. An alias "QuantLinear" is defined below. The base code is a copy of nn.Linear, see detailed
comment of original arguments there.
Quantization descriptors are passed in in kwargs. If not presents, default_quant_desc_input and
default_quant_desc_weight are used.
Keyword Arguments:
quant_desc_input: An instance of :class:`QuantDescriptor <pytorch_quantization.tensor_quant.QuantDescriptor>`.
Quantization descriptor of input.
quant_desc_wegiht: An instance of :class:`QuantDescriptor <pytorch_quantization.tensor_quant.QuantDescriptor>`.
Quantization descriptor of weight.
Raises:
ValueError: If unsupported arguments are passed in.
KeyError: If unsupported kwargs are passed in.
Readonly properties:
- input_quantizer:
- weight_quantizer:
Static methods:
- set_default_quant_desc_input: Set default_quant_desc_input
- set_default_quant_desc_weight: Set default_quant_desc_weight
"""
default_quant_desc_input = tensor_quant.QUANT_DESC_8BIT_PER_TENSOR
default_quant_desc_weight = tensor_quant.QUANT_DESC_8BIT_LINEAR_WEIGHT_PER_ROW
def __init__(self, in_features, out_features, bias=True, **kwargs):
super(QuantLinear, self).__init__(in_features, out_features, bias)
quant_desc_input, quant_desc_weight = _utils.pop_quant_desc_in_kwargs(self.__class__, **kwargs)
self.init_quantizer(quant_desc_input, quant_desc_weight)
def forward(self, input):
quant_input = self._input_quantizer(input)
quant_weight = self._weight_quantizer(self.weight)
output = F.linear(quant_input, quant_weight, bias=self.bias)
return output
Linear = QuantLinear
@@ -0,0 +1,163 @@
#
# SPDX-FileCopyrightText: Copyright (c) 1993-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
"""Quantized Pooling
Base code is from nn.pooling, details of Module and original argument can be found there.
Module names are intentionally kept same as unquantized version so that they can be dropped into preexisting model
easily, and load pretrained weight. Aliases with Quant prefix are defined and are encouraged to be used explicitly
when start scratch.
"""
from torch.nn.modules import pooling
from . import _utils
__all__ = [
"MaxPool1d", "QuantMaxPool1d", "MaxPool2d", "QuantMaxPool2d", "MaxPool3d", "QuantMaxPool3d",
"AvgPool1d", "QuantAvgPool1d", "AvgPool2d", "QuantAvgPool2d", "AvgPool3d", "QuantAvgPool3d",
"AdaptiveAvgPool1d", "QuantAdaptiveAvgPool1d", "AdaptiveAvgPool2d", "QuantAdaptiveAvgPool2d",
"AdaptiveAvgPool3d", "QuantAdaptiveAvgPool3d"
]
class QuantMaxPool1d(pooling.MaxPool1d, _utils.QuantInputMixin):
"""Quantized 1D maxpool"""
def __init__(self, kernel_size, stride=None, padding=0, dilation=1,
return_indices=False, ceil_mode=False, **kwargs):
super(QuantMaxPool1d, self).__init__(kernel_size, stride, padding, dilation,
return_indices, ceil_mode)
quant_desc_input = _utils.pop_quant_desc_in_kwargs(self.__class__, input_only=True, **kwargs)
self.init_quantizer(quant_desc_input)
def forward(self, input):
quant_input = self._input_quantizer(input)
return super(QuantMaxPool1d, self).forward(quant_input)
class QuantMaxPool2d(pooling.MaxPool2d, _utils.QuantInputMixin):
"""Quantized 2D maxpool"""
def __init__(self, kernel_size, stride=None, padding=0, dilation=1,
return_indices=False, ceil_mode=False, **kwargs):
super(QuantMaxPool2d, self).__init__(kernel_size, stride, padding, dilation,
return_indices, ceil_mode)
quant_desc_input = _utils.pop_quant_desc_in_kwargs(self.__class__, input_only=True, **kwargs)
self.init_quantizer(quant_desc_input)
def forward(self, input):
quant_input = self._input_quantizer(input)
return super(QuantMaxPool2d, self).forward(quant_input)
class QuantMaxPool3d(pooling.MaxPool3d, _utils.QuantInputMixin):
"""Quantized 3D maxpool"""
def __init__(self, kernel_size, stride=None, padding=0, dilation=1,
return_indices=False, ceil_mode=False, **kwargs):
super(QuantMaxPool3d, self).__init__(kernel_size, stride, padding, dilation,
return_indices, ceil_mode)
quant_desc_input = _utils.pop_quant_desc_in_kwargs(self.__class__, input_only=True, **kwargs)
self.init_quantizer(quant_desc_input)
def forward(self, input):
quant_input = self._input_quantizer(input)
return super(QuantMaxPool3d, self).forward(quant_input)
class QuantAvgPool1d(pooling.AvgPool1d, _utils.QuantInputMixin):
"""Quantized 1D average pool"""
def __init__(self, kernel_size, stride=None, padding=0, ceil_mode=False,
count_include_pad=True, **kwargs):
super(QuantAvgPool1d, self).__init__(kernel_size, stride, padding, ceil_mode,
count_include_pad)
quant_desc_input = _utils.pop_quant_desc_in_kwargs(self.__class__, input_only=True, **kwargs)
self.init_quantizer(quant_desc_input)
def forward(self, input):
quant_input = self._input_quantizer(input)
return super(QuantAvgPool1d, self).forward(quant_input)
class QuantAvgPool2d(pooling.AvgPool2d, _utils.QuantInputMixin):
"""Quantized 2D average pool"""
def __init__(self, kernel_size, stride=None, padding=0, ceil_mode=False,
count_include_pad=True, divisor_override=None, **kwargs):
super(QuantAvgPool2d, self).__init__(kernel_size, stride, padding, ceil_mode,
count_include_pad, divisor_override)
quant_desc_input = _utils.pop_quant_desc_in_kwargs(self.__class__, input_only=True, **kwargs)
self.init_quantizer(quant_desc_input)
def forward(self, input):
quant_input = self._input_quantizer(input)
return super(QuantAvgPool2d, self).forward(quant_input)
class QuantAvgPool3d(pooling.AvgPool3d, _utils.QuantInputMixin):
"""Quantized 3D average pool"""
def __init__(self, kernel_size, stride=None, padding=0, ceil_mode=False,
count_include_pad=True, divisor_override=None, **kwargs):
super(QuantAvgPool3d, self).__init__(kernel_size, stride, padding, ceil_mode,
count_include_pad, divisor_override)
quant_desc_input = _utils.pop_quant_desc_in_kwargs(self.__class__, input_only=True, **kwargs)
self.init_quantizer(quant_desc_input)
def forward(self, input):
quant_input = self._input_quantizer(input)
return super(QuantAvgPool3d, self).forward(quant_input)
class QuantAdaptiveAvgPool1d(pooling.AdaptiveAvgPool1d, _utils.QuantInputMixin):
"""Quantized 1D adaptive average pool"""
def __init__(self, output_size, **kwargs):
super(QuantAdaptiveAvgPool1d, self).__init__(output_size)
quant_desc_input = _utils.pop_quant_desc_in_kwargs(self.__class__, input_only=True, **kwargs)
self.init_quantizer(quant_desc_input)
def forward(self, input):
quant_input = self._input_quantizer(input)
return super(QuantAdaptiveAvgPool1d, self).forward(quant_input)
class QuantAdaptiveAvgPool2d(pooling.AdaptiveAvgPool2d, _utils.QuantInputMixin):
"""Quantized 2D adaptive average pool"""
def __init__(self, output_size, **kwargs):
super(QuantAdaptiveAvgPool2d, self).__init__(output_size)
quant_desc_input = _utils.pop_quant_desc_in_kwargs(self.__class__, input_only=True, **kwargs)
self.init_quantizer(quant_desc_input)
def forward(self, input):
quant_input = self._input_quantizer(input)
return super(QuantAdaptiveAvgPool2d, self).forward(quant_input)
class QuantAdaptiveAvgPool3d(pooling.AdaptiveAvgPool3d, _utils.QuantInputMixin):
"""Quantized 3D adaptive average pool"""
def __init__(self, output_size, **kwargs):
super(QuantAdaptiveAvgPool3d, self).__init__(output_size)
quant_desc_input = _utils.pop_quant_desc_in_kwargs(self.__class__, input_only=True, **kwargs)
self.init_quantizer(quant_desc_input)
def forward(self, input):
quant_input = self._input_quantizer(input)
return super(QuantAdaptiveAvgPool3d, self).forward(quant_input)
# Define alias with Quant prefix
MaxPool1d = QuantMaxPool1d
MaxPool2d = QuantMaxPool2d
MaxPool3d = QuantMaxPool3d
AvgPool1d = QuantAvgPool1d
AvgPool2d = QuantAvgPool2d
AvgPool3d = QuantAvgPool3d
AdaptiveAvgPool1d = QuantAdaptiveAvgPool1d
AdaptiveAvgPool2d = QuantAdaptiveAvgPool2d
AdaptiveAvgPool3d = QuantAdaptiveAvgPool3d
@@ -0,0 +1,467 @@
#
# SPDX-FileCopyrightText: Copyright (c) 1993-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
"""RNN implementation in python
originally copied from https://github.com/pytorch/pytorch/blob/v0.4.1/torch/nn/modules/rnn.py
backend is changed to _functions/rnn.py
"""
import math
import torch
import warnings
import itertools
import numbers
from torch import nn
from torch.nn import Parameter
from torch.nn.utils.rnn import PackedSequence
from pytorch_quantization import tensor_quant
from pytorch_quantization.nn._functions import quant_rnn
from . import _utils
__all__ = ["QuantLSTM", "QuantLSTMCell", "LSTM", "LSTMCell"]
class QuantRNNBase(nn.Module, _utils.QuantMixin):
default_quant_desc_weight = tensor_quant.QUANT_DESC_8BIT_LINEAR_WEIGHT_PER_ROW
def __init__(self, mode, input_size, hidden_size,
num_layers=1, bias=True, batch_first=False,
dropout=0, bidirectional=False, proj_size=0, **kwargs):
super(QuantRNNBase, self).__init__()
self.mode = mode
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 = dropout
self.dropout_state = {}
self.bidirectional = bidirectional
self.proj_size = proj_size
num_directions = 2 if bidirectional else 1
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 and num_layers == 1:
warnings.warn("dropout option adds dropout after all but last "
"recurrent layer, so non-zero dropout expects "
"num_layers greater than 1, but got dropout={} and "
"num_layers={}".format(dropout, num_layers))
if proj_size < 0:
raise ValueError("proj_size should be a positive integer or zero to disable projections")
if proj_size > 0:
raise ValueError("proj_size is not supported in pytorch-quantization yet")
if mode == 'LSTM':
gate_size = 4 * hidden_size
elif mode == 'GRU':
gate_size = 3 * hidden_size
else:
gate_size = hidden_size
self._all_weights = []
for layer in range(num_layers):
for direction in range(num_directions):
layer_input_size = input_size if layer == 0 else hidden_size * num_directions
w_ih = Parameter(torch.Tensor(gate_size, layer_input_size))
w_hh = Parameter(torch.Tensor(gate_size, hidden_size))
b_ih = Parameter(torch.Tensor(gate_size))
b_hh = Parameter(torch.Tensor(gate_size))
layer_params = (w_ih, w_hh, b_ih, b_hh)
suffix = '_reverse' if direction == 1 else ''
param_names = ['weight_ih_l{}{}', 'weight_hh_l{}{}']
if bias:
param_names += ['bias_ih_l{}{}', 'bias_hh_l{}{}']
param_names = [x.format(layer, suffix) for x in param_names]
for name, param in zip(param_names, layer_params):
setattr(self, name, param)
self._all_weights.append(param_names)
self.flatten_parameters()
self.reset_parameters()
quant_desc_input, quant_desc_weight = _utils.pop_quant_desc_in_kwargs(self.__class__, **kwargs)
self.init_quantizer(quant_desc_input, quant_desc_weight, num_layers=num_layers * (1 + bidirectional))
def flatten_parameters(self):
"""Resets parameter data pointer so that they can use faster code paths.
Right now, this works only if the module is on the GPU and cuDNN is enabled.
Otherwise, it's a no-op.
"""
any_param = next(self.parameters()).data
if not any_param.is_cuda or not torch.backends.cudnn.is_acceptable(any_param):
self._data_ptrs = []
return
# If any parameters alias, we fall back to the slower, copying code path. This is
# a sufficient check, because overlapping parameter buffers that don't completely
# alias would break the assumptions of the uniqueness check in
# Module.named_parameters().
unique_data_ptrs = set(p.data_ptr() for l in self.all_weights for p in l)
if len(unique_data_ptrs) != sum(len(l) for l in self.all_weights):
self._data_ptrs = []
return
with torch.cuda.device_of(any_param):
import torch.backends.cudnn.rnn as rnn
weight_arr = list(itertools.chain.from_iterable(self.all_weights))
weight_stride0 = len(self.all_weights[0])
# NB: This is a temporary hack while we still don't have Tensor
# bindings for ATen functions
with torch.no_grad():
# NB: this is an INPLACE function on weight_arr, that's why the
# no_grad() is necessary.
weight_buf = torch._cudnn_rnn_flatten_weight(weight_arr, weight_stride0, self.input_size,
rnn.get_cudnn_mode(self.mode), self.hidden_size,
self.proj_size, self.num_layers, self.batch_first,
bool(self.bidirectional))
self._param_buf_size = weight_buf.size(0)
self._data_ptrs = list(p.data.data_ptr() for p in self.parameters())
def _apply(self, fn):
ret = super(QuantRNNBase, self)._apply(fn)
self.flatten_parameters()
return ret
def reset_parameters(self):
stdv = 1.0 / math.sqrt(self.hidden_size)
for weight in self.parameters():
weight.data.uniform_(-stdv, stdv)
def check_forward_args(self, input, hidden, batch_sizes):
is_input_packed = batch_sizes is not None
expected_input_dim = 2 if is_input_packed else 3
if input.dim() != expected_input_dim:
raise RuntimeError(
'input must have {} dimensions, got {}'.format(
expected_input_dim, input.dim()))
if self.input_size != input.size(-1):
raise RuntimeError(
'input.size(-1) must be equal to input_size. Expected {}, got {}'.format(
self.input_size, input.size(-1)))
if is_input_packed:
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)
def check_hidden_size(hx, expected_hidden_size, msg='Expected hidden size {}, got {}'):
if tuple(hx.size()) != expected_hidden_size:
raise RuntimeError(msg.format(expected_hidden_size, tuple(hx.size())))
if self.mode == 'LSTM':
check_hidden_size(hidden[0], expected_hidden_size,
'Expected hidden[0] size {}, got {}')
check_hidden_size(hidden[1], expected_hidden_size,
'Expected hidden[1] size {}, got {}')
else:
check_hidden_size(hidden, expected_hidden_size)
def forward(self, input, hx=None):
is_packed = isinstance(input, PackedSequence)
if is_packed:
input, batch_sizes, sorted_indices, unsorted_indices = input
max_batch_size = batch_sizes[0]
max_batch_size = int(max_batch_size)
else:
batch_sizes = None
max_batch_size = input.size(0) if self.batch_first else input.size(1)
if hx is None:
num_directions = 2 if self.bidirectional else 1
hx = input.new_zeros(self.num_layers * num_directions,
max_batch_size, self.hidden_size,
requires_grad=False)
if self.mode == 'LSTM':
hx = (hx, hx)
has_flat_weights = list(p.data.data_ptr() for p in self.parameters()) == self._data_ptrs
if has_flat_weights:
first_data = next(self.parameters()).data
assert first_data.storage().size() == self._param_buf_size
flat_weight = first_data.new().set_(first_data.storage(), 0, torch.Size([self._param_buf_size]))
else:
flat_weight = None
self.check_forward_args(input, hx, batch_sizes)
func = quant_rnn.RNN(
self.mode,
self.input_size,
self.hidden_size,
num_layers=self.num_layers,
batch_first=self.batch_first,
dropout=self.dropout,
train=self.training,
bidirectional=self.bidirectional,
dropout_state=self.dropout_state,
variable_length=is_packed,
flat_weight=flat_weight
)
output, hidden = func(input, self.all_weights, hx, batch_sizes, self._input_quantizers, self._weight_quantizers)
if is_packed:
output = PackedSequence(output, batch_sizes)
return output, hidden
def extra_repr(self):
s = '{input_size}, {hidden_size}'
if self.num_layers != 1:
s += ', num_layers={num_layers}'
if self.bias is not True:
s += ', bias={bias}'
if self.batch_first is not False:
s += ', batch_first={batch_first}'
if self.dropout != 0:
s += ', dropout={dropout}'
if self.bidirectional is not False:
s += ', bidirectional={bidirectional}'
return s.format(**self.__dict__)
def __setstate__(self, d):
super(QuantRNNBase, self).__setstate__(d)
self.__dict__.setdefault('_data_ptrs', [])
if 'all_weights' in d:
self._all_weights = d['all_weights']
if isinstance(self._all_weights[0][0], str):
return
num_layers = self.num_layers
num_directions = 2 if self.bidirectional else 1
self._all_weights = []
for layer in range(num_layers):
for direction in range(num_directions):
suffix = '_reverse' if direction == 1 else ''
weights = ['weight_ih_l{}{}', 'weight_hh_l{}{}', 'bias_ih_l{}{}', 'bias_hh_l{}{}']
weights = [x.format(layer, suffix) for x in weights]
if self.bias:
self._all_weights += [weights]
else:
self._all_weights += [weights[:2]]
@property
def all_weights(self):
return [[getattr(self, weight) for weight in weights] for weights in self._all_weights]
class QuantRNN(QuantRNNBase):
r"""Applies a multi-layer Elman RNN with `tanh` or `ReLU` non-linearity to an
input sequence.
"""
def __init__(self, *args, **kwargs):
if 'proj_size' in kwargs:
raise ValueError("proj_size argument is only supported for LSTM, not RNN or GRU")
if 'nonlinearity' in kwargs:
if kwargs['nonlinearity'] == 'tanh':
mode = 'RNN_TANH'
elif kwargs['nonlinearity'] == 'relu':
mode = 'RNN_RELU'
else:
raise ValueError("Unknown nonlinearity '{}'".format(
kwargs['nonlinearity']))
del kwargs['nonlinearity']
else:
mode = 'RNN_TANH'
super(QuantRNN, self).__init__(mode, *args, **kwargs)
class QuantLSTM(QuantRNNBase):
r"""Applies a multi-layer long short-term memory (LSTM) RNN to an input
sequence.
"""
def __init__(self, *args, **kwargs):
super(QuantLSTM, self).__init__('LSTM', *args, **kwargs)
class GRU(QuantRNNBase):
r"""Applies a multi-layer gated recurrent unit (GRU) RNN to an input sequence.
"""
def __init__(self, *args, **kwargs):
super(GRU, self).__init__('GRU', *args, **kwargs)
class QuantRNNCellBase(nn.Module, _utils.QuantMixin):
default_quant_desc_weight = tensor_quant.QUANT_DESC_8BIT_LINEAR_WEIGHT_PER_ROW
def extra_repr(self):
s = '{input_size}, {hidden_size}'
if 'bias' in self.__dict__ and self.bias is not True:
s += ', bias={bias}'
if 'nonlinearity' in self.__dict__ and self.nonlinearity != "tanh":
s += ', nonlinearity={nonlinearity}'
return s.format(**self.__dict__)
def check_forward_input(self, input):
if input.size(1) != self.input_size:
raise RuntimeError(
"input has inconsistent input_size: got {}, expected {}".format(
input.size(1), self.input_size))
def check_forward_hidden(self, input, hx, hidden_label=''):
if input.size(0) != hx.size(0):
raise RuntimeError(
"Input batch size {} doesn't match hidden{} batch size {}".format(
input.size(0), hidden_label, hx.size(0)))
if hx.size(1) != self.hidden_size:
raise RuntimeError(
"hidden{} has inconsistent hidden_size: got {}, expected {}".format(
hidden_label, hx.size(1), self.hidden_size))
class QuantRNNCell(QuantRNNCellBase):
r"""An Elman RNN cell with tanh or ReLU non-linearity.
"""
def __init__(self, input_size, hidden_size, bias=True, nonlinearity="tanh"):
super(QuantRNNCell, self).__init__()
self.input_size = input_size
self.hidden_size = hidden_size
self.bias = bias
self.nonlinearity = nonlinearity
self.weight_ih = Parameter(torch.Tensor(hidden_size, input_size))
self.weight_hh = Parameter(torch.Tensor(hidden_size, hidden_size))
if bias:
self.bias_ih = Parameter(torch.Tensor(hidden_size))
self.bias_hh = Parameter(torch.Tensor(hidden_size))
else:
self.register_parameter('bias_ih', None)
self.register_parameter('bias_hh', None)
self.reset_parameters()
def reset_parameters(self):
stdv = 1.0 / math.sqrt(self.hidden_size)
for weight in self.parameters():
weight.data.uniform_(-stdv, stdv)
def forward(self, input, hx=None):
self.check_forward_input(input)
if hx is None:
hx = input.new_zeros(input.size(0), self.hidden_size, requires_grad=False)
self.check_forward_hidden(input, hx)
if self.nonlinearity == "tanh":
func = quant_rnn.RNNTanhCell
elif self.nonlinearity == "relu":
func = quant_rnn.RNNReLUCell
else:
raise RuntimeError(
"Unknown nonlinearity: {}".format(self.nonlinearity))
return func(
input, hx,
self.weight_ih, self.weight_hh,
self.bias_ih, self.bias_hh,
)
class QuantLSTMCell(QuantRNNCellBase):
r"""A long short-term memory (LSTM) cell.
"""
def __init__(self, input_size, hidden_size, bias=True, **kwargs):
super(QuantLSTMCell, self).__init__()
self.input_size = input_size
self.hidden_size = hidden_size
self.bias = bias
self.weight_ih = Parameter(torch.Tensor(4 * hidden_size, input_size))
self.weight_hh = Parameter(torch.Tensor(4 * hidden_size, hidden_size))
if bias:
self.bias_ih = Parameter(torch.Tensor(4 * hidden_size))
self.bias_hh = Parameter(torch.Tensor(4 * hidden_size))
else:
self.register_parameter('bias_ih', None)
self.register_parameter('bias_hh', None)
self.reset_parameters()
quant_desc_input, quant_desc_weight = _utils.pop_quant_desc_in_kwargs(self.__class__, **kwargs)
self.init_quantizer(quant_desc_input, quant_desc_weight)
def reset_parameters(self):
stdv = 1.0 / math.sqrt(self.hidden_size)
for weight in self.parameters():
weight.data.uniform_(-stdv, stdv)
def forward(self, input, hx=None):
self.check_forward_input(input)
if hx is None:
hx = input.new_zeros(input.size(0), self.hidden_size, requires_grad=False)
hx = (hx, hx)
self.check_forward_hidden(input, hx[0], '[0]')
self.check_forward_hidden(input, hx[1], '[1]')
return quant_rnn.LSTMCell(
input, hx,
self.weight_ih, self.weight_hh,
self.bias_ih, self.bias_hh,
self._input_quantizer, self._weight_quantizer
)
class GRUCell(QuantRNNCellBase):
r"""A gated recurrent unit (GRU) cell
"""
def __init__(self, input_size, hidden_size, bias=True):
super(GRUCell, self).__init__()
self.input_size = input_size
self.hidden_size = hidden_size
self.bias = bias
self.weight_ih = Parameter(torch.Tensor(3 * hidden_size, input_size))
self.weight_hh = Parameter(torch.Tensor(3 * hidden_size, hidden_size))
if bias:
self.bias_ih = Parameter(torch.Tensor(3 * hidden_size))
self.bias_hh = Parameter(torch.Tensor(3 * hidden_size))
else:
self.register_parameter('bias_ih', None)
self.register_parameter('bias_hh', None)
self.reset_parameters()
def reset_parameters(self):
stdv = 1.0 / math.sqrt(self.hidden_size)
for weight in self.parameters():
weight.data.uniform_(-stdv, stdv)
def forward(self, input, hx=None):
self.check_forward_input(input)
if hx is None:
hx = input.new_zeros(input.size(0), self.hidden_size, requires_grad=False)
self.check_forward_hidden(input, hx)
return quant_rnn.GRUCell(
input, hx,
self.weight_ih, self.weight_hh,
self.bias_ih, self.bias_hh,
)
LSTM = QuantLSTM
LSTMCell = QuantLSTMCell
@@ -0,0 +1,456 @@
#
# SPDX-FileCopyrightText: Copyright (c) 1993-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
"""TensorQuantizer Module"""
import math
from absl import logging
import torch
from torch import nn
from pytorch_quantization.tensor_quant import QuantDescriptor, tensor_quant, fake_tensor_quant, scaled_e4m3
from pytorch_quantization.nn.modules.clip import Clip
from pytorch_quantization import calib
import pytorch_quantization.utils as quant_utils
__all__ = ['TensorQuantizer']
class TensorQuantizer(nn.Module):
"""Tensor quantizer module
This module uses tensor_quant or fake_tensor_quant function to quantize a tensor. And wrappers variable, moving
statistics we'd want when training a quantized network.
Experimental features:
``clip`` stage learns range before enabling quantization.
``calib`` stage runs calibration
Args:
quant_desc: An instance of :func:`QuantDescriptor <pytorch_quantization.tensor_quant.QuantDescriptor>`.
disabled: A boolean. If True, by pass the whole module returns input. Default False.
if_quant: A boolean. If True, run main quantization body. Default True.
if_clip: A boolean. If True, clip before quantization and learn amax. Default False.
if_calib: A boolean. If True, run calibration. Not implemented yet. Settings of calibration will probably
go to :func:`QuantDescriptor <pytorch_quantization.tensor_quant.QuantDescriptor>`.
Raises:
Readonly Properties:
- axis:
- fake_quant:
- scale:
- step_size:
Mutable Properties:
- num_bits:
- unsigned:
- amax:
"""
_enable_onnx_export = False
def __init__(self, quant_desc=QuantDescriptor(), disabled=False, if_quant=True, if_clip=False, if_calib=False):
"""Initialize quantizer and set up required variables"""
super(TensorQuantizer, self).__init__()
# Expand quant_desc. Use quant_desc.dict would be eaiser, but adding one-by-one explicitly gives more control
self._num_bits = quant_desc.num_bits
self._fake_quant = quant_desc.fake_quant
self._axis = quant_desc.axis
self._scale_amax = quant_desc.scale_amax
self._learn_amax = quant_desc.learn_amax
self._unsigned = quant_desc.unsigned
self._narrow_range = quant_desc.narrow_range
self._scale = None if not quant_desc.fake_quant else 1.
self._disabled = disabled
self._if_quant = if_quant
self._if_clip = False
self._if_calib = if_calib
if quant_desc.amax is not None:
self.register_buffer('_amax', torch.tensor(quant_desc.amax))
# Clip module consumes a lot of memory, so only create it if learn_amax is True
if self._learn_amax:
init_amax = quant_desc.amax if quant_desc.amax is not None else 1.
self.clip = Clip(-init_amax, init_amax, learn_min=True, learn_max=True)
# It makes more sense to enable clip stage (which learns amax) if learn_amax is true
self.enable_clip()
if if_clip:
self.enable_clip()
if quant_desc.calib_method == "histogram":
logging.info("Creating histogram calibrator")
self._calibrator = calib.HistogramCalibrator(num_bits=self._num_bits,
axis=self._axis,
unsigned=self._unsigned)
elif quant_desc.calib_method == "max":
logging.info("Creating Max calibrator")
self._calibrator = calib.MaxCalibrator(num_bits=self._num_bits, axis=self._axis, unsigned=self._unsigned)
# pylint:disable=missing-docstring
@property
def num_bits(self):
return self._num_bits
@property
def maxbound(self):
if self._num_bits == (4, 3):
return 448.0
return (1 << (self._num_bits - 1 + int(self._unsigned))) - 1
@property
def unsigned(self):
return self._unsigned
@property
def scale(self):
if self._fake_quant:
logging.error("Fake quantize mode doesn't use scale explicitly!")
if self._scale is None:
logging.critical("Accessing scale before quantizing any tensor!")
return self._scale
@property
def pre_quant_scale(self):
if not hasattr(self, "_pre_quant_scale"):
return None
return self._pre_quant_scale
@property
def amax(self):
if not hasattr(self, "_amax"):
return None
return self._amax
@property
def step_size(self):
if not hasattr(self, "_amax"):
logging.error("step_size is undefined under dynamic amax mode!")
return None
return self._amax / (2.0**(self._num_bits - 1 + int(self._unsigned)) - 1.0)
@property
def axis(self):
return self._axis
@property
def fake_quant(self):
return self._fake_quant
@property
def narrow_range(self):
return self._narrow_range
def disable(self):
"""Bypass the module"""
self._disabled = True
def enable(self):
self._disabled = False
def disable_clip(self):
"""Disable clip stage"""
self._if_clip = False
self.clip.clip_value_min.requires_grad = False
self.clip.clip_value_max.requires_grad = False
def enable_clip(self):
"""Enable clip stage"""
logging.warning("Enable `clip` stage for amax learning.")
if not self._learn_amax:
raise ValueError("learn_amax is False. Cannot enable clip.")
self.clip.clip_value_min.requires_grad = True
self.clip.clip_value_max.requires_grad = True
self._if_clip = True
def disable_calib(self):
logging.warning("Disable {}".format(self._calibrator.__class__.__name__))
self._if_calib = False
def enable_calib(self):
if self._calibrator is None:
raise ValueError("Calibrator was not created, cannot enable calibration.")
logging.info("Enable {}".format(self._calibrator.__class__.__name__))
self._if_calib = True
def disable_quant(self):
logging.info("Disable `quant` stage.")
self._if_quant = False
def enable_quant(self):
logging.info("Enable `quant` stage.")
self._if_quant = True
@amax.setter
def amax(self, value):
if value is None:
logging.error("Setting amax no None is meaningless.")
else:
if isinstance(value, torch.Tensor):
logging.warning("amax setter is not designed to take tensor.")
if not hasattr(self, "_amax"):
self.register_buffer('_amax', torch.tensor(value))
else:
value = torch.tensor(value, device=self._amax.device)
if self._amax.shape != value.shape:
raise RuntimeError("Changing shape when setting amax is not allowed.")
self._amax.data.copy_(value.data)
@pre_quant_scale.setter
def pre_quant_scale(self, value):
if value is None:
logging.error("Setting pre_quant_scale no None is meaningless.")
else:
if not hasattr(self, "_pre_quant_scale"):
self.register_buffer('_pre_quant_scale', torch.tensor(value))
else:
value = torch.tensor(value, device=self._pre_quant_scale.device)
if self._pre_quant_scale.shape != value.shape:
raise RuntimeError("Changing shape when setting pre_quant_scale is not allowed.")
self._pre_quant_scale.data.copy_(value.data)
@num_bits.setter
def num_bits(self, value):
self._num_bits = value
@unsigned.setter
def unsigned(self, value):
self._unsigned = value
@narrow_range.setter
def narrow_range(self, value):
self._narrow_range = value
# pylint:enable=missing-docstring
def load_calib_amax(self, *args, **kwargs):
"""Load amax from calibrator.
Updates the amax buffer with value computed by the calibrator, creating it if necessary.
*args and **kwargs are directly passed to compute_amax, except "strict" in kwargs. Refer to
compute_amax for more details.
"""
strict = kwargs.pop("strict", True)
if getattr(self, '_calibrator', None) is None:
raise RuntimeError("Calibrator not created.")
calib_amax = self._calibrator.compute_amax(*args, **kwargs)
if calib_amax is None:
err_msg = "Calibrator returned None. This usually happens when calibrator hasn't seen any tensor."
if not strict:
logging.warning(err_msg)
logging.warning("Set amax to NaN!")
calib_amax = torch.tensor(math.nan)
else:
raise RuntimeError(err_msg + " Passing 'strict=False' to `load_calib_amax()` will ignore the error.")
logging.warning("Load calibrated amax, shape={}.".format(calib_amax.shape))
logging.log_first_n(logging.WARNING, "Call .cuda() if running on GPU after loading calibrated amax.", 1)
if not hasattr(self, '_amax'):
self.register_buffer("_amax", calib_amax.data)
else:
self._amax.copy_(calib_amax)
def init_learn_amax(self):
"""Initialize learned amax from fixed amax"""
if self._learn_amax is False:
raise RuntimeError("Called init_learn_amax with learn_amax=False.")
logging.warning("Load amax as initial value for amax learning!")
if self._amax.numel() != 1:
logging.warning("Per channel learned amax not supported. Initializing with max(amax).")
init_amax = torch.max(self._amax)
else:
init_amax = self._amax
self.clip.clip_value_min.data.copy_(-init_amax.data)
self.clip.clip_value_max.data.copy_(init_amax.data)
def _get_amax(self, inputs):
"""get amax from buffer or compute it dynamically."""
if hasattr(self, '_amax'):
amax = self._amax
else:
if self._axis is None:
reduce_axis = None
else:
reduce_axis = []
# Swap axis to reduce
axis = self._axis if isinstance(self._axis, (list, tuple)) else [self._axis]
for i in range(inputs.dim()):
if not i in axis:
reduce_axis.append(i)
amax = quant_utils.reduce_amax(inputs, axis=reduce_axis, keepdims=True).detach()
if self._scale_amax is not None:
amax = amax.detach() * self._scale_amax
amax = amax.data
# cast amax to float32 if it is in a lower precision dtype
if amax.dtype not in (torch.double, torch.float):
amax = amax.float()
return amax
def _quant_forward(self, inputs):
"""Quantized forward pass."""
if self._learn_amax:
inputs = self.clip(inputs)
amax = torch.max(-self.clip.clip_value_min, self.clip.clip_value_max).detach()
else:
amax = self._get_amax(inputs)
if self._fake_quant:
outputs = fake_tensor_quant(inputs, amax, self._num_bits, self._unsigned, self._narrow_range)
else:
outputs, self._scale = tensor_quant(inputs, amax, self._num_bits, self._unsigned)
return outputs
def _check_onnx_readiness(self, inputs):
"""Check if quantizer is ready for ONNX export."""
assert hasattr(
self, '_amax'), ("Quantizer has not been calibrated. ONNX export requires the quantizer to be calibrated."
"Calibrate and load amax before exporting to ONNX.")
if self._if_calib:
logging.warning("Quantizer is in calibration mode. "
"Please complete calibration before exporting to ONNX for correct results.")
amax = self._get_amax(inputs)
# We only support scalar amax for E4M3 ONNX export
if isinstance(self.num_bits, tuple):
assert amax.numel() == 1, ("E4M3 supports ONNX export only for per-tensor quantization."
" Per-tensor quantization requires scalar amax. "
f"Received non-scalar amax of shape: {amax.shape}")
def forward(self, inputs):
"""Apply tensor_quant function to inputs
Args:
inputs: A Tensor of type float32.
Returns:
outputs: A Tensor of type output_dtype
"""
if self._enable_onnx_export:
self._check_onnx_readiness(inputs)
# Activation scaling for smoothquant
if self.pre_quant_scale is not None:
inputs = inputs * self.pre_quant_scale
if self._disabled:
return inputs
outputs = inputs
if self._if_calib:
if self._calibrator is None:
raise RuntimeError("Calibrator was not created.")
# Shape is only known when it sees the first tensor
self._calibrator.collect(inputs)
if self._if_clip:
if not self._learn_amax:
raise RuntimeError("Clip without learning amax is not implemented.")
outputs = self.clip(inputs)
if self._if_quant:
if not isinstance(self._num_bits, tuple):
outputs = self._quant_forward(inputs)
else:
E, M = self._num_bits
outputs = scaled_e4m3(inputs, self._get_amax(inputs), E, M)
return outputs
def _short_amax(self, fmt='.4f'):
"""Short description of amax
Returns:
'dynamic': if _amax is not registered
'amax': if _amax is per-tensor
'[min, max](size)': if _amax is per-channel
"""
if not hasattr(self, '_amax'):
return 'dynamic'
if self._amax is None:
return "None"
if self._amax.numel() == 1:
return '{:{fmt}}'.format(self._amax.item(), fmt=fmt)
return '[{:{fmt}}, {:{fmt}}]({})'.format(self._amax.min().item(),
self._amax.max().item(),
self._amax.numel(),
fmt=fmt)
def extra_repr(self):
if self._disabled:
return "disabled"
s = "{}{}bit".format("unsigned " if self._unsigned else "", self._num_bits)
s += " narrow" if (self._narrow_range) else ""
s += " fake" if (self._fake_quant) else ""
s += " axis={}".format(self._axis) if self._axis is not None else " per-tensor"
s += " amax={}".format(self._short_amax())
s += " *{}".format(self._scale_amax) if self._scale_amax else ""
s += " pre_quant_scale" if self.pre_quant_scale is not None else ""
s += " learned" if (self._learn_amax) else ""
s += " calibrator={}".format(self._calibrator.__class__.__name__) if (self._calibrator is not None) else ""
s += " scale={}".format(self._scale) if self._scale is not None else ""
s += " quant" if (self._if_quant) else ""
s += " clip" if (self._if_clip) else ""
s += " calib" if (self._if_calib) else ""
return s
def _load_from_state_dict(self, state_dict, prefix, *args, **kwargs):
"""Overloaded module function
Adds warnings during state_dict loading.
A workaround is implemented for loading amax from checkpoint and only supports CUDA.
Args:
state_dict: A dict containing the state of the top level module
prefix: A string that prefixes all of this modules state in state_dict, e.g. 'model.conv1.'
"""
dst_has_amax = '_amax' in self._buffers
src_has_amax = prefix + '_amax' in state_dict
if not src_has_amax and dst_has_amax:
logging.error("{}: No amax in state_dict.".format(prefix[:-1]))
elif src_has_amax and not dst_has_amax:
logging.debug(("{}: No '_amax' buffer to load amax into."
" '_amax` will be created as WAR for now. "
"This behavior will change in future.").format(prefix[:-1]))
self.register_buffer("_amax", state_dict[prefix + '_amax'].data.cuda())
elif src_has_amax and dst_has_amax:
logging.warning("{}: Overwriting amax.".format(prefix[:-1]))
dst_has_pre_quant_scale = '_pre_quant_scale' in self._buffers
src_has_pre_quant_scale = prefix + '_pre_quant_scale' in state_dict
if not src_has_pre_quant_scale and dst_has_pre_quant_scale:
logging.error("{}: No pre_quant_scale in state_dict.".format(prefix[:-1]))
elif src_has_pre_quant_scale and not dst_has_pre_quant_scale:
logging.debug(("{}: No '_pre_quant_scale' buffer to load pre_quant_scale into."
" '_pre_quant_scale` will be created as WAR for now. "
"This behavior will change in future.").format(prefix[:-1]))
self.register_buffer("_pre_quant_scale", state_dict[prefix + '_pre_quant_scale'].data.cuda())
elif src_has_pre_quant_scale and dst_has_pre_quant_scale:
logging.warning("{}: Overwriting pre_quant_scale.".format(prefix[:-1]))
super(TensorQuantizer, self)._load_from_state_dict(state_dict, prefix, *args, **kwargs)
@@ -0,0 +1,131 @@
#
# SPDX-FileCopyrightText: Copyright (c) 1993-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
"""Helper functions for quant optimizer/trainer"""
import re
from absl import logging
def match_parameters(model, patterns):
"""Returns an generator over module parameters if name matches key
It is useful to group parameters, and apply different functions to different group. This function provides an easy
way to group them.
Args:
model: A Module
patterns: A list of strings that will be used to match parameter names. If parameter name contains any pattern,
it will be yield
Yields:
param: Module parameters
"""
for name, param in model.named_parameters():
for pattern in patterns:
if re.search(pattern, name):
yield param
def group_parameters(model, patterns_list, lrs=None, momentums=None, weight_decays=None):
"""Group parameters for using per-parameters option in optimizer
Returns a list of dict that matches Pytorch optimizer fashion, see
https://pytorch.org/docs/stable/optim.html#per-parameter-options for more details.
Example:
>>> [
>>> {'params': model.base.parameters()},
>>> {'params': model.classifier.parameters(), 'lr': 1e-3}
>>> ]
Parameters will be grouped w.r.t first level of the keys_list. e.g. `keys_list=[['conv1', 'conv2'], ['conv3']]` will
return 2 groups, one with `conv1` and `conv2` in name, and the other with `conv3` in name.
If lr, momentum or weight_decay are supplied, they will be added to the group as well.
Args:
model: A module
patterns_list: A list of list of strings. WARNING: patters must be EXCLUSIVE, the function doesn't
perform exclusive check.
lrs: A list of float with same length as keys_list or None.
momentums: A list of float with same length as keys_list or None.
weight_decays: A list of float with same length as keys_list or None.
Returns:
param_group: A list of dict
"""
param_groups = []
for pattern in patterns_list:
if not isinstance(pattern, list):
raise TypeError("patterns_list must be list of list of patterns")
param_groups.append({'params': match_parameters(model, pattern)})
if lrs is not None:
if len(lrs) != len(patterns_list):
raise TypeError("len(lrs) must match len(patterns_list)")
for i, lr in enumerate(lrs):
param_groups[i]['lr'] = lr
if momentums is not None:
if len(momentums) != len(patterns_list):
raise TypeError("len(momentums) must match len(patterns_list)")
for i, momentum in enumerate(momentums):
param_groups[i]['momentum'] = momentum
if weight_decays is not None:
if len(weight_decays) != len(patterns_list):
raise TypeError("len(weight_decays) must match len(patterns_list)")
for i, weight_decay in enumerate(weight_decays):
param_groups[i]['weight_decay'] = weight_decay
return param_groups
def freeze_parameters(model, patterns):
"""Set requires_grad to False if patterns match name
Args:
model: A Module
patterns: A list of strings that will be used to match parameter names. If parameter name contains any pattern,
it will be frozen.
"""
for name, param in model.named_parameters():
for pattern in patterns:
if re.search(pattern, name):
logging.warning("Freeze %s.", name)
param.requires_grad = False
def quant_weight_inplace(model):
"""Make quantization inplace
Search for quantized modules including QuantConvNd and QuantLinear, make weight quantization in place using
weight_quantizer.
Most publications of quantization aware training uses STE by default, which is really an approximation of
derivative of the nondifferentiable quantization function, which works to some extended but by no means the F=ma of
the problem.
Inplace quantization can be used to implement relax-and-round, which is a common method in Discrete Optimization's
or Integer Programming.
"""
for name, module in model.named_modules():
if hasattr(module, '_weight_quantizer') and module.weight_quantizer is not None:
if not module.weight_quantizer.fake_quant:
logging.warning(("In-place real quantization is VERY dangerous and should be used for inference only. "
"Make sure that is the desired behavior."))
logging.warning("In-place quantize weight of %s", name)
module.weight.data.copy_(module.weight_quantizer(module.weight))
@@ -0,0 +1,182 @@
#
# SPDX-FileCopyrightText: Copyright (c) 1993-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
"""Dynamically replace the modules with quantized versions."""
from collections import namedtuple
from contextlib import contextmanager
import torch
from pytorch_quantization import nn as quant_nn
__all__ = ['initialize', 'deactivate', 'enable_onnx_export']
# Definition of the named tuple that is used to store mapping of the quantized modules
_quant_entry = namedtuple('quant_entry', 'orig_mod mod_name replace_mod')
# Global member of the file that contains the mapping of quantized modules
_DEFAULT_QUANT_MAP = [
_quant_entry(torch.nn, "Conv1d", quant_nn.QuantConv1d),
_quant_entry(torch.nn, "Conv2d", quant_nn.QuantConv2d),
_quant_entry(torch.nn, "Conv3d", quant_nn.QuantConv3d),
_quant_entry(torch.nn, "ConvTranspose1d", quant_nn.QuantConvTranspose1d),
_quant_entry(torch.nn, "ConvTranspose2d", quant_nn.QuantConvTranspose2d),
_quant_entry(torch.nn, "ConvTranspose3d", quant_nn.QuantConvTranspose3d),
_quant_entry(torch.nn, "Linear", quant_nn.QuantLinear),
_quant_entry(torch.nn, "LSTM", quant_nn.QuantLSTM),
_quant_entry(torch.nn, "LSTMCell", quant_nn.QuantLSTMCell),
_quant_entry(torch.nn, "AvgPool1d", quant_nn.QuantAvgPool1d),
_quant_entry(torch.nn, "AvgPool2d", quant_nn.QuantAvgPool2d),
_quant_entry(torch.nn, "AvgPool3d", quant_nn.QuantAvgPool3d),
_quant_entry(torch.nn, "AdaptiveAvgPool1d", quant_nn.QuantAdaptiveAvgPool1d),
_quant_entry(torch.nn, "AdaptiveAvgPool2d", quant_nn.QuantAdaptiveAvgPool2d),
_quant_entry(torch.nn, "AdaptiveAvgPool3d", quant_nn.QuantAdaptiveAvgPool3d),
]
class QuantModuleReplacementHelper():
"""To help replace torch.nn modules with quantized versions.
This module is used to replace (by monkey patching) the torch.nn modules with their
quantized versions as provided by either tool's internal implementation or any other
user provided custom module.
Attributes:
orginal_func_map: A dict. Maintains the original torch.nn module mapping.
quant_support_list: A list. Contains the names of modules for which a quantized
version is provided by the tool.
quant_map: A dict. Contains the map of the module name and its quantized versions.
quant_switch_opt: A dict. A map to indicate which modules to be left unreplaced with
their quantized versions. This dict is updated by a list provided from the user
which indicates the modules to leave out in monkey patching.
"""
def __init__(self):
# Will hold the original modules to be replaced back
self.orginal_func_map = set()
# Maintains the list of supported quantized modules by the tool as default
self.default_quant_map = _DEFAULT_QUANT_MAP
# Will hold the final quantized modules after checking if user supplied any
# custom quantized functions.
self.quant_map = set()
def prepare_state(self, float_module_list=None, custom_map=None):
"""
Prepare the internal variables that would used in the monkey patching mechanism later.
1. Set up the list of quantized modules that are supported by the tool for torch.nn.
2. Set up the custom mapping for modules other than torch.nn.
3. Use the float_module_list to switch off the monkey patching replacement for user indicated modules
"""
# For the default quantized modules supported, generate the quant_map
for item in self.default_quant_map:
if float_module_list is not None and item.mod_name in float_module_list:
# Skip this module if this is present in the float_module_list
continue
else:
# append the modules into the variable that will be used in monkey patching
self.quant_map.add(item)
# also store the original module to be used in reverse monkey patching
self.orginal_func_map.add(
_quant_entry(item.orig_mod, item.mod_name, getattr(item.orig_mod, item.mod_name)))
# Add custom modules to the quant_map
if custom_map is not None:
for item in custom_map:
# append the custom modules to the list that will be used in monkey patching
# Note that we convert a tuple to a named tuple here
self.quant_map.add(_quant_entry(item[0], item[1], item[2]))
# also store the original module in another list which will be used to reverse monkey patching
self.orginal_func_map.add(_quant_entry(item[0], item[1], getattr(item[0], item[1])))
def apply_quant_modules(self):
"""
For the modules registered in the quant_map, simply monkey patch them and also store the
original modules so that they could be later replaced back.
"""
for entry in self.quant_map:
setattr(entry.orig_mod, entry.mod_name, entry.replace_mod)
def restore_float_modules(self):
"""
Reverse the effect of monkey patch by using the orginal_func_map to replace back the
original modules.
"""
for entry in self.orginal_func_map:
setattr(entry.orig_mod, entry.mod_name, entry.replace_mod)
def initialize(float_module_list=None, custom_quant_modules=None):
"""Dynamic module replacement using monkey patching.
Dynamically monkey patches the modules with their quantized versions. Internally, the
state is maintained by a helper class object which helps in replacing the original
modules back.
Args:
float_module_list: A list. User supplied list which indicates which modules to not monkey patch.
custom_quant_modules: A dict. A mapping provided by user to indicate any other module apart
from torch.nn and its corresponding quantized version.
Returns:
nothing.
Typical usage example:
# Define the deny list for torch.nn modules and custom map for modules other than torch.nn.
float_module_list = ["Linear"]
custom_quant_modules = [(torch.nn, "Linear", quant_nn.QuantLinear)]
## Monkey patch the modules
pytorch_quantization.quant_modules.initialize(float_module_list, custom_modules)
## Use the quantized modules
pytorch_quantization.quant_modules.deactivate()
"""
_quant_module_helper_object.prepare_state(float_module_list, custom_quant_modules)
_quant_module_helper_object.apply_quant_modules()
def deactivate():
"""Dynamic module replacement which reverses the monkey patching.
Dynamically replaces back the original modules that were monkey patched earlier
in the initialize() function call using helper class object which maintains the state.
"""
_quant_module_helper_object.restore_float_modules()
# Global object that maintains the state of the modules that are replaced.
_quant_module_helper_object = QuantModuleReplacementHelper()
@contextmanager
def enable_onnx_export():
"""Context manager to enable onnx export.
.. code-block:: python
with pytorch_quantization.enable_onnx_export():
# export onnx model
torch.onnx.export(model, ...)
"""
quant_nn.TensorQuantizer._enable_onnx_export = True
yield
quant_nn.TensorQuantizer._enable_onnx_export = False
@@ -0,0 +1,624 @@
#
# SPDX-FileCopyrightText: Copyright (c) 1993-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
"""Basic tensor quantization functions"""
import numpy as np
import numbers
import yaml
from absl import logging
import torch
import torch._C._onnx as _C_onnx
from torch.autograd import Function
from pytorch_quantization import cuda_ext
from torch.onnx import symbolic_helper
class ScaledQuantDescriptor():
"""Supportive descriptor of quantization
Describe how a tensor should be quantized. A QuantDescriptor and a tensor defines a quantized tensor.
Args:
num_bits: An integer or a tuple of two integers.
Specifically, `num_bits` can be:
#. A positive integer argument for integer qunatization. `num_bits` specify
the number of bits used for integer quantization.
#. Constant integer tuple (4,3) for E4M3 floating point quantization emulating
Nvidia's FP8 quantization. E4M3 quantization only supports per-tensor quantization.
Default: 8.
name: Seems a nice thing to have
Keyword Arguments:
fake_quant: A boolean. If True, use fake quantization mode. Default True.
axis: None, int or tuple of int. axes which will have its own max for computing scaling factor.
If None (the default), use per tensor scale.
Must be in the range [-rank(input_tensor), rank(input_tensor)).
e.g. For a KCRS weight tensor, quant_axis=(0) will yield per channel scaling.
Default None.
amax: A float or list/ndarray of floats of user specified absolute max range. If supplied,
ignore quant_axis and use this to quantize. If learn_amax is True, will be used to initialize
learnable amax. Default None.
learn_amax: A boolean. If True, learn amax. Default False.
scale_amax: A float. If supplied, multiply amax by scale_amax. Default None. It is useful for some
quick experiment.
calib_method: A string. One of ["max", "histogram"] indicates which calibration to use. Except the simple
max calibration, other methods are all hisogram based. Default "max".
unsigned: A Boolean. If True, use unsigned. Default False.
Raises:
TypeError: If unsupported type is passed in.
Read-only properties:
- fake_quant:
- name:
- learn_amax:
- scale_amax:
- axis:
- calib_method:
- num_bits:
- amax:
- unsigned:
"""
def __init__(self, num_bits=8, name=None, **kwargs):
if isinstance(num_bits, int):
if num_bits < 0:
raise ValueError("num_bits must be > 0, not {}.".format(num_bits))
if num_bits == 0:
logging.error("num_bits is 0. This will result in the tensor being quantized to all zeros."
" This mode should only be used for debugging purposes.")
elif num_bits != (4, 3):
raise TypeError("num_bits must be a postive integer or tuple (4,3), not {}.".format(type(num_bits)))
self._num_bits = num_bits
if not isinstance(name, str) and name is not None:
raise TypeError("name must be a string or None, not {}.".format(type(name)))
self._name = name
self._fake_quant = kwargs.pop('fake_quant', True)
self._axis = kwargs.pop('axis', None)
if self._axis is not None:
logging.debug("Meaning of axis has changed since v2.0. Make sure to update.")
self._learn_amax = kwargs.pop('learn_amax', False)
if self._learn_amax and self._axis is not None:
raise TypeError("axis is ignored and must be None when learn_amax is true, got {}.".format(type(
self._axis)))
amax = kwargs.pop('amax', None)
if amax is not None:
if not isinstance(amax, float) and not isinstance(amax, list) and not isinstance(amax, np.ndarray):
raise TypeError("amax must be float, list or ndarray, not {}".format(type(amax)))
# Make it single precision array
self._amax = np.array(amax, dtype=np.float32)
else:
self._amax = amax
self._scale_amax = kwargs.pop('scale_amax', None)
self._calib_method = kwargs.pop('calib_method', "max")
self._unsigned = kwargs.pop('unsigned', False)
self._narrow_range = kwargs.pop('narrow_range', False)
if kwargs:
raise TypeError("Unused keys: {}".format(kwargs.keys()))
# pylint:disable=missing-docstring
@property
def num_bits(self):
return self._num_bits
@property
def fake_quant(self):
return self._fake_quant
@property
def axis(self):
return self._axis
@property
def amax(self):
return self._amax
@property
def learn_amax(self):
return self._learn_amax
@property
def scale_amax(self):
return self._scale_amax
@property
def name(self):
return self._name
@property
def calib_method(self):
return self._calib_method
@property
def unsigned(self):
return self._unsigned
@property
def narrow_range(self):
return self._narrow_range
# pylint:enable=missing-docstring
def __str__(self):
s = (self._name + ': ') if self._name is not None else 'QuantDescriptor'
s += "({}{}bit".format("unsigned " if self._unsigned else "", self._num_bits)
s += " fake" if self._fake_quant else " real"
s += " axis={}".format(self._axis if self._axis is not None else " per-tensor")
if isinstance(self._amax, torch.Tensor):
s += " amax={}".format(
np.array2string(self._amax.cpu().numpy().flatten(), edgeitems=1, formatter={'all': "{:.2e}".format}))
elif self._amax is not None:
s += " amax={_amax}"
s += " full_range"
if self._learn_amax:
s += " learn_amax"
if self._scale_amax:
s += " scale_amax={_scale_amax}"
s += ")"
return s.format(**self.__dict__)
def __eq__(self, rhs):
"""Compare 2 descriptors"""
return self.__dict__ == rhs.__dict__
def dict(self):
"""Serialize to dict
The build-in __dict__ method returns all the attributes, which includes those have default value and have
protected prefix "_". This method only returns those have values other than the default one and don't have _ in
key. Construct a instance by dict returned by this method should get exactly the same instance.
"""
obj_dict = {}
obj_dict['num_bits'] = self._num_bits
obj_dict['name'] = self._name
if not self._fake_quant:
obj_dict['fake_quant'] = self._fake_quant
if self._axis is not None:
obj_dict['axis'] = self._axis
if self._amax is not None:
obj_dict['amax'] = self._amax.tolist()
if self._scale_amax is not None:
obj_dict['scale_amax'] = self._scale_amax
if self._learn_amax:
obj_dict['learn_amax'] = self._learn_amax
if self._unsigned:
obj_dict['unsigned'] = self._unsigned
return obj_dict
def to_yaml(self):
"""Create yaml serialization
Some attributes need special treatment to have human readable form, including amax, axis.
"""
obj_dict = self.dict()
if "axis" in obj_dict:
obj_dict['axis'] = list(obj_dict['axis'])
return yaml.dump(obj_dict, width=120)
@classmethod
def from_yaml(cls, yaml_str):
"""Create descriptor from yaml str"""
obj_dict = yaml.safe_load(yaml_str)
if 'axis' in obj_dict:
obj_dict['axis'] = tuple(obj_dict['axis'])
quant_desc = cls(**obj_dict)
return quant_desc
QuantDescriptor = ScaledQuantDescriptor
# Predefined descriptors
QUANT_DESC_8BIT_PER_TENSOR = QuantDescriptor(num_bits=8)
QUANT_DESC_UNSIGNED_8BIT_PER_TENSOR = QuantDescriptor(num_bits=8, unsigned=True)
QUANT_DESC_8BIT_CONV1D_WEIGHT_PER_CHANNEL = QuantDescriptor(num_bits=8, axis=(0))
QUANT_DESC_8BIT_CONV2D_WEIGHT_PER_CHANNEL = QuantDescriptor(num_bits=8, axis=(0))
QUANT_DESC_8BIT_CONV3D_WEIGHT_PER_CHANNEL = QuantDescriptor(num_bits=8, axis=(0))
QUANT_DESC_8BIT_LINEAR_WEIGHT_PER_ROW = QuantDescriptor(num_bits=8, axis=(0))
QUANT_DESC_8BIT_CONVTRANSPOSE1D_WEIGHT_PER_CHANNEL = QuantDescriptor(num_bits=8, axis=(1))
QUANT_DESC_8BIT_CONVTRANSPOSE2D_WEIGHT_PER_CHANNEL = QuantDescriptor(num_bits=8, axis=(1))
QUANT_DESC_8BIT_CONVTRANSPOSE3D_WEIGHT_PER_CHANNEL = QuantDescriptor(num_bits=8, axis=(1))
@torch.jit.script
def _fake_tensor_quant_backward(inputs, amax, grad_outputs):
zero = grad_outputs.new_zeros(1)
grad_inputs = torch.where(inputs.abs() <= amax, grad_outputs, zero)
return grad_inputs
def _onnx_int8_helper(g, inputs, amax, num_bits, unsigned, narrow_range):
assert num_bits == 8, "Only INT8 ONNX export is supported for now."
maxbound = (1 << (num_bits - 1 + int(unsigned))) - 1
if amax.numel() == 1:
zero_point, axis = torch.tensor(0.0, device=amax.device), None
else:
amax_init_shape = amax.shape
amax = amax.squeeze().data
assert len(amax.shape) == 1, "ONNX does not support multi-axis quantization."
zero_point = torch.zeros_like(amax, dtype=torch.int32).data
axis = list(amax_init_shape).index(list(amax.shape)[0])
zero_point = g.op("Constant", value_t=zero_point)
if not unsigned:
assert not narrow_range, "ONNX does not support unsigned narrow range INT8."
zero_point = g.op("Cast", zero_point, to_i=_C_onnx.TensorProtoDataType.INT8)
else:
zero_point = g.op("Cast", zero_point, to_i=_C_onnx.TensorProtoDataType.UINT8)
scale = amax / maxbound
scale.masked_fill_(scale == 0, 1.0)
scale = g.op("Constant", value_t=scale)
input_type = inputs.type().scalarType()
# Q inputs are currently constrained to FP32 due to a similar limitation in ORT
# custom ops, so cast the input if needed.
if input_type == "Half" or input_type == "BFloat16":
inputs = g.op("Cast", inputs, to_i=_C_onnx.TensorProtoDataType.FLOAT)
quantized = g.op("QuantizeLinear", inputs, scale, zero_point, axis_i=axis)
out = g.op("DequantizeLinear", quantized, scale, zero_point, axis_i=axis)
# DQ outputs are currently constrained to FP32 due to a similar limitation in ORT
# custom ops, so cast the output if needed.
if input_type == "Half":
out = g.op("Cast", out, to_i=_C_onnx.TensorProtoDataType.FLOAT16)
elif input_type == "BFloat16":
out = g.op("Cast", out, to_i=_C_onnx.TensorProtoDataType.BFLOAT16)
return out
class FakeTensorQuantFunction(Function):
"""Fake version of TensorQuantFunction use CUDA extension"""
@staticmethod
@symbolic_helper.parse_args("v", "t", "i", "b", "b")
def symbolic(g, inputs, amax, num_bits=8, unsigned=False, narrow_range=True):
return _onnx_int8_helper(g, inputs, amax, num_bits, unsigned, narrow_range)
@staticmethod
def forward(ctx, inputs, amax, num_bits=8, unsigned=False, narrow_range=True):
ctx.save_for_backward(inputs, amax)
def legacy_quant_func():
# The LegacyFakeTensorQuantFunction support cpu and amax with any shape that can be broadcasted to inputs.
outputs, scale = _tensor_quant(inputs, amax, num_bits, unsigned, narrow_range)
return outputs / scale.to(inputs.dtype)
if not inputs.is_cuda:
outputs = legacy_quant_func()
else:
try:
if amax.numel() == 1:
outputs = cuda_ext.fake_tensor_quant(inputs, amax, num_bits, unsigned, narrow_range)
else:
axis = amax.shape.index(amax.numel())
outputs = cuda_ext.fake_tensor_quant_with_axis(inputs, amax.squeeze(), axis, num_bits, unsigned,
narrow_range)
except (RuntimeError, ValueError) as error:
outputs = legacy_quant_func()
return outputs
@staticmethod
def backward(ctx, grad_outputs):
inputs, amax = ctx.saved_tensors
return _fake_tensor_quant_backward(inputs, amax, grad_outputs), None, None, None, None
def _onnx_fp8_quantize(g, inputs, scale_inv):
"""Helper Function for Quantization"""
output_shape = torch.onnx.symbolic_helper._get_tensor_sizes(inputs)
# Q inputs are currently constrained to FP32 due to a similar limitation in ORT
# custom ops, so cast the input if needed.
if inputs.type().scalarType() == "Half" or inputs.type().scalarType() == "BFloat16":
inputs = g.op("Cast", inputs, to_i=_C_onnx.TensorProtoDataType.FLOAT)
scale = g.op("Constant", value_t=torch.tensor(scale_inv))
q_op = g.op("trt::TRT_FP8QuantizeLinear", inputs,
scale).setType(inputs.type().with_dtype(torch.uint8).with_sizes(output_shape))
return q_op
def _onnx_fp8_dequantize(g, inputs, scale_inv, otype=None):
"""Helper Function for Dequantization"""
output_shape = torch.onnx.symbolic_helper._get_tensor_sizes(inputs)
scale = g.op("Constant", value_t=torch.tensor(scale_inv))
out = g.op("trt::TRT_FP8DequantizeLinear", inputs,
scale).setType(inputs.type().with_dtype(torch.float32).with_sizes(output_shape))
# DQ outputs are currently constrained to FP32 due to a similar limitation in ORT
# custom ops, so cast the output if needed.
if otype == "Half":
out = g.op("Cast", out, to_i=_C_onnx.TensorProtoDataType.FLOAT16)
elif otype == "BFloat16":
out = g.op("Cast", out, to_i=_C_onnx.TensorProtoDataType.BFLOAT16)
return out
class ScaledE4M3Function(Function):
"""E4M3fy input with scale"""
@staticmethod
@symbolic_helper.parse_args("v", "t", "i", "b", "b")
def symbolic(g, inputs, amax=None, E=4, M=3):
if amax is None:
scale = 1.0
else:
scale = 448.0 / amax
scale = float(scale.masked_fill_(scale == 0, 1.0))
input_type = inputs.type().scalarType()
q_tensor = _onnx_fp8_quantize(g, inputs, 1.0 / scale)
return _onnx_fp8_dequantize(g, q_tensor, 1.0 / scale, input_type)
@staticmethod
def forward(ctx, inputs, amax=None, E=4, M=3):
if E != 4 or M != 3:
raise NotImplementedError("Only support E=4 & M=3 for now.")
ctx.save_for_backward(inputs)
ctx.amax = amax
zero_mask = (inputs.abs() < 1. / (1 << 24))
if amax is None:
outputs = cuda_ext.fake_e4m3fy(inputs)
else:
# FP8 ONNX export requires scalar `scale`.
# To simplify implementation, amax is enforced to be a scalar.
scale = 448.0 / amax
outputs = cuda_ext.fake_e4m3fy(inputs * scale) / scale
# Zero out values that are tiny.
# Tiny values could lead to tiny amax and then large scale which cause overflow/saturation
# and won't go back to normal value after dividing by scale. The right behavior is to mark them
# as zero which also get rid of inf/nan
outputs[zero_mask] = 0.
return outputs
@staticmethod
def backward(ctx, grad_outputs):
inputs, = ctx.saved_tensors
amax = torch.tensor(ctx.amax if ctx.amax is not None else 448.0, dtype=torch.float32, device=inputs.device)
grad_inputs = _fake_tensor_quant_backward(inputs, amax, grad_outputs)
return grad_inputs, None, None, None
class TensorQuantFunction(Function):
"""A universal tensor quantization function
Take an input tensor, output an quantized tensor. The granularity of scale can be interpreted from the
shape of amax.
output_dtype indicates whether the quantized value will be stored in integer or float. The reason we want to store
it in float is the pytorch function takes the quantized value may not accept integer input, e.g. Conv2D.
It uses 2^num_bits -1 values instead of 2^num_bits. e.g., for num_bits=8, it uses [-127, 127] instead of [-128, 127]
"""
@staticmethod
@symbolic_helper.parse_args("v", "t", "i", "b", "b")
def symbolic(g, inputs, amax, num_bits=8, unsigned=False, narrow_range=True):
return _onnx_int8_helper(g, inputs, amax, num_bits, unsigned, narrow_range)
@staticmethod
def forward(ctx, inputs, amax, num_bits=8, unsigned=False, narrow_range=True):
"""
Follow tensorflow convention, max value is passed in and used to decide scale, instead of inputing scale
directly. Though inputing scale directly may be more natural to use.
Args:
ctx: A Context object to store tensors for backward.
inputs: A Tensor of type float32.
amax: A Tensor of type float32. Inputs will be quantized within range [-amax, amax]
amax will be broadcasted to inputs tensor.
num_bits: A integer used to calculate scaling factor, scale = (2^(num_bits-1) - 1) / max
Effectively, it indicates how many integer bits is used to represent the value. Default 8.
output_dtype: A type of Tensor. torch.int32 or torch.float32.
unsigned: A boolean. Use unsigned integer range. E.g. [0, 255] for num_bits=8. Default False.
narrow_range: A boolean. Use symmetric integer range for signed quantization
E.g. [-127,127] instead of [-128,127] for num_bits=8. Default True.
Returns:
outputs: A Tensor of type output_dtype.
scale: A Tensor of type float32. outputs / scale will dequantize outputs tensor.
Raises:
ValueError:
"""
ctx.save_for_backward(inputs, amax)
outputs, scale = _tensor_quant(inputs, amax, num_bits, unsigned, narrow_range)
# Check if scale overflows FP16
if outputs.dtype == torch.half and scale.max() > 65504:
raise ValueError("scale is too large for FP16 with amax={}".format(amax))
return outputs, scale.to(inputs.dtype)
@staticmethod
def backward(ctx, grad_outputs, grad_scale):
"""
Implements straight through estimation with clipping. For -amax <= input <= amax
the gradient passes straight through, otherwise the gradient is zero.
Args:
ctx: A Context object with saved tensors from forward.
grad_outputs: A tensor of gradient of outputs.
grad_scale: A tensor of gradient of scale.
Returns:
grad_inputs: A tensor of gradient.
"""
inputs, amax = ctx.saved_tensors
zero = grad_outputs.new_zeros(1) # create a zero tensor with the same type and device
grad_inputs = torch.where(inputs.abs() <= amax, grad_outputs, zero)
return grad_inputs, None, None, None, None
class LegacyFakeTensorQuantFunction(Function):
"""Fake version of TensorQuantFunction
See comments of TensorQuantFunction, arguments are the same.
"""
@staticmethod
def forward(ctx, inputs, amax, num_bits=8, unsigned=False, narrow_range=True):
ctx.save_for_backward(inputs, amax)
outputs, scale = _tensor_quant(inputs, amax, num_bits, unsigned, narrow_range)
return outputs / scale.to(inputs.dtype)
@staticmethod
def backward(ctx, grad_outputs):
inputs, amax = ctx.saved_tensors
zero = grad_outputs.new_zeros(1)
grad_inputs = torch.where(inputs.abs() <= amax, grad_outputs, zero)
return grad_inputs, None, None, None, None
def _tensor_quant(inputs, amax, num_bits=8, unsigned=False, narrow_range=True):
"""Shared function body between TensorQuantFunction and FakeTensorQuantFunction"""
# Fine scale, per channel scale will be handled by broadcasting, which could be tricky. Pop a warning.
if isinstance(amax, torch.Tensor) and inputs.dim() != amax.dim():
logging.debug("amax %s has different shape than inputs %s. Make sure broadcast works as expected!", amax.size(),
inputs.size())
logging.debug("{} bits quantization on shape {} tensor.".format(num_bits, inputs.size()))
if unsigned:
if inputs.min() < 0.:
raise TypeError("Negative values encountered in unsigned quantization.")
# Computation must be in FP32 to prevent potential over flow.
input_dtype = inputs.dtype
if inputs.dtype == torch.half:
inputs = inputs.float()
if amax.dtype == torch.half:
amax = amax.float()
min_amax = amax.min()
if min_amax < 0:
raise ValueError("Negative values in amax")
max_bound = torch.tensor((2.0**(num_bits - 1 + int(unsigned))) - 1.0, device=amax.device)
if unsigned:
min_bound = 0
elif narrow_range:
min_bound = -max_bound
else:
min_bound = -max_bound - 1
scale = max_bound / amax
epsilon = 1. / (1 << 24)
if min_amax <= epsilon: # Treat amax smaller than minimum representable of fp16 0
zero_amax_mask = (amax <= epsilon)
scale[zero_amax_mask] = 0 # Value quantized with amax=0 should all be 0
outputs = torch.clamp((inputs * scale).round_(), min_bound, max_bound)
if min_amax <= epsilon:
scale[zero_amax_mask] = 1. # Return 1 makes more sense for values quantized to 0 with amax=0
if input_dtype == torch.half:
outputs = outputs.half()
return outputs, scale
class FakeAffineTensorQuantFunction(Function):
"""Fake version of affine quantization
gemmlowp style scale+shift quantization. See more details in
https://github.com/google/gemmlowp/blob/master/doc/quantization.md.
We DO NOT recommend affine quantization on weights for performance reason. There might be value to affine quantize
activation as it can be cancelled by bias and comes with no performance penalty. This functionality is only added
for experimental purpose.
"""
@staticmethod
def forward(ctx, inputs, min_range, max_range, num_bits=8):
"""
As it will be only applied on activation with per tensor granularity, broadcast is not needed.
Args:
ctx: Pytorch convention.
inputs: A Tensor of type float32.
min_range: A float.
max_range: A float.
num_bits: An integer
Returns:
outputs: A Tensor of type output_dtype
"""
logging.debug("{} bits quantization on shape {} tensor.".format(num_bits, inputs.size()))
ctx.save_for_backward(inputs, min_range, max_range)
step_size = (max_range - min_range) / (2.0**num_bits - 1)
min_bound = -2.0**(num_bits - 1)
max_bound = 2.0**(num_bits - 1) - 1
quant_zero = torch.round(min_range / step_size) - min_bound
quantized = torch.round(inputs / step_size) - quant_zero
quantized = torch.clamp(quantized, min_bound, max_bound)
outputs = (quantized + quant_zero) * step_size
return outputs
@staticmethod
def backward(ctx, grad_outputs):
"""
Args:
ctx: Pytorch convention.
grad_output: A tensor of gradient of outputs
Returns:
grad_inputs: A tensor of gradient
"""
inputs, min_range, max_range = ctx.saved_tensors
zero = grad_outputs.new_zeros(1)
grad_inputs = torch.where((inputs <= max_range) * (inputs >= min_range), grad_outputs, zero)
return grad_inputs, None, None, None
tensor_quant = TensorQuantFunction.apply
legacy_fake_tensor_quant = LegacyFakeTensorQuantFunction.apply
fake_tensor_quant = FakeTensorQuantFunction.apply
fake_affine_tensor_quant = FakeAffineTensorQuantFunction.apply
scaled_e4m3 = ScaledE4M3Function.apply
@@ -0,0 +1,21 @@
#
# SPDX-FileCopyrightText: Copyright (c) 1993-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
"""Main entry of all utils"""
from .reduce_amax import reduce_amax
@@ -0,0 +1,27 @@
#
# SPDX-FileCopyrightText: Copyright (c) 1993-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
"""A WAR for codes that messes up logging format"""
import logging
def reset_logger_handler():
"""Remove all handler in root logger"""
root_logger = logging.getLogger()
while root_logger.handlers:
root_logger.removeHandler(root_logger.handlers[0])
@@ -0,0 +1,63 @@
#
# SPDX-FileCopyrightText: Copyright (c) 1993-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
"""Function to get absolute maximum of a tensor
Follow numpy fashion, which is more generic as pytorch's
"""
import torch
def reduce_amax(input, axis=None, keepdims=True):
"""Compute the absolute maximum value of a tensor.
Reduces input_tensor along the dimensions given in axis. Unless keepdims is true,
the rank of the tensor is reduced by 1 for each entry in axis. If keepdims is true,
the reduced dimensions are retained with length 1.
.. note::
Gradient computeation is disabled as this function is never meant learning reduces amax
Args:
input: Input tensor
axis: The dimensions to reduce. None or int or tuple of ints. If None (the default),
reduces all dimensions. Must be in the range [-rank(input_tensor), rank(input_tensor)).
keepdims: A boolean. If true, retains reduced dimensions with length 1. Default True
granularity: DEPRECTED. specifies if the statistic has to be calculated at tensor or channel granularity
Returns:
The reduced tensor.
Raises:
ValueError: Any axis which doesn't make sense or is not supported
ValueError: If unknown granularity is passed in.
"""
with torch.no_grad():
output = input.abs()
if axis is None:
output = torch.max(output)
else:
if isinstance(axis, int):
output, _ = torch.max(output, dim=axis, keepdim=keepdims)
else:
if isinstance(axis, tuple) and len(axis) > input.dim():
raise ValueError("Cannot reduce more axes than tensor's dim.")
for i in axis:
output, _ = torch.max(output, dim=i, keepdim=True)
if not keepdims or output.numel() == 1:
output.squeeze_()
return output