Skip to content

perf(runtime): skip TensorRT engine fakification during export by reporting tracing_mode "real" - #4489

Merged
lanluo-nvidia merged 1 commit into
pytorch:mainfrom
Conarnar:fix/trtengine-tracing-mode-real
Aug 20, 2026
Merged

perf(runtime): skip TensorRT engine fakification during export by reporting tracing_mode "real"#4489
lanluo-nvidia merged 1 commit into
pytorch:mainfrom
Conarnar:fix/trtengine-tracing-mode-real

Conversation

@Conarnar

Copy link
Copy Markdown
Contributor

Description

torch.export fakifies every TorchBind script object it traces through, and for a TensorRT engine that is expensive twice over:

  • maybe_to_fake_obj calls __obj_flatten__, which runs TRTEngine::serialize() — a full cuda_engine->serialize() plus base64 — and FakeTRTEngine.__obj_unflatten__ then base64-decodes it again.
  • FakeScriptObject.__init__ deep-copies the real object, which for a TorchBind class goes through def_pickle: serialize, then a full deserializeCudaEngine of a second copy onto the device.

The fake never reads any of it. FakeTRTEngine assigns the blob to self.serialized_engine and every field it actually uses is metadata. ExecuTorch re-runs decompositions during to_edge_transform_and_lower, so this happens roughly three times per engine, and the cost grows with engine count and size.

torch already provides the opt-out: tracing_with_real() returns True for an object whose tracing_mode() reports "real", and maybe_to_fake_obj then hands the real object to the meta kernel untouched. This adds that method to the TorchBind engine.

Why this is safe

fake_tensorrt_execute_engine already handles being given a real engine: it branches on real_obj and otherwise calls get_serialized_metadata() directly, taking output shapes from the symbolic expressions stored in the engine's metadata. Nothing executes and nothing mutates the engine during tracing.

The Python TRTEngine has reported "real" for exactly this reason since it was added — its docstring cites tracing_with_real. The TorchBind class simply never did the same.

Measurements

The mechanism is torch.export fakification, so any export carrying TensorRT engines benefits; the numbers below come from a two-layer Gemma4-MoE exported through the hybrid TensorRT + CUDA ExecuTorch path (3 engines), fresh Inductor cache, otherwise identical runs:

wallfakification
before706.3 s517.9 s
after180.4 s0.0 s

The exported program is unchanged: same delegate count, operators and values, and byte-identical delegate payloads.

Testing

Verified by rebuilding and re-running the export above with no patches: fakification drops to zero and the artifact is byte-identical to the baseline. Existing tests/py/dynamo/executorch and tests/py/dynamo/lowering/test_buffer_lifting.py pass (75).

No automated test is included. Nothing in tests/ currently exercises fakification of an engine, and the meaningful property — that export stops serializing the engine per fakify call — is a timing assertion. The cheapest real check would be asserting tracing_mode() == "real" on a built engine, which requires a GPU. Happy to add that if you want it gated.

torch.export fakifies every torchbind script object it traces through. For a
TensorRT engine that is enormously expensive twice over: maybe_to_fake_obj calls
__obj_flatten__, which runs TRTEngine::serialize() (a full
cuda_engine->serialize() plus base64) and then base64-decodes it again in
FakeTRTEngine.__obj_unflatten__, and FakeScriptObject.__init__ deep-copies the
engine through its pickle, which serializes and then fully deserializes another
copy onto the device. The fake stores the resulting blob and never reads it --
every field it uses is metadata. ExecuTorch re-runs decompositions during
to_edge_transform_and_lower, so this happens roughly three times per engine, and
the cost grows with engine count and size.
torch offers an opt-out: tracing_with_real() returns True for objects whose
tracing_mode() reports "real", and maybe_to_fake_obj then hands the real object
to the meta kernel untouched. fake_tensorrt_execute_engine already handles that
case -- it branches on real_obj and otherwise reads get_serialized_metadata(),
taking output shapes from the stored symbolic expressions -- so nothing executes
or mutates the engine during tracing. The Python TRTEngine has reported "real"
for this reason since it was added; the TorchBind class simply never did.
Measured on a 2-layer Gemma4-MoE hybrid TensorRT + CUDA ExecuTorch export
(3 engines): 706s -> 180s wall, with the fakification phase going from 518s to
0.0s. The exported program is unchanged -- identical delegate count, operators,
values, and byte-identical delegate payloads.
@github-actionsgithub-actionsBot added component: core Issues re: The core compiler component: runtime labels Aug 14, 2026
Conarnar added a commit to Conarnar/TensorRT that referenced this pull request Aug 18, 2026
Export resolves engine info for two purposes, and neither wants the base64 form
serialize() produces. validate_engine_program reads only flags;
replace_execute_engine wants the engine as a byte tensor. Both were paying an
encode in C++ and, for the second, a matching decode here.
Take each half from the accessor that provides it:
metadata_only=True on get_engine_info_from_state and _resolve_engine_info,
so a metadata reader never triggers engine serialization.
_resolve_engine_tensor returns the engine as a uint8 tensor directly, which
also drops the torch.frombuffer rebuild.
Both fall back to the old path when the runtime lacks the accessors, so this
does not require a matching runtime. They are missing only when the
Torch-TensorRT C++ library is older than this Python package -- a source or
editable build where only the Python half was rebuilt. The symptom would
otherwise be silence, since the fallback is correct and merely slower, so it
warns once per accessor naming the cause.
_resolve_engine_object is factored out of _resolve_engine_info because the
engine arg is a get_attr before ExecuTorch lifts constants and a placeholder
after; handling only the first made the tensor accessor silently fall back to
base64 on every graph that had been through staging, which is exactly the case
that matters.
Nothing is cached. With the accessors present a metadata read is a member read
away from free, so there is nothing worth memoizing; a runtime without them
re-serializes per read, which is the price of keeping this change small.
backend.py and partitioner.py are deliberately untouched. Their engine reads go
through the no_op_placeholder branch, which already carries the engine as a
tensor argument and serializes nothing, so routing them through the accessors
would add a parameter that changes no work.
Measured on a base carrying pytorch#4473, with pytorch#4489 present in the loaded runtime,
exporting a model with a 67MB engine; six runs per configuration, interleaved.
The passes that read engine info go from 1.40s to 0.11s and the ExecuTorch
lowering phase from 2.35s to 1.08s, a 54% reduction. Engine-state reads are
nearly all of that: 1.07s to 0.07s, with the Python-side base64 decode going
from 0.26s to nothing. Serializing the engine is only about 0.07s of a 0.54s
__getstate__, so most of what disappears is the base64 encode and the string
copies feeding it. A metadata-only read costs 10-20us.
Conarnar added a commit to Conarnar/TensorRT that referenced this pull request Aug 18, 2026
Export resolves engine info for two purposes, and neither wants the base64 form
serialize() produces. validate_engine_program reads only flags;
replace_execute_engine wants the engine as a byte tensor. Both were paying an
encode in C++ and, for the second, a matching decode here.
Take each half from the accessor that provides it:
metadata_only=True on get_engine_info_from_state and _resolve_engine_info,
so a metadata reader never triggers engine serialization.
_resolve_engine_tensor returns the engine as a uint8 tensor directly, which
also drops the torch.frombuffer rebuild.
Both fall back to the old path when the runtime lacks the accessors, so this
does not require a matching runtime. They are missing only when the
Torch-TensorRT C++ library is older than this Python package -- a source or
editable build where only the Python half was rebuilt. The symptom would
otherwise be silence, since the fallback is correct and merely slower, so it
warns once per accessor naming the cause.
_resolve_engine_object is factored out of _resolve_engine_info because the
engine arg is a get_attr before ExecuTorch lifts constants and a placeholder
after; handling only the first made the tensor accessor silently fall back to
base64 on every graph that had been through staging, which is exactly the case
that matters.
Nothing is cached. With the accessors present a metadata read is a member read
away from free, so there is nothing worth memoizing; a runtime without them
re-serializes per read, which is the price of keeping this change small.
backend.py and partitioner.py are deliberately untouched. Their engine reads go
through the no_op_placeholder branch, which already carries the engine as a
tensor argument and serializes nothing, so routing them through the accessors
would add a parameter that changes no work.
Measured on a base carrying pytorch#4473, with pytorch#4489 present in the loaded runtime,
exporting a model with a 67MB engine; six runs per configuration, interleaved.
The passes that read engine info go from 1.40s to 0.11s and the ExecuTorch
lowering phase from 2.35s to 1.08s, a 54% reduction. Engine-state reads are
nearly all of that: 1.07s to 0.07s, with the Python-side base64 decode going
from 0.26s to nothing. Serializing the engine is only about 0.07s of a 0.54s
__getstate__, so most of what disappears is the base64 encode and the string
copies feeding it. A metadata-only read costs 10-20us.
Conarnar added a commit to Conarnar/TensorRT that referenced this pull request Aug 19, 2026
Export resolves engine info for two purposes, and neither wants the base64 form
serialize() produces. validate_engine_program reads only flags;
replace_execute_engine wants the engine as a byte tensor. Both were paying an
encode in C++ and, for the second, a matching decode here.
Take each half from the accessor that provides it:
metadata_only=True on get_engine_info_from_state and _resolve_engine_info,
so a metadata reader never triggers engine serialization.
_resolve_engine_tensor returns the engine as a uint8 tensor directly, which
also drops the torch.frombuffer rebuild.
Both fall back to the old path when the runtime lacks the accessors, so this
does not require a matching runtime. They are missing only when the
Torch-TensorRT C++ library is older than this Python package -- a source or
editable build where only the Python half was rebuilt. The symptom would
otherwise be silence, since the fallback is correct and merely slower, so it
warns once per accessor naming the cause.
_resolve_engine_object is factored out of _resolve_engine_info because the
engine arg is a get_attr before ExecuTorch lifts constants and a placeholder
after; handling only the first made the tensor accessor silently fall back to
base64 on every graph that had been through staging, which is exactly the case
that matters.
Nothing is cached. With the accessors present a metadata read is a member read
away from free, so there is nothing worth memoizing; a runtime without them
re-serializes per read, which is the price of keeping this change small.
backend.py and partitioner.py are deliberately untouched. Their engine reads go
through the no_op_placeholder branch, which already carries the engine as a
tensor argument and serializes nothing, so routing them through the accessors
would add a parameter that changes no work.
Measured on a base carrying pytorch#4473, with pytorch#4489 present in the loaded runtime,
exporting a model with a 67MB engine; six runs per configuration, interleaved.
The passes that read engine info go from 1.40s to 0.11s and the ExecuTorch
lowering phase from 2.35s to 1.08s, a 54% reduction. Engine-state reads are
nearly all of that: 1.07s to 0.07s, with the Python-side base64 decode going
from 0.26s to nothing. Serializing the engine is only about 0.07s of a 0.54s
__getstate__, so most of what disappears is the base64 encode and the string
copies feeding it. A metadata-only read costs 10-20us.
@shoumikhin

shoumikhin commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Reviewed this and it looks correct to me. Below is what I checked, then two things I would like to see before it lands.

The mechanism is exactly as you describe.tracing_with_real returns True only for "real", and maybe_to_fake_obj then returns the object untouched, so no fakification happens:

tracing_mode='real' -> True (skips fakification)
tracing_mode='fake' -> False
no tracing_mode -> False
tracing_mode='sometimes' -> AssertionError: can be either real or fake

A typo would assert loudly rather than quietly degrade, which is a nice property. The Python engine has reported "real" since it was added (_TRTEngine.py:419), so the two engine types disagreed and this aligns them. That is the strongest argument for the change.

Every consumer that could notice the difference still works. The meta kernel branches on hasattr(..., "real_obj") (register_meta_ops.py:203), and a real engine takes the else, which calls get_serialized_metadata(). That method is registered on the real class (register_jit_hooks.cpp:86), and the kernel only reads metadata and computes shapes, so nothing executes or mutates. _resolve_lifted_custom_obj unwraps only when the object is fake and otherwise passes it through, so it handles both.

One spot to glance at: unwrap_tensor_dtype (utils.py:596) matches FakeScriptObject and its else raises, so a real engine reaching it would throw. I do not think engine objects flow into it, but it is the one place the fake/real distinction is unguarded.

Two requests:

  1. A test. tracing_mode, tracing_with_real, FakeTRTEngine and maybe_to_fake_obj have zero hits in the whole tests/ tree, so nothing pins this behaviour in either direction. Asserting tracing_mode() == "real" plus equal export output with and without fakification would cover it.
  2. A number. The mechanism is clearly right, but I did not measure the saving, and "roughly three times per engine" is carrying a lot of the argument. A before and after on a model with several engines (export time, or peak host memory) would make this easy to approve.

Also worth noting FakeTRTEngine is registered for tensorrt::Engine, the same class that now reports "real", so it should no longer be constructed. Nothing seems lost, since its methods are metadata accessors the real engine implements natively, but a short comment saying it is kept for compatibility would stop the next reader assuming it is live.

@lanluo-nvidia
lanluo-nvidia merged commit 1527186 into pytorch:mainAug 20, 2026
57 of 63 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@Conarnar@shoumikhin@lanluo-nvidia