Skip to content

Commit ea00fc9

Browse files
committed
Let a method input feed several delegates on one device without a copy
With skip_h2d_for_method_inputs, a method input had to have exactly one user, because its spec is changed in place to the delegate's device. A program that calls one lowered module more than once, or passes one input to two delegates, could not keep its inputs on the device. Changing the spec is safe when every user is a delegate on that same device, so that case is now allowed. An input that a host op or a delegate on another device also reads is still refused.
1 parent f100efd commit ea00fc9

2 files changed

Lines changed: 80 additions & 9 deletions

File tree

‎exir/passes/propagate_device_pass.py‎

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,17 @@ def _is_placeholder(self, node: torch.fx.Node) -> bool:
195195
"""Check if a node is a graph-level input (placeholder)."""
196196
return node.op == "placeholder"
197197

198+
def _delegate_device(
199+
self, graph_module: torch.fx.GraphModule, node: torch.fx.Node
200+
) -> Optional[tuple[schema.DeviceType, int]]:
201+
"""The target device of a delegate call node, or None for any other node."""
202+
if node.op != "call_function" or node.target != executorch_call_delegate:
203+
return None
204+
lowered_module = _get_lowered_module(graph_module, node)
205+
if lowered_module is None:
206+
return None
207+
return _get_target_device_from_compile_specs(lowered_module)
208+
198209
def _feeds_directly_to_output(self, node: torch.fx.Node) -> bool:
199210
"""Check if all users of a node are output nodes."""
200211
return all(user.op == "output" for user in node.users)
@@ -223,17 +234,22 @@ def _insert_h2d_copies(
223234
and self._is_placeholder(arg)
224235
and not arg_spec.const
225236
):
226-
# TODO(gasoonjia): support skip_h2d_for_method_inputs for
227-
# multiple-user placeholder inputs.
228-
if len(arg.users) != 1:
237+
# The spec is shared by every reader, so all of them must be
238+
# delegates on this same device.
239+
if not all(
240+
self._delegate_device(graph_module, user)
241+
== (target_device_type, device_index)
242+
for user in arg.users
243+
):
229244
raise RuntimeError(
230-
f"skip_h2d_for_method_inputs=True requires placeholder "
231-
f"'{arg.name}' to have exactly one user, but it has "
232-
f"{len(arg.users)} users. The placeholder is shared by "
233-
f"multiple consumers, so its TensorSpec cannot be safely "
234-
f"mutated in-place to the delegate's device. Either disable "
245+
f"skip_h2d_for_method_inputs=True requires every user of "
246+
f"placeholder '{arg.name}' to be a delegate on the same "
247+
f"device, but it has {len(arg.users)} users. The placeholder "
248+
f"is shared by consumers that need it elsewhere, so its "
249+
f"TensorSpec cannot be safely mutated in-place to the "
250+
f"delegate's device. Either disable "
235251
f"skip_h2d_for_method_inputs, or ensure the placeholder is "
236-
f"used exclusively by this delegate."
252+
f"used only by delegates on one device."
237253
)
238254
_set_device_on_spec(arg_spec, target_device_type, device_index)
239255
changed = True

‎exir/tests/test_propagate_device_pass.py‎

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1302,6 +1302,61 @@ def forward(self, a):
13021302
self.assertTrue(all(not spec.const for spec in copies))
13031303
self.assertTrue(all(spec.mem_id is not None for spec in copies))
13041304

1305+
def test_skip_h2d_for_input_shared_by_device_delegates(self):
1306+
"""With skip_h2d_for_method_inputs=True, a method input read by two
1307+
delegates on the same device is placed on that device once, with no
1308+
_h2d_copy, as when a program calls one lowered module several times."""
1309+
1310+
class Model(torch.nn.Module):
1311+
def forward(self, a, b):
1312+
c = torch.sin(torch.add(a, b))
1313+
return torch.add(a, c)
1314+
1315+
model = Model()
1316+
inputs = (torch.randn(2, 2), torch.randn(2, 2))
1317+
et_config = ExecutorchBackendConfig(
1318+
emit_stacktrace=False,
1319+
propagate_device_config=PropagateDeviceConfig(
1320+
skip_h2d_for_method_inputs=True
1321+
),
1322+
enable_non_cpu_memory_planning=True,
1323+
)
1324+
for pipeline, program, gm in self._get_executorch_program(
1325+
model, inputs, DeviceAwarePartitioner("cuda:0"), et_config
1326+
):
1327+
with self.subTest(pipeline=pipeline):
1328+
nodes = _collect_device_copy_nodes(gm)
1329+
self.assertEqual(len(nodes.delegate_nodes), 2)
1330+
# Only sin's output, computed on the host, is copied.
1331+
self.assertEqual(len(nodes.h2d_nodes), 1)
1332+
cuda_ph, cpu_ph = self._collect_placeholders_by_device(gm)
1333+
self.assertEqual(sorted(n.name for n in cuda_ph), ["a", "b"])
1334+
self._assert_nodes_buffer_device(
1335+
cuda_ph, program, DeviceType.CUDA, pipeline, "Placeholder"
1336+
)
1337+
1338+
def test_skip_h2d_refuses_input_shared_with_host_op(self):
1339+
"""A method input that a host op also reads cannot move to the device."""
1340+
1341+
class Model(torch.nn.Module):
1342+
def forward(self, a, b):
1343+
return torch.add(a, b), torch.sin(a)
1344+
1345+
et_config = ExecutorchBackendConfig(
1346+
emit_stacktrace=False,
1347+
propagate_device_config=PropagateDeviceConfig(
1348+
skip_h2d_for_method_inputs=True
1349+
),
1350+
enable_non_cpu_memory_planning=True,
1351+
)
1352+
with self.assertRaisesRegex(Exception, "delegate on the same"):
1353+
self._get_executorch_program(
1354+
Model(),
1355+
(torch.randn(2, 2), torch.randn(2, 2)),
1356+
DeviceAwarePartitioner("cuda:0"),
1357+
et_config,
1358+
)
1359+
13051360
def test_tensorspec_repr_includes_device(self):
13061361
spec = TensorSpec(dtype=torch.float32, shape=torch.Size([2, 3]))
13071362
repr_str = repr(spec)

0 commit comments

Comments
 (0)