-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Add QAT support and pass hooks to QuantizationRecipe. #21935
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,7 +8,7 @@ | |
| from abc import ABCMeta, abstractmethod | ||
| from dataclasses import dataclass | ||
| from enum import Enum, EnumMeta | ||
| from typing import Callable, List, Optional | ||
| from typing import Callable, Iterable, List, Optional | ||
|
|
||
| import torch | ||
| from executorch.exir import EdgeProgramManager, ExportedProgram | ||
|
|
@@ -88,16 +88,50 @@ class QuantizationRecipe: | |
| """ | ||
| Configuration recipe for quantization. | ||
|
|
||
| This class holds the configuration parameters for quantizing a model. | ||
| This class holds the configuration parameters for quantizing a model, supporting | ||
| both post-training quantization (PTQ) and quantization-aware training (QAT). | ||
|
|
||
| Attributes: | ||
| quantizers: Optional list of quantizers for model quantization | ||
| quantizers: Optional list of quantizers for model quantization. | ||
| ao_quantization_configs: Optional list of AOQuantizationConfig objects that pair | ||
| AOBaseConfig with optional filter functions | ||
| AOBaseConfig with optional filter functions. | ||
| is_qat: If True, use the QAT flow (prepare_qat_pt2e -> train_fn -> convert_pt2e). | ||
| If False (default), use the PTQ flow (prepare_pt2e -> calibrate -> convert_pt2e). | ||
| calibration_inputs_fn: Optional callable returning an iterable of input tuples used for | ||
| PTQ calibration. When None (default), the example inputs are used. | ||
| Ignored when is_qat=True. | ||
| train_fn: Callable that receives the prepared GraphModule and trains it. | ||
| Required when is_qat=True; ignored otherwise. | ||
| pre_prepare_passes: Optional list of callables applied to the captured GraphModule | ||
| before prepare_pt2e / prepare_qat_pt2e. | ||
| Each callable receives a GraphModule and must return a GraphModule. | ||
| post_prepare_passes: Optional list of callables applied to the prepared GraphModule | ||
| after prepare_pt2e / prepare_qat_pt2e and before calibration / training. | ||
| Each callable receives a GraphModule and must return a GraphModule. | ||
| pre_convert_passes: Optional list of callables applied to the GraphModule after | ||
| calibration (PTQ) or training (QAT) and before convert_pt2e. | ||
| Each callable receives a GraphModule and must return a GraphModule. | ||
| post_convert_passes: Optional list of callables applied to the GraphModule after convert_pt2e. | ||
| Each callable receives a GraphModule and must return a GraphModule. | ||
| """ | ||
|
|
||
| quantizers: Optional[List[Quantizer]] = None | ||
| ao_quantization_configs: Optional[List[AOQuantizationConfig]] = None | ||
| is_qat: bool = False | ||
| calibration_inputs_fn: Optional[Callable[[], Iterable[tuple]]] = None | ||
| train_fn: Optional[Callable[["torch.fx.GraphModule"], None]] = None | ||
| pre_prepare_passes: Optional[ | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: If possible, I'd forbid |
||
| List[Callable[["torch.fx.GraphModule"], "torch.fx.GraphModule"]] | ||
| ] = None | ||
| post_prepare_passes: Optional[ | ||
| List[Callable[["torch.fx.GraphModule"], "torch.fx.GraphModule"]] | ||
| ] = None | ||
| pre_convert_passes: Optional[ | ||
| List[Callable[["torch.fx.GraphModule"], "torch.fx.GraphModule"]] | ||
| ] = None | ||
| post_convert_passes: Optional[ | ||
| List[Callable[["torch.fx.GraphModule"], "torch.fx.GraphModule"]] | ||
| ] = None | ||
|
|
||
| def get_quantizers(self) -> Optional[List[Quantizer]]: | ||
| """ | ||
|
|
@@ -249,60 +283,135 @@ def _combine_recipes( # noqa: C901 | |
| Returns: | ||
| Combined ExportRecipe for multi-backend deployment | ||
| """ | ||
| # Extract components from individual recipes | ||
| all_partitioners = [] | ||
| all_quantizers = [] | ||
| all_ao_quantization_configs = [] | ||
| all_pre_edge_passes = [] | ||
| all_transform_passes = [] | ||
|
|
||
| # Scalar fields that must be identical across all recipes. | ||
| def _assert_agree(field_name: str, values: list) -> None: | ||
| unique = set(values) | ||
| if len(unique) > 1: | ||
| raise ValueError( | ||
| f"Cannot combine recipes with conflicting '{field_name}' values: {unique}" | ||
| ) | ||
|
|
||
| # Collect all components. | ||
| all_partitioners: list = [] | ||
| all_quantizers: list = [] | ||
| all_ao_quantization_configs: list = [] | ||
| all_pre_edge_passes: list = [] | ||
| all_edge_transform_passes: list = [] | ||
| all_edge_manager_transform_passes: list = [] | ||
| all_pre_prepare_passes: list = [] | ||
| all_post_prepare_passes: list = [] | ||
| all_pre_convert_passes: list = [] | ||
| all_post_convert_passes: list = [] | ||
| combined_backend_config = None | ||
|
|
||
| is_qat_values: list = [] | ||
| train_fn_values: list = [] | ||
| calibration_inputs_fn_values: list = [] | ||
| strict_values: list = [] | ||
| mode_values: list = [] | ||
| pipeline_stages_values: list = [] | ||
| source_transform_in_place_values: list = [] | ||
|
|
||
| for recipe in backend_recipes: | ||
| # Collect pre-edge transform passes | ||
| if recipe.aten_transform_passes: | ||
| all_pre_edge_passes.extend(recipe.aten_transform_passes) | ||
|
|
||
| # Collect partitioners from lowering recipes | ||
| if recipe.lowering_recipe and recipe.lowering_recipe.partitioners: | ||
| all_partitioners.extend(recipe.lowering_recipe.partitioners) | ||
|
|
||
| # Collect transform passes from lowering recipes | ||
| if recipe.lowering_recipe and recipe.lowering_recipe.edge_transform_passes: | ||
| all_transform_passes.extend( | ||
| recipe.lowering_recipe.edge_transform_passes | ||
| ) | ||
|
|
||
| # Collect for quantize stage | ||
| if quantization_recipe := recipe.quantization_recipe: | ||
| # Collect PT2E quantizers | ||
| if quantization_recipe.quantizers: | ||
| all_quantizers.extend(quantization_recipe.quantizers) | ||
|
|
||
| # Collect source transform configs | ||
| if quantization_recipe.ao_quantization_configs: | ||
| all_ao_quantization_configs.extend( | ||
| quantization_recipe.ao_quantization_configs | ||
| if lr := recipe.lowering_recipe: | ||
| if lr.partitioners: | ||
| all_partitioners.extend(lr.partitioners) | ||
| if lr.edge_transform_passes: | ||
| all_edge_transform_passes.extend(lr.edge_transform_passes) | ||
| if lr.edge_manager_transform_passes: | ||
| all_edge_manager_transform_passes.extend( | ||
| lr.edge_manager_transform_passes | ||
| ) | ||
|
|
||
| # Use the first backend config as base | ||
| if qr := recipe.quantization_recipe: | ||
| if qr.quantizers: | ||
| all_quantizers.extend(qr.quantizers) | ||
| if qr.ao_quantization_configs: | ||
| all_ao_quantization_configs.extend(qr.ao_quantization_configs) | ||
| is_qat_values.append(qr.is_qat) | ||
| train_fn_values.append(qr.train_fn) | ||
| calibration_inputs_fn_values.append(qr.calibration_inputs_fn) | ||
| if qr.pre_prepare_passes: | ||
| all_pre_prepare_passes.extend(qr.pre_prepare_passes) | ||
| if qr.post_prepare_passes: | ||
| all_post_prepare_passes.extend(qr.post_prepare_passes) | ||
| if qr.pre_convert_passes: | ||
| all_pre_convert_passes.extend(qr.pre_convert_passes) | ||
| if qr.post_convert_passes: | ||
| all_post_convert_passes.extend(qr.post_convert_passes) | ||
|
|
||
| strict_values.append(recipe.strict) | ||
| mode_values.append(recipe.mode) | ||
| pipeline_stages_values.append( | ||
| tuple(recipe.pipeline_stages) if recipe.pipeline_stages else None | ||
| ) | ||
| source_transform_in_place_values.append(recipe.source_transform_in_place) | ||
|
|
||
| if combined_backend_config is None and recipe.executorch_backend_config: | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I believe this logic causes the first |
||
| combined_backend_config = copy.deepcopy( | ||
| recipe.executorch_backend_config | ||
| ) | ||
|
|
||
| # Create combined quantization recipe | ||
| # Validate fields that must agree across all recipes. | ||
| _assert_agree("strict", strict_values) | ||
| _assert_agree("mode", mode_values) | ||
| _assert_agree("pipeline_stages", pipeline_stages_values) | ||
| _assert_agree("source_transform_in_place", source_transform_in_place_values) | ||
|
|
||
| # is_qat must agree across all recipes that carry a QuantizationRecipe. | ||
| _assert_agree("is_qat", is_qat_values) | ||
| # train_fn must have at most one non-None value across all recipes. | ||
| non_none_train_fns = [f for f in train_fn_values if f is not None] | ||
| if len(non_none_train_fns) > 1: | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also |
||
| raise ValueError( | ||
| "Cannot combine recipes: more than one recipe provides a train_fn." | ||
| ) | ||
| # Multiple calibration_inputs_fn values are chained into a single factory. | ||
| non_none_calib_fns = [f for f in calibration_inputs_fn_values if f is not None] | ||
| if len(non_none_calib_fns) > 1: | ||
| _fns = non_none_calib_fns | ||
|
|
||
| def _combined_calib_fn(): | ||
| for _fn in _fns: | ||
| yield from _fn() | ||
|
|
||
| combined_calib_fn = _combined_calib_fn | ||
| else: | ||
| combined_calib_fn = non_none_calib_fns[0] if non_none_calib_fns else None | ||
|
|
||
| # Build combined QuantizationRecipe. | ||
| combined_quantization_recipe = None | ||
| if all_quantizers or all_ao_quantization_configs: | ||
| if ( | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: I'd add an INFO log message in the |
||
| all_quantizers | ||
| or all_ao_quantization_configs | ||
| or all_pre_prepare_passes | ||
| or all_post_prepare_passes | ||
| or all_pre_convert_passes | ||
| or all_post_convert_passes | ||
| ): | ||
| combined_quantization_recipe = QuantizationRecipe( | ||
| quantizers=all_quantizers if all_quantizers else None, | ||
| ao_quantization_configs=( | ||
| all_ao_quantization_configs if all_ao_quantization_configs else None | ||
| ), | ||
| quantizers=all_quantizers or None, | ||
| ao_quantization_configs=all_ao_quantization_configs or None, | ||
| is_qat=is_qat_values[0] if is_qat_values else False, | ||
| train_fn=non_none_train_fns[0] if non_none_train_fns else None, | ||
| calibration_inputs_fn=combined_calib_fn, | ||
| pre_prepare_passes=all_pre_prepare_passes or None, | ||
| post_prepare_passes=all_post_prepare_passes or None, | ||
| pre_convert_passes=all_pre_convert_passes or None, | ||
| post_convert_passes=all_post_convert_passes or None, | ||
| ) | ||
|
|
||
| # Create combined lowering recipe | ||
| combined_lowering_recipe = None | ||
| if all_partitioners or all_transform_passes: | ||
| if ( | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: Same thing here, I'd add a INFO log message in the |
||
| all_partitioners | ||
| or all_edge_transform_passes | ||
| or all_edge_manager_transform_passes | ||
| ): | ||
| edge_compile_config = None | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I know this is not part of your changes, but the handling of |
||
| for recipe in backend_recipes: | ||
| if ( | ||
|
|
@@ -313,10 +422,9 @@ def _combine_recipes( # noqa: C901 | |
| break | ||
|
|
||
| combined_lowering_recipe = LoweringRecipe( | ||
| partitioners=all_partitioners if all_partitioners else None, | ||
| edge_transform_passes=( | ||
| all_transform_passes if all_transform_passes else None | ||
| ), | ||
| partitioners=all_partitioners or None, | ||
| edge_transform_passes=all_edge_transform_passes or None, | ||
| edge_manager_transform_passes=all_edge_manager_transform_passes or None, | ||
| edge_compile_config=edge_compile_config or EdgeCompileConfig(), | ||
| ) | ||
|
|
||
|
|
@@ -326,7 +434,19 @@ def _combine_recipes( # noqa: C901 | |
| return cls( | ||
| name=recipe_name, | ||
| quantization_recipe=combined_quantization_recipe, | ||
| aten_transform_passes=all_pre_edge_passes, | ||
| aten_transform_passes=all_pre_edge_passes or None, | ||
| lowering_recipe=combined_lowering_recipe, | ||
| executorch_backend_config=combined_backend_config, | ||
| strict=strict_values[0] if strict_values else True, | ||
| mode=mode_values[0] if mode_values else Mode.RELEASE, | ||
| pipeline_stages=( | ||
| list(pipeline_stages_values[0]) | ||
| if pipeline_stages_values and pipeline_stages_values[0] is not None | ||
| else None | ||
| ), | ||
| source_transform_in_place=( | ||
| source_transform_in_place_values[0] | ||
| if source_transform_in_place_values | ||
| else False | ||
| ), | ||
| ) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -22,7 +22,11 @@ | |
| from torch._export.pass_base import PassType | ||
| from torch.fx.passes.infra.pass_manager import PassManager as GraphModulePassManager | ||
| from torchao.quantization import quantize_ | ||
| from torchao.quantization.pt2e.quantize_pt2e import convert_pt2e, prepare_pt2e | ||
| from torchao.quantization.pt2e.quantize_pt2e import ( | ||
| convert_pt2e, | ||
| prepare_pt2e, | ||
| prepare_qat_pt2e, | ||
| ) | ||
| from torchao.quantization.pt2e.quantizer import ( | ||
| ComposableQuantizer, | ||
| Quantizer as TorchAOPT2EQuantizer, | ||
|
|
@@ -423,6 +427,15 @@ def _get_quantizer_for_prepare_pt2e(self, quantizers: List[Any]): | |
| else: | ||
| raise ValueError("No quantizers detected") | ||
|
|
||
| @staticmethod | ||
| def _apply_passes( | ||
| model: "torch.fx.GraphModule", | ||
| passes: Optional[List[Callable]], | ||
| ) -> "torch.fx.GraphModule": | ||
| for pass_fn in passes or []: | ||
| model = pass_fn(model) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: I'd implement some kind of error handling, or at least logging. |
||
| return model | ||
|
|
||
| def run(self, artifact: PipelineArtifact) -> None: | ||
| if not self._quantization_recipe or not self._quantization_recipe.quantizers: | ||
| logging.info( | ||
|
|
@@ -433,6 +446,7 @@ def run(self, artifact: PipelineArtifact) -> None: | |
|
|
||
| assert isinstance(artifact.data, dict) | ||
|
|
||
| recipe = self._quantization_recipe | ||
| models = artifact.data | ||
| example_inputs = artifact.get_context("example_inputs") | ||
|
|
||
|
|
@@ -447,15 +461,53 @@ def run(self, artifact: PipelineArtifact) -> None: | |
| inputs = example_inputs[method_name][0] | ||
| captured_graph = torch.export.export(model, inputs, strict=True).module() | ||
|
|
||
| quantizer = self._get_quantizer_for_prepare_pt2e( | ||
| self._quantization_recipe.quantizers # pyre-ignore | ||
| # Pass 1: pre-prepare passes. | ||
| captured_graph = self._apply_passes( | ||
| captured_graph, recipe.pre_prepare_passes | ||
| ) | ||
| prepared_model = prepare_pt2e(captured_graph, quantizer) | ||
|
|
||
| for calibration_input in example_inputs[method_name]: | ||
| prepared_model(*calibration_input) | ||
| quantizer = self._get_quantizer_for_prepare_pt2e(recipe.quantizers) | ||
|
|
||
| if recipe.is_qat: | ||
| if recipe.train_fn is None: | ||
| raise ValueError("train_fn must be provided when is_qat=True") | ||
| prepared_model = prepare_qat_pt2e(captured_graph, quantizer) | ||
|
|
||
| # Pass 2: post-prepare passes. | ||
| prepared_model = self._apply_passes( | ||
| prepared_model, recipe.post_prepare_passes | ||
| ) | ||
|
|
||
| recipe.train_fn(prepared_model) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Model should be put into
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. AI pointed out that the |
||
| else: | ||
| prepared_model = prepare_pt2e(captured_graph, quantizer) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Model should be put into |
||
|
|
||
| # Pass 2: post-prepare passes. | ||
| prepared_model = self._apply_passes( | ||
| prepared_model, recipe.post_prepare_passes | ||
| ) | ||
|
|
||
| # Use custom calibration inputs when provided; fall back to example inputs. | ||
| if recipe.calibration_inputs_fn is not None: | ||
| calibration_inputs = recipe.calibration_inputs_fn() | ||
| else: | ||
| calibration_inputs = example_inputs[method_name] | ||
|
|
||
| for calibration_input in calibration_inputs: | ||
| prepared_model(*calibration_input) | ||
|
|
||
| # Pass 3: pre-convert passes. | ||
| prepared_model = self._apply_passes( | ||
| prepared_model, recipe.pre_convert_passes | ||
| ) | ||
|
|
||
| quantized_model = convert_pt2e(prepared_model) | ||
|
|
||
| # Pass 4: post-convert passes. | ||
| quantized_model = self._apply_passes( | ||
| quantized_model, recipe.post_convert_passes | ||
| ) | ||
|
|
||
| quantized_models[method_name] = quantized_model | ||
|
|
||
| self._artifact = artifact.copy_with_new_data(quantized_models) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just wondering, will there be some documentation update on how to use the recipes with all the newly added options? Or a real world example?