Skip to content

Commit bf54e2f

Browse files
shoumikhinfacebook-github-bot
authored andcommitted
Make CUDA/AOTI partitioner composable after another delegate (pytorch#20077)
Summary: `AotiPartitioner.partition` tagged every `call_function` node, including `executorch_call_delegate` calls already lowered by an earlier partitioner. So when `CudaPartitioner` runs as a second partitioner — e.g. after a TensorRT partition in a stacked `.pte` where TensorRT lowers the ops it can and the CUDA backend handles the rest — it tried to re-delegate the foreign delegate node, producing a malformed nested delegate. This is the blocker to composing the two backends in one `.pte`. Tag only the non-lowered nodes, reusing the existing `get_non_lowered_nodes` helper (which already excludes `executorch_call_delegate` calls and their output getitems), so the partitioner claims just the remaining ops and composes cleanly after another backend. In the single-partitioner case there are no delegate nodes, so `get_non_lowered_nodes` returns every `call_function` and behavior is unchanged. Mirrored in fbcode and xplat. Differential Revision: D107690797
1 parent 0d904b6 commit bf54e2f

2 files changed

Lines changed: 56 additions & 1 deletion

File tree

backends/aoti/aoti_partitioner.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,11 @@
1414
Partitioner,
1515
PartitionResult,
1616
)
17-
from executorch.exir.backend.utils import tag_constant_data, tag_mutated_buffer
17+
from executorch.exir.backend.utils import (
18+
get_non_lowered_nodes,
19+
tag_constant_data,
20+
tag_mutated_buffer,
21+
)
1822
from torch._export.utils import is_buffer, is_lifted_tensor_constant, is_param
1923
from torch.export.exported_program import ExportedProgram
2024

@@ -60,8 +64,17 @@ def is_control_flow(node: torch.fx.Node) -> bool:
6064
torch.ops.higher_order.while_loop,
6165
]
6266

67+
# Nodes already lowered by an earlier partitioner (e.g. a preceding
68+
# TensorRT partition) appear as executorch_call_delegate calls and their
69+
# output getitems; re-delegating them would nest a foreign delegate. Tag
70+
# only the remaining non-lowered ops so this partitioner composes after
71+
# others.
72+
non_lowered_nodes = set(get_non_lowered_nodes(exported_program.graph))
73+
6374
for node in exported_program.graph.nodes:
6475
if node.op == "call_function":
76+
if node not in non_lowered_nodes:
77+
continue
6578
node.meta["delegation_tag"] = tag
6679
# Tag get_attr nodes that are used by control flow operations
6780
elif node.op == "get_attr":

backends/cuda/tests/test_cuda_partitioner.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@
44
# This source code is licensed under the BSD-style license found in the
55
# LICENSE file in the root directory of this source tree.
66

7+
import operator
78
import unittest
89
from typing import Tuple
910

1011
import torch
1112
from executorch.backends.cuda.cuda_partitioner import CudaPartitioner
1213
from executorch.exir.backend.partitioner import PartitionResult
14+
from executorch.exir.delegate import executorch_call_delegate
1315
from torch.export import export
1416

1517

@@ -222,3 +224,43 @@ def forward(self, x: torch.Tensor) -> torch.Tensor:
222224
expected_tag,
223225
f"Constant placeholder {node.name} has tag '{actual_tag}' but expected '{expected_tag}'",
224226
)
227+
228+
def test_does_not_retag_already_lowered_delegate(self) -> None:
229+
"""
230+
A node already lowered by a previous partitioner appears as an
231+
executorch_call_delegate call plus its output getitem. The CUDA
232+
partitioner must not re-tag those, so it can run after another backend
233+
(e.g. TensorRT) and only claim the remaining ops.
234+
"""
235+
236+
class AddModule(torch.nn.Module):
237+
def forward(self, x: torch.Tensor) -> torch.Tensor:
238+
return x + x
239+
240+
exported_program = export(AddModule(), (torch.randn(3, 4),), strict=True)
241+
graph_module = exported_program.graph_module
242+
graph = graph_module.graph
243+
244+
placeholder = next(n for n in graph.nodes if n.op == "placeholder")
245+
aten_node = next(
246+
n
247+
for n in graph.nodes
248+
if n.op == "call_function" and n.target != operator.getitem
249+
)
250+
251+
# Splice in a fake, already-lowered delegate (call + output getitem), as a
252+
# preceding partitioner (e.g. TensorRT) would have produced.
253+
graph_module.lowered_module_0 = torch.nn.Module()
254+
with graph.inserting_before(aten_node):
255+
lowered = graph.get_attr("lowered_module_0")
256+
delegate = graph.call_function(
257+
executorch_call_delegate, (lowered, placeholder)
258+
)
259+
delegate_output = graph.call_function(operator.getitem, (delegate, 0))
260+
graph.lint()
261+
262+
CudaPartitioner([]).partition(exported_program)
263+
264+
self.assertNotIn("delegation_tag", delegate.meta)
265+
self.assertNotIn("delegation_tag", delegate_output.meta)
266+
self.assertIn("delegation_tag", aten_node.meta)

0 commit comments

Comments
 (0)