Repository navigation
UCT/ROCM: Add removal of remote mapping when owning buffer is freed to drop IPC - #12077
Conversation
|
🤖 Starting review — findings will be posted here when done. |
|
🤖 CI Triage Agent — TL;DR: The "Codestyle / commit title" job failed because the PR's commit title Full analysisSummary: Azure job Root cause: Not a product/infrastructure bug — a contribution-guideline violation. The check validates each non-merge commit title in Secondary (cosmetic CI bug, not the cause of this failure): in Implicated commit: [REDACTED:Hex High Entropy String] (PR #12077, branch File: buildlib/tools/codestyle.sh:14 (regex) — triggered by the commit title of 5296538; CI step wiring at buildlib/pr/codestyle.yml:17-30 Suggested fix:
Related: PR #12077; guidelines referenced by the job: https://github.com/openucx/ucx/wiki/Guidance-for-contributors#general-guidelines; commit-range change cfd9cd1 (BUILD/CI: Limit PR commit range checks, #11527)
|
|
🤖 CI Triage Agent — TL;DR: The Full analysisSummary: Azure Pipelines job "UCX PR (Codestyle / commit title)" build 138799 failed in the Bash step with Root cause: Secondary observation: the pipeline step runs Implicated commit: [REDACTED:Hex High Entropy String] (PR #12077, branch File: Suggested fix: Reword the offending commit message to include a module prefix, then force-push: Keep the prefix in uppercase/slash/underscore characters only ( Optional CI hardening: in Related: PR #12077; check introduced/limited by
|
5296538 to
0c17a03
Compare
| ucs_list_for_each_safe(region, tmp, deferred, list) { | ||
| ucs_list_del(®ion->list); | ||
| if (region->retired_signal.handle != 0) { | ||
| if (hsa_signal_destroy(region->retired_signal) != |
There was a problem hiding this comment.
blocker: this destroys the signal while the async handler armed in uct_rocm_ipc_cache_region_watch() is still registered on it (purge, invalidate, stale-region and the value <= 0 paths all get here), so HSA keeps waiting on a destroyed signal and the handler's refcount is never dropped, leaking the region. handler_done only prevents re-entry, it does not deregister the handler.
| key->pid_ns = ucs_sys_get_ns(UCS_SYS_NS_TYPE_PID); | ||
|
|
||
| /* One IPC signal per export. Every importer of this key watches it. */ | ||
| if (uct_rocm_ipc_export_add(key->address, key->length, &key->ipc_signal) != |
There was a problem hiding this comment.
this creates one IPC signal per mem_reg and the export is freed only on buffer free (mem_dereg deliberately keeps it), and export_add dedups nothing on the active list, so repeated map/unmap of the same buffer accumulates signals until the buffer is freed. IPC signals are a limited resource (this is exactly the HSA_STATUS_ERROR_OUT_OF_RESOURCES the new code returns) - can we reuse the active export for the same address instead?
There was a problem hiding this comment.
Implemented export reuse per allocation.
| return; | ||
| } | ||
|
|
||
| uct_rocm_ipc_export_reap(ucs_get_tid(), 0, UCT_ROCM_IPC_EXPORT_REAP_TID); |
There was a problem hiding this comment.
reaping by tid (UCT_ROCM_IPC_EXPORT_REAP_TID) means only the freeing thread can destroy these, and only if it calls rocm_ipc progress; if the allocating/freeing thread is not a progress thread (or the app only uses rocm_copy), the signals stay until UCS_STATIC_CLEANUP. can we reap all retired exports once the free is known to be complete, instead of keying on tid?
|
|
||
| void uct_rocm_ipc_export_progress(void) | ||
| { | ||
| if (ucs_likely(ucs_atomic_fadd32(&uct_rocm_ipc_export_retired_count, 0) == |
There was a problem hiding this comment.
ucs_atomic_fadd32(&count, 0) is a bus-locked RMW executed on every uct_rocm_ipc_iface_progress() call; a plain load is enough for the fast exit (see docs/OptimizationStyle.md on avoiding atomics on the data path):
| if (ucs_likely(ucs_atomic_fadd32(&uct_rocm_ipc_export_retired_count, 0) == | |
| void uct_rocm_ipc_export_progress(void) | |
| { | |
| if (ucs_likely(uct_rocm_ipc_export_retired_count == 0)) { | |
| return; | |
| } |
| typedef struct uct_rocm_ipc_key { | ||
| hsa_amd_ipc_memory_t ipc; | ||
| /* Owner sets this signal to 0 when the allocation is released */ | ||
| hsa_amd_ipc_signal_t ipc_signal; |
There was a problem hiding this comment.
does this break rkey compatibility? the new ipc_signal field changes rkey_packed_size and the importer now hard-fails (ucs_error + UCS_ERR_INVALID_PARAM) when the field is zero - worth confirming the intended compatibility story.
| ucs_list_del(&exp->list); | ||
| ucs_list_add_tail(&destroy_list, &exp->list); | ||
| } | ||
| while (ucs_atomic_fadd32(&uct_rocm_ipc_export_retired_count, 0) != 0) { |
There was a problem hiding this comment.
minor: drain loop of atomic decrements under the lock where a plain assignment is enough - we already hold the lock here:
| while (ucs_atomic_fadd32(&uct_rocm_ipc_export_retired_count, 0) != 0) { | |
| uct_rocm_ipc_export_retired_count = 0; |
|
PR-level: pls split the unrelated reformatting in PR-level: there is no test for the new export/attach/free flow ( |
|
🤖 Starting review — findings will be posted here when done. |
| key->pid_ns = ucs_sys_get_ns(UCS_SYS_NS_TYPE_PID); | ||
|
|
||
| /* One IPC signal per export. Every importer of this key watches it. */ | ||
| if (uct_rocm_ipc_export_add(key->address, key->length, &key->ipc_signal) != |
There was a problem hiding this comment.
Each mem_reg of the same allocation adds another export entry plus HSA IPC signal, and mem_dereg never removes it, so a register/deregister loop (or several sub-ranges of one allocation) leaks signals and list entries until the buffer is freed. Please key the export on the allocation base address and refcount it instead of adding one per pack_key.
| typedef struct uct_rocm_ipc_key { | ||
| hsa_amd_ipc_memory_t ipc; | ||
| /* Owner sets this signal to 0 when the allocation is released */ | ||
| hsa_amd_ipc_signal_t ipc_signal; |
There was a problem hiding this comment.
This grows the packed rkey by sizeof(hsa_amd_ipc_signal_t) (32B), and docs/OptimizationStyle.md says to avoid enlarging the remote memory key at all costs. Can we exchange the signal once per (pid, pid_ns, dev_num) - the same granularity as the remote cache - instead of per rkey?
|
|
||
| void uct_rocm_ipc_export_progress(void) | ||
| { | ||
| if (ucs_likely(ucs_atomic_fadd32(&uct_rocm_ipc_export_retired_count, 0) == |
There was a problem hiding this comment.
In uct_rocm_ipc_export_progress: this puts a bus-locked xadd on every progress call, and as soon as any export is retired by another thread, every progress call also takes the global spinlock and walks the whole retired list. Please keep the pending count per thread so progress stays lock-free when this thread retired nothing (docs/OptimizationStyle.md: no locks/atomics in the data path).
| ucs_list_for_each_safe(region, tmp, deferred, list) { | ||
| ucs_list_del(®ion->list); | ||
| if (region->retired_signal.handle != 0) { | ||
| if (hsa_signal_destroy(region->retired_signal) != |
There was a problem hiding this comment.
In uct_rocm_ipc_cache_region_finalize: please confirm hsa_signal_destroy is safe while an async handler is still armed on that signal. In the purge / UCS_STATIC_CLEANUP path the exporter usually has not freed yet, so the handler is still registered and holds a region reference; if it fires later it dereferences region->cache, already freed by uct_rocm_ipc_destroy_cache, and the already destroyed uct_rocm_ipc_remote_cache.lock. There is no API to cancel hsa_amd_signal_async_handler.
| ucs_status_t status; | ||
|
|
||
| ucs_list_head_init(&deferred); | ||
| if (uct_rocm_ipc_ipc_signal_is_empty(®ion->key.ipc_signal)) { |
There was a problem hiding this comment.
In uct_rocm_ipc_cache_region_watch: does this break importing an rkey packed by a peer running an older UCX? ipc_signal is all-zeros there and the whole mapping now fails instead of behaving as before.
| ucs_list_del(&exp->list); | ||
| ucs_list_add_tail(&destroy_list, &exp->list); | ||
| } | ||
| while (ucs_atomic_fadd32(&uct_rocm_ipc_export_retired_count, 0) != 0) { |
There was a problem hiding this comment.
Suggestion:
uct_rocm_ipc_export_retired_count = 0;
All writers of the counter hold the lock, so a plain store is enough here.
| uintptr_t start, end; | ||
| pid_t tid; | ||
|
|
||
| (void)arg; |
There was a problem hiding this comment.
Minor: please drop (void)arg; (and the same at line 393) - UCX callbacks just leave unused parameters unused.
|
Residual coverage gap beyond what was already raised: nothing exercises the async detach path ( |
0c17a03 to
d4328ff
Compare
|
🤖 Starting review — findings will be posted here when done. |
| key->pid_ns = ucs_sys_get_ns(UCS_SYS_NS_TYPE_PID); | ||
|
|
||
| /* One IPC signal per export. Every importer of this key watches it. */ | ||
| if (uct_rocm_ipc_export_add(key->address, key->length, &key->ipc_signal) != |
There was a problem hiding this comment.
pls release the export entry in mem_dereg, or refcount entries per base address: pack_key adds a new entry plus a new HSA IPC signal on every registration (including repeated registrations of sub-ranges of the same base allocation, since key->address is the allocation base), and nothing removes it until the buffer is freed, so the active list and the number of IPC signals grow without bound. uct_rocm_ipc_export_mem_free_cb also walks this whole list under a global spinlock on every ROCm free.
| region->in_pgtable = 0; | ||
| } | ||
|
|
||
| if ((mapped_addr != NULL) && |
There was a problem hiding this comment.
detach now runs from the HSA async handler thread, while other threads may still be using a mapped_addr that uct_rocm_ipc_cache_map_memhandle returned earlier - the cache-hit path hands out region->mapped_addr with no reference, and uct_rocm_ipc_md_mem_elem_pack bakes the offset into device memory. how is an in-flight hsa_amd_memory_async_copy protected from the VA being unmapped under it?
|
|
||
| void uct_rocm_ipc_export_progress(void) | ||
| { | ||
| if (ucs_likely(ucs_atomic_fadd32(&uct_rocm_ipc_export_retired_count, 0) == |
There was a problem hiding this comment.
ucs_atomic_fadd32(..., 0) is a locked read-modify-write, so every progress call dirties this shared cache line even when there is nothing to reap. pls declare the counter volatile and read it directly here - progress is a per-poll path (docs/OptimizationStyle.md: avoid bus-locked instructions on the data path).
| return; | ||
| } | ||
|
|
||
| uct_rocm_ipc_export_reap(ucs_get_tid(), 0, UCT_ROCM_IPC_EXPORT_REAP_TID); |
There was a problem hiding this comment.
a retired export is reaped only by the thread that freed it, so if that thread never calls progress again the counter stays non-zero and every progress call on every other thread walks the retired list under the spinlock, and the signal is only freed at static cleanup. can we reap by address or by a free-generation counter instead of tid?
| int match; | ||
|
|
||
| /* Still inside the free callback. Do not drop the signal yet. */ | ||
| if (uct_rocm_ipc_export_in_free) { |
There was a problem hiding this comment.
minor: uct_rocm_ipc_export_in_free is always 0 here - reap is reachable only from uct_rocm_ipc_export_add and uct_rocm_ipc_export_progress, which the freeing thread cannot call while it is inside the free callback. pls remove the flag and the comment.
|
Duplicate check: I did not re-raise the already-reported items (unrelated reformatting in Residual coverage gap beyond what was already raised: nothing exercises repeated register/deregister of the same allocation, which is where the export-table growth above becomes visible; this needs a ROCm-hardware test rather than CI inspection. |
d4328ff to
4425339
Compare
|
🤖 Starting review — findings will be posted here when done. |
| return; | ||
| } | ||
|
|
||
| uct_rocm_ipc_export_reap(ucs_get_tid(), 0, UCT_ROCM_IPC_EXPORT_REAP_TID); |
There was a problem hiding this comment.
Retired exports are only reaped by the thread that freed the memory: uct_rocm_ipc_export_progress() passes UCT_ROCM_IPC_EXPORT_REAP_TID with the calling thread's tid, and reap() matches exp->retired_tid == tid. Exports retired by a thread that never calls progress (app frees on the main thread while progress runs on a worker thread, or the freeing thread exits) are never reaped, so their HSA signals and export entries live until UCS_STATIC_CLEANUP and uct_rocm_ipc_export_retired_count stays non-zero forever — which makes every uct_rocm_ipc_iface_progress() call take the global export spinlock and walk the whole retired list. Can we reap independently of the tid?
| ucs_rw_spinlock_write_lock(&uct_rocm_ipc_remote_cache.lock); | ||
| if (ucs_atomic_cswap32(®ion->deactivate, 0, 0) != 0) { | ||
| ucs_rw_spinlock_write_unlock(&uct_rocm_ipc_remote_cache.lock); | ||
| hsa_signal_destroy(local_signal); |
There was a problem hiding this comment.
Region reference leaked when the claim was lost after arming the handler: uct_rocm_ipc_cache_region_watch() takes a region reference and arms uct_rocm_ipc_cache_signal_cb on local_signal. If region->deactivate was already set by purge/invalidate_regions/stale-drop, the handler is still armed, but this path destroys local_signal and only drops the watcher reference in out_unref. So uct_rocm_ipc_cache_signal_cb never runs, the handler's reference is never dropped and the region is leaked (and the ROCr handler thread is left waiting on a destroyed signal).
|
|
||
| void uct_rocm_ipc_export_progress(void) | ||
| { | ||
| if (ucs_likely(ucs_atomic_fadd32(&uct_rocm_ipc_export_retired_count, 0) == |
There was a problem hiding this comment.
Minor: uct_rocm_ipc_export_retired_count is only modified under uct_rocm_ipc_export_lock, so the atomic RMWs are not needed, and this ucs_atomic_fadd32(ptr, 0) is a bus-locked instruction used only to read the value on the progress path (docs/OptimizationStyle.md: avoid bus-locked instructions on the data path). The while loop in uct_rocm_ipc_export_cleanup (L283-L285) can then be a plain assignment of 0.
| int match; | ||
|
|
||
| /* Still inside the free callback. Do not drop the signal yet. */ | ||
| if (uct_rocm_ipc_export_in_free) { |
There was a problem hiding this comment.
uct_rocm_ipc_export_in_free looks unreachable: uct_rocm_ipc_export_reap() is only called from uct_rocm_ipc_export_add() and uct_rocm_ipc_export_progress(), and neither is reachable from the UCM free callback — when is this guard hit? If never, please drop uct_rocm_ipc_export_in_free (REVIEW.md: avoid redundant state).
|
🤖 CI Triage Agent — TL;DR: The ASAN GPU gtest job failed on a single protocol‑selection test, Full analysisSummary: Root cause: The test is a pure protocol‑selection assertion, not a data‑path test. It runs with Implicated commit: Not this PR. The provenance/estimation test family and the File: test/gtest/ucp/test_ucp_proto.cc:805‑813 (test body) and test/gtest/ucp/test_ucp_proto.cc:438 ( Suggested fix: Strengthen the precondition so the test only runs where the forced scheme can actually be selected:
Related: PR #12077 (the PR under test, unrelated to the failure); prior diagnosis of this exact test name in Azure protocol selection: #11858, #11861, #11864, and #11685 / #11990. |
4425339 to
3e32241
Compare
|
🤖 CI Triage Agent — TL;DR: The ROCm build fails to compile Full analysisSummary: Root cause: Two distinct source defects introduced by the merge commit on this branch:
Implicated commit: File: Suggested fix:
Related: PR #12077 (branch
|
8aa2e5e to
3e32241
Compare
|
🤖 Starting review — findings will be posted here when done. |
|
|
||
| ucs_list_head_init(&deferred); | ||
| ucs_rw_spinlock_write_lock(&uct_rocm_ipc_remote_cache.lock); | ||
| claimed = uct_rocm_ipc_cache_region_drop_locked(region, &deferred); |
There was a problem hiding this comment.
The async handler detaches the mapping as soon as the free event fires — what prevents a detach while a put/get zcopy issued on this mapping is still in flight? region->refcount tracks only pgtable/handler/watcher, not outstanding operations (cuda_ipc refcounts per map/unmap for this reason).
|
|
||
| if ((mapped_addr != NULL) && | ||
| (hsa_amd_ipc_memory_detach(mapped_addr) != HSA_STATUS_SUCCESS)) { | ||
| ucs_fatal("failed to unmap addr:%p", mapped_addr); |
There was a problem hiding this comment.
| ucs_fatal("failed to unmap addr:%p", mapped_addr); | |
| ucs_warn("failed to unmap addr:%p", mapped_addr); |
A failed detach in a cleanup path should not abort the process; the current code uses ucs_warn. Same for the two unwind paths in uct_rocm_ipc_cache_map_region.
|
|
||
| ucs_list_head_init(&deferred); | ||
| ucs_rw_spinlock_write_lock(&uct_rocm_ipc_remote_cache.lock); | ||
| if ((ucs_atomic_cswap32(®ion->deactivate, 0, 0) != 0) || |
There was a problem hiding this comment.
deactivate, handler_done, signal_owner and in_pgtable are only touched while the remote-cache write lock is held, so can we make them plain fields and drop the atomics? ucs_atomic_cswap32(®ion->deactivate, 0, 0) as a plain read is hard to follow.
| void uct_rocm_ipc_export_progress(void) | ||
| { | ||
| /* Writers hold the export lock. A stale zero only delays reaping. */ | ||
| if (ucs_likely(uct_rocm_ipc_export_retired_count == 0)) { |
There was a problem hiding this comment.
uct_rocm_ipc_export_retired_count is written under uct_rocm_ipc_export_lock but read here without it; pls make it volatile (or read it under the lock), otherwise tsan flags the race.
|
|
||
|
|
||
| static int | ||
| uct_rocm_ipc_ipc_signal_is_empty(const hsa_amd_ipc_signal_t *ipc_signal) |
There was a problem hiding this comment.
| uct_rocm_ipc_ipc_signal_is_empty(const hsa_amd_ipc_signal_t *ipc_signal) | |
| uct_rocm_ipc_signal_is_empty(const hsa_amd_ipc_signal_t *ipc_signal) |
minor: ipc is duplicated in the name; pls update the call site too.
| uct_rocm_ipc_export_t *exp, *tmp; | ||
| uintptr_t start, end; | ||
|
|
||
| (void)arg; |
There was a problem hiding this comment.
minor: pls drop (void)arg; here and (void)value; in uct_rocm_ipc_cache_signal_cb; UCX leaves unused callback parameters as-is.
| } uct_rocm_ipc_export_t; | ||
|
|
||
|
|
||
| static ucs_list_link_t uct_rocm_ipc_export_active; |
There was a problem hiding this comment.
| static ucs_list_link_t uct_rocm_ipc_export_active; | |
| static ucs_list_link_t uct_rocm_ipc_export_active; | |
| static ucs_list_link_t uct_rocm_ipc_export_retired; | |
| static ucs_spinlock_t uct_rocm_ipc_export_lock; | |
| static uint32_t uct_rocm_ipc_export_retired_count; |
minor: pls align the declarations on one column (docs/CodeStyle.md).
|
Residual coverage gaps: beyond what was already raised, nothing exercises a free event arriving while a zcopy is in flight on the imported mapping, and nothing exercises |
|
🤖 Starting review — findings will be posted here when done. |
|
|
||
| if ((mapped_addr != NULL) && | ||
| (hsa_amd_ipc_memory_detach(mapped_addr) != HSA_STATUS_SUCCESS)) { | ||
| ucs_fatal("failed to unmap addr:%p", mapped_addr); |
There was a problem hiding this comment.
pls keep this as ucs_warn - the detach failure escalated from ucs_warn to ucs_fatal. drop_locked runs from uct_rocm_ipc_cache_purge at static cleanup (via uct_rocm_ipc_destroy_cache), so a detach failure (e.g. the exporting peer already exited, which is a normal IPC scenario) would now abort every importer at exit instead of warning. docs/CodeStyle.md says a destructor should handle an error by printing a warning or error message.
| ucs_fatal("failed to unmap addr:%p", mapped_addr); | |
| if ((mapped_addr != NULL) && | |
| (hsa_amd_ipc_memory_detach(mapped_addr) != HSA_STATUS_SUCCESS)) { | |
| ucs_warn("failed to unmap addr:%p", mapped_addr); | |
| } |
Same escalation applies to the two new ucs_fatal detach calls in uct_rocm_ipc_cache_map_region (around lines 542 and 579).
| if (ucs_unlikely(hsa_status != HSA_STATUS_SUCCESS)) { | ||
| ucs_error("%s: failed to open ipc mem handle. addr:%p len:%lu hsa_status:%d", | ||
| cache->name, (void *)key->address, key->length, hsa_status); | ||
| ucs_error("%s: failed to open ipc mem handle. addr:%p len:%lu", |
There was a problem hiding this comment.
pls keep hsa_status in the message - the rewrapped log line lost the hsa_status:%d argument while hsa_status is still available, so the actual HSA failure cause is no longer printed. docs/LoggingStyle.md requires printing the exact cause of the error.
| ucs_error("%s: failed to open ipc mem handle. addr:%p len:%lu", | |
| ucs_error("%s: failed to open ipc mem handle. addr:%p len:%lu " | |
| "hsa_status:%d", | |
| cache->name, (void*)key->address, key->length, hsa_status); |
| int claimed; | ||
|
|
||
| (void)value; | ||
| if (ucs_atomic_cswap32(®ion->handler_done, 0, 1) != 0) { |
There was a problem hiding this comment.
is handler_done needed? hsa_amd_signal_async_handler calls the handler once per registration and uct_rocm_ipc_cache_signal_cb returns false, so it cannot re-enter. The region already carries deactivate, signal_owner, in_pgtable, and refcount; REVIEW.md asks to avoid redundant state.
| } uct_rocm_ipc_export_t; | ||
|
|
||
|
|
||
| static ucs_list_link_t uct_rocm_ipc_export_active; |
There was a problem hiding this comment.
minor: inconsistent alignment in the new globals block - pls align these on the column.
| static ucs_list_link_t uct_rocm_ipc_export_active; | |
| static ucs_list_link_t uct_rocm_ipc_export_active; | |
| static ucs_list_link_t uct_rocm_ipc_export_retired; | |
| static ucs_spinlock_t uct_rocm_ipc_export_lock; | |
| static uint32_t uct_rocm_ipc_export_retired_count; | |
| static ucs_init_once_t uct_rocm_ipc_export_handler_once = | |
| UCS_INIT_ONCE_INITIALIZER; |
|
Residual coverage gap: nothing exercises a detach failure in the teardown path, which is exactly where the new |
|
🤖 Starting review — findings will be posted here when done. |
|
every imported region now holds an attached IPC signal plus an armed HSA async handler until the exporter frees the buffer, and this cache never evicts regions (there is no
if ((region->signal.handle == 0) ||
(hsa_signal_load_scacquire(region->signal) > 0)) {
ucs_trace("%s: rocm_ipc cache hit addr:%p size:%lu region:"
UCS_PGT_REGION_FMT,
cache->name, (void*)key->address, key->length,
UCS_PGT_REGION_ARG(®ion->super));
*region_p = region;
return UCS_OK;
}Residual coverage gaps beyond what was already raised: nothing exercises a large number of simultaneously imported regions, which is where the per-region signal/handler accumulation becomes visible, and nothing measures the added per-operation signal read on the zcopy path. Both need ROCm hardware with two processes rather than CI inspection. |
|
🤖 Starting review — findings will be posted here when done. |
|
In Residual coverage gaps: nothing exercises an exporter with |
|
🤖 Starting review — findings will be posted here when done. |
| uint64_t remote_addr, uct_rkey_t rkey, | ||
| uct_completion_t *comp) | ||
| { | ||
| uct_rocm_ipc_unpacked_rkey_t *unpacked = |
There was a problem hiding this comment.
minor: super is at offset 0, so &unpacked->super is literally rkey and this path never touches the new fields - pls keep uct_rocm_ipc_key_t *key = (uct_rocm_ipc_key_t*)rkey; here and in get_zcopy to avoid growing an already large diff with a no-op cast round-trip.
| static ucs_config_field_t uct_rocm_ipc_cache_config_table[] = { | ||
| {"SIGNAL_INVALIDATE", "y", | ||
| "Detach cached ROCm IPC mappings when the exported buffer is freed.\n" | ||
| "The IPC signal remains part of the packed rkey when this is n.", |
There was a problem hiding this comment.
minor: in uct_rocm_ipc_cache_config_table, the second line describes the packed-rkey layout rather than what the user gets, and it reads as if the signal is still exported when disabled (it is zeroed by export_add). Suggested:
| "The IPC signal remains part of the packed rkey when this is n.", | |
| "Detach cached ROCm IPC mappings when the exported buffer is freed.\n" | |
| "When set to n, a mapping is replaced only when the peer's IPC handle\n" | |
| "changes. The packed rkey layout is the same in both cases.", |
|
In Residual coverage gaps beyond what was already raised: nothing exercises a first |
|
🤖 Starting review — findings will be posted here when done. |
|
Config table help text: the first help line is duplicated, so it shows up twice in
minor: Residual coverage gaps: nothing exercises the exporter retired-export list in a free-heavy / registration-idle pattern, which is where the signal accumulation becomes visible (needs ROCm hardware with two processes rather than CI inspection); and nothing checks the new |
|
🤖 Starting review — findings will be posted here when done. |
|
🤖 Review complete — no issues found. |
There was a problem hiding this comment.
I verified the logic both by reading the code and running tests, and it looks correct for both potential scenarios (using the ipc_signals to trigger a release of the ipc mapping as well as disabling the feature).
Except for the one comment about the location of the new parameter, this is PR is good as far as I can tell.
|
🤖 Starting review — findings will be posted here when done. |
|
In
In Wire compatibility of the new
In-flight zcopy / missing reference on the Residual coverage gaps: nothing exercises the pre-arm window (free event arriving between |
|
🤖 Starting review — findings will be posted here when done. |
|
In |
|
🤖 CI Triage Agent — TL;DR: The build failed on a single gtest, Full analysisSummary: Root cause: The test forces Implicated commit: Test assertion introduced by 9cc0808 "UCP/RNDV: use tag-only signal for CUDA staging force" (Nathan Bellalou); most likely behavior shift from [REDACTED:Hex High Entropy String] (#12041, Guy Ealey Morag) / af8174b (#12039, Thomas Vegas). Not commit 63a1ccd (PR #12077). File: test/gtest/ucp/test_ucp_proto.cc:710-737 (assertions at :728-730 and :736); instantiation at test/gtest/ucp/test_ucp_proto.cc:858 Suggested fix: 1) Re-run the Azure job for PR #12077 — the failure is unrelated to the ROCm IPC-cache change. 2) Make the test robust instead of asserting on a formatted protocol string: gate it on the forced-pipeline path actually being selectable (as the sibling test does by first checking for Related: PR #12077 (this PR), and the same test reported failing on unrelated PRs #12054, #12058, #12081, #11990; relevant protocol-selection changes: #12041, #12039. Note: the Azure log API returned only the last ~500 lines of the job, which contain the gtest summary but not the individual
|
edgargabriel
left a comment
There was a problem hiding this comment.
Looks good to me, and also passed in my testing.
However, because this is a massive PR, I will hold back merging the PR to happen after two smaller ones:
This way we can pin that UCX hash for a NIXL task with all required fixes before this PR is merged, just in case some issues pop up. I expect those two PRs to be merged within the next 1-2 days.
…o drop IPC (openucx#12077) * UCT/ROCM: Add removal of remote mapping when owning buffer is freed to drop IPC * UCT/ROCM: review fixes * UCT/ROCM: fix for review feedback * UCT/ROCM: added environment variable to control cache invalidate * UCT/ROCM: Implemented export reuse per allocation * UCT/ROCM: Cleanup of minor review items * UCT/ROCM: Fix for teardown lifetime issue and implemented export reuse per allocation. * UCT/ROCM: Added hash based lookup * UCT/ROCM: Fixes based on review * UCT/ROCM: Fix for review feedback * UCT/ROCM: Merged two cache-hit branches * UCT/ROCM: Added early return * UCT/ROCM: Fixes based on review feedback * UCT/ROCM: Fixes to address reviewer requests * UCT/ROCM: Moved config parameter * UCT/ROCM: Additional minor changes from review
What?
The ROCm IPC cache now drops a remote mapping when the owning buffer is freed.
Why?
ROCm IPC mappings stay cached, because opening a handle is expensive.
hipFreecannot finish while any importer still has that mapping attached, so the exporter publishes one IPC signal per buffer and importers detach as soon as that signal becomes0. The signal is not destroyed until every importer that could be blocking the free already holds its own reference.How?
Wire format
uct_rocm_ipc_key_tcarries the existinghsa_amd_ipc_memory_tplus onehsa_amd_ipc_signal_t. The packedrkeysize is the whole key, so this is a same-node wire-format change and not a public UCT or UCP API change. The signal handle is created once, at pack time, and is the same value for every peer. An empty signal is rejected on import.Exporter
uct_rocm_ipc_pack_key()creates the memory handle, thenuct_rocm_ipc_export_add()creates anHSA_AMD_SIGNAL_IPCsignal with value1and exports it into the key. The local signal is owned by a process-global export table keyed by buffer address, not by the memory handle.mem_deregonly frees the key. The signal stays until the allocation is actually released.The first export installs a
UCM MEM_TYPE_FREEhandler at priority 100, ahead of the registration cache at 1000. Installation is lazy, because UCM events may not be enabled at static init. On a ROCm or ROCm-managed free, before the original free runs, the handler stores 0 to each overlapping signal and moves those exports from the active list to the retired list. It does not destroy the signal in the callback. The callback does not allocate.Retired exports are destroyed later by
uct_rocm_ipc_export_progress(), called fromuct_rocm_ipc_iface_progress(). A plain retired count skips the export lock when nothing is waiting. Any progress thread may reap, because importers attach the signal before they map memory. Process cleanup unregisters the UCM handler, destroys both lists, and clears the count. The count is written only under the export lock.Importer cache
The cache remains process-global and is keyed by owner pid, pid namespace, and device. A page table in each cache is keyed by remote address. A hit whose IPC memory bytes match returns the existing local mapping and does not arm another handler.
On a miss, still under the remote-cache write lock:
hsa_signal_destroyand the async handler are not called while the cache lock is held. The lock is not recursive, and the ROCr handler thread takes it.Unmap
Three references cover a region: the page table, the watcher that arms the handler, and the handler. deactivate claims unmap once. The claim winner detaches the mapping immediately, unlinks the page table, and queues the region. Purge, range invalidate, and a stale IPC byte string are all claims. Detach happens in the handler rather than on iface progress, because hipFree is blocked until the mapping is gone.
Signal ownership is separate. Before arming, the watcher marks the handler as the signal owner. A claim that wins before that destroys the signal itself. A claim that wins after that only detaches memory and leaves the signal for the callback, so ROCr is not left waiting on a destroyed signal and the handler reference is not leaked. If the region was already claimed before the handler existed, the watcher does not arm.
The callback is one-shot. It claims unmap if nobody else has, destroys the signal when it still owns it, and drops its region reference. Returning false disarms it. handler_done ignores the re-entry that hsa_signal_destroy can cause.
After a successful arm, the watcher loads the signal again. If it is already
0, it claims unmap immediately so the free does not wait for the ROCr thread. The handler still owns the signal and its reference.Paths that do not watch
CACHE_IPC_HANDLES=nstill creates the export signal, because the key is packed at registration time. The uncached zcopy path attaches memory for one copy and detaches when that copy completes. It does not watch the signal. Device mem_elem_pack and rkey_ptr always go through the cache, which is the same rule as before.Copy completion still uses the existing per-iface signal pool in rocm_signal.c. Those signals are unrelated to the IPC lifetime signal.
What stays out of scope
No IPC policy was added to the UCM ROCm hooks. UCM only delivers the free event. The cache, the export table, and the handler all stay in src/uct/rocm/ipc.