Skip to content

Commit 62c562d

Browse files
committed
feat: custom binding names for out of runtime deployment
1 parent b13b69d commit 62c562d

8 files changed

Lines changed: 1524 additions & 197 deletions

File tree

docsrc/tutorials/deployment/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,5 @@ complex-valued model support.
1111
serving_torch_tensorrt_with_triton
1212
cross_compile_windows
1313
Example: Cross-runtime Compilation for Windows <../_rendered_examples/dynamo/cross_runtime_compilation_for_windows>
14+
Example: Naming engine I/O bindings <../_rendered_examples/dynamo/engine_converter_binding_names>
1415
distributed_inference

examples/dynamo/README.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,5 @@ Model Zoo
2626
* :ref:`_torch_export_sam2`: Compiling SAM2 model using AOT workflow (`ir=dynamo`)
2727
* :ref:`_torch_export_flux_dev`: Compiling FLUX.1-dev model using AOT workflow (`ir=dynamo`)
2828
* :ref:`debugger_example`: Debugging Torch-TensorRT Compilation
29-
* :ref:`torch_export_3d_rope`: Compiling a 3D RoPE video-transformer block with complex numerics support
29+
* :ref:`torch_export_3d_rope`: Compiling a 3D RoPE video-transformer block with complex numerics support
30+
* :ref:`engine_converter_binding_names`: Naming input / output bindings when emitting a raw serialized TRT engine
Lines changed: 333 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,333 @@
1+
"""
2+
.. _engine_converter_binding_names:
3+
4+
Naming Engine Bindings with ``convert_exported_program_to_serialized_trt_engine``
5+
=================================================================================
6+
7+
When you use ``torch_tensorrt.dynamo.convert_exported_program_to_serialized_trt_engine``
8+
to produce a raw serialized TensorRT engine, the engine's binding names are
9+
determined by Torch-TensorRT's default policy:
10+
11+
* **Inputs** get the FX placeholder names from the exported program (typically
12+
the names of your ``forward()`` arguments).
13+
* **Outputs** get auto-generated names ``output0``, ``output1``, etc.
14+
15+
Many production runtimes (Triton Inference Server, custom C++ harnesses,
16+
ONNX-style integrations) bind tensors by name rather than position, and the
17+
auto-generated names often don't line up with what the rest of the serving
18+
stack expects. The engine converter exposes three keyword arguments that
19+
let you supply binding names shaped like your model's inputs and return
20+
value:
21+
22+
* ``arg_input_binding_names`` — pytree of strings matching ``arg_inputs``
23+
* ``kwarg_input_binding_names`` — pytree of strings matching ``kwarg_inputs``
24+
* ``output_binding_names`` — pytree of strings matching the model's return
25+
26+
The shape of each kwarg directly mirrors how you already pass the values
27+
themselves: ``arg_input_binding_names`` lines up with ``arg_inputs``,
28+
``kwarg_input_binding_names`` lines up with ``kwarg_inputs``.
29+
30+
A note on "the return shape"
31+
----------------------------
32+
33+
A Python function always returns exactly one value. ``return a, b`` is a
34+
single tuple-shaped return value; ``return {"x": a, "y": b}`` is a single
35+
dict-shaped return value. Whatever that value is, the exported program
36+
captures it as a pytree. Its *leaves* — the individual tensors at the
37+
bottom of the structure — become engine bindings, and you supply names in
38+
the same pytree shape. Inputs work the same way: ``arg_inputs`` is itself
39+
a pytree (a tuple of positional values, each of which can be a tensor or
40+
a nested collection of tensors); ``kwarg_inputs`` is a dict-shaped pytree.
41+
42+
How it works
43+
------------
44+
45+
The exported program already carries pytree specs (``args_spec`` for
46+
``arg_inputs``, ``kwargs_spec`` for ``kwarg_inputs``, ``out_spec`` for the
47+
return value) that fully describe the structure of inputs and outputs.
48+
When you provide binding names as a pytree of strings, Torch-TensorRT
49+
runs ``pytree.tree_flatten`` and compares the resulting ``TreeSpec``
50+
against the exported program's spec. When they match, the flat list of
51+
names maps 1:1 to FX's flattened placeholder / output order — no runtime
52+
queue, no in-band validation, just an up-front structural check.
53+
"""
54+
55+
# %%
56+
import tensorrt as trt
57+
import torch
58+
import torch_tensorrt
59+
from torch_tensorrt.dynamo._compiler import BindingNameMismatchError
60+
61+
DEVICE = torch.device("cuda", 0)
62+
63+
64+
# %%
65+
# Helpers
66+
# --------
67+
#
68+
# A pair of small helpers: one reads binding names off a deserialized
69+
# engine, the other actually runs the engine via the native TRT Python
70+
# API. The "execute via native TRT" path is what production deployments
71+
# use — the whole point of this API is that the binding names you supply
72+
# are the names you'll bind by at execution time, not just metadata in
73+
# the engine file.
74+
75+
76+
def deserialize(engine_bytes: bytes) -> trt.ICudaEngine:
77+
runtime = trt.Runtime(trt.Logger(trt.Logger.WARNING))
78+
return runtime.deserialize_cuda_engine(engine_bytes)
79+
80+
81+
def binding_names(engine: trt.ICudaEngine, mode: trt.TensorIOMode) -> list[str]:
82+
return [
83+
engine.get_tensor_name(i)
84+
for i in range(engine.num_io_tensors)
85+
if engine.get_tensor_mode(engine.get_tensor_name(i)) == mode
86+
]
87+
88+
89+
_TRT_TO_TORCH_DTYPE = {
90+
trt.DataType.FLOAT: torch.float32,
91+
trt.DataType.HALF: torch.float16,
92+
trt.DataType.INT32: torch.int32,
93+
trt.DataType.INT64: torch.int64,
94+
trt.DataType.BOOL: torch.bool,
95+
trt.DataType.BF16: torch.bfloat16,
96+
}
97+
98+
99+
def run_engine(engine: trt.ICudaEngine, named_inputs: dict) -> dict:
100+
"""Execute an engine via the native TRT Python API.
101+
102+
``named_inputs`` is a {binding_name: contiguous CUDA tensor} dict.
103+
Returns {binding_name: output tensor}. Demonstrates that the
104+
user-supplied binding names are what production C++/Python TRT
105+
runtime code will bind by.
106+
"""
107+
context = engine.create_execution_context()
108+
for name, tensor in named_inputs.items():
109+
context.set_input_shape(name, tuple(tensor.shape))
110+
context.set_tensor_address(name, tensor.data_ptr())
111+
112+
outputs = {}
113+
for i in range(engine.num_io_tensors):
114+
name = engine.get_tensor_name(i)
115+
if engine.get_tensor_mode(name) != trt.TensorIOMode.OUTPUT:
116+
continue
117+
shape = tuple(context.get_tensor_shape(name))
118+
dtype = _TRT_TO_TORCH_DTYPE[engine.get_tensor_dtype(name)]
119+
out = torch.empty(shape, dtype=dtype, device=DEVICE)
120+
context.set_tensor_address(name, out.data_ptr())
121+
outputs[name] = out
122+
123+
stream = torch.cuda.Stream(device=DEVICE)
124+
with torch.cuda.stream(stream):
125+
context.execute_async_v3(stream.cuda_stream)
126+
stream.synchronize()
127+
return outputs
128+
129+
130+
# %%
131+
# Case 1 — positional args, tuple-shaped return
132+
# ----------------------------------------------
133+
#
134+
# Start with the most common shape: ``forward(x)`` returning a 2-tuple.
135+
# ``arg_input_binding_names`` mirrors ``arg_inputs`` (a 1-tuple here);
136+
# ``output_binding_names`` mirrors the return tuple.
137+
138+
139+
class TwoHeads(torch.nn.Module):
140+
def forward(self, x: torch.Tensor):
141+
return torch.relu(x), torch.tanh(x)
142+
143+
144+
two_heads = TwoHeads().eval().cuda().half()
145+
x = torch.randn(2, 3, device=DEVICE, dtype=torch.float16)
146+
exported = torch.export.export(two_heads, (x,))
147+
148+
engine_bytes = torch_tensorrt.dynamo.convert_exported_program_to_serialized_trt_engine(
149+
exported,
150+
arg_inputs=(x,),
151+
arg_input_binding_names=("input_image",),
152+
output_binding_names=("relu_out", "tanh_out"),
153+
require_full_compilation=True,
154+
min_block_size=1,
155+
use_python_runtime=False,
156+
immutable_weights=True,
157+
)
158+
engine = deserialize(engine_bytes)
159+
print("Case 1 inputs:", binding_names(engine, trt.TensorIOMode.INPUT))
160+
print("Case 1 outputs:", binding_names(engine, trt.TensorIOMode.OUTPUT))
161+
# Case 1 inputs: ['input_image']
162+
# Case 1 outputs: ['relu_out', 'tanh_out']
163+
164+
# Run the engine through the native TRT API using the names we requested.
165+
trt_outs = run_engine(engine, {"input_image": x.contiguous()})
166+
with torch.no_grad():
167+
ref_relu, ref_tanh = two_heads(x)
168+
torch.testing.assert_close(trt_outs["relu_out"], ref_relu, rtol=1e-2, atol=1e-2)
169+
torch.testing.assert_close(trt_outs["tanh_out"], ref_tanh, rtol=1e-2, atol=1e-2)
170+
print("Case 1 native TRT run matches PyTorch.")
171+
172+
173+
# %%
174+
# Case 2 — keyword-only inputs
175+
# ------------------------------
176+
#
177+
# When the model takes keyword arguments, you pass ``kwarg_inputs`` and
178+
# match its shape with ``kwarg_input_binding_names``. Note we leave
179+
# ``arg_input_binding_names`` unset because ``arg_inputs`` is empty.
180+
181+
182+
class KwargOnly(torch.nn.Module):
183+
def forward(self, image: torch.Tensor, positions: torch.Tensor):
184+
return image + positions
185+
186+
187+
kwarg_only = KwargOnly().eval().cuda().half()
188+
image = torch.randn(2, 3, device=DEVICE, dtype=torch.float16)
189+
positions = torch.randn(2, 3, device=DEVICE, dtype=torch.float16)
190+
kw_exported = torch.export.export(
191+
kwarg_only,
192+
args=(),
193+
kwargs={"image": image, "positions": positions},
194+
)
195+
196+
engine_bytes = torch_tensorrt.dynamo.convert_exported_program_to_serialized_trt_engine(
197+
kw_exported,
198+
arg_inputs=(),
199+
kwarg_inputs={"image": image, "positions": positions},
200+
kwarg_input_binding_names={"image": "rgb_in", "positions": "pos_in"},
201+
output_binding_names="combined",
202+
require_full_compilation=True,
203+
min_block_size=1,
204+
use_python_runtime=False,
205+
immutable_weights=True,
206+
)
207+
engine = deserialize(engine_bytes)
208+
print("Case 2 inputs:", sorted(binding_names(engine, trt.TensorIOMode.INPUT)))
209+
print("Case 2 outputs:", binding_names(engine, trt.TensorIOMode.OUTPUT))
210+
# Case 2 inputs: ['pos_in', 'rgb_in']
211+
# Case 2 outputs: ['combined']
212+
213+
trt_outs = run_engine(
214+
engine,
215+
{"rgb_in": image.contiguous(), "pos_in": positions.contiguous()},
216+
)
217+
with torch.no_grad():
218+
ref = kwarg_only(image=image, positions=positions)
219+
torch.testing.assert_close(trt_outs["combined"], ref, rtol=1e-2, atol=1e-2)
220+
print("Case 2 native TRT run matches PyTorch.")
221+
222+
223+
# %%
224+
# Case 3 — nested collections as inputs and outputs
225+
# --------------------------------------------------
226+
#
227+
# Inputs and outputs can be arbitrary nested collections of tensors —
228+
# tuples of dicts of tensors, lists of tuples, anything ``pytree`` can
229+
# flatten. The binding-name kwargs follow the same nesting. Here the
230+
# model takes a tuple of two cameras (each a dict of two tensors) and
231+
# returns a dict of feature stacks.
232+
233+
234+
class CameraTower(torch.nn.Module):
235+
def forward(self, cameras: tuple, bias: torch.Tensor):
236+
feats = []
237+
for cam in cameras:
238+
feats.append(cam["rgb"] + cam["depth"] + bias)
239+
return {"primary": feats[0], "secondary": feats[1]}
240+
241+
242+
def _cam():
243+
return {
244+
"rgb": torch.randn(2, 3, device=DEVICE, dtype=torch.float16),
245+
"depth": torch.randn(2, 3, device=DEVICE, dtype=torch.float16),
246+
}
247+
248+
249+
camera_tower = CameraTower().eval().cuda().half()
250+
cameras = (_cam(), _cam())
251+
bias = torch.randn(2, 3, device=DEVICE, dtype=torch.float16)
252+
nested_exported = torch.export.export(camera_tower, args=(cameras, bias))
253+
254+
engine_bytes = torch_tensorrt.dynamo.convert_exported_program_to_serialized_trt_engine(
255+
nested_exported,
256+
arg_inputs=(cameras, bias),
257+
arg_input_binding_names=(
258+
(
259+
{"rgb": "cam0_rgb", "depth": "cam0_depth"},
260+
{"rgb": "cam1_rgb", "depth": "cam1_depth"},
261+
),
262+
"global_bias",
263+
),
264+
output_binding_names={"primary": "p_feats", "secondary": "s_feats"},
265+
require_full_compilation=True,
266+
min_block_size=1,
267+
use_python_runtime=False,
268+
immutable_weights=True,
269+
)
270+
engine = deserialize(engine_bytes)
271+
print("Case 3 inputs:", sorted(binding_names(engine, trt.TensorIOMode.INPUT)))
272+
print("Case 3 outputs:", sorted(binding_names(engine, trt.TensorIOMode.OUTPUT)))
273+
# Case 3 inputs: ['cam0_depth', 'cam0_rgb', 'cam1_depth', 'cam1_rgb', 'global_bias']
274+
# Case 3 outputs: ['p_feats', 's_feats']
275+
276+
trt_outs = run_engine(
277+
engine,
278+
{
279+
"cam0_rgb": cameras[0]["rgb"].contiguous(),
280+
"cam0_depth": cameras[0]["depth"].contiguous(),
281+
"cam1_rgb": cameras[1]["rgb"].contiguous(),
282+
"cam1_depth": cameras[1]["depth"].contiguous(),
283+
"global_bias": bias.contiguous(),
284+
},
285+
)
286+
with torch.no_grad():
287+
ref = camera_tower(cameras, bias)
288+
torch.testing.assert_close(trt_outs["p_feats"], ref["primary"], rtol=1e-2, atol=1e-2)
289+
torch.testing.assert_close(trt_outs["s_feats"], ref["secondary"], rtol=1e-2, atol=1e-2)
290+
print("Case 3 native TRT run matches PyTorch.")
291+
292+
293+
# %%
294+
# Case 4 — structural validation
295+
# -------------------------------
296+
#
297+
# If the shape of any of the binding-name kwargs doesn't match the
298+
# exported program's spec, the converter raises
299+
# ``BindingNameMismatchError`` before any TensorRT network construction.
300+
# The error message shows the expected structure plus a leaf-position
301+
# listing — you can read the correct shape off the error and re-run.
302+
303+
try:
304+
torch_tensorrt.dynamo.convert_exported_program_to_serialized_trt_engine(
305+
exported,
306+
arg_inputs=(x,),
307+
output_binding_names=("only_one",), # wrong arity for the 2-tuple return
308+
require_full_compilation=True,
309+
min_block_size=1,
310+
use_python_runtime=False,
311+
immutable_weights=True,
312+
)
313+
except BindingNameMismatchError as err:
314+
print("Caught BindingNameMismatchError as expected.")
315+
print(str(err).splitlines()[0])
316+
317+
318+
# %%
319+
# Notes
320+
# -----
321+
#
322+
# * The binding-name kwargs are *parallel* to the input kwargs they refer
323+
# to: ``arg_input_binding_names`` matches ``arg_inputs``,
324+
# ``kwarg_input_binding_names`` matches ``kwarg_inputs``. Skip either
325+
# one if the corresponding input slot is empty.
326+
# * Duplicate names within any individual list, or across inputs and
327+
# outputs, are rejected at the API boundary — TensorRT requires
328+
# binding names to be globally unique.
329+
# * This API is **only** available on
330+
# ``convert_exported_program_to_serialized_trt_engine``. ``compile()``
331+
# and ``dynamo.compile()`` produce ``TorchTensorRTModule`` artifacts
332+
# whose runtime depends on the default naming policy, so they
333+
# intentionally don't expose this knob.

0 commit comments

Comments
 (0)