# Copyright (c) 2021 - present / Neuralmagic, Inc. All Rights Reserved. # # 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 functools import wraps from math import ceil from typing import Optional import torch from compressed_tensors.quantization.quant_args import ( QuantizationArgs, QuantizationStrategy, round_to_quantized_type, ) from compressed_tensors.quantization.quant_config import QuantizationStatus from compressed_tensors.quantization.quant_scheme import QuantizationScheme from compressed_tensors.quantization.utils import ( calculate_range, compute_dynamic_scales_and_zp, ) from compressed_tensors.utils import safe_permute from torch.nn import Module __all__ = [ "quantize", "dequantize", "fake_quantize", "wrap_module_forward_quantized", "forward_quantize", ] @torch.no_grad() def quantize( x: torch.Tensor, scale: torch.Tensor, zero_point: torch.Tensor, args: QuantizationArgs, dtype: Optional[torch.dtype] = None, g_idx: Optional[torch.Tensor] = None, ) -> torch.Tensor: """ Quantize the input tensor x using the QuantizationStrategy specified in args. Quantization can be done per tensor, channel, token or group. For group quantization, the group_size must be divisible by the column size. The input scale and zero_points are reshaped to support vectorization (Assumes 1 is the channel dimension) :param x: Input tensor :param scale: scale tensor :param zero_point: zero point tensor :param args: quantization args dictating how to quantize x :param dtype: optional dtype to cast the quantized output to :param g_idx: optional mapping from column index to group index :return: fake quantized tensor """ return _process_quantization( x=x, scale=scale, zero_point=zero_point, args=args, dtype=dtype, do_quantize=True, do_dequantize=False, g_idx=g_idx, ) @torch.no_grad() def dequantize( x_q: torch.Tensor, scale: torch.Tensor, zero_point: Optional[torch.Tensor] = None, args: Optional[QuantizationArgs] = None, dtype: Optional[torch.dtype] = None, g_idx: Optional[torch.Tensor] = None, ) -> torch.Tensor: """ Dequantize a quantized input tensor x_q based on the strategy specified in args. If args is not provided, the strategy will be inferred. :param x: quantized input tensor :param scale: scale tensor :param zero_point: zero point tensor :param args: quantization args used to quantize x_q :param dtype: optional dtype to cast the dequantized output to :param g_idx: optional mapping from column index to group index :return: dequantized float tensor """ if args is None: if scale.ndim == 0 or scale.ndim == 1: args = QuantizationArgs(strategy=QuantizationStrategy.TENSOR) elif scale.ndim == 2: if scale.shape[1] == 1: args = QuantizationArgs(strategy=QuantizationStrategy.CHANNEL) else: group_size = int(x_q.shape[1] / scale.shape[1]) args = QuantizationArgs( strategy=QuantizationStrategy.GROUP, group_size=group_size ) else: raise ValueError( f"Could not infer a quantization strategy from scale with {scale.ndim} " "dimmensions. Expected 0 or 2 dimmensions." ) if dtype is None: dtype = scale.dtype return _process_quantization( x=x_q, scale=scale, zero_point=zero_point, args=args, do_quantize=False, do_dequantize=True, dtype=dtype, g_idx=g_idx, ) @torch.no_grad() def fake_quantize( x: torch.Tensor, scale: torch.Tensor, zero_point: torch.Tensor, args: QuantizationArgs, g_idx: Optional[torch.Tensor] = None, ) -> torch.Tensor: """ Fake quantize the input tensor x by quantizing then dequantizing with the QuantizationStrategy specified in args. Quantization can be done per tensor, channel, token or group. For group quantization, the group_size must be divisible by the column size. The input scale and zero_points are reshaped to support vectorization (Assumes 1 is the channel dimension) :param x: Input tensor :param scale: scale tensor :param zero_point: zero point tensor :param args: quantization args dictating how to quantize x :param g_idx: optional mapping from column index to group index :return: fake quantized tensor """ return _process_quantization( x=x, scale=scale, zero_point=zero_point, args=args, do_quantize=True, do_dequantize=True, g_idx=g_idx, ) @torch.no_grad() def _process_quantization( x: torch.Tensor, scale: torch.Tensor, zero_point: torch.Tensor, args: QuantizationArgs, g_idx: Optional[torch.Tensor] = None, dtype: Optional[torch.dtype] = None, do_quantize: bool = True, do_dequantize: bool = True, ) -> torch.Tensor: q_min, q_max = calculate_range(args, x.device) group_size = args.group_size if args.strategy == QuantizationStrategy.GROUP: output_dtype = dtype if dtype is not None else x.dtype output = torch.zeros_like(x).to(output_dtype) columns = output.shape[1] # TODO: make validation step for inputs while scale.ndim < 2: # pad scale and zero point dims for slicing scale = scale.unsqueeze(1) zero_point = zero_point.unsqueeze(1) if zero_point is not None else None if columns >= group_size: if columns % group_size != 0: raise ValueError( "tensor column shape must be divisble " f"by the given group_size {group_size}" ) # support column-order (default) quantization as well as other orderings # such as activation ordering. Below checks if g_idx has been initialized is_column_order = g_idx is None or -1 in g_idx if is_column_order: num_groups = int(ceil(columns / group_size)) group_sizes = torch.full((num_groups,), group_size, dtype=torch.int) else: group_indices, group_sizes = torch.unique(g_idx, return_counts=True) group_sizes = group_sizes[torch.argsort(group_indices)] perm = torch.argsort(g_idx) x = safe_permute(x, perm, dim=1) # TODO: experiment with vectorizing for loop for performance end = 0 for index, group_count in enumerate(group_sizes): sc = scale[:, index].view(-1, 1) zp = zero_point[:, index].view(-1, 1) if zero_point is not None else None start = end end = start + group_count if do_quantize: output[:, start:end] = _quantize( x[:, start:end], sc, zp, q_min, q_max, args, dtype=dtype, ) if do_dequantize: input = output[:, start:end] if do_quantize else x[:, start:end] output[:, start:end] = _dequantize(input, sc, zp) if not is_column_order: output = safe_permute(output, torch.argsort(perm), dim=1) else: # covers channel, token and tensor strategies if do_quantize: output = _quantize( x, scale, zero_point, q_min, q_max, args, dtype=dtype, ) if do_dequantize: output = _dequantize(output if do_quantize else x, scale, zero_point) return output def wrap_module_forward_quantized(module: Module, scheme: QuantizationScheme): # expects a module already initialized and injected with the parameters in # initialize_module_for_quantization if hasattr(module.forward, "__func__"): forward_func_orig = module.forward.__func__ else: forward_func_orig = module.forward.func @wraps(forward_func_orig) # ensures docstring, names, etc are propagated def wrapped_forward(self, *args, **kwargs): if not getattr(module, "quantization_enabled", True): # quantization is disabled on forward passes, return baseline # forward call return forward_func_orig.__get__(module, module.__class__)(*args, **kwargs) input_ = args[0] compressed = module.quantization_status == QuantizationStatus.COMPRESSED if scheme.input_activations is not None: # prehook should calibrate activations before forward call input_ = forward_quantize(module, input_, "input", scheme.input_activations) if scheme.weights is not None and not compressed: # calibrate and (fake) quantize weights when applicable unquantized_weight = self.weight.data.clone() self.weight.data = forward_quantize( module, self.weight, "weight", scheme.weights ) # perform wrapped forward call output = forward_func_orig.__get__(module, module.__class__)( input_, *args[1:], **kwargs ) # restore back to unquantized_value if scheme.weights is not None and not compressed: self.weight.data = unquantized_weight if scheme.output_activations is not None: # forward-hook should calibrate/forward_quantize if ( module.quantization_status == QuantizationStatus.CALIBRATION and not scheme.output_activations.dynamic ): return output output = forward_quantize( module, output, "output", scheme.output_activations ) return output # bind wrapped forward to module class so reference to `self` is correct bound_wrapped_forward = wrapped_forward.__get__(module, module.__class__) # set forward to wrapped forward setattr(module, "forward", bound_wrapped_forward) def forward_quantize( module: Module, value: torch.Tensor, base_name: str, args: "QuantizationArgs" ) -> torch.Tensor: # in compressed mode, the weight is already compressed and quantized so we don't # need to run fake quantization if ( module.quantization_status == QuantizationStatus.COMPRESSED and base_name == "weight" ): return value if value.numel() == 0: # if the tensor is empty, # skip quantization return value g_idx = getattr(module, "weight_g_idx", None) if args.dynamic: # dynamic quantization - determine the scale/zp on the fly scale, zero_point = compute_dynamic_scales_and_zp(value=value, args=args) else: # static quantization - get scale and zero point from layer scale = getattr(module, f"{base_name}_scale") zero_point = getattr(module, f"{base_name}_zero_point", None) return fake_quantize( x=value, scale=scale, zero_point=zero_point, args=args, g_idx=g_idx, ) @torch.no_grad() def _quantize( x: torch.Tensor, scale: torch.Tensor, zero_point: torch.Tensor, q_min: torch.Tensor, q_max: torch.Tensor, args: QuantizationArgs, dtype: Optional[torch.dtype] = None, ) -> torch.Tensor: scaled = x / scale if zero_point is not None: scaled += zero_point.to(x.dtype) # clamp first because cast isn't guaranteed to be saturated (ie for fp8) clamped_value = torch.clamp( scaled, q_min, q_max, ) quantized_value = round_to_quantized_type(clamped_value, args) if dtype is not None: quantized_value = quantized_value.to(dtype) return quantized_value @torch.no_grad() def _dequantize( x_q: torch.Tensor, scale: torch.Tensor, zero_point: torch.Tensor = None, dtype: Optional[torch.dtype] = None, ) -> torch.Tensor: dequant_value = x_q.to(scale.dtype) if zero_point is not None: dequant_value = dequant_value - zero_point.to(scale.dtype) dequant_value = dequant_value * scale if dtype is not None: dequant_value = dequant_value.to(dtype) return dequant_value