Multi-optimization profile for executorch #4417
Replies: 5 comments 4 replies
|
What does 3.2 When to use: Shapes drive selection mean? |
Do you think auto should be default? |
What does this mean? |
I like this but lets call this a OptimizationProfileGuard or OptProfileGuard, similar to StreamGuards or other similar mechanisms in PyTorch |
Review of RFC #4417: Multi-Optimization-Profile Support in ExecuTorchThanks for putting this RFC together. I support the feature and think we should keep moving forward with it. The problem is real, especially for LLMs. Prefill and decode are very different workloads: Using separate TensorRT optimization profiles is like giving the engine two gears: Keeping both profiles in one engine is useful because the profiles can share the same weights. Discovering the profiles from the deserialized TensorRT engine also makes sense, and I agree that the TR01 format probably does not need to change. My recommendation is to approve the direction but revise the runtime-control design before implementation. Main issue: users normally run an ExecuTorch
|
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Multi-Optimization-Profile Support in the ExecuTorch Backend
torch_tensorrt.executorch(Python export),cpp/src/torch_tensorrt/executorch/TensorRTBackend.cpp(C++ runtime), TR01 blob formatother/executorch.md1. Problem
A TensorRT engine can carry N optimization profiles — one weight set, several
kernel tunings, each valid over a different input-shape range. This matters for
bimodal workloads like LLM inference:
input_ids[B, 32..2048][B, 1]One profile spanning
[1, 2048]picks kernels wrong for both phases; separatedecode/prefill profiles let TensorRT tune each path and the runtime switch per
call.
Profiles are declared with
torch_tensorrt.Input(profiles=[{min_shape, opt_shape, max_shape}, ...])— anordered list whose index is the profile index — and selected at inference
by integer index (or
"auto"). There are no profile names.The standard runtime (Python
TRTEngine/ C++torch::classes::tensorrt::Engine)already does this: it reconstructs per-profile bounds by index from the
deserialized engine (
core/runtime/TRTEngine.cpp:633) and switches viaset_active_profile/auto_select_profile(TRTEngine.cpp:687-782). Selectionis purely runtime; a freshly loaded engine defaults to index 0, auto-select off
(
TRTEngine.h:297-299).The ExecuTorch export path does not support this, for three reasons:
No libtorch / TRT C++ runtime at inference.
output_format="executorch"(
other/executorch.md§1) produces a.pterun by a small C++ runner with onlyExecuTorch + TensorRT + CUDA;
TRTEngine::set_active_profiledoes not exist there.The backend hardcodes profile 0.
initialize_input_profiles()(
TensorRTBackend.cpp:170-191) reads bounds from profile 0 only:A multi-profile engine loads, but only profile 0 is ever used.
No Python at inference, so no context manager. A
.pteruns viamethod.execute(); selection must be expressible through a C++ runtime hook.This design extends multi-profile support to the ExecuTorch path so a
multi-profile-compiled graph can be saved as a
.pteand run with the rightprofile picked at inference — by index.
2. Goals / Non-goals
Goals
init(), validate against the active profile, switch viasetOptimizationProfileAsync.(default 0), opt-in lazy first-fit auto-select, sticky "keep the active profile
if it still fits".
re-loading the
.pte.Input(profiles=[...]))..ptes load identically.Non-goals
other/executorch.md§4.2:engines come from Dynamo, ExecuTorch only wraps them).
into the artifact (see §5, §7).
3. Design summary
The ExecuTorch runtime reimplements the standard runtime's index-based logic
(
setup_optimization_profiles,profile_fits,auto_select_profile,set_active_profile) without libtorch. Selection is entirely runtime, via twopaths:
TensorRTBackend(§6.3)execute()(§6.2), enabled via the hookOpt-in: with no runtime call a
.ptebehaves as today (active_profile_index == 0, auto off). A single-profile.pteadditionally hasnum_optimization_profiles == 1and never switches — byte-for-byte identical to current behavior.4. TR01 blob format: no change
Everything the backend needs is reconstructable from the deserialized engine:
num_profiles— fromengine->getNbOptimizationProfiles().(min, max)— fromengine->getProfileShape(name, p, kMIN | kMAX).Since selection is index-based and made at runtime, there are no names and no
policy to persist. TR01 metadata,
TENSORRT_MAGIC("TR01"), andHEADER_FORMATare untouched.
5. Python export
5.1 No code change to the export path
No Python-side transformation is required.
TensorRTPartitioner— no change. The active profile is runtime executionstate, not a graph property. Unlike
target_device(which the partitioner setsbecause
PropagateDevicePassbakes it into the.pte'sextra_tensor_info),profile selection has no AOT effect — it changes no nodes, lowering, engine
bytes, or memory planning (outputs are pre-allocated at the max envelope
regardless). Nothing for the partitioner to do.
TensorRTBackend.preprocess()— no change. Builds the TR01 blob as today;all N profiles are inside the serialized engine (§4).
_replace_execute_engine_for_executorch— no change. Profile-countagnostic.
All profile behavior lives in the C++ backend (§6).
5.2 All engines share the same profile count by construction
The runtime hooks (§6.3) assume every TRT delegate in a
.ptehas the samenumber of optimization profiles, in the same order. No extra enforcement is
needed to guarantee this — it is a direct consequence of how profiles are
specified at compile time:
optimization_profiles=[prefill, decode, ...]listto
torch_tensorrt.dynamo.compile.into. For each partition, the interpreter calls
add_optimization_profileonce per list entry.
getNbOptimizationProfiles(), andprofile index
irefers to the same shape regime (e.g. "decode") in everyengine.
Because there is exactly one profile list and it is applied uniformly, a single
integer index is meaningful across all delegates in the
.pte.5.3 Enforcing the invariant at save time
Because the invariant is what makes the whole-method
set_active_profile(§6.3) atomic and index-meaningful, we validate it explicitly in
_save_as_executorchrather than trust it implicitly:settings.optimization_profilesin itsserialized_metadata, and theprofile count is also recoverable from the engine bytes), assert that every
TRT partition reports the same profile count.
offending partitions and their counts, rather than letting a runner discover
it via a confusing per-delegate index error at inference.
This is the one small Python-side addition; it is validation only (no graph or
blob mutation) and is a no-op for the common single-engine
.pte.6. C++ backend changes
Add the same multi-profile members/methods the standard runtime has, adapted to
EngineHandle.6.1
EngineHandleextensions +initialize_input_profiles()Extend the handle (
cpp/include/torch_tensorrt/executorch/TensorRTBackend.h):Rewrite
initialize_input_profiles()(TensorRTBackend.cpp:170-191) to enumerateall profiles instead of hardcoding
0, buildingprofile_dynamic_dims: gather(min, max)for every dim across every profile, keeping only dims that vary withina profile (
min != max) or differ across profiles (a dim with the same fixedextent everywhere can't distinguish profiles; TensorRT validates it at
setInputShape). Shape-tensor inputs are still rejected.init()leaves the defaults (active_profile_index = 0,auto_select_profiles = false) and does not parsecompile_specs— matching the standard runtime's loadbehavior.
6.2
execute()bounds check + auto-select (TensorRTBackend.cpp:398-479)Today the per-input check compares against profile 0. Two changes:
active_profile_index.num_profiles > 1 && auto_select_profiles:thrashing when shapes alternate).
0..N-1, pick the first whose[min, max]contain every input, then
setOptimizationProfileAsync(p, stream).Overlapping profiles resolve to the lowest matching index — declare the more
specific regime (e.g. decode) first, or pin manually.
Error::InvalidArgumentpointing at the pin hook (§6.3).Invariants:
CudaStreamGuard-scoped stream asenqueueV3(nodefault-stream sync).
inferShapes— whichexecute()already does each call, so no extra bookkeeping.6.3 Runtime pin: new index-based backend hook
Selection is runtime, so the primary API is a runner-callable pin that bypasses
method.execute(). Backends are addressable via theBackendregistry(
register_backendat the bottom ofTensorRTBackend.cpp), so add a smallindex-based side-channel.
Invariant: all TRT delegates in a
.pteshare the same profile count.Every engine is built with the same number of optimization profiles, in the
same order (profile index
imeans the same regime — e.g. "decode" — acrossall engines). This makes a single integer index globally meaningful, so the
whole-method pin can be atomic and strict: it validates the index against
the common profile count once and either applies to every delegate or fails
without touching any of them. §5.3 covers how the invariant is enforced at
export time.
set_active_profilewalksmethod's delegates, finds eachEngineHandle*ownedby
TensorRTBackend, validates the index, and calls the same switch helper as theauto-select path. Runner pattern:
Opt-in and additive: runners that only call
method.execute()get profile 0 /auto off — no code change.
7. Alternatives considered
target_device, §5); baking it would conflate partitioning with execution and diverge from the standard runtime..pteper profile8. Backward compatibility
.ptes load unchanged. A single-profile engine reportsgetNbOptimizationProfiles() == 1→num_profiles == 1,active_profile_index == 0, auto off, emptyprofile_dynamic_dims— today's profile-0 path..ptes on old runners. An oldlibexecutorch_trt_backend.areadsprofile 0 only — no crash, just no switching.
Input(profiles=[...])→ samecode path as today.
9. Portability considerations
enqueueV3directly; a switchjust needs
setOptimizationProfileAsync+ theinferShapesit already runs.target_deviceis AOT (baked into the.pte); profile selection is a runtime call with no serialized footprint — hencepartitioner vs C++-hook layering.
.pte. Every engine is built with the same profile count andorder (§5.3), so a single index is meaningful across all of them. The
whole-method
set_active_profilevalidates the index once against that commoncount and applies atomically (all delegates switch, or none do and it returns
Error::InvalidArgument). The per-DelegateHandlevariant (§6.3) still lets acaller pin one engine to a different index than the rest when engines must run
different regimes concurrently.
10. Implementation plan
Ordered for isolated landing/testing:
EngineHandle(§6.1); rewriteinitialize_input_profiles()to buildprofile_dynamic_dims.execute()stilluses index 0. Existing tests untouched.
execute(). Addprofile_fits+auto_select(sticky-then-first-fit). Test a two-profile engine (
[1,1]and[1,MAX])alternating inputs + an overlap-ordering case.
_save_as_executorch(§5.3). Unit test: a hand-constructed multi-engine graphwith mismatched counts raises a clear error; matching counts pass.
set_active_profile/set_auto_select_profiles/OptimizationProfileGuard+ build registration. Reference-runner test: pin 1 then 0, assertthe switch and correct outputs; also test
set_auto_select_profiles(true)and anout-of-range index returning
Error::InvalidArgumentwithout switching anydelegate.
examples/torchtrt_executorch_example/; updateother/executorch.md.Steps 1–3 are independently useful and don't depend on the hooks.
11. Open questions
All reactions