Skip to content

Fix device placement of constants, shared inputs and delegate outputs - #24088

Open
shoumikhin wants to merge 16 commits into
pytorch:mainfrom
shoumikhin:device-pass-constants
Open

shoumikhin wants to merge 16 commits into
pytorch:mainfrom
shoumikhin:device-pass-constants

Conversation

@shoumikhin

@shoumikhin shoumikhin commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

What is wrong today

When a delegate runs on a device (such as CUDA), PropagateDevicePass adds a host-to-device copy (_h2d_copy) before each delegate input and a device-to-host copy (_d2h_copy) after each output. skip_h2d_for_method_inputs and skip_d2h_for_method_outputs skip those copies for method inputs and outputs.

Six cases go wrong:

  1. A non-contiguous parameter, such as a transposed weight, is stored contiguous, but the delegate was lowered with its original strides.
  2. A host op that writes a delegate output in place, directly or through a view. The pass removes the copy to the host and back before the next delegate on that device, so that delegate reads the old value. When the op is an in-place op such as relu_, to_executorch also fails in memory planning.
  3. A lowered module that returns one tensor, called from another module. Its output gets no _d2h_copy, so a host op reads device memory.
  4. A returned output that something else also reads. With skip_d2h_for_method_outputs, it still comes back on the host.
  5. A constant that feeds a device delegate. to_executorch fails, or, with the input skip flag, the delegate gets a host pointer labeled as device memory.
  6. A method input read by several delegates on one device. The skip flag refuses it.

What this change does

Roughly one commit per item:

  • to_edge now makes a parameter that is neither contiguous nor channels-last contiguous before partitioning, as it already does for constants and buffers. This applies to every backend. With _skip_dim_order=True, only the parameter itself changes: ops computed from it keep the old strides in the graph, so a delegate that reads such an op's result can expect the wrong layout, as for buffers and constants today.
  • The copies between two delegates stay when a host op writes the tensor in place before the second delegate reads it. A write to a view counts too (for that case to_executorch needs ToOutVarPass(ignore_to_out_var_failure=True), because aten.as_strided has no out variant yet).
  • The result of such an in-place op shares the memory of the host copy, so memory planning accepts the program.
  • A single-tensor delegate output gets its _d2h_copy right after the call.
  • With skip_d2h_for_method_outputs, a returned output now stays on the device even when another op reads it. If a host op writes its host copy in place, a device copy of the written tensor is returned, a later delegate on that device that reads the same copy shares it, and a warning says that this copies on every call.
  • Such a constant always gets a device copy and a warning with its size, or a clear error if it is neither contiguous nor channels-last.
  • With the input skip flag, a method input may feed several delegates on one device. If anything else reads it, the error names that reader.
  • A tensor read by several delegates on one device is copied there once and kept until the last one runs, unless an op writes it in place in between.

What was tested

  • exir/tests/test_propagate_device_pass.py: all tests pass at every commit.
  • exir/tests/test_passes.py: the new parameter cases, one on the pass and one through to_edge with and without _skip_dim_order, fail without the fix. A small fix in the same pass makes a missing placeholder raise its intended error, with a test.
  • backends/cuda/tests/test_device_placement.py (new, needs a CUDA GPU): runs a transposed parameter and a buffer read by two delegates on real CUDA delegates and compares with eager. The unittest-cuda job in cuda.yml now also runs when the device pass, its config, or the dim order pass changes, so this test runs on such changes. An earlier version of this test ran on a CUDA GPU: it passed, and without the parameter fix to_executorch refused the transposed parameter. CI runs the current version.

Follow-up issues are being filed for keeping constants in device memory and for the smaller follow-ups from review.

Copilot AI balanced review requested due to automatic review settings October 22, 2026 19:09
@shoumikhin shoumikhin added the release notes: exir Changes to any dialects and passes on these dialects, such as memory planning label Oct 22, 2026
@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Oct 22, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@pytorch-bot

pytorch-bot Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

🔗 Helpful Links

🧪 See artifacts and rendered test results at hud.pytorch.org/pr/pytorch/executorch/24088

Note: Links to docs will display an error until the docs builds have been completed.

❌ 3 New Failures

As of commit 0ac8098 with merge base 185668b (image):

NEW FAILURES - The following jobs have failed:

This comment was automatically generated by Dr. CI and updates every 15 minutes.

@shoumikhin
shoumikhin force-pushed the device-pass-constants branch 2 times, most recently from 3aadad6 to 894b0fa Compare October 22, 2026 23:08
Copilot AI balanced review requested due to automatic review settings October 22, 2026 23:08

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@Gasoonjia Gasoonjia left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for find and fixing the issue! I left some thoughts and we can discuss.

new_spec = copy.copy(spec)
new_spec.init_mem_planning_fields()
# A copy's output is planned memory, even when it copies a constant.
new_spec.const = False

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i feel like it is a liitle bit dangerous if we overwrite the const attribute silently here. Maybe we can rename the function to highlight it, or create a new function. WDYT?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. I renamed the helper to _copy_output_spec. Its docstring now says the output is always planned memory, so it is never a constant, even when the tensor it copies is one. It is only used to build the spec of an _h2d_copy or _d2h_copy output, so no other spec gets its const flag changed.

if (
self.skip_h2d_for_method_inputs
and self._is_placeholder(arg)
and not arg_spec.const

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

im ok with this change to fix the correctness isssue but from api design perspective it doesn't quite in line with skip_h2d_for_method_inputs: constant var is also pert of method inputs (although implictly), and if user turn it on but still need to do h2d copy for constant it is not elegant.

My understanding is we may need to have devcie-resident constant materialization so that we can treat contant as regular input, in the device support scenerio.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that a constant is a kind of method input too. The problem is that today the runtime always points a constant at its data in host memory (in the program or in the external data file), whatever device its spec says. So if we skip the copy, the delegate gets a host pointer that is marked as device memory. Until the runtime can load constants into device memory, we need the copy to stay correct. I added a line to the comment on skip_h2d_for_method_inputs that says constants are still copied. Device-resident constants sound like a good follow-up, and then this flag could skip their copy too.

@shoumikhin
shoumikhin force-pushed the device-pass-constants branch from 894b0fa to ea00fc9 Compare October 23, 2026 02:25
Copilot AI balanced review requested due to automatic review settings October 23, 2026 02:25

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@shoumikhin shoumikhin changed the title Copy constants that feed a device delegate into device memory Fix device placement of constants and shared inputs that feed device delegates Oct 23, 2026
@shoumikhin
shoumikhin force-pushed the device-pass-constants branch from ea00fc9 to e12db30 Compare October 23, 2026 04:13
Copilot AI balanced review requested due to automatic review settings October 23, 2026 04:13

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@shoumikhin
shoumikhin force-pushed the device-pass-constants branch from e12db30 to 71dd63f Compare October 23, 2026 09:44
Copilot AI balanced review requested due to automatic review settings October 23, 2026 09:44

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@shoumikhin shoumikhin changed the title Fix device placement of constants and shared inputs that feed device delegates Fix device placement of constants, shared inputs and delegate outputs Oct 23, 2026
Copilot AI balanced review requested due to automatic review settings October 23, 2026 18:03
@shoumikhin
shoumikhin force-pushed the device-pass-constants branch from 71dd63f to e1ed5ce Compare October 23, 2026 18:03

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota.

@shoumikhin
shoumikhin force-pushed the device-pass-constants branch from e1ed5ce to 635d39e Compare October 23, 2026 19:15
Copilot AI balanced review requested due to automatic review settings October 23, 2026 19:15
@shoumikhin

Copy link
Copy Markdown
Contributor Author

Since the last push, I rebased on main and made these changes:

  • New commit: when a host op writes a delegate output in place, for example a mutable custom op left on the host, the copies to the next delegate on the same device now stay. Before, that delegate read the old value.
  • With skip_d2h_for_method_outputs, if a host op writes the host copy of a returned delegate output, the method now returns a device copy of the written tensor, so that output stays on the device.
  • Copy reuse skips a tensor that an op writes in place. The docs and comments now say that a reused copy stays in device memory until its last reader.
  • A constant that is copied on every call now logs a warning with its name and size.
  • The skip_h2d_for_method_inputs error names the write-back when the method changes the input, and the CUDA docs state the rules more precisely.
  • New and updated tests cover these changes, plus unplanned outputs with skip_d2h_for_method_outputs.

Tested: all device pass tests pass at every commit, and 12 programs with real CUDA delegates on an H100 match eager PyTorch.

@pytorch-bot
pytorch-bot Bot deployed to upload-benchmark-results October 24, 2026 03:28 Active
@Gasoonjia

Copy link
Copy Markdown
Contributor

sry can you give me a brief introduction about why we need to update the dim order parts for the device input changing?

@shoumikhin

Copy link
Copy Markdown
Contributor Author

@Gasoonjia It is needed by the constants change in this PR.

When a constant or a parameter feeds a CUDA delegate from outside the delegate, this PR now copies it to the device. That copy moves the bytes the way the program stores them. The program stores a tensor that is neither contiguous nor channels-last, such as a transposed weight, as contiguous. Without the dim order change, the delegate is compiled for the weight's original strides, so it would read that copy in the wrong order. That is why the constants change refuses such a constant with an error instead of copying it.

to_edge already makes such constants and buffers contiguous before partitioning, so a delegate is compiled for the layout the program stores. Parameters were left out. The dim order change adds them to the same pass.

So without it, a model with a transposed weight that two CUDA delegates read fails to export. With it, the model exports and matches eager on CUDA.

@Gasoonjia

Copy link
Copy Markdown
Contributor

Thanks for the explanation! The changes for the current target LGTM. Since this PR touches the dim order pass, which every backend runs in to_edge, I added more CI labels for better coverage.

Some thoughts:

  1. Most cases in the summary have tests now, but case 1 (non-contiguous parameter) is only covered at the pass level by test_convert_constant_dim_order_parameter. Can we add an end-to-end test that shows a delegate reading the wrong elements without this fix? Also our test now focuz on host operator sharing input/output with device delegate, can you add test for two delegate sharing same input/output but these two delegates on different device?
  2. Storing a non-contiguous parameter as contiguous follows what the emitter already does for constants and buffers, but it is still a hacky, silent layout change, and it now affects every backend. I'm OK keeping it in this PR. As a follow-up, I'd prefer a clear signal, e.g. a warning, or an error when such a parameter feeds a delegate that compiles its strides in, instead of silently rewriting it.
  3. Please create an issue to track device-resident constants, and link it in the skip_h2d_for_method_inputs comment.
  4. With skip_h2d, an input shared by a CPU delegate and a GPU delegate is currently refused at AOT (the "other device type" case in test_skip_h2d_refuses_input_read_off_device). That's fine as a first step. Do we plan to support it later by keeping the input on device and inserting a D2H copy for the CPU reader?
  5. On the user interface: when skip_h2d/skip_d2h is set, users expect no per-call copies at the method boundary. Two cases still copy silently: (a) constants, which get an H2D on every call and only log a warning; (b) with skip_d2h, a returned output written in place on the host, which gets a D2H + H2D round-trip on every call with no warning at all. Can we at least warn for (b), and consider making both clearer, e.g. an opt-in flag to turn them into AOT errors?

I will OOO next week i will defer to @JacobSzwejbka to review and stamp.

The emitter stores a constant tensor that is neither contiguous nor
channels-last as contiguous. convert_constant_dim_order_pass makes lifted
constants and buffers contiguous in to_edge, before partitioning, so a
delegate is lowered with the layout the program will store. Parameters
were left out.

So a non-contiguous parameter that a delegate reads as an input, such as a
transposed weight left outside the delegate, was lowered with its original
strides but stored contiguous. A delegate that compiles those strides in,
like the CUDA backend, would read the wrong elements from a copy of the
stored bytes.

The pass now converts such parameters too, keeping them nn.Parameter.
When two delegates on the same device are connected, PropagateDevicePass
removes the device -> host -> device copy pair between them, and the
second delegate reads the first one's device output directly.

That is wrong when a host op writes the host copy in place in between.
A mutable custom op that stays on the host (like a KV cache update)
does this: to_executorch removes auto_functionalized before this pass,
so the op writes its argument in place. The second delegate then reads
the device tensor from before the write and gets a stale value.

Now the pass keeps the round-trip when any op that reads the host copy
also writes it, according to the op's schema. The check reuses
_schema_writes_input from the buffer write-back pass. It does not look at
where the write happens, so it also keeps a round-trip whose second
delegate runs before the write.

The new test runs a mutable host op between two delegates on cuda:0
and checks that the second delegate reads a fresh copy of the written
host tensor. It fails without the change.
…item

A lowered module that returns one tensor and is called from another module
has no getitem after its call. The device pass only placed copies after
getitems, so such an output got no _d2h_copy. A host op that read it got
device memory, and another delegate on the same device got an _h2d_copy
with a device source, which the copy kernel rejects at run time.

The device-to-host copy is now inserted after the call itself in that case.
The existing elision then removes the round-trip between two calls on the
same device.
…tputs is set

With skip_d2h_for_method_outputs, a delegate output that the method returns
should stay on the device. The pass skipped the device-to-host copy only
when the graph output was the copy's only reader, and it decided this while
it was still inserting copies. So a returned output that something else
also read was returned on the host:

- A delegate on the same device: its input copy was still a reader at that
  point. The round-trip was removed later, but the host copy stayed.
- A host op, or a delegate on another device: these need the host copy, so
  the graph output kept reading it too.

For example, `a = f(x, y); return a, torch.sin(a)` returned `a` on the host.

The pass now inserts every device-to-host copy, removes the round-trips,
and then points the graph output at the device tensor. A copy stays only
for the readers that still need it.

When a host op writes the host copy in place, the device tensor never sees
the write, so the output reads a new device copy of the written host copy.
A constant that feeds a delegate on a device, such as a buffer the
partitioner leaves outside the delegate, broke device memory planning.

- Without skip_h2d_for_method_inputs, its _h2d_copy cloned the constant's
  spec, constant flag included, so memory planning skipped the copy's
  output and emission failed.
- With it, the constant was treated as a method input: its placeholder was
  tagged with the device while its data stays in the program on the host,
  so the delegate got a host pointer labeled as device memory.

A copy's output is now never a constant, and skip_h2d_for_method_inputs
applies only to placeholders that are not constants. The config comment and
the CUDA docs say that this copy runs on every call, and the pass logs a
warning that names the constant and its size, so the cost is visible at
export time.

The copy moves the constant's bytes as they are stored. The program stores
a constant that is neither contiguous nor channels-last as contiguous, so
such a constant, which a pass that runs after to_edge can create, is now
refused with a clear error instead of getting a copy with the wrong layout.
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, and the error names that
reader and the device the input was going to. When one of the readers is
the write-back of a mutated input or mutable buffer, the error names it
and says so, and for a mutable buffer it points to tag_mutated_buffer,
which lets the partitioner keep the buffer inside the delegate. The CUDA
docs now list these rules.
When several delegates on one device read the same tensor, the pass made a
separate host-to-device copy for each of them. A constant read by three
delegates on cuda:0, such as a lookup table, was copied three times on every
call. Method inputs were copied the same way when skip_h2d_for_method_inputs
is off.

The pass visits delegates in graph order, so a copy made for an earlier
delegate on the same device already comes before the later ones. It now
reuses that copy. A tensor read on two devices still gets one copy for each
device. The warning for a constant copied on every call is logged once per
copy.

The reused copy stays in device memory from the first delegate that reads it
to the last, so memory planning cannot give that space to other tensors in
between. That is the cost of saving the copy on every call.

A tensor that any op writes in place, such as an argument of a mutable
custom op or a mutable buffer with its write-back, is not reused, because
an earlier copy can hold the value from before the write. The check does
not look at where the write happens, so it also covers writes before the
first reader or after the last one, where reuse would be safe. Those
tensors get one copy for each delegate, as before.

For example, a constant read by delegates on cuda:0, cuda:1 and cuda:0 now
gets two copies instead of three.
SpecPropPass gives the result of an in-place op the TensorSpec of the
tensor it writes, so memory planning puts both in one buffer. When a host
op writes a device delegate output in place, for example relu_ or
index_put_ made by reinplace_pass, PropagateDevicePass moves the op to the
host copy of the output, but the result kept the spec of the device
output. Memory planning then refused the program:

  In-place node aten_relu__default ... result 0 does not share the
  TensorSpec of its mutated input _d2h_copy_default.

This happened both for an output read through getitem and for a single
tensor returned by the call.

When the pass moves readers to the host copy, it already pointed the graph
output at the copy's spec. It now does the same for in-place results and
their own readers, so they share the copy's spec. A returned in-place
result is the result of a host op, so the method returns it from the host,
also with skip_d2h_for_method_outputs.

The new test runs relu_ and then mul_ on a delegate output, for getitem
and for a single tensor, with skip_d2h_for_method_outputs off and on. It
fails without this change.
Before PropagateDevicePass reuses a device copy of a tensor, removes a
device -> host -> device round-trip, or returns a delegate output on the
device instead of its host copy, it checks that no host op writes that
tensor in place. The check only looked at ops that write the tensor itself.

A mutable custom op on the host can also write a view of the tensor. When
to_executorch removes auto_functionalized, it makes that view again, for
example with aten.as_strided, and the op writes the view. The check missed
this, so a later delegate, or the method output, could get the value from
before the write. For example:

  t = x * y
  a = t + y                           # delegate on cuda:0
  accumulate(t.view(-1), y.view(-1))  # host op, writes t
  return a, t + y                     # delegate on cuda:0

Here the second delegate read the device copy of t made before accumulate.

The check now also counts writes to views of the tensor, and to views of
those, as the op schemas declare them. The result of an in-place op counts
as a view of its input. A view_copy does not, because it still makes its
own tensor at this point, and ReplaceViewCopyWithViewPass turns it into a
view later only where no write can tell the difference.

The tests for round-trip removal, returned outputs and copy reuse each get
a case that writes through a view. These cases fail without this change.
aten.as_strided has no out variant yet, so in these cases to_executorch is
told to keep it.
The device pass tests covered a delegate output read by delegates on two
devices and a constant read on two devices. A method input read by
delegates on different devices was only covered with
skip_h2d_for_method_inputs, where it is refused.

The new test has delegates on cuda:0, cuda:1 and cuda:0 that read the
same method input. The second and third each read the output of the
delegate before them, which is on the other device. The input gets one
copy for each device, and both delegates on cuda:0 read the same one.
Each output crosses to the other device through the host. The test fails
if copy reuse ignores the device index.
The device pass fixes were only tested on the graph and on memory
planning. This adds tests that lower small models to real CUDA delegates,
run them, and compare the results with eager PyTorch. Each add becomes its
own delegate, and parameters and buffers stay outside the delegates, so
the program copies them to the device.

- A transposed parameter. Without the parameter fix, to_executorch fails,
  because the copy would move the contiguous bytes the program stores to a
  delegate compiled for the transposed strides, and the pass refuses that.
- A buffer read by two delegates.

The tests need a CUDA GPU and are skipped otherwise.
With skip_h2d_for_method_inputs and skip_d2h_for_method_outputs, users
expect no copies at the method boundary. Two cases still copy on every
call:

- a constant that feeds a device delegate from outside it, because its
  data is stored on the host. The pass already logs a warning with its
  name and size.
- with skip_d2h_for_method_outputs, a returned delegate output that a
  custom host op with a mutable argument, such as a KV cache update, writes
  in place. The method returns a device copy of the written host
  copy, so the output goes to the host and back on every call. This was
  silent.

The second case now logs a warning too. It gives the output's size, the
device and the op that writes it, and suggests moving that op into the
delegate. To name the op, the write check now has a variant,
_in_place_writer, that returns the writer. The config comment for
skip_d2h_for_method_outputs and the CUDA backend docs mention the copies.

The test for the second case now checks the warning, and fails without it.
PropagateDevicePass keeps a device -> host -> device round-trip between
two delegates on one device, and does not reuse a device copy of a
tensor, when a host op writes the tensor in place. It looked for such a
write anywhere in the graph. But a write that runs after the delegate
has read the copy cannot change what the delegate reads. So some
programs paid a copy on every call that main did not make, for example
when reinplace_pass turns a host op into an in-place op, which it only
does on the last use of a tensor, or when a mutable buffer is written
back after its readers.

The round-trip now stays only when the host copy is written before it is
copied back to the device. A copy is reused only when no write runs
between it and the delegate that would reuse it, and the newest copy on
that device is the one considered. The graph order is the order the
method runs in, so it decides what comes before.

The new test writes two tensors on the host after the second of two
delegates on one device has read them. The round-trip is removed and
the tensor both delegates read gets one copy. The test fails without
this change, and the tests that write between the delegates still pass.
With skip_d2h_for_method_outputs, when a host op writes the host copy of
a returned delegate output in place, the method returns a device copy of
the written tensor. If a later delegate on the same device also reads
that tensor, it already reads a device copy of the written host copy, so
the method made two identical copies on every call.

The method output now reads the copy that the later delegate reads, when
no op writes the tensor in place between that copy and the end of the
method. Otherwise it still gets its own copy.

The new test returns a delegate output that a host op writes and that a
second delegate on the same device reads. There is one device copy, read
by both. The test fails without this change.
…der_pass

_update_placeholder_meta looks up the input spec of a lifted tensor and
then its placeholder. The second check tested the input spec again, which
was already known to exist, so a missing placeholder failed later with an
AttributeError on None instead of the intended "Missing placeholder"
error. Parameters now go through this function too.

The new test points a parameter's input spec at a name with no
placeholder and expects the error. It fails without this change.
backends/cuda/tests/test_device_placement.py runs programs whose
host/device copies come from PropagateDevicePass and whose parameter
layout comes from convert_constant_dim_order_pass. The CUDA workflow did
not start for changes to those passes, so a change to them alone could
merge without running the test.

The workflow's pull_request paths and the unittest-cuda job now include
those two passes and the device pass config, the same way they already
include the pybindings paths for the pybindings device test. The other
CUDA jobs keep their own path checks, so they do not start for these
files.
@shoumikhin
shoumikhin force-pushed the device-pass-constants branch from aea6d31 to 0ac8098 Compare October 24, 2026 13:06
Copilot AI balanced review requested due to automatic review settings October 24, 2026 13:06

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota.

@shoumikhin

Copy link
Copy Markdown
Contributor Author

@Gasoonjia Thanks for the review. I pushed these changes:

  1. Tests. The new backends/cuda/tests/test_device_placement.py lowers a model with a transposed parameter to real CUDA delegates, runs it, and compares with eager. I could not make a delegate read wrong elements. The program copies the parameter into device memory for the delegate, and this PR refuses that copy for a constant that is neither contiguous nor channels-last. So on a CUDA GPU, without the parameter fix, an earlier version of this test failed in to_executorch with that error. A new CPU test in test_passes.py also checks that to_edge makes the parameter contiguous, with and without _skip_dim_order. The CUDA unit test job now also runs when the device passes or the dim order pass change. For two devices, test_tensors_shared_by_delegates_on_different_devices checks the graph for delegates on cuda:0 and cuda:1 that read the same input and each other's outputs: the input gets one copy per device, and each output crosses through the host.
  2. Agreed. I am filing a follow-up issue for this.
  3. I am filing the issue for device-resident constants, and will link it from the skip_h2d_for_method_inputs comment once it exists.
  4. Yes, as a follow-up, not in this PR. It fits the pass: keep the input on the device and add a device-to-host copy for the CPU reader, as the pass already does for delegate outputs. It goes in the same follow-up issue as point 2.
  5. Case (b) now logs a warning with the output's size, its device, and the op that writes it, and suggests moving that op into the delegate. I left the opt-in error out of this PR because it adds a new config option, and will list it in the same follow-up issue.

@pytorch-bot
pytorch-bot Bot deployed to upload-benchmark-results October 24, 2026 15:28 Active

This branch was successfully deployed

1 active deployment
upload-benchmark-results — 0ac8098f Deployed Oct 24, 2026 by pytorch-bot[bot] via upload-benchmark-results #21276
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ciflow/cuda ciflow/metal ciflow/mlx ciflow/periodic ciflow/trunk CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. release notes: exir Changes to any dialects and passes on these dialects, such as memory planning

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants