Skip to content

Commit 8d16914

Browse files
Conarnarshoumikhinguac e2e
authored
feat(executorch): copy-back for non-KV mutable buffers in the TensorRT delegate (#4459)
Co-authored-by: Anthony Shoumikhin <shoumikhin@meta.com> Co-authored-by: guac e2e <guac@localhost> Co-authored-by: x <x>
1 parent d991562 commit 8d16914

16 files changed

Lines changed: 3588 additions & 70 deletions

py/torch_tensorrt/_compile.py

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1148,6 +1148,9 @@ def _extract_tensor(obj: Any) -> Any:
11481148
# whichever exporter produced the program. aot_inductor is left
11491149
# undeclared: whether an aliased in-place mutation survives
11501150
# functionalization under inductor is unverified.
1151+
#
1152+
# Copy-back is gated per-branch below: the legacy exporter declares it
1153+
# itself and consumes the trailing values while doing so.
11511154
if not retrace:
11521155
from torch_tensorrt.dynamo._exporter import export
11531156

@@ -1173,6 +1176,22 @@ def _extract_tensor(obj: Any) -> Any:
11731176
_declare_aliased_kv_mutations_on_ep,
11741177
)
11751178

1179+
# On this path the legacy exporter is what declares copy-back, except
1180+
# under output_format="executorch", where torch_tensorrt.executorch
1181+
# .export() declares it for whatever source shape it is handed. The
1182+
# remaining combination leaves it undeclared.
1183+
if (
1184+
not _use_legacy
1185+
and output_format != "executorch"
1186+
and module.meta.get("_copyback_mutation_buffers")
1187+
):
1188+
logger.warning(
1189+
"Module has non-KV mutable buffer(s) needing copy-back, but "
1190+
"retrace=False with use_legacy_exporter=False does not declare "
1191+
"them. The saved program's signature will not reflect those "
1192+
"updates. Use the legacy exporter, or retrace=True."
1193+
)
1194+
11761195
if output_format == "exported_program":
11771196
# Must precede normalization, which rewrites the engine constants
11781197
# this pass reads aliased_io from.
@@ -1235,6 +1254,7 @@ def _extract_tensor(obj: Any) -> Any:
12351254
if node.op == "placeholder" and "val" in node.meta
12361255
for dim in getattr(node.meta["val"], "shape", [])
12371256
)
1257+
_use_legacy = False
12381258
if has_symbolic_metadata and dynamic_shapes is not None:
12391259
from torch_tensorrt.dynamo._exporter import export
12401260

@@ -1283,6 +1303,16 @@ def _extract_tensor(obj: Any) -> Any:
12831303
_declare_aliased_kv_mutations_on_ep,
12841304
)
12851305

1306+
# create_trt_exp_program already declares the copy-back mutations and
1307+
# consumes their trailing outputs, so what trails the graph on that path
1308+
# is a genuine user output. Only the torch.export paths leave the values
1309+
# to be reclassified.
1310+
_copyback_bufs = (
1311+
[]
1312+
if _use_legacy
1313+
else module.meta.get("_copyback_mutation_buffers", [])
1314+
)
1315+
12861316
if output_format == "aot_inductor" and any(
12871317
getattr(sub, "aliased_io", None)
12881318
for _sub_name, sub in module.named_modules()
@@ -1297,7 +1327,9 @@ def _extract_tensor(obj: Any) -> Any:
12971327
if output_format == "exported_program":
12981328
# Must precede normalization, which rewrites the engine constants
12991329
# this pass reads aliased_io from.
1300-
exp_program = _declare_aliased_kv_mutations_on_ep(exp_program)
1330+
exp_program = _declare_aliased_kv_mutations_on_ep(
1331+
exp_program, copyback_buffers=_copyback_bufs
1332+
)
13011333
_normalize_engine_constants_to_python(exp_program)
13021334
function_overload_with_kwargs(
13031335
torch.export.save,
@@ -1318,7 +1350,9 @@ def _extract_tensor(obj: Any) -> Any:
13181350
package_path=file_path,
13191351
)
13201352
elif output_format == "executorch":
1321-
exp_program = _declare_aliased_kv_mutations_on_ep(exp_program)
1353+
exp_program = _declare_aliased_kv_mutations_on_ep(
1354+
exp_program, copyback_buffers=_copyback_bufs
1355+
)
13221356
_save_as_executorch(
13231357
exp_program,
13241358
file_path,

py/torch_tensorrt/dynamo/_compiler.py

Lines changed: 74 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@
4545
pre_export_lowering,
4646
)
4747
from torch_tensorrt.dynamo.lowering._buffer_lifting import (
48+
aliased_input_bindings,
49+
assert_no_kv_alias_markers_survived,
50+
assert_predicted_kv_aliased,
51+
hide_copyback_outputs,
4852
inline_lifted_buffers_into_gm,
4953
lift_mutated_buffers,
5054
)
@@ -815,7 +819,11 @@ def compile(
815819
# prerequisite for IKVCacheUpdateLayer / aliased I/O to fire on a
816820
# module-held cache). Returns a fresh GraphModule whose forward signature
817821
# reflects the new placeholders.
818-
gm, lifted_buffers = lift_mutated_buffers(gm)
822+
gm, lifted_buffers = lift_mutated_buffers(gm, settings)
823+
_copyback_mutation_buffers = gm.meta.get("_copyback_mutation_buffers", [])
824+
_copyback_bindings = gm.meta.get("_copyback_bindings", [])
825+
_predicted_kv_bindings = gm.meta.get("_predicted_kv_bindings", [])
826+
_no_kv_alias_writes = gm.meta.get("_no_kv_alias_writes", [])
819827
if lifted_buffers:
820828
# Append each lifted buffer as an engine input AFTER the user inputs.
821829
# Buffer tensors live on the gm's state; prepare an Input spec for
@@ -835,6 +843,12 @@ def compile(
835843
logger.debug(f"CPU memory usage after post_lowering: {get_cpu_memory_usage()} MB")
836844
logger.debug("Lowered Input graph: " + str(gm.graph))
837845

846+
# The marker is what keeps a re-routed write out of the engine's aliased_io, and
847+
# it rides on node.meta through every pass above. Read it back before conversion
848+
# so a pass that dropped it is named here rather than surfacing as a module that
849+
# raises on every call.
850+
assert_no_kv_alias_markers_survived(gm, _no_kv_alias_writes)
851+
838852
# Move the weights in the state_dict to CPU
839853
if offload_module_to_cpu:
840854
deallocate_module(gm)
@@ -856,6 +870,23 @@ def compile(
856870
engine_cache,
857871
graph_signature=exported_program.graph_signature,
858872
)
873+
if _copyback_mutation_buffers:
874+
trt_gm.meta["_copyback_mutation_buffers"] = _copyback_mutation_buffers
875+
# Ground-truth check on both directions of the classification: every write lift
876+
# called KV (engine-aliased, so its copy_ was dropped) must appear in a compiled
877+
# engine's aliased_io or its write-back is silently lost, and no write lift
878+
# called copy-back may appear there or the trailing output it added is one the
879+
# runtime truncates while the graph still reads it. Fail loudly for either. A
880+
# dryrun returns before conversion, so there is no ground truth to check against.
881+
assert_predicted_kv_aliased(
882+
aliased_input_bindings(
883+
getattr(sub, "aliased_io", None) for _name, sub in trt_gm.named_children()
884+
),
885+
_predicted_kv_bindings,
886+
settings,
887+
engines_built=not settings.dryrun,
888+
copyback_bindings=_copyback_bindings,
889+
)
859890
if lifted_buffers:
860891
# Inline buffers into the compiled gm as get_attr nodes + registered
861892
# buffers. The resulting gm's forward takes only user inputs; buffers
@@ -864,6 +895,12 @@ def compile(
864895
# serializable by torch_tensorrt.save / torch.export (no external
865896
# Python wrapper that would be lost on a round-trip).
866897
trt_gm = inline_lifted_buffers_into_gm(trt_gm, lifted_buffers)
898+
# A copy-back value stays on the output node, where the exporters read it and
899+
# where dead-code elimination cannot reach it, but nothing between here and the
900+
# ExecuTorch runtime writes it into the buffer. Returning it would report a
901+
# mutation the caller cannot act on, so the compiled module keeps the arity of
902+
# the model it was compiled from.
903+
hide_copyback_outputs(trt_gm, len(_copyback_mutation_buffers))
867904
return trt_gm
868905

869906

@@ -1803,6 +1840,12 @@ def convert_exported_program_to_serialized_trt_engine(
18031840
automatically; this lower-level entry point exposes the same machinery
18041841
for callers that want to manage the bindings themselves.
18051842
1843+
Only writes the engine can alias in place are supported here. A mutation the
1844+
engine cannot alias needs its new value copied back into the buffer after the
1845+
call, and this entry point reports neither which output carries which buffer nor
1846+
performs the copy, so it raises rather than returning an engine whose buffer
1847+
would never update. Use :func:`torch_tensorrt.dynamo.compile` for those models.
1848+
18061849
Arguments:
18071850
exported_program (torch.export.ExportedProgram): Source module, running torch.export on a ``torch.nn.Module``
18081851
inputs (Optional[Sequence[Sequence[Any]]]): List of specifications of input shape, dtype and memory layout for inputs to the module. This argument is required. Input Sizes can be specified as torch sizes, tuples or lists. dtypes can be specified using
@@ -1886,6 +1929,11 @@ def convert_exported_program_to_serialized_trt_engine(
18861929
**kwargs: Any,
18871930
Returns:
18881931
bytes: Serialized TensorRT engine, can either be saved to a file or deserialized via TensorRT APIs
1932+
Raises:
1933+
RuntimeError: if ``lift_mutable_buffers=True`` and the model mutates a buffer
1934+
the engine cannot alias in place, or if a write predicted to be aliased is
1935+
absent from the built engine's ``aliased_io``. Either way the buffer would
1936+
silently never update.
18891937
"""
18901938

18911939
if kwargs.get("debug", False):
@@ -2074,8 +2122,26 @@ def convert_exported_program_to_serialized_trt_engine(
20742122
# resulting bindings at runtime — they are appended after the user inputs
20752123
# in the order returned here.
20762124
lifted_buffers: List[Tuple[str, str, torch.Tensor]] = []
2125+
predicted_kv_bindings: List[str] = []
20772126
if lift_mutable_buffers:
2078-
gm, lifted_buffers = lift_mutated_buffers(gm)
2127+
gm, lifted_buffers = lift_mutated_buffers(gm, settings)
2128+
# Read before lowering: `gm` is replaced below and the meta does not follow it.
2129+
predicted_kv_bindings = gm.meta.get("_predicted_kv_bindings", [])
2130+
# A write the engine cannot alias in place is classified as copy-back: its
2131+
# new value is appended as an extra engine output, and whatever loads the
2132+
# serialized program copies that output into the buffer afterwards. This
2133+
# entry point returns engine bytes and no program, so it neither reports
2134+
# which output carries which buffer nor gives the copy anywhere to be
2135+
# declared, and the caller has no way to complete it.
2136+
copyback_buffers = gm.meta.get("_copyback_mutation_buffers", [])
2137+
if copyback_buffers:
2138+
raise RuntimeError(
2139+
"convert_exported_program_to_serialized_trt_engine cannot express the "
2140+
f"write-back for mutable buffer(s) {copyback_buffers}: the engine "
2141+
"cannot alias them in place, so the buffers would never update. Use "
2142+
"torch_tensorrt.dynamo.compile and save the result, which declares "
2143+
"the buffer mutation for the runtime to apply."
2144+
)
20792145
if lifted_buffers:
20802146
buffer_tensors = [t for _, _, t in lifted_buffers]
20812147
buffer_inputs = prepare_inputs(buffer_tensors)
@@ -2176,6 +2242,12 @@ def convert_exported_program_to_serialized_trt_engine(
21762242
)
21772243
raise RuntimeError(f"While interpreting the module got an error: {e}") from e
21782244

2245+
assert_predicted_kv_aliased(
2246+
aliased_input_bindings([interpreter_result.aliased_io]),
2247+
predicted_kv_bindings,
2248+
settings,
2249+
)
2250+
21792251
serialized_engine: bytes = interpreter_result.serialized_engine
21802252
return serialized_engine
21812253

0 commit comments

Comments
 (0)