feat(executorch): expose composable Edge export API - #4440
Open
shoumikhin wants to merge 4 commits into
Open
Conversation
shoumikhin
force-pushed
the
executorch-composable-export
branch
from
July 29, 2026 18:48
cd55b95 to
4dc2d25
Compare
shoumikhin
force-pushed
the
executorch-composable-export
branch
from
July 31, 2026 06:51
4dc2d25 to
cb3633f
Compare
cehongwang
reviewed
Jul 31, 2026
cehongwang
reviewed
Jul 31, 2026
cehongwang
reviewed
Jul 31, 2026
## What this adds
`torch_tensorrt.save(..., output_format="executorch")` turns a compiled model
into an ExecuTorch `.pte` file in one step. That is the easiest path and it stays
the recommendation.
Some workflows need to stop earlier. You may want to look at the delegated Edge
graph, run your own Edge transforms, send operations TensorRT cannot handle to
another backend, keep separate methods such as `prefill` and `decode`, add
constant methods, or pick the final ExecuTorch configuration yourself.
This adds `torch_tensorrt.executorch.export()`. It returns ExecuTorch's standard
`EdgeProgramManager`, which is the supported place to inspect and customize an
Edge program before you call `to_executorch()`.
```python
import torch_tensorrt.executorch
edge = torch_tensorrt.executorch.export(trt_module)
# Inspect or transform the Edge program here.
print(edge.exported_program().graph)
program = edge.to_executorch()
with open("model.pte", "wb") as output:
program.write_to_file(output)
# Advanced callers must also persist external tensor data when present.
program.write_tensor_data_to_file(".")
```
`save()` now calls the same implementation, so the simple and advanced paths
share one lowering path instead of two.
## Which one should I use?
Use `save()` when you just want a `.pte`. Use
`torch_tensorrt.executorch.export()` when you need control before the `.pte` is
created.
## Example: TensorRT with a fallback backend
TensorRT always gets the first chance to claim its prebuilt engine nodes. Any
partitioners you pass run afterward and can claim operations TensorRT does not
support.
```python
edge = torch_tensorrt.executorch.export(
trt_module,
partitioners=[cuda_partitioner],
)
program = edge.to_executorch()
```
## Example: multiple methods
```python
edge = torch_tensorrt.executorch.export(
{
"prefill": prefill_program,
"decode": decode_program,
},
compile_specs={
"prefill": prefill_specs,
"decode": decode_specs,
},
)
```
Method names are preserved, and each method can have its own partitioner and
compile-spec pipeline. This does not make mutable state shared between methods.
## Accepted inputs
- a TensorRT-compiled `torch.fx.GraphModule`
- an engine-bearing `torch.export.ExportedProgram`
- a mapping from method names to independent `ExportedProgram` objects
A plain `torch.nn.Module` must be compiled with Torch-TensorRT first.
## Safety and memory behavior
Rewriting engine calls changes the exported graph. To avoid damaging the program
you passed in, export first stages its own copy of the graph structure,
signatures, state containers, constants, and metadata. If rewriting or lowering
fails, your original program is left alone.
Tensor and TensorRT engine payloads are treated as read-only and shared with
that staged copy, so a multi-gigabyte engine is not duplicated. Custom transform
passes must not modify these shared payload objects.
Metadata that describes symbolic shapes is a special case. It points back to the
live shape environment that the exported program is guarded on, so it is shared
rather than copied. Copying it would both fail (the shape environment holds fake
tensors that cannot be duplicated) and detach the copy from the symbols the graph
depends on. This is what keeps dynamic-shape models working.
When a lifted TensorRT engine is replaced, the old placeholder, its
graph-signature entry, and its constant are removed. Several calls that share one
engine reuse a single materialized `uint8` payload buffer.
## Compatibility
- Existing one-step `save(..., output_format="executorch")` behavior is preserved,
including its rejection of non-list `partitioners` and `compile_specs`.
- `"executorch"` is now included in the `output_format` type hint. It was already
accepted at run time, so this only fixes the annotation.
- `.pte` serialization and external `.ptd` persistence are preserved.
- TensorRT remains the first partitioner.
- Shared dynamic dimensions declared with `Input(shared_dims=...)` remain shared.
- Zero-engine methods are allowed. A later partitioner may claim them, or portable
operators may remain undelegated.
## Testing
Tested on a CUDA GPU with TensorRT and ExecuTorch installed, building
Torch-TensorRT from source so the C++ runtime was active.
Verified by compiling a real model with TensorRT and then exporting it:
- static-shape `export()` produces a loadable `.pte`
- dynamic-shape `export()` produces a loadable `.pte`, matching the output of the
previous one-step save path
- the source `GraphModule` still holds its original engine nodes afterward
- engine payloads are shared, not re-serialized
- `save(..., output_format="executorch")` still writes a `.pte`
- `save()` still raises `TypeError` for a non-list `partitioners` or
`compile_specs`
Also ran the ExecuTorch test directory (76 passed). Two GPU tests in
`test_cuda_partitioner_composition.py` fail, and they fail the same way with this
change reverted, so they are not caused by it.
Static checks: formatting, import ordering, lint, and type checking on the changed
files.
shoumikhin
force-pushed
the
executorch-composable-export
branch
from
July 31, 2026 23:34
cb3633f to
93efd54
Compare
A partitioner can carry method-specific state. ExecuTorch backends bake the method name into the DelegationSpec built in the partitioner constructor, so reusing one instance across methods tags every method with the first method's name, and each delegate then looks up the wrong compiled method at runtime. export() broadcast a flat partitioners= sequence to every method, which made that mistake easy and silent. Reject it when there is more than one method and point at the per-method mapping form instead. A flat sequence still works for a single method, and compile_specs (plain data, not stateful objects) still broadcasts unchanged. Tests: reject a shared instance across two methods, allow a flat sequence for one method, and confirm a per-method mapping preserves each instance.
…alidation Three fixes found while reviewing the composable export path. 1. Staging shared a node.meta value wholesale whenever it contained a symbolic size or fake tensor anywhere inside it. A multi-output op stores a list of fake tensors in meta["val"], so that list stayed shared with the caller's program and an Edge transform mutating it would corrupt the caller's input. Seed only the shape-bound leaves into the deepcopy memo, so the leaves are still shared (they must be, they belong to the live ShapeEnv) while the container around them is copied. 2. A partitioner can carry method-specific state: ExecuTorch backends bake the method name into the DelegationSpec built in the constructor, so reusing one instance across methods tags every method with the first method's name and each delegate then looks up the wrong compiled method at runtime. Reject a reused instance whether it arrives as a flat sequence broadcast to every method or as the same object under two method keys. 3. ExecuTorch dispatches per-method transform passes on isinstance(passes, dict), so a Mapping that is not a dict silently ran no passes at all. Normalize it. Tests: a container holding symbolic leaves is copied while the leaves stay shared; a reused partitioner is rejected in both forms while distinct instances and the single-method flat form still work; a non-dict Mapping of transform passes is normalized to dict.
…tant methods Two review findings from adversarial testing of the composable export path. The staged Edge program shares weight and TensorRT engine storage with the programs the caller passed in. Only structure is copied. That is deliberate (it keeps a multi-gigabyte engine from being duplicated) but it was documented only in the export() docstring, while the saving guide showed transform_passes= with no caveat. An in-place edit in a transform pass, or a change to a source program after export, silently changes the caller's program and any sibling exported from it, with no warning. Document the contract where callers read it, and say plainly that neither case raises. Also document the several-methods-in-one-pte form, which was described in prose but had no example, including the requirement that each method gets its own partitioner instances. Constant-method names are baked into the .pte as method names, so reject keys that are not valid Python identifiers instead of forwarding them. Tests: invalid constant-method keys (spaces, leading digit, empty, non-string) are rejected; a valid key is forwarded unchanged.
| retrace=False, arg_inputs=inputs, | ||
| ) | ||
|
|
||
| ``save`` writes both the ``.pte`` and any external ``.ptd`` tensor-data files. |
Collaborator
There was a problem hiding this comment.
I would suggest here first add the torch_tensorrt.save() as the default method for user wants to export a executorch pte file.
then add the advanced method, when user needs a customization before serialize into a pte file.
So that we explain user both scenario clearly.
Collaborator
There was a problem hiding this comment.
Also please wait for PR: 4398 to be merged to main(I will push by the end of today.)
please rebase on top of that.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why this PR is needed
torch_tensorrt.save(..., output_format="executorch")is the easiest way to create an ExecuTorch.ptefile. It completes the whole export pipeline immediately:That remains the recommended API for common one-step exports.
Some workflows need to stop before final ExecuTorch memory planning and serialization. For example, users may want to:
prefillanddecode;This PR adds
torch_tensorrt.executorch.export(). It returns ExecuTorch's standardEdgeProgramManager, which is the supported boundary for inspecting and customizing an Edge program before callingto_executorch().Which API should I use?
Use
torch_tensorrt.save()when you want a.ptein one step.Use
torch_tensorrt.executorch.export()when you need control before the final.pteis created:save()now delegates to the same implementation, so the simple and advanced paths do not maintain separate lowering logic.Example: TensorRT with a fallback backend
TensorRT always receives the first chance to claim its prebuilt engine nodes. Additional partitioners run afterward and can claim operations that TensorRT does not support.
Assume
cuda_partitioneris an ExecuTorch partitioner configured for your target:This can produce one ExecuTorch program containing both TensorRT and CUDA delegates.
Example: multiple methods
Assume
prefill_programanddecode_programare independent, engine-bearingExportedProgramobjects, and the two spec lists are configured for their methods:The mapping preserves method names and allows per-method partitioner and compile-spec pipelines. It does not imply that mutable state is shared between methods.
Example: transforms, metadata, and ETRecord
Assume the transform passes and Edge configuration below are provided by the caller:
Accepted inputs
The new API accepts:
torch.fx.GraphModule;torch.export.ExportedProgram;ExportedProgramobjects.A plain
torch.nn.Modulemust be compiled with Torch-TensorRT first.Safety and memory behavior
Engine rewriting changes the exported graph. The new API stages an independent copy of graph structure, signatures, state containers, constants, and metadata before rewriting anything. Rewrite and lowering failures therefore do not consume or structurally modify the caller's original program.
Tensor and TensorRT engine payload storage is treated as immutable and shared with the staged structure. This avoids deep-copying potentially multi-gigabyte engines. Custom transform passes must not mutate shared payload objects.
Metadata describing symbolic shapes is also shared rather than copied. It points back into the live shape environment the exported program is guarded on, and that environment holds fake tensors which cannot be copied. Sharing it is what keeps dynamic-shape models working; copying it would also detach the copy from the shape symbols the graph depends on.
When a lifted TensorRT engine is replaced, the old placeholder, graph-signature entry, and constant are removed. Multiple calls that share one engine reuse one materialized
uint8payload buffer.Compatibility
save(..., output_format="executorch")behavior is preserved..pteserialization and external.ptdpersistence are preserved.Input(shared_dims=...)remain shared.Validation
Built from source with the Torch-TensorRT C++ runtime active, against TensorRT and
ExecuTorch, and exercised on both x86_64 and aarch64 CUDA GPUs.
Checked end to end with a real compiled TensorRT engine:
export()writes a loadable.pte;export()writes a loadable.pteof the same size the previousone-step save path produced;
GraphModulestill holds its original engine nodes afterexport();save(..., output_format="executorch")still writes a.pte;save()still raisesTypeErrorfor a non-listpartitionersorcompile_specs.Unit and integration tests in the ExecuTorch test directory: 76 passed.
Two GPU tests in
test_cuda_partitioner_composition.py,test_erfinv_routes_to_cuda_backendandtest_weighted_partition_persists_external_data, fail withSpecViolationError: Node.meta lowered_module_0 is missing val field. They fail thesame way on main without this change, so they are pre-existing and not caused by
this work.
Note that these tests only run in the L2 tier, which requires the
ci: run-l2labelon a pull request.
Static checks on the changed files: import ordering, formatting, lint, and type
checking.
Related work
PR #4433 forwards additional Edge-lowering options through one-step
save(). After this PR lands, #4433 can keepsave()as a thin wrapper overtorch_tensorrt.executorch.export()instead of extending the previous private lowering path.