1313
1414# Import to register the et_copy ops so torch.ops.et_copy is available.
1515import executorch .exir .passes ._device_copy_ops_registry # noqa: F401
16-
1716import executorch .exir .schema as schema
18-
1917import torch
2018from executorch .exir .delegate import executorch_call_delegate
2119from 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