Skip to content

Commit 8daa7f8

Browse files
authored
Elide redundant same-device copies in PropagateDevicePass (#20914)
Differential Revision: D111992997 Pull Request resolved: #20914
1 parent e500fee commit 8daa7f8

2 files changed

Lines changed: 391 additions & 5 deletions

File tree

exir/passes/propagate_device_pass.py

Lines changed: 70 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,7 @@
1313

1414
# Import to register the et_copy ops so torch.ops.et_copy is available.
1515
import executorch.exir.passes._device_copy_ops_registry # noqa: F401
16-
1716
import executorch.exir.schema as schema
18-
1917
import torch
2018
from executorch.exir.delegate import executorch_call_delegate
2119
from executorch.exir.lowered_backend_module import LoweredBackendModule
@@ -302,8 +300,67 @@ def _insert_d2h_for_getitem(
302300
)
303301
return True
304302

303+
def _elide_redundant_same_device_copies(
304+
self,
305+
graph_module: torch.fx.GraphModule,
306+
) -> None:
307+
"""Remove ``_h2d_copy(_d2h_copy(x))`` round-trips that never leave a device.
308+
309+
H2D and D2H copies are inserted per delegate boundary independently (an
310+
H2D before every device delegate input, a D2H after every device delegate
311+
output), so two consecutive delegates on the *same* device leave a
312+
redundant ``device -> host -> device`` bounce: the first delegate's output
313+
is copied to host by a ``_d2h_copy`` and immediately copied back to the
314+
same device by a ``_h2d_copy`` feeding the second delegate.
315+
316+
This walks each ``_h2d_copy`` whose input is a ``_d2h_copy`` and, when the
317+
tensor before the D2H already lives on the device the H2D copies back to,
318+
rewires the consumer straight to that device tensor and drops both copies.
319+
The device comparison is the correctness guard: a genuine cross-device
320+
move (e.g. ``cuda:0 -> host -> cuda:1``) has differing devices and is left
321+
intact. A D2H feeding other (host) consumers keeps those users and is
322+
removed only once it becomes dead.
323+
"""
324+
h2d = torch.ops.et_copy._h2d_copy.default
325+
d2h = torch.ops.et_copy._d2h_copy.default
326+
changed = False
327+
for node in list(graph_module.graph.nodes):
328+
if node.op != "call_function" or node.target != h2d:
329+
continue
330+
src = node.args[0]
331+
if not (
332+
isinstance(src, torch.fx.Node)
333+
and src.op == "call_function"
334+
and src.target == d2h
335+
):
336+
continue
337+
338+
x = src.args[0] # the tensor before the d2h (the device delegate output)
339+
x_spec = x.meta.get("spec") if isinstance(x, torch.fx.Node) else None
340+
h2d_spec = node.meta.get("spec")
341+
if not (
342+
isinstance(x_spec, TensorSpec) and isinstance(h2d_spec, TensorSpec)
343+
):
344+
continue
345+
346+
# Only elide a proven same-device round-trip.
347+
if (
348+
x_spec.device != h2d_spec.device
349+
or x_spec.device_index != h2d_spec.device_index
350+
):
351+
continue
352+
353+
node.replace_all_uses_with(x)
354+
graph_module.graph.erase_node(node)
355+
changed = True
356+
357+
if changed:
358+
# Drop the d2h copies that are now dead (kept if they still feed a
359+
# host consumer).
360+
graph_module.graph.eliminate_dead_code()
361+
305362
def call(self, graph_module: torch.fx.GraphModule) -> PassResult: # noqa: C901
306-
# Two-pass approach:
363+
# Three-pass approach:
307364
# Pass 1 – For each delegate with a target_device CompileSpec, insert
308365
# H2D copy nodes before delegate inputs and tag the delegate
309366
# output specs with the target device. Delegates without a
@@ -312,6 +369,8 @@ def call(self, graph_module: torch.fx.GraphModule) -> PassResult: # noqa: C901
312369
# (tracked in device_delegates), propagate the device onto the
313370
# getitem spec and insert a D2H copy after it so downstream
314371
# non-delegated ops receive CPU tensors.
372+
# Pass 3 – Elide the redundant device->host->device round-trips left
373+
# between consecutive same-device delegates by passes 1 and 2.
315374
changed = False
316375
device_delegates: set[torch.fx.Node] = set()
317376

@@ -387,5 +446,13 @@ def call(self, graph_module: torch.fx.GraphModule) -> PassResult: # noqa: C901
387446
)
388447
changed = True
389448

449+
# Third pass: elide the redundant device->host->device round-trips left
450+
# between consecutive same-device delegates by the per-boundary H2D/D2H
451+
# insertion above. Only meaningful when passes 1/2 modified the graph
452+
# (there was a device delegate); when nothing changed there are no copies
453+
# to elide, so skip the scan entirely.
454+
if changed:
455+
self._elide_redundant_same_device_copies(graph_module)
456+
390457
graph_module.recompile()
391458
return PassResult(graph_module, changed)

0 commit comments

Comments
 (0)