# -*- mode: python -*-
# =============================================================================
# @@-COPYRIGHT-START-@@
#
# Copyright (c) 2023, Qualcomm Innovation Center, Inc. All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its contributors
# may be used to endorse or promote products derived from this software
# without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# SPDX-License-Identifier: BSD-3-Clause
#
# @@-COPYRIGHT-END-@@
# =============================================================================
# pylint: disable=all
import math
from itertools import chain, repeat
from typing import overload, Union, Tuple, Optional
import torch
from .utils import *
@overload
def quantize(tensor: torch.Tensor, scale: torch.Tensor, offset: torch.Tensor,
bitwidth: Union[int, float], signed: bool = False,
block_size: Optional[Tuple[int, ...]] = None):
...
@overload
def quantize(tensor: torch.Tensor, scale: torch.Tensor, offset: torch.Tensor, *,
num_steps: int, signed: bool = False, block_size: Optional[Tuple[int, ...]] = None):
...
@overload
def quantize(tensor: torch.Tensor, scale: torch.Tensor, offset: torch.Tensor,
qmin: int, qmax: int, block_size: Optional[Tuple[int, ...]] = None):
...
[docs]def quantize(tensor: torch.Tensor, scale: torch.Tensor, offset: torch.Tensor,
*args, **kwargs):
r"""
Applies quantization to the input.
Precisely,
.. math::
out = clamp\left(\left\lceil\frac{input}{scale}\right\rfloor - offset, qmin, qmax\right)
If block size :math:`B = \begin{pmatrix} B_0 & B_1 & \cdots & B_{D-1} \end{pmatrix}` is specified,
this equation will be further generalized as
.. math::
out_{j_0 \cdots j_{D-1}} & = clamp\left(
\left\lceil\frac{input_{j_0 \cdots j_{D-1}}}{scale_{i_0 \cdots i_{D-1}}}\right\rfloor
- offset_{i_0 \cdots i_{D-1}}, qmin, qmax\right)\\
\text{where} \quad \forall_{0 \leq d < D} \quad i_d = \left\lfloor \frac{j_d}{B_d} \right\rfloor
This function is overloaded with the signatures listed below:
.. function:: quantize(tensor, scale, offset, bitwidth, signed=False, block_size=None)
:noindex:
Equivalent to:
.. math::
qmin=
\begin{cases}
-\left\lceil\frac{2^{bitwidth}-1}{2}\right\rceil,& \text{if } signed\\
0, & \text{otherwise (default)}
\end{cases}
qmax=
\begin{cases}
\left\lfloor\frac{2^{bitwidth}-1}{2}\right\rfloor,& \text{if } signed\\
2^{bitwidth}-1, & \text{otherwise (default)}
\end{cases}
:param Tensor tensor: Tensor to quantize
:param Tensor scale: Scale for quantization
:param Tensor offset: Offset for quantization
:param int bitwidth: Bitwidth of quantized tensor based on which :math:`qmin` and :math:`qmax` will be derived
:param bool signed: If false, the output will be mapped to positive integers only.
Otherwise, it will range over both positive and negative integers.
:param block_size: Block size
:type block_size: Tuple[int, ...], optional
.. function:: quantize(tensor, scale, offset, *, num_steps, signed=False, block_size=None)
:noindex:
Equivalent to:
.. math::
qmin=
\begin{cases}
-\left\lceil\frac{num\_steps}{2}\right\rceil,& \text{if } signed\\
0, & \text{otherwise (default)}
\end{cases}
qmax=
\begin{cases}
\left\lfloor\frac{num\_steps}{2}\right\rfloor,& \text{if } signed\\
num\_steps, & \text{otherwise (default)}
\end{cases}
:param Tensor tensor: Tensor to quantize
:param Tensor scale: Scale for quantization
:param Tensor offset: Offset for quantization
:param int num_steps: The number of steps in the quantization range based on which :math:`qmin` and :math:`qmax` will be derived
:param bool signed: If false, the output will be mapped to positive integers only.
Otherwise, it will range over both positive and negative integers.
:param block_size: Block size
:type block_size: Tuple[int, ...], optional
.. function:: quantize(tensor, scale, offset, *, qmin, qmax, block_size=None)
:noindex:
:param Tensor tensor: Tensor to quantize
:param Tensor scale: Scale for quantization
:param Tensor offset: Offset for quantization
:param int qmin: Minimum value of the quantization range
:param int qmax: Maximum value of the quantization range
:param block_size: Block size
:type block_size: Tuple[int, ...], optional
Examples:
>>> import aimet_torch.v2.quantization as Q
>>> input = torch.arange(start=-0.3, end=1.3, step=0.05)
>>> print(input)
tensor([-3.0000e-01, -2.5000e-01, -2.0000e-01, -1.5000e-01, -1.0000e-01,
-5.0000e-02, -1.1921e-08, 5.0000e-02, 1.0000e-01, 1.5000e-01,
2.0000e-01, 2.5000e-01, 3.0000e-01, 3.5000e-01, 4.0000e-01,
4.5000e-01, 5.0000e-01, 5.5000e-01, 6.0000e-01, 6.5000e-01,
7.0000e-01, 7.5000e-01, 8.0000e-01, 8.5000e-01, 9.0000e-01,
9.5000e-01, 1.0000e+00, 1.0500e+00, 1.1000e+00, 1.1500e+00,
1.2000e+00, 1.2500e+00])
>>> scale = torch.tensor(1/15)
>>> offset = torch.tensor(0.0)
>>> Q.affine.quantize(input, scale, offset, bitwidth=4)
tensor([ 0., 0., 0., 0., 0., 0., -0., 1., 2., 2., 3., 4., 4., 5.,
6., 7., 7., 8., 9., 10., 10., 11., 12., 13., 13., 14., 15., 15.,
15., 15., 15., 15.])
>>> Q.affine.quantize(input, scale, offset, num_steps=15)
tensor([ 0., 0., 0., 0., 0., 0., -0., 1., 2., 2., 3., 4., 4., 5.,
6., 7., 7., 8., 9., 10., 10., 11., 12., 13., 13., 14., 15., 15.,
15., 15., 15., 15.])
>>> Q.affine.quantize(input, scale, offset, qmin=0, qmax=15)
tensor([ 0., 0., 0., 0., 0., 0., -0., 1., 2., 2., 3., 4., 4., 5.,
6., 7., 7., 8., 9., 10., 10., 11., 12., 13., 13., 14., 15., 15.,
15., 15., 15., 15.])
"""
qmin, qmax, block_size = _parse_args(args, kwargs)
return get_backend().quantize(tensor, scale, offset, qmin, qmax, block_size)
@overload
def quantize_dequantize(tensor: torch.Tensor, scale: torch.Tensor, offset: torch.Tensor,
bitwidth: Union[int, float], signed: bool = False,
block_size: Optional[Tuple[int, ...]] = None):
...
@overload
def quantize_dequantize(tensor: torch.Tensor, scale: torch.Tensor, offset: torch.Tensor, *,
num_steps: int, signed: bool = False, block_size: Optional[Tuple[int, ...]] = None):
...
@overload
def quantize_dequantize(tensor: torch.Tensor, scale: torch.Tensor, offset: torch.Tensor,
qmin: int, qmax: int, block_size: Optional[Tuple[int, ...]] = None):
...
[docs]def quantize_dequantize(tensor: torch.Tensor, scale: torch.Tensor, offset: torch.Tensor,
*args, **kwargs):
r"""
Applies fake-quantization by quantizing and dequantizing the input.
Precisely,
.. math::
out = (\overline{input} + offset) * scale
where
.. math::
\overline{input} = clamp\left(\left\lceil\frac{input}{scale}\right\rfloor - offset, qmin, qmax\right)
If block size :math:`B = \begin{pmatrix} B_0 & B_1 & \cdots & B_{D-1} \end{pmatrix}` is specified,
this equation will be further generalized as
.. math::
out_{j_0 \cdots j_{D-1}} &= (\overline{input}_{j_0 \cdots j_{D-1}} + offset_{i_0 \cdots i_{D-1}}) * scale_{i_0 \cdots i_{D-1}}\\
\overline{input}_{j_0 \cdots j_{D-1}} &= clamp\left(
\left\lceil\frac{input_{j_0 \cdots j_{D-1}}}{scale_{i_0 \cdots i_{D-1}}}\right\rfloor
- offset_{i_0 \cdots i_{D-1}}, qmin, qmax\right)\\
\text{where } \quad \forall_{0 \leq d < D} \quad i_d = \left\lfloor \frac{j_d}{B_d} \right\rfloor
This function is overloaded with the signatures listed below:
.. function:: quantize_dequantize(tensor, scale, offset, bitwidth, signed=False, block_size=None)
:noindex:
Equivalent to:
.. math::
qmin=
\begin{cases}
-\left\lceil\frac{2^{bitwidth}-1}{2}\right\rceil,& \text{if } signed\\
0, & \text{otherwise (default)}
\end{cases}
qmax=
\begin{cases}
\left\lfloor\frac{2^{bitwidth}-1}{2}\right\rfloor,& \text{if } signed\\
2^{bitwidth}-1, & \text{otherwise (default)}
\end{cases}
:param Tensor tensor: Tensor to quantize
:param Tensor scale: Scale for quantization
:param Tensor offset: Offset for quantization
:param int bitwidth: Bitwidth of quantized tensor based on which :math:`qmin` and :math:`qmax` will be derived
:param bool signed: If false, :math:`\overline{input}` will be mapped to positive integers only.
Otherwise, :math:`\overline{input}` will range over both positive and negative integers.
:param block_size: Block size
:type block_size: Tuple[int, ...], optional
.. function:: quantize_dequantize(tensor, scale, offset, *, num_steps, signed=False, block_size=None)
:noindex:
Equivalent to:
.. math::
qmin=
\begin{cases}
-\left\lceil\frac{num\_steps}{2}\right\rceil,& \text{if } signed\\
0, & \text{otherwise (default)}
\end{cases}
qmax=
\begin{cases}
\left\lfloor\frac{num\_steps}{2}\right\rfloor,& \text{if } signed\\
num\_steps, & \text{otherwise (default)}
\end{cases}
:param Tensor tensor: Tensor to quantize
:param Tensor scale: Scale for quantization
:param Tensor offset: Offset for quantization
:param int num_steps: The number of steps in the quantization range based on which :math:`qmin` and :math:`qmax` will be derived
:param bool signed: If false, :math:`\overline{input}` will be mapped to positive integers only.
Otherwise, :math:`\overline{input}` will range over both positive and negative integers.
:param block_size: Block size
:type block_size: Tuple[int, ...], optional
.. function:: quantize_dequantize(tensor, scale, offset, *, qmin, qmax, block_size=None)
:noindex:
:param Tensor tensor: Tensor to quantize
:param Tensor scale: Scale for quantization
:param Tensor offset: Offset for quantization
:param int qmin: Minimum value of the quantization range
:param int qmax: Maximum value of the quantization range
:param block_size: Block size
:type block_size: Tuple[int, ...], optional
Examples:
>>> import aimet_torch.v2.quantization as Q
>>> input = torch.arange(start=-0.3, end=1.3, step=0.05)
>>> print(input)
tensor([-3.0000e-01, -2.5000e-01, -2.0000e-01, -1.5000e-01, -1.0000e-01,
-5.0000e-02, -1.1921e-08, 5.0000e-02, 1.0000e-01, 1.5000e-01,
2.0000e-01, 2.5000e-01, 3.0000e-01, 3.5000e-01, 4.0000e-01,
4.5000e-01, 5.0000e-01, 5.5000e-01, 6.0000e-01, 6.5000e-01,
7.0000e-01, 7.5000e-01, 8.0000e-01, 8.5000e-01, 9.0000e-01,
9.5000e-01, 1.0000e+00, 1.0500e+00, 1.1000e+00, 1.1500e+00,
1.2000e+00, 1.2500e+00])
>>> scale = torch.tensor(1/15)
>>> offset = torch.tensor(0.0)
>>> Q.affine.quantize_dequantize(input, scale, offset, bitwidth=4)
tensor([0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0667, 0.1333,
0.1333, 0.2000, 0.2667, 0.2667, 0.3333, 0.4000, 0.4667, 0.4667, 0.5333,
0.6000, 0.6667, 0.6667, 0.7333, 0.8000, 0.8667, 0.8667, 0.9333, 1.0000,
1.0000, 1.0000, 1.0000, 1.0000, 1.0000])
>>> Q.affine.quantize_dequantize(input, scale, offset, num_steps=15)
tensor([0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0667, 0.1333,
0.1333, 0.2000, 0.2667, 0.2667, 0.3333, 0.4000, 0.4667, 0.4667, 0.5333,
0.6000, 0.6667, 0.6667, 0.7333, 0.8000, 0.8667, 0.8667, 0.9333, 1.0000,
1.0000, 1.0000, 1.0000, 1.0000, 1.0000])
>>> Q.affine.quantize_dequantize(input, scale, offset, qmin=0, qmax=15)
tensor([0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0667, 0.1333,
0.1333, 0.2000, 0.2667, 0.2667, 0.3333, 0.4000, 0.4667, 0.4667, 0.5333,
0.6000, 0.6667, 0.6667, 0.7333, 0.8000, 0.8667, 0.8667, 0.9333, 1.0000,
1.0000, 1.0000, 1.0000, 1.0000, 1.0000])
"""
qmin, qmax, block_size = _parse_args(args, kwargs)
return get_backend().quantize_dequantize(tensor, scale, offset, qmin, qmax, block_size)
[docs]def dequantize(tensor: torch.Tensor, scale: torch.Tensor, offset: torch.Tensor,
block_size: Optional[Tuple[int, ...]] = None):
return get_backend().dequantize(tensor, scale, offset, block_size)
def _parse_args(args, kwargs) -> Tuple[int, int, Optional[Tuple[int, ...]]]:
bitwidth = num_steps = signed = qmin = qmax = None
# Pad positional args with None's such that len(args) == 3
args = tuple(chain(args, repeat(None, 3 - len(args))))
arg0 = kwargs.get('qmin', kwargs.get('bitwidth', args[0]))
arg1 = kwargs.get('qmax', kwargs.get('signed', args[1]))
block_size = kwargs.get('block_size', None) or args[2]
if arg0 is None:
num_steps = kwargs['num_steps']
signed = kwargs['signed']
qmin, qmax = _derive_qmin_qmax(num_steps=num_steps, signed=signed)
elif arg1 is None or isinstance(arg1, bool):
bitwidth, signed = arg0, bool(arg1)
qmin, qmax = _derive_qmin_qmax(bitwidth=bitwidth, signed=signed)
else:
qmin, qmax = arg0, arg1
assert qmin is not None
assert qmax is not None
return qmin, qmax, block_size
def _derive_qmin_qmax(*, bitwidth: int = None, num_steps: int = None, signed: bool):
if bitwidth is not None:
num_steps = 2 ** bitwidth - 1
if signed:
qmin = -math.ceil(num_steps/2)
qmax = math.floor(num_steps/2)
else:
qmin = 0
qmax = num_steps
return qmin, qmax