Repository navigation
Fix device placement of constants, shared inputs and delegate outputs - #24088
shoumikhin wants to merge 26 commits into
Conversation
🔗 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. ❌ 58 Pending, 5 Unclassified FailuresAs of commit 964a611 with merge base 5fa57d1 ( UNCLASSIFIED FAILURES - DrCI could not classify the following jobs because the workflow did not run on the merge base. The failures may be pre-existing on trunk or introduced by this PR:
This comment was automatically generated by Dr. CI and updates every 15 minutes. |
3aadad6 to
894b0fa
Compare
Gasoonjia
left a comment
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
This is now in #24144, which builds on this PR. With skip_h2d_for_method_inputs, a constant that only delegates on one device read is loaded into device memory once, when the method loads, so it needs no copy per call.
894b0fa to
ea00fc9
Compare
ea00fc9 to
e12db30
Compare
e12db30 to
71dd63f
Compare
71dd63f to
e1ed5ce
Compare
e1ed5ce to
635d39e
Compare
A partitioner can set the target_device compile spec to "cpu". The pass treated that delegate like one on a device: it copied each input with _h2d_copy and each output with _d2h_copy, both to and from the CPU. The copy kernels refuse a CPU destination for _h2d_copy and a CPU source for _d2h_copy, so such a program failed at run time. A delegate on the CPU reads and writes host memory directly, so the pass now leaves it alone, the same as a delegate without a target device. The new test lowers an add to a delegate with target device "cpu" and expects no copies. It fails without this change.
With skip_h2d_for_method_inputs, the caller passes an input that a device delegate reads in device memory. Until now, to_executorch refused such an input when anything else read it too: a host op, a delegate on the host, or a delegate on another device. The pass now keeps the input on the device for the delegates there and gives every other reader one _d2h_copy of it, the same way it already handles a device delegate output read on the host. A delegate on another device then reads a copy of that host copy. The input stays on the device of the first delegate that reads it, in graph order, and later delegates on that device find it already placed. An op that reads only the input's shape, such as sym_size, reads it on the device, since sizes stay in host memory. The copy runs on every call, so the pass logs a warning with the input's size and a reader. With skip_d2h_for_method_outputs, a method that returns the input returns it on the device, with no copy. An input that an op writes in place on the host is still refused, now with its own message, because the write would only reach the host copy. The refusal test becomes a test that the input stays on the device and that the other readers read one D2H copy, with the warning, for a host op, two delegates on the device plus a host op, a delegate without a device, a delegate on the CPU, a delegate on another device and a buffer write-back. It fails without this change. New tests return an input with both skip flags, and read only the shape of an input on the host, and expect no copy and no warning. A new refusal case covers a host op that writes the input in place, and a new CUDA test runs a program whose input on the device is also read on the host and compares with eager PyTorch.
With skip_h2d_for_method_inputs and skip_d2h_for_method_outputs, users expect no copies at the method boundary. Three copies still stay and run on every call, and to_executorch only logs a warning for them: a constant that feeds a device delegate from outside it, a method input on the device that a reader off that device also reads, and a returned output that a host op writes in place, which is copied to the host and back. A warning is easy to miss in a long export log. The new opt-in PropagateDeviceConfig option error_on_per_call_copies makes to_executorch raise with the same message instead. It applies to a constant copy only with skip_h2d_for_method_inputs, since without it every input is copied anyway. The input copy exists only with that flag, and the written output copy only with skip_d2h_for_method_outputs. The messages now format sizes with an f-string, as the error and the warning share them. The new test expects the error for all three cases, and only the warning for a constant copy without the skip flag. It fails without this change.
The notes this change adds to the CUDA backend page, about the copies that stay with skip_h2d_for_method_inputs and skip_d2h_for_method_outputs, were long paragraphs that explained the internals. They are now a short list of the copies that stay, how to remove them, how to make them an error, and which inputs the skip flag refuses. They also move after the other notes on the Python export options.
e4244eb to
2a8bff8
Compare
|
@claude review |
|
Claude finished @shoumikhin's task in 6m 9s —— View job I found one thing worth checking before merge, at low confidence: the new parameter rewrite in Detailed reviewPR Review: #24088SummaryThe PR fixes where Coverage:
Backward Compatibility
Infrastructure
Testing
RecommendationApprove Nothing blocks merging on the evidence here. Please check the shared |
|
Thanks. No code change here:
|
With skip_d2h_for_method_outputs, the method returns delegate outputs on the device. When a custom host op with a mutable argument, such as a KV cache update, writes the host copy of such an output in place, the device tensor never sees the write. The pass used to copy the written host tensor back to the device and return that copy, so every call paid for a copy to the host and another one back. The written host tensor is already the right result, so the method now returns it as it is, and the method output says it is in host memory. The copy to the host stays, because the host op needs it. The warning about it stays too, and error_on_per_call_copies still turns it into an error.
|
@claude review |
|
Claude finished @shoumikhin's task in 6m 25s —— View job I found no blockers, only minor issues: the docs and config comment say that only a custom host op sends a returned output back to the host, but the code does this for any op that writes it in place, and the per-call copy warnings for Detailed reviewPR Review: #24088SummaryThe PR fixes where Coverage:
API Design
Code Quality
Testing
RecommendationApprove None of these block merging. The doc and message fixes are one-line changes. The two suspected detection gaps are worth a quick check, or a comment explaining why they can't happen. |
The docs and the config comment said that a returned output goes back to the host only when a custom op writes it in place. The pass does this for any op outside the delegate that writes it, aten in-place ops included, so the text now says that. The warning for a method input that stays on the device was also off in two cases. When the method returns the input, the advice to put every reader in a delegate cannot work, and skip_d2h_for_method_outputs is what removes the copy. When a delegate on another device reads the input, it gets a second copy, from the host to its device, which the warning did not mention. The warning, the config comment and the docs now cover both cases.
With _skip_dim_order, to_edge runs convert_constant_dim_order_pass once, not twice, so the comment now covers both modes.
|
Thanks. Addressed in the latest push:
@claude review |
|
Claude finished @shoumikhin's task in 6m 19s —— View job I found no blockers in the latest push. The three fixes you listed are in. Four small issues remain: the docs give a KV cache update as the example for the wrong case, the output warning doesn't name the output or say it needs a host buffer, and two tests are weaker than they look. Detailed reviewPR Review: #24088SummaryThe PR fixes where Coverage:
API Design
Code Quality
Testing
RecommendationApprove None of these block merging. The doc example and warning text are one-line changes. The test gaps are worth closing, so that the host-output path under |
Some PyTorch builds turn off auto_functionalized v2 for export. There, a custom op that writes through a view writes into a view_copy, not into an as_strided of the tensor, so the tests that write through a view see a different graph and fail. Under v1 that write is lost even without these passes, so there is nothing correct to check there. Functionalize with v2 in these tests so they check the same graph in every build.
The warning for a returned output that an op writes on the host now says which output it is and that a caller passing its own output buffers must pass a host buffer for it. The docs and the config comment now give a mutable custom op left on the host as the example, since a KV cache update writes a buffer, which is a different case.
|
Thanks. In the latest push:
The test gaps (no unplanned host output case, two asserts that cannot fail) are in older tests and do not hide a bug here. Better tightened separately. @claude review |
|
Claude finished @shoumikhin's task in 6m 10s —— View job I found no blockers in the latest push, and the three changes you listed are in; four minor issues remain: one docs sentence, two test gaps from this round, and one alias question from an earlier round that hasn't been answered yet. Detailed reviewPR Review: #24088SummaryThe PR fixes where Coverage:
API Design
Testing
Code Quality
RecommendationApprove None of these block merging. The docs fix is one sentence, and the test changes are small. |
|
Thanks. Answers to the rest, without another push:
|
What is wrong today
When a delegate runs on a GPU,
PropagateDevicePasscopies its inputs to the device (_h2d_copy) and its outputs back (_d2h_copy).skip_h2d_for_method_inputsandskip_d2h_for_method_outputsremove those copies at the method's edges.to_edgedoes this silently.relu_makes memory planning fail._d2h_copy.to_executorchfail. With the input skip flag, it arrives as host memory marked as device memory.What this change does
Numbered as above:
to_edgemakes such a parameter contiguous before partitioning, and warns for each tensor whose layout it changes._d2h_copy._d2h_copyof it, with a warning. An input written in place on the host is refused.Also:
PropagateDeviceConfig(error_on_per_call_copies=True)turns the warnings in 4 to 6 into errors when the matching skip flag is set.What was tested
exir/tests/test_passes.pytests pass. All new tests but one fail without their fix.backends/cuda/tests/test_device_placement.pymatches eager PyTorch with real CUDA delegates on an H100. Theunittest-cudajob now runs when these passes change, and fails if that test skips.