| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| 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] |
|
|
| |
|
|
| while scale.ndim < 2: |
| |
| 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}" |
| ) |
|
|
| |
| |
| 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) |
|
|
| |
| 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: |
| 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): |
| |
| |
| if hasattr(module.forward, "__func__"): |
| forward_func_orig = module.forward.__func__ |
| else: |
| forward_func_orig = module.forward.func |
|
|
| @wraps(forward_func_orig) |
| def wrapped_forward(self, *args, **kwargs): |
| if not getattr(module, "quantization_enabled", True): |
| |
| |
| 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: |
| |
| input_ = forward_quantize(module, input_, "input", scheme.input_activations) |
|
|
| if scheme.weights is not None and not compressed: |
| |
| unquantized_weight = self.weight.data.clone() |
| self.weight.data = forward_quantize( |
| module, self.weight, "weight", scheme.weights |
| ) |
|
|
| |
| output = forward_func_orig.__get__(module, module.__class__)( |
| input_, *args[1:], **kwargs |
| ) |
|
|
| |
| if scheme.weights is not None and not compressed: |
| self.weight.data = unquantized_weight |
|
|
| if scheme.output_activations is not None: |
| |
| 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 |
|
|
| |
| bound_wrapped_forward = wrapped_forward.__get__(module, module.__class__) |
| |
| setattr(module, "forward", bound_wrapped_forward) |
|
|
|
|
| def forward_quantize( |
| module: Module, value: torch.Tensor, base_name: str, args: "QuantizationArgs" |
| ) -> torch.Tensor: |
|
|
| |
| |
| if ( |
| module.quantization_status == QuantizationStatus.COMPRESSED |
| and base_name == "weight" |
| ): |
| return value |
|
|
| if value.numel() == 0: |
| |
| |
| return value |
|
|
| g_idx = getattr(module, "weight_g_idx", None) |
|
|
| if args.dynamic: |
| |
| scale, zero_point = compute_dynamic_scales_and_zp(value=value, args=args) |
| else: |
| |
| 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) |
| |
| 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 |
|
|