Skip to content

Commit 4c787ab

Browse files
committed
feat: custom binding names for out of runtime deployment
1 parent 258a7e2 commit 4c787ab

8 files changed

Lines changed: 2851 additions & 1582 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: 334 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,334 @@
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+
import torch
56+
import torch_tensorrt
57+
from torch_tensorrt.dynamo._compiler import BindingNameMismatchError
58+
59+
# %%
60+
import tensorrt as trt
61+
62+
DEVICE = torch.device("cuda", 0)
63+
64+
65+
# %%
66+
# Helpers
67+
# --------
68+
#
69+
# A pair of small helpers: one reads binding names off a deserialized
70+
# engine, the other actually runs the engine via the native TRT Python
71+
# API. The "execute via native TRT" path is what production deployments
72+
# use — the whole point of this API is that the binding names you supply
73+
# are the names you'll bind by at execution time, not just metadata in
74+
# the engine file.
75+
76+
77+
def deserialize(engine_bytes: bytes) -> trt.ICudaEngine:
78+
runtime = trt.Runtime(trt.Logger(trt.Logger.WARNING))
79+
return runtime.deserialize_cuda_engine(engine_bytes)
80+
81+
82+
def binding_names(engine: trt.ICudaEngine, mode: trt.TensorIOMode) -> list[str]:
83+
return [
84+
engine.get_tensor_name(i)
85+
for i in range(engine.num_io_tensors)
86+
if engine.get_tensor_mode(engine.get_tensor_name(i)) == mode
87+
]
88+
89+
90+
_TRT_TO_TORCH_DTYPE = {
91+
trt.DataType.FLOAT: torch.float32,
92+
trt.DataType.HALF: torch.float16,
93+
trt.DataType.INT32: torch.int32,
94+
trt.DataType.INT64: torch.int64,
95+
trt.DataType.BOOL: torch.bool,
96+
trt.DataType.BF16: torch.bfloat16,
97+
}
98+
99+
100+
def run_engine(engine: trt.ICudaEngine, named_inputs: dict) -> dict:
101+
"""Execute an engine via the native TRT Python API.
102+
103+
``named_inputs`` is a {binding_name: contiguous CUDA tensor} dict.
104+
Returns {binding_name: output tensor}. Demonstrates that the
105+
user-supplied binding names are what production C++/Python TRT
106+
runtime code will bind by.
107+
"""
108+
context = engine.create_execution_context()
109+
for name, tensor in named_inputs.items():
110+
context.set_input_shape(name, tuple(tensor.shape))
111+
context.set_tensor_address(name, tensor.data_ptr())
112+
113+
outputs = {}
114+
for i in range(engine.num_io_tensors):
115+
name = engine.get_tensor_name(i)
116+
if engine.get_tensor_mode(name) != trt.TensorIOMode.OUTPUT:
117+
continue
118+
shape = tuple(context.get_tensor_shape(name))
119+
dtype = _TRT_TO_TORCH_DTYPE[engine.get_tensor_dtype(name)]
120+
out = torch.empty(shape, dtype=dtype, device=DEVICE)
121+
context.set_tensor_address(name, out.data_ptr())
122+
outputs[name] = out
123+
124+
stream = torch.cuda.Stream(device=DEVICE)
125+
with torch.cuda.stream(stream):
126+
context.execute_async_v3(stream.cuda_stream)
127+
stream.synchronize()
128+
return outputs
129+
130+
131+
# %%
132+
# Case 1 — positional args, tuple-shaped return
133+
# ----------------------------------------------
134+
#
135+
# Start with the most common shape: ``forward(x)`` returning a 2-tuple.
136+
# ``arg_input_binding_names`` mirrors ``arg_inputs`` (a 1-tuple here);
137+
# ``output_binding_names`` mirrors the return tuple.
138+
139+
140+
class TwoHeads(torch.nn.Module):
141+
def forward(self, x: torch.Tensor):
142+
return torch.relu(x), torch.tanh(x)
143+
144+
145+
two_heads = TwoHeads().eval().cuda().half()
146+
x = torch.randn(2, 3, device=DEVICE, dtype=torch.float16)
147+
exported = torch.export.export(two_heads, (x,))
148+
149+
engine_bytes = torch_tensorrt.dynamo.convert_exported_program_to_serialized_trt_engine(
150+
exported,
151+
arg_inputs=(x,),
152+
arg_input_binding_names=("input_image",),
153+
output_binding_names=("relu_out", "tanh_out"),
154+
require_full_compilation=True,
155+
min_block_size=1,
156+
use_python_runtime=False,
157+
immutable_weights=True,
158+
)
159+
engine = deserialize(engine_bytes)
160+
print("Case 1 inputs:", binding_names(engine, trt.TensorIOMode.INPUT))
161+
print("Case 1 outputs:", binding_names(engine, trt.TensorIOMode.OUTPUT))
162+
# Case 1 inputs: ['input_image']
163+
# Case 1 outputs: ['relu_out', 'tanh_out']
164+
165+
# Run the engine through the native TRT API using the names we requested.
166+
trt_outs = run_engine(engine, {"input_image": x.contiguous()})
167+
with torch.no_grad():
168+
ref_relu, ref_tanh = two_heads(x)
169+
torch.testing.assert_close(trt_outs["relu_out"], ref_relu, rtol=1e-2, atol=1e-2)
170+
torch.testing.assert_close(trt_outs["tanh_out"], ref_tanh, rtol=1e-2, atol=1e-2)
171+
print("Case 1 native TRT run matches PyTorch.")
172+
173+
174+
# %%
175+
# Case 2 — keyword-only inputs
176+
# ------------------------------
177+
#
178+
# When the model takes keyword arguments, you pass ``kwarg_inputs`` and
179+
# match its shape with ``kwarg_input_binding_names``. Note we leave
180+
# ``arg_input_binding_names`` unset because ``arg_inputs`` is empty.
181+
182+
183+
class KwargOnly(torch.nn.Module):
184+
def forward(self, image: torch.Tensor, positions: torch.Tensor):
185+
return image + positions
186+
187+
188+
kwarg_only = KwargOnly().eval().cuda().half()
189+
image = torch.randn(2, 3, device=DEVICE, dtype=torch.float16)
190+
positions = torch.randn(2, 3, device=DEVICE, dtype=torch.float16)
191+
kw_exported = torch.export.export(
192+
kwarg_only,
193+
args=(),
194+
kwargs={"image": image, "positions": positions},
195+
)
196+
197+
engine_bytes = torch_tensorrt.dynamo.convert_exported_program_to_serialized_trt_engine(
198+
kw_exported,
199+
arg_inputs=(),
200+
kwarg_inputs={"image": image, "positions": positions},
201+
kwarg_input_binding_names={"image": "rgb_in", "positions": "pos_in"},
202+
output_binding_names="combined",
203+
require_full_compilation=True,
204+
min_block_size=1,
205+
use_python_runtime=False,
206+
immutable_weights=True,
207+
)
208+
engine = deserialize(engine_bytes)
209+
print("Case 2 inputs:", sorted(binding_names(engine, trt.TensorIOMode.INPUT)))
210+
print("Case 2 outputs:", binding_names(engine, trt.TensorIOMode.OUTPUT))
211+
# Case 2 inputs: ['pos_in', 'rgb_in']
212+
# Case 2 outputs: ['combined']
213+
214+
trt_outs = run_engine(
215+
engine,
216+
{"rgb_in": image.contiguous(), "pos_in": positions.contiguous()},
217+
)
218+
with torch.no_grad():
219+
ref = kwarg_only(image=image, positions=positions)
220+
torch.testing.assert_close(trt_outs["combined"], ref, rtol=1e-2, atol=1e-2)
221+
print("Case 2 native TRT run matches PyTorch.")
222+
223+
224+
# %%
225+
# Case 3 — nested collections as inputs and outputs
226+
# --------------------------------------------------
227+
#
228+
# Inputs and outputs can be arbitrary nested collections of tensors —
229+
# tuples of dicts of tensors, lists of tuples, anything ``pytree`` can
230+
# flatten. The binding-name kwargs follow the same nesting. Here the
231+
# model takes a tuple of two cameras (each a dict of two tensors) and
232+
# returns a dict of feature stacks.
233+
234+
235+
class CameraTower(torch.nn.Module):
236+
def forward(self, cameras: tuple, bias: torch.Tensor):
237+
feats = []
238+
for cam in cameras:
239+
feats.append(cam["rgb"] + cam["depth"] + bias)
240+
return {"primary": feats[0], "secondary": feats[1]}
241+
242+
243+
def _cam():
244+
return {
245+
"rgb": torch.randn(2, 3, device=DEVICE, dtype=torch.float16),
246+
"depth": torch.randn(2, 3, device=DEVICE, dtype=torch.float16),
247+
}
248+
249+
250+
camera_tower = CameraTower().eval().cuda().half()
251+
cameras = (_cam(), _cam())
252+
bias = torch.randn(2, 3, device=DEVICE, dtype=torch.float16)
253+
nested_exported = torch.export.export(camera_tower, args=(cameras, bias))
254+
255+
engine_bytes = torch_tensorrt.dynamo.convert_exported_program_to_serialized_trt_engine(
256+
nested_exported,
257+
arg_inputs=(cameras, bias),
258+
arg_input_binding_names=(
259+
(
260+
{"rgb": "cam0_rgb", "depth": "cam0_depth"},
261+
{"rgb": "cam1_rgb", "depth": "cam1_depth"},
262+
),
263+
"global_bias",
264+
),
265+
output_binding_names={"primary": "p_feats", "secondary": "s_feats"},
266+
require_full_compilation=True,
267+
min_block_size=1,
268+
use_python_runtime=False,
269+
immutable_weights=True,
270+
)
271+
engine = deserialize(engine_bytes)
272+
print("Case 3 inputs:", sorted(binding_names(engine, trt.TensorIOMode.INPUT)))
273+
print("Case 3 outputs:", sorted(binding_names(engine, trt.TensorIOMode.OUTPUT)))
274+
# Case 3 inputs: ['cam0_depth', 'cam0_rgb', 'cam1_depth', 'cam1_rgb', 'global_bias']
275+
# Case 3 outputs: ['p_feats', 's_feats']
276+
277+
trt_outs = run_engine(
278+
engine,
279+
{
280+
"cam0_rgb": cameras[0]["rgb"].contiguous(),
281+
"cam0_depth": cameras[0]["depth"].contiguous(),
282+
"cam1_rgb": cameras[1]["rgb"].contiguous(),
283+
"cam1_depth": cameras[1]["depth"].contiguous(),
284+
"global_bias": bias.contiguous(),
285+
},
286+
)
287+
with torch.no_grad():
288+
ref = camera_tower(cameras, bias)
289+
torch.testing.assert_close(trt_outs["p_feats"], ref["primary"], rtol=1e-2, atol=1e-2)
290+
torch.testing.assert_close(trt_outs["s_feats"], ref["secondary"], rtol=1e-2, atol=1e-2)
291+
print("Case 3 native TRT run matches PyTorch.")
292+
293+
294+
# %%
295+
# Case 4 — structural validation
296+
# -------------------------------
297+
#
298+
# If the shape of any of the binding-name kwargs doesn't match the
299+
# exported program's spec, the converter raises
300+
# ``BindingNameMismatchError`` before any TensorRT network construction.
301+
# The error message shows the expected structure plus a leaf-position
302+
# listing — you can read the correct shape off the error and re-run.
303+
304+
try:
305+
torch_tensorrt.dynamo.convert_exported_program_to_serialized_trt_engine(
306+
exported,
307+
arg_inputs=(x,),
308+
output_binding_names=("only_one",), # wrong arity for the 2-tuple return
309+
require_full_compilation=True,
310+
min_block_size=1,
311+
use_python_runtime=False,
312+
immutable_weights=True,
313+
)
314+
except BindingNameMismatchError as err:
315+
print("Caught BindingNameMismatchError as expected.")
316+
print(str(err).splitlines()[0])
317+
318+
319+
# %%
320+
# Notes
321+
# -----
322+
#
323+
# * The binding-name kwargs are *parallel* to the input kwargs they refer
324+
# to: ``arg_input_binding_names`` matches ``arg_inputs``,
325+
# ``kwarg_input_binding_names`` matches ``kwarg_inputs``. Skip either
326+
# one if the corresponding input slot is empty.
327+
# * Duplicate names within any individual list, or across inputs and
328+
# outputs, are rejected at the API boundary — TensorRT requires
329+
# binding names to be globally unique.
330+
# * This API is **only** available on
331+
# ``convert_exported_program_to_serialized_trt_engine``. ``compile()``
332+
# and ``dynamo.compile()`` produce ``TorchTensorRTModule`` artifacts
333+
# whose runtime depends on the default naming policy, so they
334+
# intentionally don't expose this knob.

0 commit comments

Comments
 (0)