66# This source code is licensed under the BSD-style license found in the
77# LICENSE file in the root directory of this source tree.
88
9+ from __future__ import annotations
10+
911import copy
1012import logging
1113from abc import ABC , abstractmethod
4244
4345
4446def _drop_empty (
45- passes_by_method : Dict [str , List [PassType ]]
47+ passes_by_method : Dict [str , List [PassType ]],
4648) -> Dict [str , List [PassType ]]:
4749 return {method : p for method , p in passes_by_method .items () if p }
4850
@@ -130,10 +132,12 @@ def __init__(
130132 List [Callable [[str , ExportedProgram ], ExportedProgram ]]
131133 ] = None ,
132134 strict = True ,
135+ pre_trace_hooks : Optional [List [Callable [[str , nn .Module ], None ]]] = None ,
133136 ) -> None :
134137 super ().__init__ ()
135138 self ._aten_transform_passes = aten_transform_passes
136139 self .strict = strict
140+ self ._pre_trace_hooks = pre_trace_hooks
137141
138142 @property
139143 def stage_type (self ) -> str :
@@ -151,6 +155,11 @@ def run(self, artifact: PipelineArtifact) -> None:
151155 models = artifact .data
152156 example_inputs = artifact .get_context ("example_inputs" )
153157 dynamic_shapes = artifact .get_context ("dynamic_shapes" , {})
158+ pre_trace_hooks = (
159+ None
160+ if artifact .get_context ("pre_trace_hooks_applied" , False )
161+ else self ._pre_trace_hooks
162+ )
154163
155164 exported_programs = {}
156165
@@ -163,6 +172,9 @@ def run(self, artifact: PipelineArtifact) -> None:
163172
164173 method_dynamic_shapes = dynamic_shapes .get (method_name )
165174
175+ for hook in pre_trace_hooks or []:
176+ hook (method_name , model )
177+
166178 # Export the model
167179 exported_programs [method_name ] = torch .export .export (
168180 model ,
@@ -373,15 +385,21 @@ def run(self, artifact: PipelineArtifact) -> None:
373385
374386class SourceTransformStage (Stage ):
375387 """
376- Optional stage: Source transform stage: Apply source transformations to the model.
388+ Apply source transformations and optional AO quantization to the model.
389+
390+ Each source transform must return the nn.Module to use.
377391 """
378392
379393 def __init__ (
380394 self ,
381395 quantization_recipe : Optional [QuantizationRecipe ],
382396 in_place : bool = False ,
397+ source_transform_passes : Optional [
398+ List [Callable [[nn .Module ], nn .Module ]]
399+ ] = None ,
383400 ) -> None :
384401 self ._quantization_recipe = quantization_recipe
402+ self ._source_transform_passes = source_transform_passes
385403 self ._in_place = in_place
386404 self ._transformed_models : Dict [str , nn .Module ] = {}
387405
@@ -401,46 +419,67 @@ def run(self, artifact: PipelineArtifact) -> None:
401419 """
402420 Apply source transformations to the model.
403421 """
404- if (
405- not self ._quantization_recipe
406- or not self ._quantization_recipe .ao_quantization_configs
407- ):
422+ ao_configs = (
423+ self ._quantization_recipe .ao_quantization_configs
424+ if self ._quantization_recipe
425+ else None
426+ )
427+ if not ao_configs and not self ._source_transform_passes :
408428 logging .info (
409- "Quantization recipe is invalid to run SourceTransform , returning original artifact"
429+ "No source transform passes and no AO quantization configs for SourceTransformStage , returning original artifact"
410430 )
411431 self ._artifact = artifact
412432 return
413433
414434 assert isinstance (artifact .data , dict )
415435
436+ if ao_configs and len (ao_configs ) > 1 :
437+ raise ValueError (
438+ "AO quantization configs cannot be reliably composed together, multiple quantization configs are disallowed for source transform at this point"
439+ )
440+
416441 # A second copy of the model is not affordable for every caller, so
417442 # large models can opt out and have their own model mutated instead.
418- self ._transformed_models = (
419- artifact .data if self ._in_place else copy .deepcopy (artifact .data )
420- )
421-
422- # Apply torchao quantize_ to each model
423- for _ , model in self ._transformed_models .items ():
424- # pyre-ignore
425- if len (self ._quantization_recipe .ao_quantization_configs ) > 1 :
426- raise ValueError (
427- "AO quantization configs cannot be reliably composed together, multiple quantization configs are disallowed for source transform at this point"
428- )
429-
430- ao_config = self ._quantization_recipe .ao_quantization_configs [0 ]
431- quantize_ (model , ao_config .ao_base_config , ao_config .filter_fn )
432- unwrap_tensor_subclass (model )
443+ models = artifact .data if self ._in_place else copy .deepcopy (artifact .data )
444+
445+ # Several methods commonly share one model object. Transform each
446+ # distinct object once, or a shared model is quantized once per method.
447+ transformed_by_id : Dict [int , nn .Module ] = {}
448+ for method_name , model in list (models .items ()):
449+ original_id = id (model )
450+ if original_id not in transformed_by_id :
451+ for transform in self ._source_transform_passes or []:
452+ if not callable (transform ):
453+ raise TypeError ("Source transform passes must be callable" )
454+ model = transform (model )
455+ if not isinstance (model , nn .Module ):
456+ raise TypeError (
457+ f"Source transform { transform !r} must return an nn.Module, "
458+ f"got { type (model ).__name__ } "
459+ )
460+ if ao_configs :
461+ ao_config = ao_configs [0 ]
462+ quantize_ (model , ao_config .ao_base_config , ao_config .filter_fn )
463+ unwrap_tensor_subclass (model )
464+ transformed_by_id [original_id ] = model
465+ models [method_name ] = transformed_by_id [original_id ]
433466
434- self ._artifact = artifact .copy_with_new_data (self ._transformed_models )
467+ self ._transformed_models = models
468+ self ._artifact = artifact .copy_with_new_data (models )
435469
436470
437471class QuantizeStage (Stage ):
438472 """
439473 Optional stage: Perform post-training quantization on the model.
440474 """
441475
442- def __init__ (self , quantization_recipe : Optional [QuantizationRecipe ]) -> None :
476+ def __init__ (
477+ self ,
478+ quantization_recipe : Optional [QuantizationRecipe ],
479+ pre_trace_hooks : Optional [List [Callable [[str , nn .Module ], None ]]] = None ,
480+ ) -> None :
443481 self ._quantization_recipe = quantization_recipe
482+ self ._pre_trace_hooks = pre_trace_hooks
444483
445484 @property
446485 def stage_type (self ) -> str :
@@ -539,6 +578,10 @@ def run(self, artifact: PipelineArtifact) -> None:
539578 if recipe .is_qat :
540579 model .train ()
541580
581+ with torch .no_grad ():
582+ for hook in self ._pre_trace_hooks or []:
583+ hook (method_name , model )
584+
542585 captured_graph = torch .export .export (
543586 model , inputs , dynamic_shapes = export_dynamic_shapes , strict = True
544587 ).module ()
@@ -596,6 +639,9 @@ def run(self, artifact: PipelineArtifact) -> None:
596639 quantized_models [method_name ] = quantized_model
597640
598641 self ._artifact = artifact .copy_with_new_data (quantized_models )
642+ self ._artifact .add_context (
643+ "pre_trace_hooks_applied" , bool (self ._pre_trace_hooks )
644+ )
599645
600646
601647class ToEdgeStage (Stage ):
0 commit comments