# Copyright (c) Meta Platforms, Inc. and affiliates. # All rights reserved. # # This source code is licensed under the BSD 3-Clause license found in the # LICENSE file in the root directory of this source tree. # mypy: allow-untyped-defs import typing from dataclasses import dataclass from typing import Callable, NamedTuple, Optional import torch from torch.fx import Node from torchao.quantization.pt2e.quantizer.quantizer import Q_ANNOTATION_KEY from .quantizer import QuantizationAnnotation, QuantizationSpec # In the absence of better name, just winging it with QuantizationConfig @dataclass(eq=True, frozen=True) class QuantizationConfig: input_activation: Optional[QuantizationSpec] output_activation: Optional[QuantizationSpec] weight: Optional[QuantizationSpec] bias: Optional[QuantizationSpec] # TODO: remove, since we can use observer_or_fake_quant_ctr to express this is_qat: bool = False # Use Annotated because list[Callable].__module__ is read-only. OperatorPatternType = typing.Annotated[list[Callable], None] OperatorPatternType.__module__ = "torchao.quantization.pt2e.quantizer.utils" class OperatorConfig(NamedTuple): # fix List[str] with List[List[Union[nn.Module, FunctionType, BuiltinFunctionType]]] # Basically we are mapping a quantization config to some list of patterns. # a pattern is defined as a list of nn module, function or builtin function names # e.g. [nn.Conv2d, torch.relu, torch.add] # We have not resolved whether fusion can be considered internal details of the # quantizer hence it does not need communication to user. # Note this pattern is not really informative since it does not really # tell us the graph structure resulting from the list of ops. config: QuantizationConfig operators: list[OperatorPatternType] def get_input_act_qspec(quantization_config: Optional[QuantizationConfig]): if quantization_config is None: return None if quantization_config.input_activation is None: return None quantization_spec: QuantizationSpec = quantization_config.input_activation assert quantization_spec.qscheme in [ torch.per_tensor_affine, torch.per_tensor_symmetric, ] return quantization_spec def get_output_act_qspec(quantization_config: Optional[QuantizationConfig]): if quantization_config is None: return None if quantization_config.output_activation is None: return None quantization_spec: QuantizationSpec = quantization_config.output_activation assert quantization_spec.qscheme in [ torch.per_tensor_affine, torch.per_tensor_symmetric, ] return quantization_spec def get_weight_qspec(quantization_config: Optional[QuantizationConfig]): if quantization_config is None: return None assert quantization_config is not None if quantization_config.weight is None: return None quantization_spec: QuantizationSpec = quantization_config.weight if quantization_spec.qscheme not in [ torch.per_tensor_symmetric, torch.per_channel_symmetric, None, ]: raise ValueError( f"Unsupported quantization_spec {quantization_spec} for weight" ) return quantization_spec def get_bias_qspec(quantization_config: Optional[QuantizationConfig]): if quantization_config is None: return None assert quantization_config is not None if quantization_config.bias is None: return None quantization_spec: QuantizationSpec = quantization_config.bias assert quantization_spec.dtype == torch.float, ( "Only float dtype for bias is supported for bias right now" ) return quantization_spec def annotate_input_qspec_map(node: Node, input_node: Node, qspec): quantization_annotation = node.meta.get(Q_ANNOTATION_KEY, QuantizationAnnotation()) if quantization_annotation.input_qspec_map is None: quantization_annotation.input_qspec_map = {} quantization_annotation.input_qspec_map[input_node] = qspec node.meta[Q_ANNOTATION_KEY] = quantization_annotation def annotate_output_qspec(node: Node, qspec): quantization_annotation = node.meta.get(Q_ANNOTATION_KEY, QuantizationAnnotation()) quantization_annotation.output_qspec = qspec node.meta[Q_ANNOTATION_KEY] = quantization_annotation def get_module_name_filter(module_name: str): """Get the module_name_filter function for a given module name, the filter accepts a node and checks if the node comes from a module that has certain module name For example: node: linear_op = call_function[...](...) # comes from a module with name blocks.sub.linear1 >> module_name_filter = _get_module_name_filter("blocks.sub") >> print(module_name_filter(node)) True # the node is from "blocks.sub" based on the fully qualified name "blocks.sub.linear1" """ def module_name_filter(n: Node) -> bool: # example: { # 'L__self___sub': ("L['self'].sub", ), # 'L__self___sub_linear': ("L['self'].sub.linear", ) # } # get_attr nodes doesn't have nn_module_stack? nn_module_stack = n.meta.get("nn_module_stack", {}) def _normalize_path(n): prefix = 0 # TODO This is non standard behavior and should be removed when we migrate off capture_pre_autograd_graph. if n.startswith("L['self']."): prefix = len("L['self'].") return n[prefix:] names = [_normalize_path(n) for n, _ in nn_module_stack.values()] return module_name in names return module_name_filter def is_valid_annotation(annotation: QuantizationAnnotation) -> bool: if annotation is None: return False input_qspec_map = annotation.input_qspec_map output_qspec = annotation.output_qspec if len(input_qspec_map) == 0 and output_qspec is None: return False return True