Skip to content

Commit 67f317f

Browse files
authored
TL/CUDA: fix NVLS fallback and EC persistent hangs (#1320)
## What Fix TL/CUDA NVLS hangs/failures that broke `test_c10d_ucc.py` (e.g. `test_ddp_checkpointing_dynamic_module`) on aarch64 (Grace/GB200/VR200) and in containers without ptrace permission: - **NVLS init deadlock**: if one rank fails to import the peer multicast handle (e.g. `pidfd_getfd` EPERM in a restricted container), it fell back while the other ranks blocked forever in the collective `cuMulticastBindAddr`. All ranks now exchange import status and disable NVLS together. - **EC persistent executor hang on weakly-ordered CPUs**: the CPU↔GPU shutdown and task-publish flags in device-mapped memory used inner-shareable fences, which don't order against the GPU on aarch64, so the persistent kernel never saw the update. Use the bus (outer-shareable) fences. - **Wrong-result after NVLS fallback**: allreduce was still routed to the NVLS algorithm (based on a static HW capability check) even when NVLS did not initialize. Advertise/route NVLS allreduce only when NVLS is actually enabled. - **Diagnostics**: on a peer-fd import denial, emit one actionable warning (ptrace_scope / CAP_SYS_PTRACE / docker / enroot hints); keep the rest at debug so a supported fallback is not noisy. ## Why ? On aarch64 (GB200/VR200) and permission-restricted containers, NVLS either deadlocked at team creation (hang in the first DDP collective) or silently produced wrong results after falling back. NCCL worked, so it was UCC-specific. Root-caused on a VR200 node: the progress thread was stuck in `ucc_cuda_executor_persistent_stop`, and the peer-fd EPERM caused an asymmetric NVLS init. Fixes: **RM 5113172** ## How ? - `tl_cuda_nvls`: add `STATE_SYNC_STATUS` that allgathers each rank's import result; disable NVLS team-wide on any failure. Track `nvls.enabled` (set only after the final NVLS barrier). - `ec_cuda_executor_persistent`: publish/poll shutdown and task-post flags with `ucc_memory_bus_store_fence()` / `ucc_memory_bus_load_fence()`. - `tl_cuda_team` / `allreduce`: gate advertising and dispatch of NVLS allreduce on `nvls.enabled`. Validated on VR200 (aarch64) and H100 (x86): `test_c10d_ucc.py` passes (50 passed / 14 skipped), single-node and multinode NVLS allreduce correct, and the forced-EPERM path falls back cleanly instead of hanging.
1 parent c3e8ede commit 67f317f

5 files changed

Lines changed: 215 additions & 40 deletions

File tree

src/components/ec/cuda/ec_cuda_executor_persistent.c

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,12 @@ ucc_cuda_executor_persistent_task_post(ucc_ee_executor_t *executor,
7070
memcpy(ee_task->subtasks[0], task_args,
7171
sizeof(ucc_ee_executor_task_args_t));
7272
}
73-
ucc_memory_cpu_store_fence();
73+
/* tasks[] and pidx live in device-mapped (zero-copy) host memory shared
74+
* with the persistent kernel. Use the bus (outer-shareable) store fence so
75+
* the task args are guaranteed visible to the GPU before the updated pidx
76+
* that publishes them; the inner-shareable CPU fence does not order stores
77+
* against the GPU's shareability domain on weakly-ordered CPUs (aarch64). */
78+
ucc_memory_bus_store_fence();
7479
eee->pidx += ee_task->num_subtasks;
7580
if (ucc_ec_cuda.thread_mode == UCC_THREAD_MULTIPLE) {
7681
ucc_spin_unlock(&eee->tasks_lock);
@@ -163,7 +168,21 @@ ucc_status_t ucc_cuda_executor_persistent_stop(ucc_ee_executor_t *executor)
163168
(*st != UCC_EC_CUDA_EXECUTOR_SHUTDOWN));
164169
*st = UCC_EC_CUDA_EXECUTOR_SHUTDOWN;
165170
eee->pidx = -1;
166-
while(*st != UCC_EC_CUDA_EXECUTOR_SHUTDOWN_ACK) { }
171+
/* state/pidx live in device-mapped (zero-copy) host memory the persistent
172+
* kernel polls. Publish these stores to the GPU before spinning: on
173+
* weakly-ordered CPUs (aarch64/Grace) the store to pidx may sit in the CPU
174+
* store buffer while this loop spins, and the cheaper inner-shareable CPU
175+
* store fence only orders against other CPUs, not the GPU. The bus
176+
* (outer-shareable) store fence drains/orders the store to the domain the
177+
* GPU observes; without it the kernel never sees pidx == -1, never writes
178+
* SHUTDOWN_ACK, and this loop hangs forever. */
179+
ucc_memory_bus_store_fence();
180+
/* No load fence here: st is volatile (re-read every iteration) and points
181+
* to coherent device-mapped memory, so the kernel's SHUTDOWN_ACK write
182+
* becomes visible on its own. A fence would only order accesses, not force
183+
* a cache re-read, and there is no dependent load after the poll. */
184+
while (*st != UCC_EC_CUDA_EXECUTOR_SHUTDOWN_ACK) {
185+
}
167186
eee->super.ee_context = NULL;
168187
eee->state = UCC_EC_CUDA_EXECUTOR_INITIALIZED;
169188

src/components/tl/cuda/allreduce/allreduce.c

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,15 @@ ucc_status_t ucc_tl_cuda_allreduce_init(ucc_base_coll_args_t *coll_args,
3131
{
3232
ucc_status_t status = UCC_ERR_NOT_IMPLEMENTED;
3333
#ifdef HAVE_NVLS
34+
ucc_tl_cuda_team_t *cuda_team = ucc_derived_of(team, ucc_tl_cuda_team_t);
35+
36+
/* NVLS is the only allreduce algorithm in TL/CUDA. If NVLS did not
37+
* initialize for this team (e.g. it fell back because the peer fd import
38+
* was denied), report NOT_SUPPORTED so the collective is served by another
39+
* transport instead of dispatching to uninitialized NVLS resources. */
40+
if (!cuda_team->nvls.enabled) {
41+
return UCC_ERR_NOT_SUPPORTED;
42+
}
3443
/* Use NVLS algorithm as default */
3544
status = ucc_tl_cuda_allreduce_nvls_init(coll_args, team, task_h);
3645
#else

src/components/tl/cuda/tl_cuda_nvls.c

Lines changed: 155 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
#include <sys/syscall.h> // for pidfd_open and pidfd_getfd
1616
#include <sys/prctl.h> // for prctl()
1717
#include <unistd.h> // for close()
18+
#include <errno.h> // for EPERM/EACCES
19+
#include <string.h> // for strerror()
1820

1921
/* RHEL 8 glibc headers (kernel 4.18) don't define pidfd syscall numbers */
2022
#ifndef SYS_pidfd_open
@@ -195,6 +197,41 @@ static ucc_status_t ucc_tl_cuda_nvls_share_handles(
195197
return status;
196198
}
197199

200+
/* Log a peer-fd import failure. Always emits a debug line with the details;
201+
* when the failure is a permission denial (the common single-node case: Yama
202+
* ptrace_scope, missing CAP_SYS_PTRACE, or a seccomp filter blocking
203+
* pidfd_getfd) it additionally emits a single, actionable warning (once per
204+
* process) telling the user how to enable NVLS instead of silently losing it. */
205+
static void ucc_tl_cuda_nvls_report_peer_import_denied(
206+
ucc_tl_cuda_team_t *team, const char *what, int err)
207+
{
208+
static volatile int warned = 0;
209+
210+
tl_debug(
211+
UCC_TL_TEAM_LIB(team),
212+
"%s: %s (errno=%d); NVLS not available, falling back",
213+
what,
214+
strerror(err),
215+
err);
216+
217+
if ((err != EPERM && err != EACCES) || warned) {
218+
return;
219+
}
220+
warned = 1;
221+
tl_warn(
222+
UCC_TL_TEAM_LIB(team),
223+
"NVLS disabled: importing a peer process file descriptor was denied "
224+
"(%s). Single-node NVLS needs permission to access peer GPU memory "
225+
"handles across processes. To enable NVLS, relax ptrace "
226+
"restrictions on the host/container, e.g. host: "
227+
"'sysctl -w kernel.yama.ptrace_scope=0'; docker: add "
228+
"'--cap-add=SYS_PTRACE' (and if pidfd_getfd is blocked by seccomp, "
229+
"'--security-opt seccomp=unconfined'); enroot: run with "
230+
"'--container-remap-root' or set 'kernel.yama.ptrace_scope=0' on the "
231+
"host. Collectives fall back to another transport in the meantime.",
232+
strerror(err));
233+
}
234+
198235
static ucc_status_t ucc_tl_cuda_nvls_import_handle_posix(
199236
struct ucc_tl_cuda_team *team, ucc_tl_cuda_nvls_handle_t *share_data,
200237
CUmemGenericAllocationHandle *mc_handle)
@@ -217,20 +254,21 @@ static ucc_status_t ucc_tl_cuda_nvls_import_handle_posix(
217254

218255
pid_fd = syscall(SYS_pidfd_open, target_pid, 0);
219256
if (pid_fd < 0) {
220-
tl_error(
221-
UCC_TL_TEAM_LIB(team),
222-
"failed to open pidfd for pid %d",
223-
target_pid);
257+
/* Expected fallback condition (e.g. restricted ptrace/seccomp in a
258+
* container): report and let the team fall back to another transport
259+
* instead of emitting a scary error. */
260+
ucc_tl_cuda_nvls_report_peer_import_denied(
261+
team, "failed to open peer pidfd", errno);
224262
return UCC_ERR_NO_RESOURCE;
225263
}
226264

227265
peer_fd = syscall(SYS_pidfd_getfd, pid_fd, export_handle, 0);
228266
if (peer_fd < 0) {
229-
tl_error(
230-
UCC_TL_TEAM_LIB(team),
231-
"failed to get peer fd: %s (errno=%d)",
232-
strerror(errno),
233-
errno);
267+
/* EPERM here typically means the container lacks the permissions to
268+
* import a peer's fd (Yama ptrace_scope, missing CAP_SYS_PTRACE, or a
269+
* seccomp filter). Supported fallback condition. */
270+
ucc_tl_cuda_nvls_report_peer_import_denied(
271+
team, "failed to get peer fd", errno);
234272
close(pid_fd);
235273
return UCC_ERR_NO_RESOURCE;
236274
}
@@ -255,9 +293,10 @@ static ucc_status_t ucc_tl_cuda_nvls_import_handle_posix(
255293
}
256294

257295
if (status != UCC_OK) {
258-
tl_error(
296+
tl_debug(
259297
UCC_TL_TEAM_LIB(team),
260-
"failed to import POSIX file descriptor handle from rank 0");
298+
"failed to import POSIX file descriptor handle from rank 0; "
299+
"NVLS not available, falling back");
261300
return status;
262301
}
263302

@@ -281,9 +320,10 @@ static ucc_status_t ucc_tl_cuda_nvls_import_handle_fabric(
281320
mc_handle, &share_data->data.fabric, CU_MEM_HANDLE_TYPE_FABRIC));
282321

283322
if (status != UCC_OK) {
284-
tl_error(
323+
tl_debug(
285324
UCC_TL_TEAM_LIB(team),
286-
"failed to import fabric handle from rank 0. status (%d) %s",
325+
"failed to import fabric handle from rank 0. status (%d) %s; "
326+
"NVLS not available, falling back",
287327
status,
288328
ucc_status_string(status));
289329
return status;
@@ -427,9 +467,10 @@ ucc_status_t ucc_tl_cuda_nvls_init(
427467
&nvls->local_handle.data.posix.handle);
428468
}
429469
if (status != UCC_OK) {
430-
tl_error(
470+
tl_debug(
431471
UCC_TL_TEAM_LIB(team),
432-
"failed to create multicast object. status (%d) %s",
472+
"failed to create multicast object. status (%d) %s; "
473+
"NVLS not available, falling back",
433474
status,
434475
ucc_status_string(status));
435476
/* Keep going to unblock peers waiting in the allgather;
@@ -480,14 +521,22 @@ ucc_status_t ucc_tl_cuda_nvls_init(
480521
team->state = UCC_TL_CUDA_NVLS_STATE_IMPORT_HANDLE;
481522
// fall through
482523
case UCC_TL_CUDA_NVLS_STATE_IMPORT_HANDLE:
524+
/* Optimistically assume this rank is ready; cleared below on any local
525+
* import failure so STATE_SYNC_STATUS can disable NVLS team-wide. */
526+
nvls->init_ready = 1;
483527
/* Non-root ranks check the status field broadcast by rank 0 before
484528
* attempting to import a potentially garbage handle. */
485529
if (UCC_TL_TEAM_RANK(team) != 0) {
486530
if (nvls->share_data[0].status != UCC_OK) {
487-
tl_warn(UCC_TL_TEAM_LIB(team),
488-
"NVLS: rank 0 failed to create multicast object "
489-
"(status=%d); disabling NVLS for this team",
490-
nvls->share_data[0].status);
531+
/* Rank 0 failed to create the multicast object. Every non-root
532+
* rank observes this via the broadcast status, and rank 0 bails
533+
* out below through status_supported, so the failure is already
534+
* symmetric and it is safe to clean up directly. */
535+
tl_debug(
536+
UCC_TL_TEAM_LIB(team),
537+
"NVLS: rank 0 failed to create multicast object "
538+
"(status=%d); disabling NVLS for this team",
539+
nvls->share_data[0].status);
491540
status = nvls->share_data[0].status;
492541
nvls->status_supported = status;
493542
goto cleanup;
@@ -500,17 +549,90 @@ ucc_status_t ucc_tl_cuda_nvls_init(
500549
team, &nvls->share_data[0], &mc_handle);
501550
}
502551
if (status != UCC_OK) {
503-
goto cleanup;
552+
/* Import failed on this rank only (e.g. pidfd_getfd EPERM in a
553+
* restricted container). Do NOT clean up directly: rank 0 and
554+
* the ranks that imported successfully would block forever in
555+
* the collective cuMulticastBindAddr barrier. Record the local
556+
* failure and let STATE_SYNC_STATUS propagate it so all ranks
557+
* disable NVLS together. */
558+
nvls->init_ready = 0;
559+
} else {
560+
nvls->mc_handle = mc_handle;
504561
}
505-
nvls->mc_handle = mc_handle;
506562
}
507563
if (nvls->status_supported != UCC_OK) {
508-
// Propagate the supported status to the caller
564+
/* Rank 0 local creation failure: non-root ranks already saw this via
565+
* the broadcast status, so cleaning up here keeps the team
566+
* symmetric. */
509567
status = nvls->status_supported;
510568
goto cleanup;
511569
}
570+
team->state = UCC_TL_CUDA_NVLS_STATE_SYNC_STATUS;
571+
// fall through
572+
case UCC_TL_CUDA_NVLS_STATE_SYNC_STATUS:
573+
{
574+
/* Collectively agree on whether every rank imported the multicast
575+
* handle. If any rank failed, all ranks skip the multicast binding and
576+
* fall back together; otherwise the ranks that succeeded would deadlock
577+
* in cuMulticastBindAddr waiting for the failed rank to call
578+
* cuMulticastAddDevice. */
579+
ucc_rank_t r;
580+
581+
if (nvls->init_sync_data == NULL) {
582+
nvls->init_sync_data = (char *)ucc_malloc(
583+
UCC_TL_TEAM_SIZE(team), "nvls_init_sync");
584+
if (!nvls->init_sync_data) {
585+
status = UCC_ERR_NO_MEMORY;
586+
goto cleanup;
587+
}
588+
nvls->init_sync_data[UCC_TL_TEAM_RANK(team)] = (char)
589+
nvls->init_ready;
590+
}
591+
592+
if (team->oob_req == NULL) {
593+
status = team->oob.allgather(
594+
&nvls->init_sync_data[UCC_TL_TEAM_RANK(team)],
595+
nvls->init_sync_data,
596+
1,
597+
team->oob.coll_info,
598+
&team->oob_req);
599+
if (status != UCC_OK) {
600+
tl_error(
601+
UCC_TL_TEAM_LIB(team),
602+
"failed to initiate NVLS init status exchange");
603+
goto cleanup;
604+
}
605+
}
606+
607+
status = team->oob.req_test(team->oob_req);
608+
if (status > 0) {
609+
return UCC_INPROGRESS;
610+
}
611+
if (status < 0) {
612+
tl_error(UCC_TL_TEAM_LIB(team), "NVLS init status exchange failed");
613+
team->oob.req_free(team->oob_req);
614+
team->oob_req = NULL;
615+
goto cleanup;
616+
}
617+
team->oob.req_free(team->oob_req);
618+
team->oob_req = NULL;
619+
620+
for (r = 0; r < UCC_TL_TEAM_SIZE(team); r++) {
621+
if (nvls->init_sync_data[r] == 0) {
622+
tl_debug(
623+
UCC_TL_TEAM_LIB(team),
624+
"NVLS: rank %u could not initialize NVLS; disabling "
625+
"NVLS for the whole team and falling back",
626+
r);
627+
status = UCC_ERR_NOT_SUPPORTED;
628+
goto cleanup;
629+
}
630+
}
631+
ucc_free(nvls->init_sync_data);
632+
nvls->init_sync_data = NULL;
512633
team->state = UCC_TL_CUDA_NVLS_STATE_ADD_DEVICE;
513634
// fall through
635+
}
514636
case UCC_TL_CUDA_NVLS_STATE_ADD_DEVICE:
515637
{
516638
// Allocate physical memory
@@ -686,6 +808,9 @@ ucc_status_t ucc_tl_cuda_nvls_init(
686808
ucc_free(nvls->barrier_data);
687809
nvls->barrier_data = NULL;
688810

811+
/* NVLS is fully initialized for this team; only now may collectives be
812+
* routed to the NVLS algorithms (see ucc_tl_cuda_get_supported_colls). */
813+
nvls->enabled = 1;
689814
tl_debug(UCC_TL_TEAM_LIB(team),
690815
"NVLS init: rank %d OOB barrier complete — team ready",
691816
UCC_TL_TEAM_RANK(team));
@@ -707,6 +832,10 @@ ucc_status_t ucc_tl_cuda_nvls_init(
707832
ucc_free(nvls->barrier_data);
708833
nvls->barrier_data = NULL;
709834
}
835+
if (nvls->init_sync_data) {
836+
ucc_free(nvls->init_sync_data);
837+
nvls->init_sync_data = NULL;
838+
}
710839

711840
// Clean up CUDA resources - check local variables for partial allocations
712841
// Unmap and free multicast VA if it was reserved/mapped
@@ -807,5 +936,9 @@ ucc_status_t ucc_tl_cuda_nvls_destroy(ucc_tl_cuda_team_t *team)
807936
ucc_free(team->nvls.barrier_data);
808937
team->nvls.barrier_data = NULL;
809938
}
939+
if (team->nvls.init_sync_data) {
940+
ucc_free(team->nvls.init_sync_data);
941+
team->nvls.init_sync_data = NULL;
942+
}
810943
return UCC_OK;
811944
}

src/components/tl/cuda/tl_cuda_nvls.h

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ typedef enum {
3939
UCC_TL_CUDA_NVLS_STATE_INIT,
4040
UCC_TL_CUDA_NVLS_STATE_SHARE_HANDLES,
4141
UCC_TL_CUDA_NVLS_STATE_IMPORT_HANDLE,
42+
UCC_TL_CUDA_NVLS_STATE_SYNC_STATUS,
4243
UCC_TL_CUDA_NVLS_STATE_ADD_DEVICE,
4344
UCC_TL_CUDA_NVLS_STATE_BARRIER,
4445
} ucc_tl_cuda_nvls_state_t;
@@ -74,6 +75,18 @@ typedef struct ucc_tl_cuda_nvls {
7475
size_t gran;
7576
/* temporary buffer for STATE_BARRIER */
7677
char *barrier_data;
78+
/* Whether this rank locally succeeded in importing the multicast handle.
79+
* Exchanged across ranks in STATE_SYNC_STATUS so a per-rank import failure
80+
* (e.g. pidfd_getfd EPERM) disables NVLS on the whole team instead of
81+
* deadlocking the ranks that succeeded in cuMulticastBindAddr. */
82+
int init_ready;
83+
/* temporary buffer for STATE_SYNC_STATUS allgather */
84+
char *init_sync_data;
85+
/* Set to 1 only when NVLS initialization fully succeeded for this team.
86+
* Gates advertising the NVLS collectives: the hardware may support
87+
* multicast while NVLS init fell back (e.g. peer fd import denied), and in
88+
* that case collectives must not be routed to the NVLS algorithms. */
89+
int enabled;
7790
} ucc_tl_cuda_nvls_t;
7891

7992
typedef struct ucc_tl_cuda_nvls_control {

0 commit comments

Comments
 (0)