| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import logging |
| from typing import Generator, List, Optional, Tuple |
|
|
| import torch |
| from compressed_tensors.quantization.quant_args import ( |
| FP8_DTYPE, |
| QuantizationArgs, |
| QuantizationStrategy, |
| QuantizationType, |
| ) |
| from compressed_tensors.quantization.quant_scheme import QuantizationScheme |
| from torch import FloatTensor, IntTensor, Tensor |
| from torch.nn import Module |
| from tqdm import tqdm |
|
|
|
|
| __all__ = [ |
| "infer_quantization_status", |
| "is_module_quantized", |
| "is_model_quantized", |
| "module_type", |
| "calculate_compression_ratio", |
| "get_torch_bit_depth", |
| "can_quantize", |
| "parse_out_kv_cache_args", |
| "KV_CACHE_TARGETS", |
| "is_kv_cache_quant_scheme", |
| "iter_named_leaf_modules", |
| "iter_named_quantizable_modules", |
| "compute_dynamic_scales_and_zp", |
| "calculate_range", |
| "calculate_qparams", |
| ] |
|
|
| |
| |
| KV_CACHE_TARGETS = ["re:.*self_attn$"] |
|
|
| _LOGGER: logging.Logger = logging.getLogger(__name__) |
|
|
|
|
| def calculate_qparams( |
| min_vals: Tensor, max_vals: Tensor, quantization_args: QuantizationArgs |
| ) -> Tuple[FloatTensor, IntTensor]: |
| """ |
| :param min_vals: tensor of min value(s) to calculate scale(s) and zero point(s) |
| from |
| :param max_vals: tensor of max value(s) to calculate scale(s) and zero point(s) |
| from |
| :param quantization_args: settings to quantization |
| :return: tuple of the calculated scale(s) and zero point(s) |
| """ |
| min_vals = torch.min(min_vals, torch.zeros_like(min_vals)) |
| max_vals = torch.max(max_vals, torch.zeros_like(max_vals)) |
| device = min_vals.device |
|
|
| bit_min, bit_max = calculate_range(quantization_args, device) |
| bit_range = bit_max - bit_min |
| zp_dtype = quantization_args.pytorch_dtype() |
|
|
| if quantization_args.symmetric: |
| max_val_pos = torch.max(torch.abs(min_vals), torch.abs(max_vals)) |
| scales = max_val_pos / (float(bit_range) / 2) |
| scales = torch.clamp(scales, min=torch.finfo(torch.float32).eps) |
| zero_points = torch.zeros(scales.shape, device=device, dtype=min_vals.dtype) |
| else: |
| scales = (max_vals - min_vals) / float(bit_range) |
| scales = torch.clamp(scales, min=torch.finfo(torch.float32).eps) |
| zero_points = bit_min - (min_vals / scales) |
| zero_points = torch.clamp(zero_points, bit_min, bit_max) |
|
|
| |
| zero_points = zero_points.to(zp_dtype) |
|
|
| if scales.ndim == 0: |
| scales = scales.reshape(1) |
| zero_points = zero_points.reshape(1) |
|
|
| return scales, zero_points |
|
|
|
|
| def compute_dynamic_scales_and_zp(value: Tensor, args: QuantizationArgs): |
| """ |
| Returns the computed scales and zero points for dynamic activation |
| qunatization. |
| |
| :param value: tensor to calculate quantization parameters for |
| :param args: quantization args |
| :param reduce_dims: optional tuple of dimensions to reduce along, |
| returned scale and zero point will be shaped (1,) along the |
| reduced dimensions |
| :return: tuple of scale and zero point derived from the observed tensor |
| """ |
| if args.strategy == QuantizationStrategy.TOKEN: |
| dim = {1, 2} |
| reduce_dims = tuple(idx for idx in range(value.ndim) if idx not in dim) |
| elif args.strategy == QuantizationStrategy.TENSOR: |
| reduce_dims = None |
| else: |
| raise ValueError( |
| f"One of {QuantizationStrategy.TOKEN} or {QuantizationStrategy.TENSOR} ", |
| "must be used for dynamic quantization", |
| ) |
|
|
| if not reduce_dims: |
| min_val, max_val = torch.aminmax(value) |
| else: |
| min_val = torch.amin(value, dim=reduce_dims, keepdims=True) |
| max_val = torch.amax(value, dim=reduce_dims, keepdims=True) |
|
|
| return calculate_qparams(min_val, max_val, args) |
|
|
|
|
| def calculate_range(quantization_args: QuantizationArgs, device: str) -> Tuple: |
| """ |
| Calculated the effective quantization range for the given Quantization Args |
| |
| :param quantization_args: quantization args to get range of |
| :param device: device to store the range to |
| :return: tuple endpoints for the given quantization range |
| """ |
| if quantization_args.type == QuantizationType.INT: |
| bit_range = 2**quantization_args.num_bits |
| q_max = torch.tensor(bit_range / 2 - 1, device=device) |
| q_min = torch.tensor(-bit_range / 2, device=device) |
| elif quantization_args.type == QuantizationType.FLOAT: |
| if quantization_args.num_bits != 8: |
| raise ValueError( |
| "Floating point quantization is only supported for 8 bits," |
| f"got {quantization_args.num_bits}" |
| ) |
| fp_range_info = torch.finfo(FP8_DTYPE) |
| q_max = torch.tensor(fp_range_info.max, device=device) |
| q_min = torch.tensor(fp_range_info.min, device=device) |
| else: |
| raise ValueError(f"Invalid quantization type {quantization_args.type}") |
|
|
| return q_min, q_max |
|
|
|
|
| def infer_quantization_status(model: Module) -> Optional["QuantizationStatus"]: |
| """ |
| Checks the quantization status of a model. Assumes all modules in the model have |
| the same status, so only the first quantized model is checked. |
| |
| :param model: model to check quantization status for |
| :return: quantization status if the model is quantized, otherwise None |
| """ |
| for module in model.modules(): |
| status = getattr(module, "quantization_status", None) |
| if status is not None: |
| return status |
| return None |
|
|
|
|
| def is_module_quantized(module: Module) -> bool: |
| """ |
| Check if a module is quantized, based on the existence of a non-empty quantization |
| scheme |
| |
| :param module: pytorch module to check |
| :return: True if module is quantized, False otherwise |
| """ |
| if not hasattr(module, "quantization_scheme"): |
| return False |
|
|
| if module.quantization_scheme.weights is not None: |
| return True |
|
|
| if module.quantization_scheme.input_activations is not None: |
| return True |
|
|
| if module.quantization_scheme.output_activations is not None: |
| return True |
|
|
| return False |
|
|
|
|
| def is_model_quantized(model: Module) -> bool: |
| """ |
| Check if any modules in a model are quantized, based on the existence of a non-empty |
| quantization scheme in at least one module |
| |
| :param model: pytorch model |
| :return: True if model is quantized, False otherwise |
| """ |
|
|
| for _, submodule in iter_named_leaf_modules(model): |
| if is_module_quantized(submodule): |
| return True |
|
|
| return False |
|
|
|
|
| def module_type(module: Module) -> str: |
| """ |
| Gets a string representation of a module type |
| |
| :module: pytorch module to get type of |
| :return: module type as a string |
| """ |
| return type(module).__name__ |
|
|
|
|
| def iter_named_leaf_modules(model: Module) -> Generator[Tuple[str, Module], None, None]: |
| """ |
| Yields modules that do not have any submodules except observers. The observers |
| themselves are not yielded |
| :param model: model to get leaf modules of |
| :returns: generator tuple of (name, leaf_submodule) |
| """ |
| for name, submodule in model.named_modules(): |
| children = list(submodule.children()) |
| |
| if len(children) == 0 and "observer" in name: |
| yield name, submodule |
| else: |
| if len(children) > 0: |
| named_children, children = zip(*list(submodule.named_children())) |
| has_non_observer_children = False |
| for i in range(len(children)): |
| child_name = named_children[i] |
|
|
| if "observer" not in child_name: |
| has_non_observer_children = True |
|
|
| if not has_non_observer_children: |
| yield name, submodule |
|
|
|
|
| def iter_named_quantizable_modules( |
| model: Module, include_children: bool = True, include_attn: bool = False |
| ) -> Generator[Tuple[str, Module], None, None]: |
| """ |
| Yield name and submodule of |
| - leaf modules, set by include_children |
| - attention modyles, set by include_attn |
| |
| :param model: model to get leaf modules of |
| :param include_children: flag to get the leaf modules |
| :param inlcude_attn: flag to get the attention modules |
| :returns: generator tuple of (name, submodule) |
| """ |
| for name, submodule in model.named_modules(): |
| |
| if include_children: |
| children = list(submodule.children()) |
| if len(children) == 0 and "observer" not in name: |
| yield name, submodule |
| else: |
| if len(children) > 0: |
| named_children, children = zip(*list(submodule.named_children())) |
| has_non_observer_children = False |
| for i in range(len(children)): |
| child_name = named_children[i] |
|
|
| if "observer" not in child_name: |
| has_non_observer_children = True |
|
|
| if not has_non_observer_children: |
| yield name, submodule |
| if include_attn: |
| if name.endswith("self_attn"): |
| yield name, submodule |
|
|
|
|
| def get_torch_bit_depth(value: torch.Tensor) -> int: |
| """ |
| Determine the number of bits used to represent the dtype of a tensor |
| |
| :param value: tensor to check bit depth of |
| :return: bit depth of each element in the value tensor |
| """ |
| try: |
| bit_depth = torch.finfo(value.dtype).bits |
| except TypeError: |
| bit_depth = torch.iinfo(value.dtype).bits |
|
|
| return bit_depth |
|
|
|
|
| def can_quantize(value: torch.Tensor, quant_args: "QuantizationArgs") -> bool: |
| """ |
| Checks if value can be quantized by quant_args. |
| |
| :param value: tensor to check for quantization |
| :param quant_args: QuantizationArgs to use for quantization |
| :return: False if value is already quantized to quant_args or value is incompatible |
| with quant_args, True if value can be quantized with quant_args |
| """ |
| bit_depth = get_torch_bit_depth(value) |
| requested_depth = quant_args.num_bits |
| if bit_depth < quant_args.num_bits: |
| _LOGGER.warn( |
| f"Can't quantize tensor with bit depth {bit_depth} to {requested_depth}." |
| "The QuantizationArgs provided are not compatible with the input tensor." |
| ) |
|
|
| return bit_depth > quant_args.num_bits |
|
|
|
|
| def calculate_compression_ratio(model: Module) -> float: |
| """ |
| Calculates the quantization compression ratio of a pytorch model, based on the |
| number of bits needed to represent the total weights in compressed form. Does not |
| take into account activation quantizatons. |
| |
| :param model: pytorch module to calculate compression ratio for |
| :return: compression ratio of the whole model |
| """ |
| total_compressed = 0.0 |
| total_uncompressed = 0.0 |
| for name, submodule in tqdm( |
| iter_named_leaf_modules(model), |
| desc="Calculating quantization compression ratio", |
| ): |
| for parameter in model.parameters(): |
| uncompressed_bits = get_torch_bit_depth(parameter) |
| compressed_bits = uncompressed_bits |
| if is_module_quantized(submodule) and submodule.quantization_scheme.weights: |
| compressed_bits = submodule.quantization_scheme.weights.num_bits |
|
|
| num_weights = parameter.numel() |
| total_compressed += compressed_bits * num_weights |
| total_uncompressed += uncompressed_bits * num_weights |
|
|
| return total_uncompressed / total_compressed |
|
|
|
|
| def is_kv_cache_quant_scheme(scheme: QuantizationScheme) -> bool: |
| """ |
| Check whether the QuantizationScheme targets the kv cache. |
| It does if all the following criteria are met: |
| - the scheme targets either exactly match the KV_CACHE_TARGETS |
| or the match KV_CACHE_TARGETS regex pattern |
| - the scheme quantizes output_activations (we want to quantize the |
| outputs from the KV_CACHE_TARGETS, as their correspond to the |
| keys and values that are to be saved in the cache) |
| |
| :param scheme: The QuantizationScheme to investigate |
| :return: boolean flag |
| """ |
| for target in scheme.targets: |
| if target in KV_CACHE_TARGETS: |
| return True |
|
|
| return False |
|
|
|
|
| def parse_out_kv_cache_args( |
| quant_scheme_to_layers: List[QuantizationScheme], |
| ) -> Tuple[Optional[QuantizationArgs], List[QuantizationScheme]]: |
| """ |
| If possible, parse out the kv cache specific QuantizationArgs |
| from the list of the QuantizationSchemes. If no kv cache |
| specific QuantizationArgs available, this function acts |
| as an identity function |
| |
| :param quant_scheme_to_layers: list of QuantizationSchemes |
| :return: kv_cache_args (optional) and the (remaining or original) |
| list of the QuantizationSchemes |
| """ |
| kv_cache_quant_scheme_to_layers = [ |
| scheme for scheme in quant_scheme_to_layers if is_kv_cache_quant_scheme(scheme) |
| ] |
| quant_scheme_to_layers = [ |
| scheme |
| for scheme in quant_scheme_to_layers |
| if not is_kv_cache_quant_scheme(scheme) |
| ] |
|
|
| if kv_cache_quant_scheme_to_layers: |
| kv_cache_quant_scheme_to_layers = kv_cache_quant_scheme_to_layers[0] |
| kv_cache_args = kv_cache_quant_scheme_to_layers.output_activations |
| else: |
| kv_cache_args = None |
|
|
| return kv_cache_args, quant_scheme_to_layers |
|
|