Skip to content

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

Merged
merged 10 commits into from
May 15, 2025
Merged

Conversation

tarun292
Copy link
Contributor

@tarun292 tarun292 commented Apr 10, 2025

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:

  • class definition for ExportSession and ExportRecipe
  • Definitions for each stage in the process
  • executorch.export API which will return a session object that the user can then use to get access to the PTE file, run the model via pybindings, print delegation info etc.

Copy link

pytorch-bot bot commented Apr 10, 2025

🔗 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 SEVs

There 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.

@facebook-github-bot facebook-github-bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Apr 10, 2025
@facebook-github-bot
Copy link
Contributor

This pull request was exported from Phabricator. Differential Revision: D71946730

Copy link
Contributor

@digantdesai digantdesai left a 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.

return manager


class ExportSession:
Copy link
Contributor

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 Stages.

See -


name: Optional[str] = None
quantizer: Optional[Quantizer] = None
edge_compile_config: Optional[EdgeCompileConfig] = ( # pyre-ignore[11]: Type not defined
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mypy?

)
mode: Mode = Mode.RELEASE

def get_quantizer(self) -> Optional[Quantizer]:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit property?

from ._recipe import ExportRecipe


def export(
Copy link
Contributor

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.

Suggested change
def export(
def create_export_session(
# or
Session.__init__
# or
@classmethod
Session.create()

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,
Copy link
Contributor

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?

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
Copy link
Contributor

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

Returns:
A configured ExportSession instance with the export process completed if requested
"""
manager = ExportSession(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit

Suggested change
manager = ExportSession(
session = ExportSession(

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,
Copy link
Contributor

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()?


from ._recipe import ExportRecipe


Copy link
Contributor

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

tarun292 added a commit that referenced this pull request May 6, 2025
Pull Request resolved: #10034


ghstack-source-id: 282298048
@exported-using-ghexport

Differential Revision: [D71946730](https://our.internmc.facebook.com/intern/diff/D71946730/)
@facebook-github-bot
Copy link
Contributor

This pull request was exported from Phabricator. Differential Revision: D71946730

from .recipe import ExportRecipe


class Stage(ABC):
Copy link
Contributor

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:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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

Copy link
Contributor Author

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"
Copy link
Contributor

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(
Copy link
Contributor

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?

Copy link
Contributor Author

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:
Copy link
Contributor

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?

Copy link
Contributor

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

Copy link
Contributor Author

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):
Copy link
Contributor

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

Copy link
Contributor

@kimishpatel kimishpatel left a 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.
Copy link
Contributor

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.

Copy link
Contributor

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?

Copy link
Contributor Author

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] = {}
Copy link
Contributor

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?

Copy link
Contributor Author

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.

Comment on lines +75 to +77
self._model_dict: Dict[str, nn.Module] = {}
self._example_inputs_dict: Dict[str, List[tuple[torch.Tensor, ...]]] = {}
self._dynamic_shapes_dict: Dict[str, Any] = {}
Copy link
Contributor

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

Copy link
Contributor Author

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:
Copy link
Contributor

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.

Copy link
Contributor Author

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.

Comment on lines +156 to +157
self._partitioners = partitioners
self._transform_passes = transform_passes
Copy link
Contributor

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?

Copy link
Contributor Author

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")
Copy link
Contributor

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

Copy link
Contributor Author

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]]:
Copy link
Contributor

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?

Copy link
Contributor Author

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.

Copy link
Contributor

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
Comment on lines 719 to 730
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)
Copy link
Contributor

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

Copy link
Contributor Author

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?

Copy link
Contributor

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:
Copy link
Contributor

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(
Copy link
Contributor

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

Copy link
Contributor Author

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.

Copy link
Contributor

@kimishpatel kimishpatel left a 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,
Copy link
Contributor

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?

Copy link
Contributor Author

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.

@tarun292
Copy link
Contributor Author

@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:

  • Removed start_stage API and string input for stage name, rather just have the run_pipeline function now which runs through the entire pipeline. Changed quite a bit how the pipeline is constructed now.
  • The pipeline is a list of stages now and not a dictionary.
  • Export happens independently of quantization now.
  • Added get_artifact which gets the output of each stage.

@digantdesai
Copy link
Contributor

Thanks @tarun292 - love the pipeline design.

@tarun292 tarun292 added the release notes: exir Changes to any dialects and passes on these dialects, such as memory planning label May 14, 2025
@tarun292 tarun292 changed the base branch from gh/tarun292/4/base to main May 15, 2025 00:00
@tarun292 tarun292 merged commit 1b593ad into main May 15, 2025
84 of 85 checks passed
@tarun292 tarun292 deleted the gh/tarun292/4/head branch May 15, 2025 00:48
kedarnath03 pushed a commit to kedarnath03/executorch that referenced this pull request Jun 25, 2025
Differential Revision: [D71946730](https://our.internmc.facebook.com/intern/diff/D71946730/)

ghstack-source-id: 276661724
Pull Request resolved: pytorch/executorch#10034
kedarnath03 pushed a commit to kedarnath03/executorch that referenced this pull request Jun 25, 2025
Pull Request resolved: pytorch/executorch#10034


ghstack-source-id: 284081137
@exported-using-ghexport

Differential Revision: [D71946730](https://our.internmc.facebook.com/intern/diff/D71946730/)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. fb-exported release notes: exir Changes to any dialects and passes on these dialects, such as memory planning
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants