Skip to content

Let a device empty a copy somebody else still points at - #813

Merged
bosilca merged 1 commit into
ICLDisco:masterfrom
bosilca:topic/device-copy-placeholder
Sep 29, 2026
Merged

bosilca merged 1 commit into
ICLDisco:masterfrom
bosilca:topic/device-copy-placeholder

Conversation

@bosilca

@bosilca bosilca commented Sep 27, 2026

Copy link
Copy Markdown
Contributor

When every copy on a device is still pointed at from outside it, the victim search used to find nothing and the task stayed starved. A device can now take the memory of such a copy and leave the copy object behind as a placeholder: it stays attached to its data, marked invalid, and whoever still holds it recovers the value from the host mirror on the next stage in.

Stacked on #812; only the last commit is new here.

Copilot AI left a comment

Copy link
Copy Markdown

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.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot review overview

🟡 Changes recommended

Unresolved critical correctness issues remain in GPU copy handling, shutdown flushing, and placeholder refill.

Review effort: Lite
Findings: 3 High severity · 1 Medium severity

Open (4)

parsec_warning("GPU[%d:%s] asked to release its memory while it is still being managed."
" The copies may be in use, so nothing is flushed.",
device->device_index, device->name);
return PARSEC_ERR_DEVICE;

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.

Fixed, in two halves.

parsec_device_memory_release() only runs from device fini, where a live manager means we are tearing the device down under a thread still using its copies. Nothing up that call chain can recover from that, and the caller is already committed to destroying the streams next, so it is now a parsec_fatal rather than a silent teardown.

Separately, parsec_devices_release_memory() now reports the refusal to its caller. It still asks every device, since whether one can give its memory back says nothing about the others, and returns PARSEC_ERR_DEVICE if any was busy. A refusal is non-destructive: parsec_device_flush_lru() returns before touching a list, so the caller can simply ask again once the work it is running has drained.

Comment on lines +1317 to +1321
#if !defined(PARSEC_GPU_ALLOC_PER_TILE)
if( (NULL != gpu_elem) && parsec_data_copy_is_placeholder(gpu_elem) ) {
/* This device emptied its copy of that data to make room for another
* one. Give it memory back and let the stage in refill it: it is
* already attached and already invalid, so memory is all it misses.

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.

Right: the decision to leave a placeholder was compiled unconditionally while everything that makes one work sits behind !PARSEC_GPU_ALLOC_PER_TILE. In that mode the copy stayed attached to its old data and was then attached to the new one as well.

Rather than build a per-tile refill path, placeholders are now impossible there: allow_placeholders starts at -1, so the recoverability check is never consulted, keep_as_placeholder can never be set, and the eviction behaves exactly as it did before this PR. For what it is worth, that mode does not compile on master either: parsec_device_memory_release_list() calls memory_free() with one argument where the member takes two.

Comment on lines +3139 to +3140
if( (gpu_device->super.device_index == task->data[i].data_in->device_index) &&
(gtask->flow_info[i].source == task->data[i].data_in) ) continue;

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 catch. The in-place branch of parsec_device_data_stage_in() returns without recording a source, and it was master's unconditional skip of device-local inputs that kept that field from ever being read.

It now assigns flow_info[].source to the copy itself, so source == data_in means "this flow has no transfer of its own to close out" and only the refilled placeholder falls through. Setting it beats a NULL check: the field is not reset between push attempts, so the value to guard against could be stale rather than NULL.

Comment thread parsec/mca/device/transfer_gpu.c Outdated
Comment on lines +376 to +381
int nb_transferred = task->locals[0].value;

parsec_thread_mempool_free(es->context_mempool, task);
PARSEC_OBJ_RELEASE(gpu_task); /* no need to call release_device_task, just release the task */
gpu_device->data_avail_epoch++;
return 0;
return nb_transferred;

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.

Fixed. The counter is incremented only in the branch that puts the copy back on the clean LRU, and it is named nb_reclaimable to match the parameter of parsec_device_wake_starved_tasks().

Comment thread parsec/mca/device/device_gpu.h Outdated
Comment on lines +343 to +347
parsec_list_t gpu_mem_starved; /**< Tasks that could not reserve the device memory
* they need. They remain counted in mutex, and
* only a write-back that turns dirty blocks into
* reclaimable ones hands them back to the manager.
*/

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.

The naming is slightly confusing: gpu_mem_lru and gpu_mem_owned_lru are holding data copies while gpu_mem_starved holds tasks. Suggest renaming to starved (mirroring pending) or gpu_task_starved (more explicit).

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.

Renamed to starved_tasks. It sits between two lists of data copies, so a name that says "tasks" earns its place.

@bosilca
bosilca force-pushed the topic/device-copy-placeholder branch 2 times, most recently from 06810c1 to 4cda92b Compare September 28, 2026 22:16
devreal
devreal previously approved these changes Sep 28, 2026
A device that runs out of memory looks for a copy in its read LRU whose
memory it can take. It passes over every copy whose reference count is
above one, on the grounds that the extra reference belongs to a task
completion that has yet to run and will drop it shortly. That is no
longer the only thing such a reference can be. A copy handed to a
successor is retained by the data repository that carries the dependency,
and a reshape promise parked there retains both the copy it was given and
the one it produced. Those references last as long as the dependency
does, which can be until the end of the taskpool, and they are not
waiting on anything this device can do.

With little enough device memory, every copy in the LRU is referenced
that way and the search comes back empty. The device has memory, all of
it spoken for by copies nobody is reading, and the tasks waiting for a
block wait for a release that is never coming.

Let the search take those copies. The copy object has to survive, since
the holders point at it, but its memory does not: emptied, it stays
attached to its data, keeps the version its content had, and is marked
invalid with nothing in flight. Anybody who needs the value again gives
it memory back and stages it in from the host mirror, which is where the
value went.

Emptying a copy is only admissible when the value can be brought back, so
examine the candidate first. A shared copy whose host mirror carries the
same version can be emptied as it is. A copy holding no value at all is
free to empty, which is what a reservation that had to be given up leaves
behind. A copy the device owns, or one that is shared but ahead of the
mirror, holds the only current value; move it to the owned list, where
the write-back hands the value and the version to the host and returns it
shared, which is precisely the state that makes it recoverable. Only
start doing any of this once a full sweep has shown there is no copy left
that this device owns outright.

Three places assumed a copy always has memory behind it. A transfer
looking for a source could pick an emptied peer, and hand the copy
routine a source pointer of nil; it now passes those over, and a flow
that also writes, which never looked for an alternate source at all,
falls back on the host mirror. A source that is invalid was taken to mean
a transfer is on its way in and is worth coming back for, which is not
true of one that was emptied, so that now keys off the transfer being in
flight. And the push completion skipped any flow whose input already sits
on this device, on the grounds that no transfer was needed to bring it
in; a copy being refilled is both the input and the output of its flow,
so it was skipped and left under transfer for good, which is a state the
eviction will never take memory back from.
@bosilca
bosilca force-pushed the topic/device-copy-placeholder branch from 4cda92b to ab0f371 Compare September 29, 2026 03:22
@bosilca

bosilca commented Sep 29, 2026

Copy link
Copy Markdown
Contributor Author

This has been approved before, but as a stacked PR I had to rebase once #812 got merged.

@bosilca
bosilca merged commit 131eb90 into ICLDisco:master Sep 29, 2026
12 checks passed
@bosilca
bosilca deleted the topic/device-copy-placeholder branch September 29, 2026 03:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants