-
Notifications
You must be signed in to change notification settings - Fork 608
Recipe and Input class definitions with e2e export #10034
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
Conversation
Differential Revision: [D71946730](https://our.internmc.facebook.com/intern/diff/D71946730/) [ghstack-poisoned]
🔗 Helpful Links🧪 See artifacts and rendered test results at hud.pytorch.org/pr/pytorch/executorch/10034
Note: Links to docs will display an error until the docs builds have been completed. ❗ 1 Active SEVsThere are 1 currently active SEVs. If your PR is affected, please view them below: This comment was automatically generated by Dr. CI and updates every 15 minutes. |
This pull request was exported from Phabricator. Differential Revision: D71946730 |
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.
Looks good at a high level. Left some comments.
export/_export.py
Outdated
return manager | ||
|
||
|
||
class ExportSession: |
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.
This is a good idea, I like it. But just a pet peeve, let's refactor this before starting, and organize the logic so that it can scale better, else I fear it will end up becoming a giant bowl of spaghetti.
Suggestion, make a class Stage(ABC)
and derive others from it like Quantize(Stage)
and Export(Stage)
and then Session just manages the pipeline with Stage
s.
See -
class Tester: |
export/_recipe.py
Outdated
|
||
name: Optional[str] = None | ||
quantizer: Optional[Quantizer] = None | ||
edge_compile_config: Optional[EdgeCompileConfig] = ( # pyre-ignore[11]: Type not defined |
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.
mypy?
export/_recipe.py
Outdated
) | ||
mode: Mode = Mode.RELEASE | ||
|
||
def get_quantizer(self) -> Optional[Quantizer]: |
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.
nit property?
export/_export.py
Outdated
from ._recipe import ExportRecipe | ||
|
||
|
||
def export( |
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.
nit, WDYT about? I understand we want to do s/torch.export/executorch.export
but it feels a bit confusing given we are returning something not analogous to the model which torch.export does.
def export( | |
def create_export_session( | |
# or | |
Session.__init__ | |
# or | |
@classmethod | |
Session.create() |
export/_export.py
Outdated
model: Union[nn.Module, Dict[str, nn.Module]], | ||
example_inputs: Union[List[tuple[torch.Tensor, ...]], Dict[str, List[tuple[torch.Tensor, ...]]]], | ||
export_recipe: ExportRecipe, | ||
name: Optional[str] = None, |
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.
what is the rationale for this?
export/_export.py
Outdated
dynamic_shapes: Optional dynamic shape specifications | ||
constant_methods: Optional dictionary of constant methods | ||
artifact_dir: Optional directory to store artifacts | ||
apply_quantization: Whether to apply quantization during export, defaults to False |
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.
this feels a bit confusing
export/_export.py
Outdated
Returns: | ||
A configured ExportSession instance with the export process completed if requested | ||
""" | ||
manager = ExportSession( |
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.
nit
manager = ExportSession( | |
session = ExportSession( |
export/_export.py
Outdated
example_inputs: Union[List[tuple[torch.Tensor, ...]], Dict[str, List[tuple[torch.Tensor, ...]]]], | ||
export_recipe: ExportRecipe, | ||
name: Optional[str] = None, | ||
dynamic_shapes: Optional[Union[Any, Dict[str, Any]]] = None, |
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.
why is this not on Session.export()
?
export/_export.py
Outdated
|
||
from ._recipe import ExportRecipe | ||
|
||
|
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.
We should annotate with executorch.exir._warnings.experimental decorators
…e export" Differential Revision: [D71946730](https://our.internmc.facebook.com/intern/diff/D71946730/) [ghstack-poisoned]
Differential Revision: [D71946730](https://our.internmc.facebook.com/intern/diff/D71946730/) [ghstack-poisoned]
Pull Request resolved: #10034 ghstack-source-id: 282298048 @exported-using-ghexport Differential Revision: [D71946730](https://our.internmc.facebook.com/intern/diff/D71946730/)
This pull request was exported from Phabricator. Differential Revision: D71946730 |
from .recipe import ExportRecipe | ||
|
||
|
||
class Stage(ABC): |
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.
I like this 👍🏻
export/export.py
Outdated
pass | ||
|
||
@abstractmethod | ||
def get_outputs(self) -> Any: |
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.
def get_outputs(self) -> Any: | |
def get_artifacts(self) -> Any: |
Think this conveys the intent of this function better when the user hasn't read the docstring
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.
Updated.
|
||
@property | ||
def name(self) -> str: | ||
return "quantize" |
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.
Would be worth to comment somewhere that quantization may also happen in SourceTransformStage
) | ||
return self._executorch_program_manager.buffer | ||
|
||
def get_example_input( |
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.
I feel like this and run_method
seem a bit confusing to have in the ExportSession?
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.
The way i'm thinking is that export session lets the users do almost all the things they'd like to with an executorch model like load the model and run via pybindings, get reference inputs etc. and that's why I added these helper utils.
export/export.py
Outdated
elif stage_name == "executorch": | ||
self._executorch_program_manager = result | ||
|
||
def _run_pipeline(self, start_stage: str) -> None: |
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.
Why do we need this start_stage stuff, can we just not take any parameters and just run the all stages in self._stages?
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.
Yeah I agree. We should not have this start_stage thing. We should just construct pipeline that can just be run with each stage in sequence. Construction of the pipeline should take care of apprpriate deps
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.
Addressed in latest version.
return self._executorch_program_manager | ||
|
||
|
||
class SourceTransformStage(Stage): |
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.
NIT: have this and QuantizeStage declared in the order of the export process, this one declared at the top
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.
Left quite a few comments
@@ -0,0 +1,24 @@ | |||
# Copyright (c) Meta Platforms, Inc. and affiliates. |
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.
The directory structure is bit confusing. People coming to ET already know export and now we are teaching them of a different export.
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.
Maybe executorch/lower
or executorch/pipeline
?
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.
Thought quite a bit about this, the main reasoning to call this export too is because we're consuming the export call within the API. One of the main points of this API is that if someone wants to target ExecuTorch they don't even need to learn about torch.export (although many of them probably will).
) -> None: | ||
self._exported_program: Dict[str, ExportedProgram] = {} | ||
self._pre_edge_transform_passes = pre_edge_transform_passes | ||
self._model_dict: Dict[str, nn.Module] = {} |
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.
is this to capture more than one Module to be exported? why do we need this?
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.
Yep a user could possibly pass in multiple methods to be exported and wanted to have support for that from the beginning. Something we struggled with in ModAI by trying to add it later.
self._model_dict: Dict[str, nn.Module] = {} | ||
self._example_inputs_dict: Dict[str, List[tuple[torch.Tensor, ...]]] = {} | ||
self._dynamic_shapes_dict: Dict[str, Any] = {} |
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.
If these are one to one correspondence than I would combine them into a single struct that can be passed in the init or separately initialized
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.
I did think about that a bit, the thing is that even when doing this via a struct/dataclass the code tends to be equally cumbersome and this is more direct.
export/export.py
Outdated
self._dynamic_shapes_dict = export_config.get("dynamic_shapes", {}) | ||
|
||
# Check if we need to do export (if _exported_program is empty) | ||
if not self._exported_program: |
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.
What if you want to rexport? For different input shapes or export args? If ExportStange is strictly for one export session and to export with different args you need a new ExportStage than I will make it immutable.
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.
This wasn't needed, removed it. One export session is intended to be only for one set of input args.
self._partitioners = partitioners | ||
self._transform_passes = transform_passes |
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.
what is the relation between transformation passes and partitioners?
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.
Transform passes are what we run after to_edge e.g. edge_manager.transform(), and partitioners are the ones we use in to_backend.
export/export.py
Outdated
|
||
# If quantization is available, add it after source transform | ||
if "quantize" in self._stages: | ||
self._pipeline.insert(0, "quantize") |
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.
As I said elsewhere I think quantize should not be responsible for running export. We should run export, run quantizer, prepare, convert, and run export again. This call can be managed within here by detecting the need to run export again if we have pt2e quantize requested
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.
We need to store this because it's used in all the later stages such as to_edge etc.
export/export.py
Outdated
# Return the outputs | ||
return self._stages[stage_name].get_outputs() | ||
|
||
def _get_stage_inputs(self, stage_name: str) -> tuple[Any, Dict[str, Any]]: |
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.
Why do we need to do this here instead of just initializing each stage with the inputs they need. This way call to run is simply run
?
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.
I thought a lot about this too, in order to deal with quantize_ and later on other things such as custom op replacement in eager mode we do need to do it and it's quite a bit of complexity that'll be better hidden from the users.
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.
deal with quantize_ and later on other things such as custom op replacement in eager mode
Again eager mode should not belong to this. It is coupling unrelated things together.
export/export.py
Outdated
for i in range(start_index, len(self._pipeline)): | ||
stage_name = self._pipeline[i] | ||
self._current_stage_index = i | ||
|
||
# Get the primary input and configuration parameters for this stage | ||
primary_input, config_params = self._get_stage_inputs(stage_name) | ||
|
||
# Run the stage | ||
result = self._run_stage(stage_name, primary_input, config_params) | ||
|
||
# Store the result | ||
self._store_stage_result(stage_name, result) |
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.
Yeah at thsi point i expect jsut iterate through stages and run without needing to figure out input and storage stage outputs. Outputs of each stage should be there in get_output/artifact
if needed
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.
This means that the quantization recipe doesn't necessarily have configs to use with quantize_ but might still have pt2e quantization. In that case it doesn't make sense to throw an error right?
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.
Not sure I follow what you mean.
export/export.py
Outdated
elif stage_name == "executorch": | ||
self._executorch_program_manager = result | ||
|
||
def _run_pipeline(self, start_stage: str) -> None: |
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.
Yeah I agree. We should not have this start_stage thing. We should just construct pipeline that can just be run with each stage in sequence. Construction of the pipeline should take care of apprpriate deps
# The original code expects this to be a tuple of tensors | ||
return self._example_inputs[method_name][0] | ||
|
||
def run_method( |
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.
why is this part of this class
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.
I think that's a valid point, but i've felt like having all the quantization configuration related items in one class would generally be easier for recipe authors. We when creating the pipeline decide what source transforms need to be run and what is in pt2e. This isn't set in stone though, we'll definitely iterate on this as we add more recipes in XNNPack, CoreML etc. and we can arrive at a different structure over the next few weeks if the current one isn't flexible enough.
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.
I didnt "request changes" if you feel strongly about landing, but I think we should consider addressing these comments
def run( | ||
self, | ||
models: Dict[str, Any], | ||
export_config: Optional[Dict[str, Any]] = None, |
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.
Looking at this PR again, why isn't this a part of the ExportRecipe?
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.
This contains example inputs and dynamic shapes which shouldn't be a part of the recipe.
…e export" Differential Revision: [D71946730](https://our.internmc.facebook.com/intern/diff/D71946730/) [ghstack-poisoned]
Differential Revision: [D71946730](https://our.internmc.facebook.com/intern/diff/D71946730/) [ghstack-poisoned]
@kimishpatel @jackzhxng thanks for the detailed review, really appreciate it! Addressed all of your comments: In the latest version the major changes i made are:
|
Thanks @tarun292 - love the pipeline design. |
…e export" Differential Revision: [D71946730](https://our.internmc.facebook.com/intern/diff/D71946730/) [ghstack-poisoned]
Differential Revision: [D71946730](https://our.internmc.facebook.com/intern/diff/D71946730/) [ghstack-poisoned]
…e export" Differential Revision: [D71946730](https://our.internmc.facebook.com/intern/diff/D71946730/) [ghstack-poisoned]
Differential Revision: [D71946730](https://our.internmc.facebook.com/intern/diff/D71946730/) [ghstack-poisoned]
Differential Revision: [D71946730](https://our.internmc.facebook.com/intern/diff/D71946730/) ghstack-source-id: 276661724 Pull Request resolved: pytorch/executorch#10034
Pull Request resolved: pytorch/executorch#10034 ghstack-source-id: 284081137 @exported-using-ghexport Differential Revision: [D71946730](https://our.internmc.facebook.com/intern/diff/D71946730/)
Based on the discussion in #9027
This PR adds the executorch.export API and all the supporting components required for it. At a high level the executorch.export API takes in a model, example inputs and a recipe, then underneath the hood executes all the steps required to export, quantize and lower the model based on the recipe.
The pipeline consists of a staged setup where each major step in the process such as Export, Quantization etc. is listed as a separate stage and a chain of these is formed and then executed. The result of this will be that we'll get a PTE file which can then be executed on device.
The major new components added in this PR are: