Repository navigation
Add skip_h2d_for_constants to keep constants in device memory - #24144
shoumikhin wants to merge 32 commits into
Conversation
🔗 Helpful Links🧪 See artifacts and rendered test results at hud.pytorch.org/pr/pytorch/executorch/24144
Note: Links to docs will display an error until the docs builds have been completed. ❌ 7 Unclassified FailuresAs of commit 7f587a9 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. |
fb3f67e to
f1833e1
Compare
|
@claude review |
|
Claude finished @shoumikhin's task in 4m 51s —— View job The approach is sound, but please fix a few things before merging: Detailed reviewPR Review: #24144I reviewed only the last two commits ( SummaryWith
API Design
Code Quality
Testing
Performance
RecommendationRequest Changes: the main blockers are the missing opt-out (or documented cost) for keeping constants on the device, the duplicate storage of a shared weight, and making the CUDA test actually check the feature. |
f1833e1 to
fc3ce9c
Compare
|
Thanks for the review. Changed in the new push:
Not changed, and why:
|
|
@claude review |
|
Claude finished @shoumikhin's task in 3m 52s —— View job Before this can merge, please delete the Detailed reviewPR Review: #24144SummaryThis PR adds the opt-in option Infrastructure
API Design
Code Quality
Testing
RecommendationRequest Changes: the stray |
fc3ce9c to
cd8691c
Compare
|
Thanks. Changed in the new push: the stray Not changed, and why:
|
|
@claude review |
|
Claude finished @shoumikhin's task in 4m 17s —— View job I found no blockers in the last two commits; what is left is one point about the Detailed reviewPR Review: #24144SummaryThis PR adds the opt-in option API Design
Code Quality
Testing
RecommendationApprove: none of the findings blocks the merge. The |
|
Thanks. Answers, with no code change:
|
| it to `False` while asking for either skip raises a `ValueError`, because copy insertion happens | ||
| during device-aware memory planning. | ||
|
|
||
| **With `skip_h2d_for_method_inputs`, an input that a device delegate reads stays on that device.** |
There was a problem hiding this comment.
can you keep this .md file more succinct and readable by humans
There was a problem hiding this comment.
Shortened, see the new version. On top of the shorter text from #24088, this PR now adds only one short paragraph on skip_h2d_for_constants, what it does and what it costs, and a pointer to it from the list of copies that stay.
|
it's hard to review stacked PR. not clear what is in the previous and what is new Can you use ghstack? |
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.
A memory-planned tensor can have initial data, such as a mutable buffer with a starting value. The runtime loads that data into the tensor's planned memory when it loads the method, with the data loader or the named data map. Both write with the CPU, so they cannot fill a planned buffer that lives in device memory, such as CUDA memory. The tensor parser now checks the device of the planned buffer, which the memory manager's planned memory records. For a device buffer it loads the data into host memory and copies it with the device allocator registered for that device, once, at method load. An empty tensor needs no copy. A host buffer is loaded as before. Program gets a private load_mutable_subsegment(), which loads a part of a mutable segment into a new buffer and checks the same bounds as load_mutable_subsegment_into(). Both share the code that finds the part. The program test loads the same weights both ways and expects the same bytes, and expects the same errors for bad indices and sizes.
With skip_h2d_for_method_inputs, a method input that a device delegate reads stays in device memory. A constant, such as a weight or a buffer the method does not change, was still copied to the device on every call, because its data is stored in the program, on the host. The new opt-in PropagateDeviceConfig option skip_h2d_for_constants keeps a constant that only delegates on one device read in device memory. PropagateDevicePass tags it with that device instead of adding a copy. Memory planning gives it planned device memory for the whole method, also when graph inputs are not planned, because the method reads it on every call. The emitter stores its data as the initial state of that memory, as it does for a mutable buffer with an initial value, and the runtime loads it there once, when it loads the method. Each method that reads the constant keeps its own copy, so this is opt-in. A constant that anything else reads, such as a host op or a delegate on another device, is still copied, with the same warning, and error_on_per_call_copies turns that copy into an error under this option too. The new Python test checks that a buffer or parameter read only by delegates on one device has no copy and no warning, has planned device memory for the whole method with its data as the initial state, with and without planned graph inputs, and that a constant also read on the host or on a second device, or any constant without the option, is still copied. It fails without this change. export_program_with_device_info.py now also exports such a program, once with the constant in the program and once in a separate .ptd file. Two new C++ tests parse the constant of each into planned memory on a mock CUDA device and check that its data was copied there. tensor_parser_device_test now also builds and runs with CMake. A new CUDA test runs a program whose constant is kept in device memory twice, with different inputs, and compares with eager PyTorch, with error_on_per_call_copies set so it fails if the constant is copied.
The warning for a constant copied to the device on every call only said to move it into the delegate. Without skip_h2d_for_constants, it now also suggests the option when only delegates on that device read the constant. With the option, it names the other reader that keeps the constant off the device.
When a method loads, the initial data of a tensor planned in device memory is copied there through the device allocator. On CUDA that copy can still be running when it returns: it is asynchronous on a caller stream, and from pageable memory even a plain cudaMemcpy may return before the data lands. The first run then reads the tensor on its own stream. If that stream is not ordered after the copy, for example when the method loads with one caller stream and runs with another, or runs on a non-blocking stream such as a green context stream, it can read the tensor before its data is there. DeviceAllocator gets synchronize(), which waits for the copies that the allocator made on the calling thread. The default does nothing, for allocators whose copies finish before they return. CudaAllocator waits for the stream its copies use: the caller stream when one is set, otherwise the default stream of the device. So a load does not wait for unrelated work on other streams. The runtime calls it after each such copy at load, so the per-call copies are unchanged.
a55dfd0 to
fc0d846
Compare
|
Thanks. In the latest push:
HIP aliases for the test are left for a separate change, since I cannot run it on ROCm. @claude review |
|
Claude finished @shoumikhin's task in 3m 25s —— View job Nothing in the last two commits blocks the merge; what's left is a load-time sync that still runs once per tensor, a warning that can name the graph Detailed reviewPR Review: #24144SummaryI reviewed only Code Quality
Testing
Performance
RecommendationApprove: none of these blocks the merge. |
|
Thanks. Answers to the rest, without another push:
|
|
@claude review |
|
Claude finished @shoumikhin's task in 5m 15s —— View job Nothing in this PR's own five commits ( Detailed reviewPR Review: #24144SummaryThis PR adds the opt-in option I reviewed I left out points settled in earlier rounds:
I also checked and dropped these findings:
API Design
RecommendationApprove: neither finding blocks the merge. |
Loading the initial data of a tensor planned in device memory added about 3.7 KB to the smallest runtime binary, even though that binary never uses device memory. Move the steps that only matter for device memory behind the DeviceAllocator interface: - DeviceAllocator gets two virtual functions with default bodies: copy_from_host() copies host data to the device and waits, and load_segment_into() loads data loader bytes and copies them. The defaults stay in the header, so they are only compiled where a concrete allocator exists. - Program::load_mutable_subsegment_into() takes a device allocator, null for host memory, and hands the located bytes to it, instead of a second function that returns a host buffer. - The tensor parser only finds the allocator of the planned buffer and picks the path. One error now comes earlier: when a planned buffer is in device memory but no allocator is registered for that device, loading fails with NotFound before the data is looked up, also for an empty tensor. The smallest runtime binary now grows by about 1.6 KB for this feature instead of 3.7 KB.
a094831 to
1f367f7
Compare
|
@claude review About the last review: I kept |
|
Claude finished @shoumikhin's task in 4m 55s —— View job Nothing in this PR's own six commits ( Detailed reviewPR Review: #24144SummaryThis PR adds the opt-in I reviewed I accept the author's reason for keeping I checked and dropped these findings:
API Design
RecommendationApprove: the one finding is a wording fix in the docs and does not block the merge. |
The CUDA guide said "both flags" need enable_non_cpu_memory_planning, but skip_h2d_for_constants needs it too. Also say that skip_h2d_for_constants works on its own, without skip_h2d_for_method_inputs or unplanned inputs, so readers do not assume the input requirements apply to it.
1f367f7 to
7f587a9
Compare
|
@claude review |
|
Claude finished @shoumikhin's task in 6m 21s —— View job I found nothing to fix in this PR's own six commits ( Detailed reviewPR Review: #24144SummaryThis PR adds the opt-in option I reviewed I left out points that earlier rounds already settled with the author:
I checked and dropped these new findings:
RecommendationApprove: no findings remain. This is still a stacked PR on #24088, so it needs the planned rebase onto |
This builds on #24088, which must merge first. Review only the last 6 commits: shoumikhin/executorch@device-pass-constants...device-resident-constants
What is wrong today
With
skip_h2d_for_method_inputs, the inputs of a device delegate arrive in device memory. But constants (weights and read-only buffers) are stored on the host, in the program or its data file, so the method copies them to the device on every call.What this change does
New option
PropagateDeviceConfig(skip_h2d_for_constants=True)keeps a constant that only delegates on one device read in that device's memory. The runtime loads it there once, at load.PropagateDevicePasstags it with the device instead of adding a copy.DeviceAllocator::copy_from_host()andload_segment_into()load it on the host, copy it, and wait. Their default bodies stay in the header, so builds without an allocator skip them.DeviceAllocator::synchronize()does nothing by default, and on CUDA waits for the stream the copy used.Limits:
.pteholding its data stores it twice.error_on_per_call_copies.What was tested
exir/tests/test_propagate_device_pass.pytests check that only such constants skip the copy and get device memory, even withoutskip_h2d_for_method_inputs. They fail without this change.tensor_parser_device_test.cpptests load such a constant from the program and from a.ptdfile onto a mock CUDA device, checking its data and the wait. They fail without the runtime change.test_cuda_allocator.cppchecks thatsynchronize()waits for the caller and default streams.program_test.cppchecks that program data loaded into device memory goes through the allocator.error_on_per_call_copiesset, runs such a program twice and matches eager PyTorch on an H100.test/size_test.cpp, unoptimized x86-64) grows by 1,632 bytes.