TensorPlay
latest (dev)
Copy
View Markdown

Latest development documentation · Updated 2026-10-15

Source code for tensorplay.ao.quantization.observer

"""Calibration statistics and uniform quantization parameter selection."""
import math
import warnings
from abc import ABC, abstractmethod
from collections import OrderedDict
from dataclasses import dataclass
from enum import Enum, auto
from functools import partial
import tensorplay
from tensorplay import nn

QUANT_MIN = -128
QUANT_MAX = 127

def is_per_tensor(qscheme):
    return qscheme == tensorplay.per_tensor_affine or qscheme == tensorplay.per_tensor_symmetric

def is_per_channel(qscheme):
    return qscheme in [tensorplay.per_channel_affine, tensorplay.per_channel_affine_float_qparams, tensorplay.per_channel_symmetric]

def check_min_max_valid(min_val, max_val):
    if min_val.numel() == 0 or max_val.numel() == 0:
        warnings.warn('must run observer before calling calculate_qparams. ' + 'Returning default values.', stacklevel=2)
        return False
    if min_val.dim() == 0 or max_val.dim() == 0:
        if min_val == float('inf') and max_val == float('-inf'):
            warnings.warn('must run observer before calling calculate_qparams. ' + 'Returning default values.', stacklevel=2)
            return False
        if min_val > max_val:
            raise AssertionError(f'min {min_val} should be less than max {max_val}')
    elif tensorplay.any(min_val > max_val):
        raise AssertionError(f'min {min_val} should be less than max {max_val}')
    return True

def calculate_qmin_qmax(quant_min, quant_max, has_customized_qrange, dtype, reduce_range):
    if has_customized_qrange:
        if dtype in [tensorplay.qint32, tensorplay.int32]:
            initial_quant_min, initial_quant_max = (0, 2 ** 32 - 1)
        else:
            initial_quant_min, initial_quant_max = (0, 255)
        custom_quant_min, custom_quant_max = (quant_min, quant_max)
        if custom_quant_min is not None and custom_quant_max is not None:
            initial_quant_min, initial_quant_max = (custom_quant_min, custom_quant_max)
        qrange_len = initial_quant_max - initial_quant_min + 1
        if dtype in [tensorplay.qint8, tensorplay.int8]:
            if not 0 < qrange_len <= 256:
                raise AssertionError('quantization range should be positive and not exceed the maximum bit range (=256).')
        elif dtype in [tensorplay.qint32, tensorplay.int32]:
            if not 0 < qrange_len <= 2 ** 32:
                raise AssertionError('quantization range should be positive and not exceed the maximum bit range (=4294967296).')
        if reduce_range:
            quant_min, quant_max = (quant_min // 2, quant_max // 2)
    elif dtype in [tensorplay.qint8, tensorplay.int8]:
        if reduce_range:
            quant_min, quant_max = (-64, 63)
        else:
            quant_min, quant_max = (-128, 127)
    elif dtype in [tensorplay.quint8, tensorplay.uint8]:
        if reduce_range:
            quant_min, quant_max = (0, 127)
        else:
            quant_min, quant_max = (0, 255)
    elif dtype in [tensorplay.qint32, tensorplay.int32]:
        quant_min, quant_max = (-1 * 2 ** 31, 2 ** 31 - 1)
    elif dtype == tensorplay.uint16:
        quant_min, quant_max = (0, 2 ** 16 - 1)
    elif dtype == tensorplay.int16:
        quant_min, quant_max = (-2 ** 15, 2 ** 15 - 1)
    else:
        quant_min, quant_max = (0, 15)
    return (quant_min, quant_max)

def validate_qmin_qmax(quant_min, quant_max):
    if not quant_min <= 0 <= quant_max:
        raise AssertionError('Used-specified quantization range must include 0.')
    if quant_min >= quant_max:
        raise AssertionError('qmin must be strictly less than qmax for user-specified quantization range.')

class _PartialWrapper:

    def __init__(self, p):
        self.p = p
        self.callable_args = {}

    def __call__(self, *args, **keywords):
        for arg_name in self.callable_args:
            if arg_name not in keywords:
                keywords = {**keywords, arg_name: self.callable_args[arg_name]()}
        return self.p(*args, **keywords)

    def __repr__(self):
        return self.p.__repr__() + self.callable_args.__repr__()

    def with_args(self, **kwargs):
        return _with_args(self, **kwargs)

    def with_callable_args(self, **kwargs):
        result = _PartialWrapper(p=self.p)
        result.callable_args = {**self.callable_args, **kwargs}
        return result

def _with_args(cls_or_self, **kwargs):
    r = _PartialWrapper(partial(cls_or_self, **kwargs))
    return r

def _with_callable_args(cls_or_self, **kwargs):
    r = _PartialWrapper(partial(cls_or_self))
    return r.with_callable_args(**kwargs)

class ObserverBase(nn.Module):

    def __init__(self, dtype, is_dynamic=False):
        super().__init__()
        self.dtype = dtype
        self.is_dynamic = is_dynamic

    def forward(self, x):
        pass

    def calculate_qparams(self, **kwargs):
        pass
    with_args = classmethod(_with_args)
    with_callable_args = classmethod(_with_callable_args)

class UniformQuantizationObserverBase(ObserverBase):
    _version = 3

    def __init__(self, dtype=tensorplay.quint8, qscheme=tensorplay.per_tensor_affine, reduce_range=False, quant_min=None, quant_max=None, factory_kwargs=None, eps=tensorplay.finfo(tensorplay.float32).eps, is_dynamic=False, **kwargs):
        factory_kwargs = tensorplay.nn.factory_kwargs(factory_kwargs)
        super().__init__(dtype=dtype, is_dynamic=is_dynamic, **kwargs)
        self.qscheme = qscheme
        if reduce_range:
            warnings.warn('Please use quant_min and quant_max to specify the range for observers.                     reduce_range will be deprecated in a future release of quantization.', stacklevel=2)
        self.reduce_range = reduce_range
        self.register_buffer('eps', tensorplay.tensor([eps], **factory_kwargs))
        if self.qscheme not in (tensorplay.per_tensor_affine, tensorplay.per_tensor_symmetric, tensorplay.per_channel_affine, tensorplay.per_channel_symmetric, tensorplay.per_channel_affine_float_qparams):
            raise AssertionError('Default Observer only works for per_tensor_affine, per_tensor_symmetric, per_channel_affine, per_channel_symmetric and per_channel_float_qparams quantization scheme')
        _ALLOWED_DTYPES = (tensorplay.qint8, tensorplay.quint8, tensorplay.quint4x2, tensorplay.quint1x8, tensorplay.qint32, tensorplay.int8, tensorplay.uint8, tensorplay.int16, tensorplay.int32, tensorplay.float8_e5m2, tensorplay.float8_e4m3fn, tensorplay.uint16)
        if self.dtype not in _ALLOWED_DTYPES:
            raise AssertionError(f'Default Observer only works for {_ALLOWED_DTYPES} data type')
        self.has_customized_qrange = quant_min is not None and quant_max is not None
        if self.has_customized_qrange:
            validate_qmin_qmax(quant_min, quant_max)
        self.quant_min, self.quant_max = calculate_qmin_qmax(quant_min, quant_max, self.has_customized_qrange, self.dtype, self.reduce_range)

    def _load_from_state_dict(self, state_dict, prefix, local_metadata, strict, missing_keys, unexpected_keys, error_msgs):
        version = local_metadata.get('version', None)
        if version is None or version == 1:
            eps = tensorplay.tensor([tensorplay.finfo(tensorplay.float32).eps])
            state_dict[prefix + 'eps'] = eps
        super()._load_from_state_dict(state_dict, prefix, local_metadata, strict, missing_keys, unexpected_keys, error_msgs)

    def _validate_qmin_qmax(self, quant_min, quant_max):
        if not quant_min <= 0 <= quant_max:
            raise AssertionError('Used-specified quantization range must include 0.')
        if quant_min >= quant_max:
            raise AssertionError('qmin must be strictly less than qmax for user-specified quantization range.')

    def _derive_qparams(self, min_val, max_val):
        if not check_min_max_valid(min_val, max_val):
            return (tensorplay.tensor([1.0], device=min_val.device.type), tensorplay.tensor([0], device=min_val.device.type))
        quant_min, quant_max = (self.quant_min, self.quant_max)
        min_val_neg = tensorplay.minimum(min_val, tensorplay.zeros_like(min_val))
        max_val_pos = tensorplay.maximum(max_val, tensorplay.zeros_like(max_val))
        device = min_val_neg.device
        scale = tensorplay.ones(min_val_neg.size(), dtype=tensorplay.float32, device=device)
        zero_point = tensorplay.zeros(min_val_neg.size(), dtype=tensorplay.int64, device=device)
        if self.qscheme == tensorplay.per_tensor_symmetric or self.qscheme == tensorplay.per_channel_symmetric:
            max_val_pos = tensorplay.maximum(-min_val_neg, max_val_pos)
            scale = _qparam_div(max_val_pos, float(quant_max - quant_min) / 2)
            scale = tensorplay.maximum(scale, self.eps)
            if self.dtype in [tensorplay.quint8, tensorplay.uint8]:
                if self.has_customized_qrange:
                    zero_point = zero_point.new_full(zero_point.size(), (quant_min + quant_max) // 2)
                else:
                    zero_point = zero_point.new_full(zero_point.size(), 128)
            elif self.dtype == tensorplay.uint16:
                zero_point = zero_point.new_full(zero_point.size(), 2 ** 15)
        elif self.qscheme == tensorplay.per_channel_affine_float_qparams:
            scale = _qparam_div(max_val - min_val, float(quant_max - quant_min))
            scale = tensorplay.where(scale > self.eps, scale, tensorplay.ones_like(scale))
            zero_point = -1 * min_val / scale
        else:
            scale = _qparam_div(max_val_pos - min_val_neg, float(quant_max - quant_min))
            scale = tensorplay.maximum(scale, self.eps)
            zero_point = quant_min - tensorplay.round(min_val_neg / scale).to(tensorplay.int)
            zero_point = tensorplay.clamp(zero_point, quant_min, quant_max)
        if scale.dim() == 0:
            scale = scale.reshape(1)
        if zero_point.dim() == 0:
            zero_point = zero_point.reshape(1)
        return (scale, zero_point)

    def reset_min_max_vals(self):
        raise NotImplementedError('Cannot reset min/max values in the given observer.')


[docs]
class MinMaxObserver(UniformQuantizationObserverBase):

    def __init__(self, dtype=tensorplay.quint8, qscheme=tensorplay.per_tensor_affine, reduce_range=False, quant_min=None, quant_max=None, factory_kwargs=None, eps=tensorplay.finfo(tensorplay.float32).eps, is_dynamic=False, **kwargs):
        if not is_per_tensor(qscheme):
            raise NotImplementedError("MinMaxObserver's qscheme only support per_tensor_symmetric                     and per_tensor_affine.")
        super().__init__(dtype=dtype, qscheme=qscheme, reduce_range=reduce_range, quant_min=quant_min, quant_max=quant_max, factory_kwargs=factory_kwargs, eps=eps, is_dynamic=is_dynamic, **kwargs)
        factory_kwargs = tensorplay.nn.factory_kwargs(factory_kwargs)
        self.register_buffer('min_val', tensorplay.tensor(float('inf'), **factory_kwargs))
        self.register_buffer('max_val', tensorplay.tensor(float('-inf'), **factory_kwargs))
        if self.qscheme == tensorplay.per_tensor_symmetric and self.reduce_range and (self.dtype == tensorplay.quint8):
            raise NotImplementedError('Cannot reduce range for symmetric                                        quantization for quint8')

    def forward(self, x_orig):
        if x_orig.numel() == 0:
            return x_orig
        x = x_orig.detach()
        x = x.to(self.min_val.dtype)
        min_val_cur, max_val_cur = tensorplay.aminmax(x)
        min_val = tensorplay.minimum(min_val_cur, self.min_val)
        max_val = tensorplay.maximum(max_val_cur, self.max_val)
        self.min_val.copy_(min_val)
        self.max_val.copy_(max_val)
        return x_orig

    def calculate_qparams(self):
        return self._derive_qparams(self.min_val, self.max_val)

    def extra_repr(self):
        return f'min_val={self.min_val}, max_val={self.max_val}'

    def reset_min_max_vals(self):
        self.min_val.fill_(float('inf'))
        self.max_val.fill_(float('-inf'))



[docs]
class MovingAverageMinMaxObserver(MinMaxObserver):

    def __init__(self, averaging_constant=0.01, dtype=tensorplay.quint8, qscheme=tensorplay.per_tensor_affine, reduce_range=False, quant_min=None, quant_max=None, eps=tensorplay.finfo(tensorplay.float32).eps, is_dynamic=False, **kwargs):
        if not is_per_tensor(qscheme):
            raise NotImplementedError(f"MovingAverageMinMaxObserver's qscheme only support                 per_tensor_symmetric and per_tensor_affine.                 but got: {qscheme}")
        self.averaging_constant = averaging_constant
        if is_dynamic and self.averaging_constant != 1:
            raise NotImplementedError(f"MovingAverageMinMaxObserver doesn't support dynamic quantization for averaging constant of {self.averaging_constant}")
        super().__init__(dtype=dtype, qscheme=qscheme, reduce_range=reduce_range, quant_min=quant_min, quant_max=quant_max, eps=eps, is_dynamic=is_dynamic, **kwargs)

    def forward(self, x_orig):
        if x_orig.numel() == 0:
            return x_orig
        x = x_orig.detach()
        x = x.to(self.min_val.dtype)
        min_val = self.min_val
        max_val = self.max_val
        if min_val == float('inf') and max_val == float('-inf'):
            min_val, max_val = tensorplay.aminmax(x)
        else:
            min_val_cur, max_val_cur = tensorplay.aminmax(x)
            min_val = min_val + self.averaging_constant * (min_val_cur - min_val)
            max_val = max_val + self.averaging_constant * (max_val_cur - max_val)
        self.min_val.copy_(min_val)
        self.max_val.copy_(max_val)
        return x_orig



[docs]
class PerChannelMinMaxObserver(UniformQuantizationObserverBase):

    def __init__(self, ch_axis=0, dtype=tensorplay.quint8, qscheme=tensorplay.per_channel_affine, reduce_range=False, quant_min=None, quant_max=None, factory_kwargs=None, eps=tensorplay.finfo(tensorplay.float32).eps, is_dynamic=False, **kwargs):
        if not is_per_channel(qscheme):
            raise NotImplementedError("PerChannelMinMaxObserver's qscheme only support                     per_channel_symmetric, per_channel_affine and per_channel_affine_float_qparams.")
        if is_dynamic:
            raise NotImplementedError("PerChannelMinMaxObserver doesn't support dynamic quantization")
        super().__init__(dtype=dtype, qscheme=qscheme, reduce_range=reduce_range, quant_min=quant_min, quant_max=quant_max, factory_kwargs=factory_kwargs, eps=eps, is_dynamic=is_dynamic, **kwargs)
        factory_kwargs = tensorplay.nn.factory_kwargs(factory_kwargs)
        self.ch_axis = ch_axis
        self.register_buffer('min_val', tensorplay.tensor([], **factory_kwargs))
        self.register_buffer('max_val', tensorplay.tensor([], **factory_kwargs))
        if self.qscheme == tensorplay.per_channel_symmetric and self.reduce_range and (self.dtype == tensorplay.quint8):
            raise NotImplementedError('Cannot reduce range for symmetric quantization for quint8')

    def forward(self, x_orig):
        return self._forward(x_orig)

    def _forward(self, x_orig):
        if x_orig.numel() == 0:
            return x_orig
        x = x_orig.detach()
        min_val = self.min_val
        max_val = self.max_val
        x_dim = x.size()
        new_axis_list = [i for i in range(len(x_dim))]
        new_axis_list[self.ch_axis] = 0
        new_axis_list[0] = self.ch_axis
        y = x.permute(new_axis_list)
        y = y.to(self.min_val.dtype)
        y = tensorplay.flatten(y, start_dim=1)
        if min_val.numel() == 0 or max_val.numel() == 0:
            min_val, max_val = tensorplay.aminmax(y, dim=1)
        else:
            min_val_cur, max_val_cur = tensorplay.aminmax(y, dim=1)
            min_val = tensorplay.minimum(min_val_cur, min_val)
            max_val = tensorplay.maximum(max_val_cur, max_val)
        self.min_val.resize_(min_val.shape)
        self.max_val.resize_(max_val.shape)
        self.min_val.copy_(min_val)
        self.max_val.copy_(max_val)
        return x_orig

    def calculate_qparams(self):
        return self._derive_qparams(self.min_val, self.max_val)

    def extra_repr(self):
        return f'min_val={self.min_val}, max_val={self.max_val}'

    def _load_from_state_dict(self, state_dict, prefix, local_metadata, strict,
                              missing_keys, unexpected_keys, error_msgs):
        for name in ('min_val', 'max_val'):
            old_name = name.replace('_val', '_vals')
            if prefix + old_name in state_dict and prefix + name not in state_dict:
                state_dict[prefix + name] = state_dict.pop(prefix + old_name)
            if prefix + name in state_dict:
                getattr(self, name).resize_(state_dict[prefix + name].shape)
        super()._load_from_state_dict(state_dict, prefix, local_metadata, strict,
                                     missing_keys, unexpected_keys, error_msgs)

    def reset_min_max_vals(self):
        self.min_val.resize_(0)
        self.max_val.resize_(0)



[docs]
class MovingAveragePerChannelMinMaxObserver(PerChannelMinMaxObserver):

    def __init__(self, averaging_constant=0.01, ch_axis=0, dtype=tensorplay.quint8, qscheme=tensorplay.per_channel_affine, reduce_range=False, quant_min=None, quant_max=None, eps=tensorplay.finfo(tensorplay.float32).eps, is_dynamic=False, **kwargs):
        if not is_per_channel(qscheme):
            raise NotImplementedError("MovingAveragePerChannelMinMaxObserver's qscheme only support                     per_channel_symmetric, per_channel_affine and per_channel_affine_float_qparams.")
        if is_dynamic:
            raise NotImplementedError("MovingAveragePerChannelMinMaxObserver doesn't support dynamic quantization")
        super().__init__(ch_axis=ch_axis, dtype=dtype, qscheme=qscheme, reduce_range=reduce_range, quant_min=quant_min, quant_max=quant_max, eps=eps, is_dynamic=is_dynamic, **kwargs)
        self.averaging_constant = averaging_constant

    def forward(self, x_orig):
        if x_orig.numel() == 0:
            return x_orig
        x = x_orig.detach()
        x = x.to(self.min_val.dtype)
        min_val = self.min_val
        max_val = self.max_val
        x_dim = x.size()
        new_axis_list = [i for i in range(len(x_dim))]
        new_axis_list[self.ch_axis] = 0
        new_axis_list[0] = self.ch_axis
        y = x.permute(new_axis_list)
        y = tensorplay.flatten(y, start_dim=1)
        if min_val.numel() == 0 or max_val.numel() == 0:
            min_val, max_val = tensorplay.aminmax(y, dim=1)
        else:
            min_val_cur, max_val_cur = tensorplay.aminmax(y, dim=1)
            min_val = min_val + self.averaging_constant * (min_val_cur - min_val)
            max_val = max_val + self.averaging_constant * (max_val_cur - max_val)
        self.min_val.resize_(min_val.shape)
        self.max_val.resize_(max_val.shape)
        self.min_val.copy_(min_val)
        self.max_val.copy_(max_val)
        return x_orig



[docs]
class HistogramObserver(UniformQuantizationObserverBase):

    def __init__(self, bins=2048, dtype=tensorplay.quint8, qscheme=tensorplay.per_tensor_affine, reduce_range=False, quant_min=None, quant_max=None, factory_kwargs=None, eps=tensorplay.finfo(tensorplay.float32).eps, is_dynamic=False, **kwargs):
        if not is_per_tensor(qscheme):
            raise NotImplementedError("HistogramObserver's qscheme only support per_tensor_symmetric                     and per_tensor_affine.")
        if is_dynamic:
            raise NotImplementedError("HistogramObserver doesn't support dynamic quantization")
        super().__init__(dtype=dtype, qscheme=qscheme, reduce_range=reduce_range, quant_min=quant_min, quant_max=quant_max, factory_kwargs=factory_kwargs, eps=eps, is_dynamic=is_dynamic, **kwargs)
        factory_kwargs = tensorplay.nn.factory_kwargs(factory_kwargs)
        self.bins = bins
        self.register_buffer('histogram', tensorplay.zeros(self.bins, **factory_kwargs))
        self.register_buffer('min_val', tensorplay.tensor(float('inf'), **factory_kwargs))
        self.register_buffer('max_val', tensorplay.tensor(float('-inf'), **factory_kwargs))
        self.dst_nbins = 2 ** (32 if self.dtype in (tensorplay.qint32, tensorplay.int32) else 8 if self.dtype in (tensorplay.qint8, tensorplay.quint8) else tensorplay.iinfo(self.dtype).bits)
        self.upsample_rate = 16

    def _get_norm(self, delta_begin, delta_end, density):
        norm = (delta_end * delta_end * delta_end - delta_begin * delta_begin * delta_begin) / 3
        return density * norm

    def _compute_quantization_error(self, next_start_bin, next_end_bin):
        bin_width = (self.max_val.item() - self.min_val.item()) / self.bins
        dst_bin_width = bin_width * (next_end_bin - next_start_bin + 1) / self.dst_nbins
        if dst_bin_width == 0.0:
            return 0.0
        src_bin = tensorplay.arange(self.bins, device=self.histogram.device)
        src_bin_begin = (src_bin - next_start_bin) * bin_width
        src_bin_end = src_bin_begin + bin_width
        dst_bin_of_begin = tensorplay.clamp(tensorplay.div(src_bin_begin, dst_bin_width, rounding_mode='floor'), 0, self.dst_nbins - 1)
        dst_bin_of_begin_center = (dst_bin_of_begin + 0.5) * dst_bin_width
        dst_bin_of_end = tensorplay.clamp(tensorplay.div(src_bin_end, dst_bin_width, rounding_mode='floor'), 0, self.dst_nbins - 1)
        density = self.histogram / bin_width
        norm = tensorplay.zeros(self.bins, device=self.histogram.device)
        delta_begin = src_bin_begin - dst_bin_of_begin_center
        delta_end = dst_bin_width / 2
        norm += self._get_norm(delta_begin, tensorplay.ones(self.bins, device=self.histogram.device) * delta_end, density)
        norm += (dst_bin_of_end - dst_bin_of_begin - 1) * self._get_norm(-dst_bin_width / 2, dst_bin_width / 2, density)
        dst_bin_of_end_center = dst_bin_of_end * dst_bin_width + dst_bin_width / 2
        delta_begin = -dst_bin_width / 2
        delta_end = src_bin_end - dst_bin_of_end_center
        norm += self._get_norm(delta_begin, delta_end, density)
        return norm.sum().item()

    def _non_linear_param_search(self):
        if self.histogram.size()[0] != self.bins:
            raise AssertionError('bins mismatch')
        bin_width = (self.max_val - self.min_val) / self.bins
        total = tensorplay.sum(self.histogram).item()
        cSum = tensorplay.cumsum(self.histogram, dim=0)
        stepsize = 1e-05
        alpha = 0.0
        beta = 1.0
        start_bin = 0
        end_bin = self.bins - 1
        norm_min = float('inf')
        while alpha < beta:
            next_alpha = alpha + stepsize
            next_beta = beta - stepsize
            l = start_bin
            r = end_bin
            while l < end_bin and cSum[l] < next_alpha * total:
                l = l + 1
            while r > start_bin and cSum[r] > next_beta * total:
                r = r - 1
            next_start_bin = start_bin
            next_end_bin = end_bin
            if l - start_bin > end_bin - r:
                next_start_bin = l
                alpha = next_alpha
            else:
                next_end_bin = r
                beta = next_beta
            if next_start_bin == start_bin and next_end_bin == end_bin:
                continue
            norm = self._compute_quantization_error(next_start_bin, next_end_bin)
            if norm > norm_min:
                break
            norm_min = norm
            start_bin = next_start_bin
            end_bin = next_end_bin
        new_min = self.min_val + bin_width * start_bin
        new_max = self.min_val + bin_width * (end_bin + 1)
        return (new_min, new_max)

    def _upscale_histogram(self, histogram, orig_min, orig_max, update_min, update_max):
        histogram = histogram.repeat_interleave(self.upsample_rate) / self.upsample_rate
        bin_size = (orig_max - orig_min) / (self.bins * self.upsample_rate)
        mid_points_histogram = tensorplay.linspace(float(orig_min), float(orig_max), self.bins * self.upsample_rate + 1, device=orig_min.device)[:-1].to(histogram.device) + 0.5 * bin_size
        boundaries_new_histogram = tensorplay.linspace(float(update_min), float(update_max), self.bins + 1, device=update_min.device).to(histogram.device)
        bucket_assignments = tensorplay.bucketize(mid_points_histogram, boundaries_new_histogram, right=True) - 1
        bucket_assignments[bucket_assignments >= self.bins] = self.bins - 1
        bucket_assignments[bucket_assignments < 0] = 0
        update_histogram = tensorplay.bincount(bucket_assignments, weights=histogram, minlength=self.bins)
        return update_histogram

    def _combine_histograms(self, orig_hist, orig_min, orig_max, update_hist, update_min, update_max):
        if update_min == orig_min and update_max == orig_max:
            return orig_hist + update_hist
        if orig_min == orig_max:
            bin_value = tensorplay.sum(orig_hist)
            transformed_orig_hist = tensorplay.histc(orig_min, bins=self.bins, min=float(update_min), max=float(update_max)) * bin_value
            return transformed_orig_hist + update_hist
        if update_min > orig_min:
            raise AssertionError('update_min must be <= orig_min')
        if update_max < orig_max:
            raise AssertionError('update_max must be >= orig_max')
        transformed_orig_hist = self._upscale_histogram(orig_hist, orig_min, orig_max, update_min, update_max)
        return update_hist + transformed_orig_hist

    def reset_histogram(self, x, min_val, max_val):
        self.min_val.resize_(min_val.shape)
        self.min_val.copy_(min_val)
        self.max_val.resize_(max_val.shape)
        self.max_val.copy_(max_val)
        if min_val.numel() != 1 or max_val.numel() != 1:
            raise AssertionError('histogram min/max values must be scalar.')
        new_histogram = tensorplay.histc(x, self.bins, min=float(min_val), max=float(max_val))
        self.histogram.resize_(new_histogram.shape)
        self.histogram.copy_(new_histogram)

    def forward(self, x_orig):
        if x_orig.numel() == 0:
            return x_orig
        x = x_orig.detach()
        x_min, x_max = tensorplay.aminmax(x)
        if x_min == -tensorplay.inf or x_max == tensorplay.inf:
            warnings.warn('inf detected in input tensor, ignoring input', stacklevel=2)
            x = x[x.abs() != tensorplay.inf]
            if x.numel() == 0:
                return x_orig
            x_min, x_max = tensorplay.aminmax(x)
        current_min = self.min_val
        current_max = self.max_val
        is_uninitialized = self.min_val == float('inf') or self.max_val == float('-inf')
        if is_uninitialized:
            self.reset_histogram(x, x_min, x_max)
        else:
            update_min, update_max = (x_min, x_max)
            new_min = tensorplay.minimum(current_min, update_min)
            new_max = tensorplay.maximum(current_max, update_max)
            new_min, new_max = (new_min.detach(), new_max.detach())
            update_histogram = tensorplay.histc(x, self.bins, min=float(new_min), max=float(new_max)).to(self.histogram.device)
            if new_min == current_min and new_max == current_max:
                combined_histogram = self.histogram + update_histogram
                self.histogram.resize_(combined_histogram.shape)
                self.histogram.copy_(combined_histogram)
            else:
                combined_histogram = self._combine_histograms(self.histogram, current_min, current_max, update_histogram, new_min, new_max)
                self.histogram.resize_(combined_histogram.shape)
                self.histogram.copy_(combined_histogram)
                self.min_val.resize_(new_min.shape)
                self.min_val.copy_(new_min)
                self.max_val.resize_(new_max.shape)
                self.max_val.copy_(new_max)
        return x_orig

    def calculate_qparams(self):
        is_uninitialized = self.min_val == float('inf') and self.max_val == float('-inf')
        if is_uninitialized:
            warnings.warn('must run observer before calling calculate_qparams.                                    Returning default scale and zero point ', stacklevel=2)
            return (tensorplay.tensor([1.0], device=self.min_val.device.type), tensorplay.tensor([0], device=self.min_val.device.type))
        if self.bins != len(self.histogram):
            raise AssertionError('The number of bins in histogram should be equal to the number of bins supplied while making this observer')
        new_min, new_max = self._non_linear_param_search()
        return self._derive_qparams(new_min, new_max)

    def _save_to_state_dict(self, destination, prefix, keep_vars):
        super()._save_to_state_dict(destination, prefix, keep_vars)
        destination[prefix + 'min_val'] = self.min_val
        destination[prefix + 'max_val'] = self.max_val

    def _load_from_state_dict(self, state_dict, prefix, local_metadata, strict, missing_keys, unexpected_keys, error_msgs):
        version = local_metadata.get('version', None)
        if version is None or version < 3:
            min_val_name, max_val_name = (prefix + 'min_val', prefix + 'max_val')
            if min_val_name in state_dict:
                if state_dict[min_val_name].shape == tensorplay.Size([0]):
                    state_dict[min_val_name] = tensorplay.tensor(float('inf'))
            if max_val_name in state_dict:
                if state_dict[max_val_name].shape == tensorplay.Size([0]):
                    state_dict[max_val_name] = tensorplay.tensor(float('-inf'))
        local_state = ['min_val', 'max_val']
        for name in local_state:
            key = prefix + name
            if key in state_dict:
                val = state_dict[key]
                setattr(self, name, val)
            elif strict:
                missing_keys.append(key)
        super()._load_from_state_dict(state_dict, prefix, local_metadata, strict, missing_keys, unexpected_keys, error_msgs)

    def extra_repr(self):
        return f'min_val={self.min_val}, max_val={self.max_val}'



[docs]
class FixedQParamsObserver(ObserverBase):

    def __init__(self, scale, zero_point, dtype=tensorplay.quint8, qscheme=tensorplay.per_tensor_affine, quant_min=0, quant_max=255, is_dynamic=False, **kwargs):
        if is_dynamic:
            raise NotImplementedError("FixedQParamsObserver doesn't support dynamic quantization")
        super().__init__(dtype=dtype, is_dynamic=is_dynamic, **kwargs)
        self.quant_min = quant_min
        self.quant_max = quant_max
        self.register_buffer('scale', tensorplay.tensor([scale], dtype=tensorplay.float))
        self.register_buffer('zero_point', tensorplay.tensor([zero_point], dtype=tensorplay.int))
        self.dtype = dtype
        self.qscheme = qscheme

    def forward(self, X):
        return X

    def calculate_qparams(self):
        return (self.scale, self.zero_point)



[docs]
class PlaceholderObserver(ObserverBase):

    def __init__(self, dtype=tensorplay.float32, custom_op_name='', compute_dtype=None, quant_min=None, quant_max=None, qscheme=None, eps=None, is_dynamic=False):
        super().__init__(dtype=dtype, is_dynamic=is_dynamic)
        if qscheme is None:
            qscheme = tensorplay.per_tensor_affine
        if eps is None:
            eps = tensorplay.finfo(tensorplay.float32).eps
        self.dtype = dtype
        self.qscheme = qscheme
        self.quant_min = quant_min
        self.quant_max = quant_max
        self.eps = eps
        self.custom_op = custom_op_name
        if compute_dtype:
            is_dynamic = True
            warnings.warn('Please use `is_dynamic` instead of `compute_dtype`.                     `compute_dtype` will be deprecated in a future release                     of quantization.', stacklevel=2)

    def forward(self, x):
        return x

    def extra_repr(self):
        return f'dtype={self.dtype}, is_dynamic={self.is_dynamic}'

    def calculate_qparams(self):
        raise Exception('calculate_qparams should not be called for PlaceholderObserver')


class RecordingObserver(ObserverBase):
    __annotations__ = {'tensor_val': list[tensorplay.Tensor | None]}

    def __init__(self, dtype=tensorplay.quint8):
        super().__init__(dtype=dtype, is_dynamic=False)
        self.tensor_val = []

    def forward(self, x):
        self.tensor_val.append(x.clone())
        return x

    def calculate_qparams(self):
        raise Exception('calculate_qparams should not be called for RecordingObserver')

    def get_tensor_value(self):
        return self.tensor_val

class NoopObserver(ObserverBase):

    def __init__(self, dtype=tensorplay.float16, custom_op_name=''):
        super().__init__(dtype=dtype, is_dynamic=False)
        self.dtype = dtype
        self.custom_op = custom_op_name

    def forward(self, x):
        return x

    def calculate_qparams(self):
        raise Exception('calculate_qparams should not be called for NoopObserver')

class ReuseInputObserver(ObserverBase):

    def __init__(self):
        super().__init__(tensorplay.quint8, is_dynamic=False)

    def forward(self, x):
        return x

    def calculate_qparams(self):
        raise Exception('calculate_qparams should not be called for ReuseInputObserver')


class MappingType(Enum):
    """How a floating point range is mapped onto the integer grid.

    SYMMETRIC maps the symmetric float range around zero onto the full
    integer range.  SYMMETRIC_NO_CLIPPING_ERR derives the scale from the
    per-side min/max separately so negative values round with less error.
    ASYMMETRIC maps the observed float range directly onto the integer range.
    """

    SYMMETRIC = auto()
    SYMMETRIC_NO_CLIPPING_ERR = auto()
    ASYMMETRIC = auto()


class ZeroPointDomain(Enum):
    """Whether the zero point lives in the integer or floating point domain.

    INT: quantized = round(float / scale) + zero_point (integer)
    FLOAT: quantized = round((float - (zero_point - scale * mid)) / scale)
    NONE: quantized = round(float / scale)
    """

    INT = auto()
    FLOAT = auto()
    NONE = auto()


class TorchAODType(Enum):
    """Placeholder for narrow integer dtypes without a native representation."""

    INT1 = auto()
    INT2 = auto()
    INT3 = auto()
    INT4 = auto()
    INT5 = auto()
    INT6 = auto()
    INT7 = auto()


@dataclass(frozen=True)
class Granularity:
    """Base class for quantization granularity descriptors."""


@dataclass(frozen=True)
class PerBlock(Granularity):
    """Per-block granularity with an explicit block size."""

    block_size: tuple[int, ...]


@dataclass(frozen=True)
class PerTensor(Granularity):
    """A single set of quantization parameters for the whole tensor."""


@dataclass(frozen=True)
class PerAxis(Granularity):
    """One set of quantization parameters per slice along ``axis``."""

    axis: int


@dataclass(frozen=True)
class PerGroup(Granularity):
    """One set of quantization parameters per group of ``group_size`` elements."""

    group_size: int


class PerRow(Granularity):
    """Row-wise granularity, a per-axis special case used by float8 matmuls."""


class PerToken(Granularity):
    """One set of quantization parameters per token (last dimension slice)."""


def get_block_size(
    input_shape: tuple[int, ...], granularity: Granularity
) -> tuple[int, ...]:
    """Derive the block size from an input shape and a granularity type."""
    if not isinstance(granularity, Granularity):
        raise AssertionError(
            "Please provide an instance of Granularity, not subclass of it"
        )
    if isinstance(granularity, PerTensor):
        return input_shape
    elif isinstance(granularity, PerAxis):
        block_size = list(input_shape)
        block_size[granularity.axis] = 1
        return tuple(block_size)
    elif isinstance(granularity, PerRow):
        return (1,) * (len(input_shape) - 1) + (input_shape[-1],)
    elif isinstance(granularity, PerGroup):
        if len(input_shape) != 2:
            raise AssertionError(
                f"Expecting input shape dim to be 2 for per group quantization, gotinput shape: {input_shape}"
            )
        return (1, granularity.group_size)
    elif isinstance(granularity, PerToken):
        block_size = [1] * len(input_shape)
        block_size[-1] = input_shape[-1]
        return tuple(block_size)
    raise ValueError(f"Unsupported Granularity: {granularity}")


class AffineQuantizedObserverBase(ABC, nn.Module):
    """Observer for affine quantization with an explicit granularity.

    Stores the mapping configuration plus the scale/zero-point dtypes and
    zero-point domain used by affine quantized tensors.
    """

    with_args = classmethod(_with_args)

    def __init__(
        self,
        mapping_type: MappingType,
        target_dtype: tensorplay.dtype,
        granularity: Granularity,
        quant_min: int | None = None,
        quant_max: int | None = None,
        eps: float | None = None,
        scale_dtype: tensorplay.dtype | None = None,
        zero_point_dtype: tensorplay.dtype | None = None,
        preserve_zero: bool = True,
        zero_point_domain: ZeroPointDomain | None = ZeroPointDomain.INT,
        **kwargs,
    ):
        super().__init__()
        if granularity is None:
            raise AssertionError("granularity is None")
        self.mapping_type = mapping_type
        self.target_dtype = target_dtype
        self.granularity = granularity
        self.quant_min = quant_min
        self.quant_max = quant_max
        self.eps = eps
        self.scale_dtype = scale_dtype
        self.zero_point_dtype = zero_point_dtype
        self.preserve_zero = preserve_zero
        self.zero_point_domain = zero_point_domain
        self.block_size = None
        self.original_dtype = None

    @abstractmethod
    def forward(self, input):
        """Record statistics and return the original input tensor."""

    @abstractmethod
    def calculate_qparams(self) -> tuple:
        """Return the (scale, zero_point) tuple for this observer."""

    def convert(self, model, observer_node):
        """Rewrite the observer node into its quantized representation.

        Requires the graph-mode lowering hooks for affine quantized tensors,
        which are not part of this package yet.
        """
        raise NotImplementedError(
            "AffineQuantizedObserverBase.convert requires graph-mode affine lowering"
        )


def with_args(**kwargs):
    return lambda cls: _with_args(cls, **kwargs)


def _record(self, x):
    return self(x)


def _observation_state(self):
    return self.state_dict()


def _load_observation_state(self, state):
    return self.load_state_dict(state)


def _legacy_qparams(min_val, max_val, quant_min=QUANT_MIN, quant_max=QUANT_MAX):
    lo, hi = min(float(min_val), 0.), max(float(max_val), 0.)
    scale = max((hi - lo) / (quant_max - quant_min), 1.1920928955078125e-7)
    return scale, max(quant_min, min(quant_max, quant_min - round(lo / scale)))


ObserverBase.record = _record
ObserverBase.observation_state = _observation_state
ObserverBase.load_observation_state = _load_observation_state
ObserverBase._calculate_qparams = staticmethod(_legacy_qparams)
MinMaxObserver.reset = MinMaxObserver.reset_min_max_vals
PerChannelMinMaxObserver.reset = PerChannelMinMaxObserver.reset_min_max_vals


def _reset_histogram(self):
    self.histogram.zero_()
    self.min_val.fill_(float('inf'))
    self.max_val.fill_(float('-inf'))


HistogramObserver.reset = _reset_histogram

default_observer = MinMaxObserver.with_args(quant_min=0, quant_max=127)
default_weight_observer = MinMaxObserver.with_args(dtype=tensorplay.qint8, qscheme=tensorplay.per_tensor_symmetric)
default_per_channel_weight_observer = PerChannelMinMaxObserver.with_args(dtype=tensorplay.qint8, qscheme=tensorplay.per_channel_symmetric)
default_histogram_observer = HistogramObserver.with_args(quant_min=0, quant_max=127)
default_dynamic_quant_observer = PlaceholderObserver.with_args(dtype=tensorplay.quint8, quant_min=0, quant_max=255, is_dynamic=True)
default_float_qparams_observer = PerChannelMinMaxObserver.with_args(dtype=tensorplay.quint8, qscheme=tensorplay.per_channel_affine_float_qparams)
default_float_qparams_observer_4bit = PerChannelMinMaxObserver.with_args(dtype=tensorplay.quint4x2, qscheme=tensorplay.per_channel_affine_float_qparams,ch_axis=0)
default_fixed_qparams_range_neg1to1_observer = FixedQParamsObserver.with_args(scale=2./256, zero_point=128)
default_fixed_qparams_range_0to1_observer = FixedQParamsObserver.with_args(scale=1./256, zero_point=0)
default_placeholder_observer = PlaceholderObserver
weight_observer_range_neg_127_to_127 = MinMaxObserver.with_args(
    dtype=tensorplay.qint8,
    qscheme=tensorplay.per_tensor_symmetric,
    quant_min=-127,
    quant_max=127,
    eps=2**-12,
)
per_channel_weight_observer_range_neg_127_to_127 = PerChannelMinMaxObserver.with_args(
    dtype=tensorplay.qint8,
    qscheme=tensorplay.per_channel_symmetric,
    quant_min=-127,
    quant_max=127,
    eps=2**-12,
)
default_debug_observer = RecordingObserver
default_reuse_input_observer = ReuseInputObserver
default_symmetric_fixed_qparams_observer = default_fixed_qparams_range_neg1to1_observer
default_affine_fixed_qparams_observer = default_fixed_qparams_range_0to1_observer



[docs]
def get_observer_state_dict(model):
    from .fake_quantize import FakeQuantizeBase
    state = OrderedDict()
    for name, module in model.named_modules():
        if isinstance(module, (ObserverBase, FakeQuantizeBase)):
            module._save_to_state_dict(state, name + '.' if name else '', False)
    return state




[docs]
def load_observer_state_dict(model, obs_dict):
    expected = get_observer_state_dict(model)
    if set(expected) != set(obs_dict):
        raise RuntimeError('Observer state keys do not match the model')
    state = model.state_dict()
    state.update(obs_dict)
    model.load_state_dict(state)



def _qparam_div(value, denominator):
    if value.device.type == 'cuda':
        return value * (1.0 / denominator)
    return value / denominator
Ask DeepWiki