Skip to content

Commit 440a430

Browse files
authored
executorch: derive the TensorRT delegate target_device from the engine's real device index (#4329)
1 parent fb25363 commit 440a430

2 files changed

Lines changed: 180 additions & 11 deletions

File tree

py/torch_tensorrt/executorch/partitioner.py

Lines changed: 62 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# ExecuTorch partitioner: partition by execute_engine nodes.
22

3+
import logging
34
from typing import Callable, Dict, List, Optional, Tuple
45

56
import torch
@@ -12,7 +13,12 @@
1213
from executorch.exir.backend.utils import tag_constant_data
1314
from torch.export import ExportedProgram
1415
from torch.fx.passes.infra.partitioner import CapabilityBasedPartitioner
15-
from torch_tensorrt.executorch.backend import TensorRTBackend
16+
from torch_tensorrt.dynamo.runtime._TorchTensorRTModule import DEVICE_IDX
17+
from torch_tensorrt.executorch.backend import (
18+
TensorRTBackend,
19+
_get_engine_info_from_edge_program,
20+
_parse_device_id,
21+
)
1622
from torch_tensorrt.executorch.operator_support import TensorRTOperatorSupport
1723

1824
# Key recognized by ExecuTorch's PropagateDevicePass that tags delegate I/O
@@ -29,6 +35,8 @@
2935
except ImportError:
3036
_TARGET_DEVICE_COMPILE_SPEC_KEY = "target_device"
3137

38+
logger = logging.getLogger(__name__)
39+
3240

3341
class TensorRTPartitioner(Partitioner): # type: ignore[misc]
3442
"""Partitions the graph for TensorRT delegation.
@@ -42,6 +50,11 @@ class TensorRTPartitioner(Partitioner): # type: ignore[misc]
4250
Callers targeting a non-default GPU should pre-populate
4351
``compile_specs`` with the desired ``CompileSpec("target_device",
4452
b"cuda:<index>")`` to override the default.
53+
54+
Note: ``target_device`` is AOT metadata only -- it drives ExecuTorch's
55+
PropagateDevicePass tagging at export time. At runtime the C++ backend
56+
selects the GPU from the device baked into the serialized engine blob,
57+
not from this value.
4558
"""
4659

4760
def __init__(
@@ -50,21 +63,45 @@ def __init__(
5063
) -> None:
5164
super().__init__()
5265
self.compile_specs = list(compile_specs) if compile_specs else []
53-
# Mirror CudaPartitioner: emit a target_device CompileSpec so that
54-
# ExecuTorch's PropagateDevicePass tags delegate I/O TensorSpecs with
55-
# the correct device, which is then serialized into the .pte's
56-
# extra_tensor_info.device_type field.
57-
if not any(
66+
# Mirror CudaPartitioner: a target_device CompileSpec drives ExecuTorch's
67+
# PropagateDevicePass, which tags delegate I/O TensorSpecs with the device
68+
# and serializes it into the .pte's extra_tensor_info. When the caller pins
69+
# it we use that verbatim; otherwise it is derived per export from the
70+
# engine's real device in partition() (engine nodes are not available here)
71+
# so a cuda:N engine is not mislabeled cuda:0.
72+
self._has_explicit_target_device = any(
5873
s.key == _TARGET_DEVICE_COMPILE_SPEC_KEY for s in self.compile_specs
59-
):
60-
self.compile_specs.append(
61-
CompileSpec(_TARGET_DEVICE_COMPILE_SPEC_KEY, b"cuda:0")
62-
)
74+
)
6375
self.delegation_spec = DelegationSpec(
6476
backend_id=TensorRTBackend.__name__,
6577
compile_specs=self.compile_specs,
6678
)
6779

80+
def _resolve_target_device(self, exported_program: ExportedProgram) -> bytes:
81+
"""Best-effort ``target_device`` for the delegate-boundary TensorSpecs.
82+
83+
Reuses the backend's own engine-info extraction so the device index
84+
cannot drift from the runtime blob. Any extraction failure -- no single
85+
engine node (zero or multiple TRT partitions) or an unreadable index --
86+
falls back to ``cuda:0``; per-partition multi-GPU labeling is left to a
87+
follow-up.
88+
"""
89+
try:
90+
engine_info = _get_engine_info_from_edge_program(exported_program)
91+
return f"cuda:{_parse_device_id(engine_info[DEVICE_IDX])}".encode()
92+
except Exception as e:
93+
# Broad by design: any extraction failure must fall back, not abort
94+
# the export. Warn so a non-default GPU silently labeled cuda:0 stays
95+
# diagnosable.
96+
logger.warning(
97+
"Could not derive target_device from the TensorRT engine (%s); "
98+
"falling back to cuda:0. A non-default GPU engine may be "
99+
'mislabeled -- pin it via CompileSpec("target_device", '
100+
'b"cuda:<index>").',
101+
e,
102+
)
103+
return b"cuda:0"
104+
68105
def partition(self, exported_program: ExportedProgram) -> PartitionResult:
69106
capability_partitioner = CapabilityBasedPartitioner(
70107
exported_program.graph_module,
@@ -73,12 +110,26 @@ def partition(self, exported_program: ExportedProgram) -> PartitionResult:
73110
)
74111
partition_list = capability_partitioner.propose_partitions()
75112

113+
if self._has_explicit_target_device:
114+
delegation_spec = self.delegation_spec
115+
else:
116+
delegation_spec = DelegationSpec(
117+
backend_id=TensorRTBackend.__name__,
118+
compile_specs=self.compile_specs
119+
+ [
120+
CompileSpec(
121+
_TARGET_DEVICE_COMPILE_SPEC_KEY,
122+
self._resolve_target_device(exported_program),
123+
)
124+
],
125+
)
126+
76127
partition_tags: Dict[str, DelegationSpec] = {}
77128
for partition in partition_list:
78129
tag = f"tensorrt_{partition.id}"
79130
for node in partition.nodes:
80131
node.meta["delegation_tag"] = tag
81-
partition_tags[tag] = self.delegation_spec
132+
partition_tags[tag] = delegation_spec
82133

83134
tag_constant_data(exported_program)
84135

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
from types import SimpleNamespace
2+
3+
import pytest
4+
5+
executorch = pytest.importorskip("executorch.exir")
6+
7+
import torch # noqa: E402
8+
from executorch.exir.backend.compile_spec_schema import CompileSpec # noqa: E402
9+
from torch_tensorrt.dynamo.runtime._TorchTensorRTModule import ( # noqa: E402
10+
DEVICE_IDX,
11+
ENGINE_IDX,
12+
SERIALIZATION_LEN,
13+
)
14+
from torch_tensorrt.executorch.partitioner import ( # noqa: E402
15+
_TARGET_DEVICE_COMPILE_SPEC_KEY,
16+
TensorRTPartitioner,
17+
)
18+
19+
20+
# A realistic single-engine edge program so partition() runs the *real*
21+
# _get_engine_info_from_edge_program / _parse_device_id path. That is what
22+
# guards "an engine node is present and its device is extractable at partition()
23+
# time" -- a monkeypatched extractor would not. Mirrors the mocked edge programs
24+
# in tests/py/dynamo/executorch/test_backend.py.
25+
class _SchemaTarget:
26+
def __init__(self, name):
27+
self._schema = SimpleNamespace(name=name)
28+
29+
30+
def _engine_node(device_id):
31+
engine_info = [""] * SERIALIZATION_LEN
32+
engine_info[ENGINE_IDX] = torch.frombuffer(bytearray(b"engine"), dtype=torch.uint8)
33+
engine_info[DEVICE_IDX] = device_id
34+
return SimpleNamespace(
35+
op="call_function",
36+
target=_SchemaTarget("tensorrt::no_op_placeholder_for_execute_engine"),
37+
args=(["x"], *engine_info),
38+
name="trt_node",
39+
)
40+
41+
42+
def _edge_program(*nodes):
43+
return SimpleNamespace(
44+
graph_module=SimpleNamespace(graph=SimpleNamespace(nodes=list(nodes))),
45+
constants={},
46+
)
47+
48+
49+
class _FakeCapabilityPartitioner:
50+
def __init__(self, *args, **kwargs):
51+
pass
52+
53+
def propose_partitions(self):
54+
return [SimpleNamespace(id=1, nodes=[SimpleNamespace(meta={})])]
55+
56+
57+
@pytest.fixture(autouse=True)
58+
def _stub_partition_internals(monkeypatch):
59+
# Both need a real fx GraphModule, so stub them out -- the engine-info
60+
# extraction under test still runs for real against the mocked node.
61+
monkeypatch.setattr(
62+
"torch_tensorrt.executorch.partitioner.CapabilityBasedPartitioner",
63+
_FakeCapabilityPartitioner,
64+
)
65+
monkeypatch.setattr(
66+
"torch_tensorrt.executorch.partitioner.tag_constant_data",
67+
lambda exported_program: None,
68+
)
69+
70+
71+
def _target_device(result):
72+
spec = result.partition_tags["tensorrt_1"]
73+
for cs in spec.compile_specs:
74+
if cs.key == _TARGET_DEVICE_COMPILE_SPEC_KEY:
75+
return cs.value
76+
return None
77+
78+
79+
@pytest.mark.unit
80+
def test_target_device_derived_for_default_gpu():
81+
result = TensorRTPartitioner().partition(_edge_program(_engine_node("0")))
82+
assert _target_device(result) == b"cuda:0"
83+
84+
85+
@pytest.mark.unit
86+
def test_target_device_derived_for_nonzero_gpu():
87+
# The bug this fixes: a cuda:1 engine must not be mislabeled cuda:0.
88+
result = TensorRTPartitioner().partition(_edge_program(_engine_node("1")))
89+
assert _target_device(result) == b"cuda:1"
90+
91+
92+
@pytest.mark.unit
93+
def test_target_device_falls_back_to_cuda0_on_multiple_engines():
94+
# >1 engine node -> real extraction raises -> contract fallback to cuda:0.
95+
result = TensorRTPartitioner().partition(
96+
_edge_program(_engine_node("1"), _engine_node("2"))
97+
)
98+
assert _target_device(result) == b"cuda:0"
99+
100+
101+
@pytest.mark.unit
102+
def test_target_device_falls_back_to_cuda0_on_malformed_graph():
103+
# An unexpected graph shape makes the real extraction raise; the broadened
104+
# except must still fall back to cuda:0 rather than abort the export.
105+
bad_node = SimpleNamespace(op="call_function", target=SimpleNamespace(), name="x")
106+
result = TensorRTPartitioner().partition(_edge_program(bad_node))
107+
assert _target_device(result) == b"cuda:0"
108+
109+
110+
@pytest.mark.unit
111+
def test_explicit_target_device_used_verbatim():
112+
# Engine reports cuda:0, but the caller pinned cuda:3 -> the pin wins and
113+
# extraction is skipped entirely.
114+
partitioner = TensorRTPartitioner(
115+
compile_specs=[CompileSpec(_TARGET_DEVICE_COMPILE_SPEC_KEY, b"cuda:3")]
116+
)
117+
result = partitioner.partition(_edge_program(_engine_node("0")))
118+
assert _target_device(result) == b"cuda:3"

0 commit comments

Comments
 (0)