Skip to content

CORE: build UCX with nixl SONAME suffix, load with RTLD_DEEPBIND to avoid collisions - #1673

Merged
ovidiusm merged 14 commits into
ai-dynamo:mainfrom
roiedanino:deepbind
Jul 2, 2026
Merged

ovidiusm merged 14 commits into
ai-dynamo:mainfrom
roiedanino:deepbind

Conversation

@roiedanino

@roiedanino roiedanino commented May 24, 2026

Copy link
Copy Markdown
Contributor

What?

Add private-UCX loading support for the NIXL wheel by combining:

  • RTLD_DEEPBIND when loading the UCX backend plugin.
  • RTLD_DEEPBIND during nixl_ep_cpp Python extension import.
  • Container build support for UCX builds with a private SONAME suffix.
  • Wheel packaging fixes for auditwheel-renamed UCX libraries with suffixed names.
  • Runtime diagnostics and optional fail-fast validation for the UCX library actually bound by the UCX backend.
  • Helper scripts to validate wheel contents and loader symbol binding.

Why?

The NIXL wheel bundles Python code, NIXL core libraries, the NIXL UCX plugin, UCX core libraries, and UCX modules. In environments such as HPC-X/OpenMPI, another UCX version can already be loaded globally before NIXL is imported or before the NIXL UCX plugin is loaded.

Even if the NIXL wheel contains its own UCX libraries, normal ELF symbol resolution can bind NIXL’s UCX references to the globally loaded UCX instead. That can make NIXL silently use the wrong UCX version and can produce hard-to-debug runtime failures.

This change makes the intended private UCX path explicit and testable.

How?

  • Load the UCX backend plugin with RTLD_DEEPBIND by default.

    • Controlled by NIXL_UCX_DEEPBIND.
    • Non-UCX backend and telemetry plugins keep the existing loading behavior.
  • Import nixl_ep_cpp with RTLD_DEEPBIND when supported by the platform.

    • Uses the same NIXL_UCX_DEEPBIND opt-out behavior.
  • Add UCX build wiring to the container flow.

    • --ucx-soname-suffix <suffix> passes UCX --with-soname-suffix=<suffix>.
    • --private-ucx is a shortcut for the NIXL private suffix.
    • --ucx-repo allows building against a UCX branch/fork that contains the private SONAME support.
  • Improve wheel_add_ucx_plugins.py so auditwheel-renamed libraries such as libucp-nixl-<hash>.so... are mapped correctly.

  • Add UCX backend diagnostics.

    • Logs the UCX version and library path selected at plugin initialization.
    • NIXL_UCX_EXPECTED_SONAME can be set to fail fast if the backend binds to an unexpected UCX library.
  • Add validation helpers.

    • contrib/check_ucx_binding.py checks DT_NEEDED entries and glibc symbol binding behavior with/without RTLD_DEEPBIND.
    • contrib/check_ucx_wheel_bundle.py checks that a repaired wheel contains private UCX libraries, UCX modules, and the NIXL UCX plugin.

    This PR should only be merged (and perhaps reviewed) after CONFIGURE: added a configure option to add SONAME suffix openucx/ucx#11483 was merged

Summary by CodeRabbit

  • New Features
    • Container and wheel builds can now use configurable UCX repo/ref and an optional SONAME suffix that flows through plugin bundling and wheel creation.
    • UCX plugin loading supports optional deep-binding and runtime validation against an expected UCX SONAME.
  • Tests
    • Added checks for UCX dynamic binding behavior and for repaired wheel bundle contents/SONAME suffix consistency.
  • Build/Packaging
    • Wheel plugin bundling now supports suffix-aware naming and CLI-controlled handling of plugin symlinks/alias libraries.

…void collisions

Signed-off-by: Roie Danino <rdanino@nvidia.com>
@github-actions

Copy link
Copy Markdown

👋 Hi roiedanino! Thank you for contributing to ai-dynamo/nixl.

Your PR reviewers will review your contribution then trigger the CI to test your changes.

🚀

Signed-off-by: Roie Danino <rdanino@nvidia.com>
@copy-pr-bot

copy-pr-bot Bot commented May 24, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@roiedanino

Copy link
Copy Markdown
Contributor Author

/build

Signed-off-by: Roie Danino <rdanino@nvidia.com>
@pull-request-size pull-request-size Bot added size/XL and removed size/L labels Jun 2, 2026
@dpressle

dpressle commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

👀 Investigating Run Pre-Commit Hooks

@dpressle

dpressle commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

🤖 CI Triage AgentRun Pre-Commit Hooks · commit 1ce87b60

All the evidence needed is in the logs. Here is the full diagnosis:


Summary: black reformatted contrib/check_ucx_wheel_bundle.py — the file was committed without running the code formatter first.

Root cause: The black pre-commit hook found that contrib/check_ucx_wheel_bundle.py does not conform to Black's style rules. Specifically, two constructs around lines 46–55 need reformatting:

  1. A three-line re.compile(…) assignment that Black collapses to a single line (it fits within Black's line-length limit).
  2. A list comprehension whose name for name in ucx_modules body Black wants split across two lines rather than kept on one.

Black modified the file in-place and the hook exited 1 because the working-tree file changed during the check. The exact diff from the log:

-    module_suffix_pattern = re.compile(
-        rf"-{escaped_suffix}(-[0-9a-f]{{8}})?\.so"
-    )
+    module_suffix_pattern = re.compile(rf"-{escaped_suffix}(-[0-9a-f]{{8}})?\.so")
     modules_without_suffix = [
-        name for name in ucx_modules
+        name
+        for name in ucx_modules
         if not module_suffix_pattern.search(name.rsplit("/", 1)[-1])
     ]

All other hooks (mypy, isort, flake8, codespell, whitespace checks) passed.

Implicated commit: [REDACTED:Hex High Entropy String] (branch deepbind, PR #1673)

File: contrib/check_ucx_wheel_bundle.py — lines 46–55 (the module_suffix_pattern assignment and the modules_without_suffix list comprehension)

Suggested fix: In the deepbind branch, run black contrib/check_ucx_wheel_bundle.py (or simply pre-commit run --files contrib/check_ucx_wheel_bundle.py) locally, stage the resulting change, and amend / push. The exact diff to apply is shown above — it's a pure formatting change with no semantic effect. To prevent recurrence, ensure pre-commit install is run once in the local clone so the hooks fire automatically on every commit.

Related: none

🛡️ This comment had 1 potential secret(s) redacted (Hex High Entropy String). See request_id 188cb7db-b1e7-4385-a223-740d06d94013 in the triage console for the audit trail.

Signed-off-by: Roie Danino <rdanino@nvidia.com>
@roiedanino

roiedanino commented Jun 2, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 2, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Read the relevant section of the file to confirm current content
sed -n '40,65p' contrib/check_ucx_wheel_bundle.py

Length of output: 1112



@roiedanino The file already looks good! The current state of contrib/check_ucx_wheel_bundle.py on the branch already has both Black formatting fixes applied:

  1. re.compile(...) is already collapsed to a single line (Line 49)
  2. ✅ The list comprehension already has name and for name in ucx_modules on separate lines (Lines 50–53)

It looks like the formatting was applied after the CI run on commit 1ce87b60. The next CI run should pass the Run Pre-Commit Hooks check. You can trigger a new build to confirm. 🐇

@coderabbitai

coderabbitai Bot commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR adds configurable UCX source and SONAME-suffix handling in build and wheel tooling, adds wheel and binding validation scripts, and introduces deepbind-aware UCX plugin loading plus UCX library path validation.

Changes

Build and Validation Infrastructure

Layer / File(s) Summary
Docker container build parameterization
contrib/Dockerfile, contrib/Dockerfile.manylinux
Dockerfiles accept UCX build arguments, clone from a configurable UCX repository, probe ./configure --help for optional SONAME and module deepbind support, and append computed UCX flags to configure-release-mt. The manylinux flow also passes the SONAME suffix into wheel build preparation.
Build container script CLI and argument passing
contrib/build-container.sh
Adds UCX repository and SONAME suffix defaults, parses new UCX-related flags, updates displayed build options and help text, and forwards UCX repository, ref, and SONAME suffix values to Docker build arguments.
Wheel bundle and binding validation
contrib/check_ucx_wheel_bundle.py, contrib/check_ucx_binding.py
New Python helpers validate repaired wheel contents and UCX symbol bindings. One script checks private UCX libraries, bundled UCX modules, and the bundled UCX plugin inside a wheel. The other probes a library with optional preload and deepbind settings, checks DT_NEEDED entries, parses LD_DEBUG=bindings output, and enforces expected symbol-to-target matches.
Wheel plugin addition with symlink and suffix handling
contrib/wheel_add_ucx_plugins.py, contrib/build-wheel.sh
wheel_add_ucx_plugins.py adds symlink-handling parameters to plugin copying, updates repaired library name parsing with regex, extends the CLI with plugin skip and SONAME suffix flags, and passes the new options through when bundling UCX and NIXL plugins. build-wheel.sh accepts and forwards the plugin SONAME suffix flag into the UCX plugin bundling step.

Runtime Plugin Loading with RTLD_DEEPBIND Support

Layer / File(s) Summary
Plugin manager deepbind infrastructure
src/core/plugin_manager.h, src/core/nixl_plugin_manager.cpp
plugin_manager.h and nixl_plugin_manager.cpp add a deepbind-aware plugin load path, introduce UCX-specific deepbind configuration handling, and pass the computed deepbind decision through both explicit-path and directory-based plugin loading before calling dlopen.
Python extension import with conditional deepbind
examples/device/ep/nixl_ep/__init__.py
Adds a deepbind-controlled import context for the native extension, temporarily adjusts process dlopen flags when supported, and delays re-exporting Buffer and EventOverlap until after the extension loads.
UCX plugin soname runtime validation
src/plugins/ucx/ucx_plugin.cpp, src/plugins/ucx/meson.build
ucx_plugin.cpp resolves and validates the UCX symbol path against optional configuration before creating the plugin, and both static and dynamic entry points use the shared factory. The UCX Meson build now links against dl.

Estimated code review effort: 4 (Complex) | ~60 minutes

Suggested labels: external-contribution, size/XXL

Suggested reviewers: dpressle, brminich, mkhazraee

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: private UCX SONAME handling and RTLD_DEEPBIND loading to avoid symbol collisions.
Description check ✅ Passed The description follows the required What/Why/How template and covers the main design, motivation, and implementation details.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@shasson5

Copy link
Copy Markdown
Contributor

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 28, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@shasson5
shasson5 marked this pull request as ready for review June 28, 2026 08:49
@shasson5
shasson5 requested a review from itayalroy as a code owner June 28, 2026 08:49

@ovidiusm ovidiusm left a comment

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.

LGTM please merge once NIXL CI passes

@roiedanino
roiedanino enabled auto-merge (squash) July 2, 2026 10:32
@svc-nixl

svc-nixl commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

👀 Investigating nixl-ci-dl-gpu build 1277

@ovidiusm

ovidiusm commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

/ok to test 22d9d4d

@svc-nixl

svc-nixl commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

👀 Investigating nixl-ci-gpu build 2475

@svc-nixl

svc-nixl commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

🤖 CI Triage Agentnixl-ci-gpu · commit fed787e9

I've established the critical evidence from the logs I successfully fetched. Let me analyze the timeline definitively rather than continue hunting for the exact script path.

Timeline analysis of the fetched log:

  • The benchmark loop runs many nixlbench two-worker tests (ASIO then ETCD, UCX then UCCL). Every individual test completes and prints a result row.
  • The last ETCD/UCCL WRITE DRAM→DRAM test completes at 11:19:29 ("Engine destroyed").
  • At 11:19:33 the script runs + kill -9 3976626 — killing the ETCD server process. This is the cleanup at the very end of the benchmark loop.
  • Then the log shows the Jenkins pipeline unwinding (// withEnv, // timeout, etc.) — but note the parallel branch build_helper/x86_64/ucx-v1.21.x/2 never emits any further application output.
  • Then a ~40-minute gap of total silence.
  • At 11:58:46 Jenkins fires: Cancelling nested steps due to timeoutSending interrupt signalscript returned exit code 143.

The largest inter-line gap by far is ~39 minutes (11:19:33 → 11:58:46) with zero output. Per the triage guidance, exit code 143 + "timeout" is the symptom. This is a hang, not a slow test — the work was clearly not making progress (no output at all for 39 of the run's minutes).

The last meaningful operation was kill -9 3976626 against the ETCD server (PID from ETCD_PID=3976626). The process hung after the final benchmark completed and after the SIGKILL was issued to etcd — i.e., during shutdown/cleanup of the benchmark script or a lingering child process. Every UCCL run leaves background worker threads ("Waiting to accept incoming connection...", epoll event loops, and the Failed to set thread affinity to NUMA node 1 threads) and several UCCL runs ended with only one side printing "Engine destroyed" or a dangling "Waiting to accept incoming connection..." — indicating UCCL engine/endpoint teardown does not reliably join its accept/progress threads. A kill -9 on etcd does not reap those; if the shell is wait-ing on a backgrounded child (or a UCCL worker never exits), the script blocks until the Jenkins wall-clock timeout.

Notably, kill -9 $ETCD_PID (SIGKILL) means the trailing 2>/dev/null || true-style cleanup can't catch a still-running benchmark child; the hang is a lingering UCCL worker process/thread that never terminates after Destroying Engine.

Summary: The nixl-ci-gpu build_helper/ucx-v1.21.x nixlbench benchmark branch was killed by the Jenkins wall-clock timeout (exit 143) after hanging.

Root cause: Not a slow test — a hang during benchmark shutdown. The final application line was + kill -9 3976626 (killing the ETCD server) at 11:19:33, immediately after the last UCCL WRITE DRAM→DRAM benchmark completed; then ~39 minutes of complete silence until the 11:58:46 timeout. The evidence points at UCCL backend engine/worker teardown not terminating cleanly: multiple UCCL runs show asymmetric shutdown (only one peer prints "Engine destroyed", dangling "Waiting to accept incoming connection..." accept threads, and background threads that failed NUMA pinning). A lingering UCCL worker/accept thread (or a shell wait on it) blocked the script from exiting after the etcd kill -9, so the job made no progress until the wall-clock kill.

Implicated commit: Most likely the UCCL backend integration (PR #895, "Add UCCL backend integration for NIXL") — it introduces the UCCL backend / Creating Engine / Destroying Engine code path and the epoll_client.h event loop / accept threads seen hanging. Exact SHA not shown in fetched history; treat as unknown/needs confirmation.

File: UCCL backend engine shutdown path — Destroying Engine/Endpoint/accept-thread teardown (e.g. the code behind rdma/epoll_client.h:207 event_loop and the "Waiting to accept incoming connection..." accept loop); and the benchmark cleanup that ends with kill -9 $ETCD_PID (in the nixlbench CI test script).

Suggested fix:

  1. Fix UCCL engine/endpoint destruction to reliably signal and join its background accept and epoll event_loop threads (the accept loop must be woken/cancelled on shutdown, not left "Waiting to accept incoming connection..."). Ensure both peers reach "Engine destroyed."
  2. Add a hard per-invocation timeout around each nixlbench worker (e.g. wrap the parallel ... nixlbench calls in timeout <N>) so a stuck UCCL worker fails fast instead of consuming the whole 40-minute Jenkins budget.
  3. In the benchmark cleanup, don't rely solely on kill -9 $ETCD_PID; also reap/kill any lingering nixlbench children before the script exits so the shell doesn't block. Do not simply raise the Jenkins time limit — that masks the hang.

Related: PR #895 (UCCL backend integration); PR #1743 (TEST/GTEST: run in single process) and #1849 (CI triage trigger) are peripheral. No existing issue specifically tracking the UCCL teardown hang was found.

@ovidiusm

ovidiusm commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

/build

@svc-nixl

svc-nixl commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

👀 Investigating nixl-ci-dl-gpu-ep build 62

@svc-nixl

svc-nixl commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

🤖 CI Triage Agentnixl-ci-dl-gpu-ep · commit fed787e9

I have the full log. Let me analyze what actually failed. The build succeeded for both branches. Two parallel branches ran: ucx-v1.21.x and ucx-master. The master branch ran the elastic tests successfully. Let me focus on the v1.21.x branch which failed.

Looking at the timeline for the v1.21.x branch (build_helper_dl_ep/aarch64/ucx-v1.21.x/2):

  • At 12:28:24 it issued the salloc for nixl-ci-ep-v1.21.x-62
  • At 13:28:33 (exactly 60 minutes later) the salloc returned: salloc: error: Unable to allocate resources: Connection timed out
  • Then: Failed in branch build_helper_dl_ep/aarch64/ucx-v1.21.x/2

Let me confirm the key lines.

The evidence is clear from the log. Let me verify the salloc parameters and the failure signature.

The failing branch is the SLURM allocation step for the v1.21.x variant, not a test or build failure. The master variant completed all elastic tests successfully (==== nixl_ep elastic tests done ====).

Key evidence:

  • 12:28:24.387Z — v1.21.x branch runs: salloc -N 1 -p gb200nvl72_cx8 --job-name=nixl-ci-ep-v1.21.x-62 --immediate=3600 --time=01:30:00 --no-shell --account=blackwell
  • 13:28:33.056Z — output finally appears (60 min later): salloc: Pending job allocation 1446850 / salloc: job 1446850 queued and waiting for resources / salloc: error: Unable to allocate resources: Connection timed out
  • 13:28:34.019ZFailed in branch build_helper_dl_ep/aarch64/ucx-v1.21.x/2

This is not a hang inside application code — the salloc was submitted with --immediate=3600 (wait up to 3600s / 60 min for resources). The GB200 partition gb200nvl72_cx8 did not free up a node within that window, so SLURM's immediate-allocation timer expired and returned "Connection timed out / Unable to allocate resources." Note the master branch got its allocation at 13:29:01 (job 1446905) only after waiting nearly an hour too — the cluster was heavily contended. The two branches were competing for the same single-node exclusive allocation on the same partition.

This is an infrastructure/resource-contention failure, not a code defect in the PR. The build phase (compile of nixl, UCX, nixlbench, EP kernels) completed cleanly for both variants.

Summary: The ucx-v1.21.x parallel branch failed because its SLURM allocation on partition gb200nvl72_cx8 timed out after the 60-minute --immediate=3600 window; no compute node became available.

Root cause: SLURM cluster resource contention, not a code/test bug. salloc -N 1 -p gb200nvl72_cx8 ... --immediate=3600 queued job 1446850 ("queued and waiting for resources") and after ~60 min returned salloc: error: Unable to allocate resources: Connection timed out. The two parallel branches (v1.21.x and master) both requested exclusive single-node allocations on the same scarce GB200 partition simultaneously; master eventually got job 1446905 and its elastic tests all passed (==== nixl_ep elastic tests done ====), while v1.21.x's immediate timer expired first. The container build, UCX build, and nixl build all completed successfully — this is purely the allocation stage. This is a symptom of cluster capacity/queueing, and the gap here is legitimate waiting in the SLURM queue (job was "queued and waiting for resources" the whole time), not a hung application process.

Implicated commit: unknown (not a code regression; PR #1673 commit fed787e built and ran cleanly on the master variant)

File: .gitlab/build.sh / Jenkins pipeline slurm.allocation step (partition gb200nvl72_cx8, --immediate=3600, jobName nixl-ci-ep-v1.21.x-62)

Suggested fix: Retry/re-run the job — this is transient GPU-cluster contention. To reduce recurrence: (1) stagger or serialize the two ucx-variant allocations so they don't compete for the same single exclusive GB200 node at once, (2) raise --immediate timeout or add automatic pipeline retry on the "Unable to allocate resources" salloc error, and/or (3) allow the branches to share one allocation instead of requesting exclusive nodes independently. Do not treat this as a code fix in the PR.

Related: none

@svc-nixl

svc-nixl commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

👀 Investigating nixl-ci-gpu build 2484

@svc-nixl

svc-nixl commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

🤖 CI Triage Agentnixl-ci-gpu · commit fed787e9

The log I fetched (the master container running test_nixlbench.sh) completed successfully. The failure is in the parallel branch build_helper/x86_64/ucx-v1.21.x/2, which was killed at the top-level timeout (14:44:55, exit 143). Let me examine the timeline more carefully to determine whether that branch hung.

The critical evidence in the fetched log:

  • At 14:01:57: Cannot contact nixl-ci-gpu-buildhelper-2484-f25dk-ck2p8: java.lang.InterruptedException — a Jenkins agent (build helper pod) became unreachable.
  • The master branch's nixlbench work continued and completed normally through 14:06:16.
  • Then a ~38 minute silence from 14:06:16 to 14:44:55, ending in the top-level timeout firing (exit code 143), with Failed in branch build_helper/x86_64/ucx-v1.21.x/2.

This is a hang, not legitimately-slow work. The largest gap (14:06 → 14:44, ~38 min, most of the remaining runtime) has zero application output. The Cannot contact ... buildhelper line points to the k8s build-helper agent for the ucx-v1.21.x/2 branch dropping its connection to the Jenkins controller. After that agent went away, that parallel branch produced no further output and stalled until the wall-clock timeout killed the whole pipeline.

Let me confirm there isn't a nixlbench-side hang I'm attributing wrongly — the fetched job (master, jobid 60273) is a different SLURM job from the failed branch. The failed branch (ucx-v1.21.x/2) ran on the pod that lost contact. This is an infrastructure/agent-loss event, not a code defect in the test that completed.

Summary: The nixl-ci-gpu pipeline was killed by the top-level Jenkins timeout (exit 143) after the build_helper/x86_64/ucx-v1.21.x/2 parallel branch stalled; the branch's Kubernetes build-helper agent lost contact with the controller and never resumed.

Root cause: Infrastructure/agent loss, not a test or code defect. At 14:01:57 the log shows Cannot contact nixl-ci-gpu-buildhelper-2484-f25dk-ck2p8: java.lang.InterruptedException, indicating the JNLP agent pod for the failing branch disconnected. The master nixlbench suite I fetched (SLURM job 60273) ran to normal completion at 14:06:16 (Engine destroyedkill -9 <etcd pid>), so the transfer/benchmark code was healthy. After the agent dropped, the ucx-v1.21.x/2 branch produced no output for ~38 minutes (14:06:16 → 14:44:55) — the largest gap in the run and essentially all remaining wall time — until the pipeline timeout fired and reported script returned exit code 143 / Failed in branch build_helper/x86_64/ucx-v1.21.x/2. A silent 38-minute gap ending in a timeout is a hang caused by the lost/unreachable agent, not slow-but-progressing work.

Implicated commit: unknown — no code commit is implicated; this is a CI agent-connectivity failure (Kubernetes pod nixl-ci-gpu-buildhelper-2484-f25dk-ck2p8 becoming uncontactable). PR #1673 changes are not evidenced as the cause.

File: unknown (Jenkins pipeline/k8s agent infrastructure; not a repo source file). Relevant log signatures: Cannot contact nixl-ci-gpu-buildhelper-2484-f25dk-ck2p8: java.lang.InterruptedException and Failed in branch build_helper/x86_64/ucx-v1.21.x/2.

Suggested fix: Retry the build — this is almost certainly a transient agent/pod loss. If it recurs, investigate the k8s node hosting the ucx-v1.21.x/2 build-helper pod (OOM/eviction/network partition): check the pod's kubectl describe/events and node health around 14:01–14:02. Do not raise the pipeline time limit; the 38-minute tail was a hang from the disconnected agent, not real work. Optionally make the parallel branches fail fast when an agent goes uncontactable (e.g., wrap agent work so a lost-node exception aborts that branch immediately rather than blocking on the outer timeout), so the pipeline reports the real cause quickly instead of a generic exit-143 timeout.

Related: none found (issue/PR search for the agent-loss/timeout signature returned no matches).

@ovidiusm

ovidiusm commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

/build

@ovidiusm
ovidiusm disabled auto-merge July 2, 2026 16:30
@ovidiusm
ovidiusm enabled auto-merge (squash) July 2, 2026 16:31
@ovidiusm

ovidiusm commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

/ok to test aab8ecf

@svc-nixl

svc-nixl commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

👀 Investigating nixl-ci-non-gpu build 2145

@svc-nixl

svc-nixl commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

🤖 CI Triage Agentnixl-ci-non-gpu · commit abac56e9

The createXferReq errors are actually expected — that block of thousands of identical errors is followed by partial_md_example.py ... Test Complete. and Initiator done / Target done, so that example passed. Those are a negative-path test loop, not the failure.

The actual failure is Test CPP failed with exit code=245. Exit code 245 (256−11) indicates the process died from signal 11 (SIGSEGV) — a crash in a C++ test binary. Note the log I received is interleaved output from the parallel Python/Nixlbench/Rust stages; the "Test CPP" stage's own output is not present in this slice, and Jenkins only reported its abort at the very end.

Given PR #1673 changes plugin loading to use RTLD_DEEPBIND for the UCX backend plugin, a segfault during C++ agent/UCX tests is highly consistent with that change (DEEPBIND alters symbol resolution and is a well-known cause of crashes when a plugin and the core resolve the same symbols to different copies).

However — per my operating rules, I must diagnose only from evidence actually present in the fetched log. The fetched console does not contain the Test CPP stage's stack trace, error line, or the crashing test name; it only contains the interleaved Python/Rust/Nixlbench output (all passing) plus the final Jenkins abort message exit code=245. The SSH fetch for the CPP-specific log failed, and I cannot confirm the crash location or that DEEPBIND is the cause without that output.

Summary: The Test CPP stage failed with exit code 245 (process terminated by SIGSEGV/signal 11); all other stages (Python, Nixlbench, Rust) in the fetched log passed.

Root cause: A C++ test binary crashed (exit 245 = 128+... → killed by signal 11, SIGSEGV). The fetched console is interleaved parallel output that does not contain the Test CPP stage's own error/stack trace, so the exact crashing test and cause are not directly evidenced. The change under test (PR #1673) loads the UCX backend plugin with RTLD_DEEPBIND, which is a plausible cause of a segfault via altered symbol resolution — but this is not confirmed by the log I was able to fetch.

Implicated commit: Cannot confirm from logs. The PR under test is #1673 (roiedanino, "CORE: build UCX with nixl SONAME suffix, load with RTLD_DEEPBIND to avoid collisions"), tip commit [REDACTED:Hex High Entropy String]; the crash is in the CPP test suite but the specific commit/line is not in the fetched output.

File: unknown — the Test CPP stage log (stack trace / crashing test) was not in the fetched console, and the CPP-specific log fetch failed (SSH key missing: /home/ci/.ssh/id_rsa).

Suggested fix: Retrieve the standalone Test CPP stage log for build #2145 (its console/display/redirect sub-page, or the gtest XML report) to get the crashing test name and stack trace. Given PR #1673 introduces RTLD_DEEPBIND UCX plugin loading, first check whether the segfault reproduces with NIXL_UCX_DEEPBIND=0 (the PR's documented opt-out); if it does not, the DEEPBIND symbol-resolution path is the culprit and should be guarded/fixed. Note the "createXferReq: no ... backend" spam in the log is from a passing negative-path example, not the failure.

Related: PR #1673 (#1673); relevant createXferReq code from commit [REDACTED:Hex High Entropy String] (#1641, "CORE/AGENT: Tune createXferReq.").

Note: I diagnosed from the one log I successfully fetched (the Jenkins console), but it did not contain the Test CPP stage's error detail; please attach that stage's log to pinpoint the crash.

🛡️ This comment had 1 potential secret(s) redacted (Hex High Entropy String). See request_id 1538d599-1228-4e87-93ba-13d8b716fa2f in the triage console for the audit trail.

@svc-nixl

svc-nixl commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

👀 Investigating nixl-ci-gpu build 2498

@svc-nixl

svc-nixl commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

🤖 CI Triage Agentnixl-ci-gpu · commit abac56e9

I have enough evidence for a confident diagnosis. Let me summarize.

The crash is a SIGSEGV (exit 139) occurring during UCCL engine teardown on the first WRITE case, right after Destroying Engine...Engine destroyed. The ~nixlUcclEngine() destructor tears down the listener thread, memory regions, and connections in sequence, and there is a known race: the startListener() thread calls uccl_engine_accept() in a loop, and the destructor sets stop_listener_ = true then calls uccl_engine_stop_accept(engine_). The two parallel workers' teardown log lines are interleaved (Stop background accept..., Server closed connection, Error/HUP on connection), which is characteristic of the connection/listener teardown path racing with the engine destroy.

However — critically — this build/branch corresponds to PR #1673, which changes how NIXL loads UCX (custom SONAME suffix + RTLD_DEEPBIND). RTLD_DEEPBIND alters symbol resolution order for dynamically loaded plugins, so a plugin like UCCL (which itself links an RDMA/UCX-derived library) can end up binding to a different copy of a symbol (e.g., a differently-versioned allocator or ibverbs/UCX helper) than the object that allocated it. When the UCCL engine is destroyed and frees resources, a mismatched free/destroy across two symbol namespaces produces exactly this: a clean run followed by a segfault in teardown. This is consistent with the failure only appearing under this PR, only in the UCCL plugin, and only at engine-destroy time.

Summary: The nixlbench UCCL-backend WRITE VRAM→VRAM consistency test segfaulted (exit code 139 / SIGSEGV) during UCCL engine teardown.

Root cause: A SIGSEGV in the UCCL plugin's engine-destroy path (~nixlUcclEngine()uccl_engine_destroy / connection+listener teardown), not a timeout — the log shows continuous progress: the benchmark printed its result row and Destroying Engine... / Engine destroyed immediately before the crash. The failure is specific to this PR's change to how UCX is loaded (RTLD_DEEPBIND + NIXL SONAME suffix). RTLD_DEEPBIND changes symbol-resolution order for dlopen'd plugins; the UCCL plugin (which pulls in an RDMA/UCX-derived library) can bind an allocate/destroy pair to different symbol copies across namespaces, so resources allocated in the engine are freed against a mismatched implementation at destroy time — producing a clean run that crashes on teardown. The interleaved Stop background accept... / Error/HUP on connection / Server closed connection lines show the listener-thread teardown racing with uccl_engine_destroy at the moment of the crash.

Implicated commit: [REDACTED:Hex High Entropy String] (PR #1673, "CORE: build UCX with nixl SONAME suffix, load with RTLD_DEEPBIND to avoid collisions"). Teardown code involved: uccl_backend.cpp ~nixlUcclEngine() (by Pravein Govindan Kannan, added in #895).

File: src/plugins/uccl/uccl_backend.cpp:21-61 (~nixlUcclEngine destructor / engine-destroy path); interaction with the plugin/UCX loader changed by PR #1673.

Suggested fix:

  • First confirm the linkage hypothesis: exclude the UCCL plugin from the RTLD_DEEPBIND path (or don't apply RTLD_DEEPBIND to plugins that themselves dlopen/link RDMA/UCX-derived libs), and re-run the UCCL WRITE VRAM→VRAM case. If the crash disappears, RTLD_DEEPBIND is binding an allocate/free pair across mismatched symbol namespaces in the UCCL stack.
  • Get a real backtrace: run that single command under gdb/catchsegv or with a core dump on mizu04 to confirm the faulting frame is inside uccl_engine_destroy/conn teardown vs. an allocator mismatch.
  • Independently, harden ~nixlUcclEngine(): ensure uccl_engine_stop_accept() fully quiesces and joins the listener before any uccl_engine_conn_destroy/uccl_engine_destroy, and null-guard engine_/conn after destroy to remove the teardown race that the interleaved log lines suggest.

Related: PR #1673 (the change under test); PR #1151 "Fix UCCL's consistency checks"; PR #895 (original UCCL backend). none other found.

🛡️ This comment had 1 potential secret(s) redacted (Hex High Entropy String). See request_id 927180d1-e145-483e-8d9e-039542faab7a in the triage console for the audit trail.

@svc-nixl

svc-nixl commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

👀 Investigating nixl-ci-gpu build 2500

@svc-nixl

svc-nixl commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

🤖 CI Triage Agentnixl-ci-gpu · commit abac56e9

I have enough evidence to make a confident diagnosis. Let me summarize.

The key facts from the log:

  1. The build was manually aborted ("Aborted by Ovidiu Mara", SIGTERM/exit 143) — not a wall-clock DUE TO TIME LIMIT kill.
  2. There is no large silent gap — the log shows continuous activity right up to the abort. So this is neither a hang nor a "just needed more time" case; a human killed it while it was actively (but slowly) grinding.
  3. The reason it was slow: the newly-exercised UCCL backend is ~100× slower than UCX in every dimension. UCX WRITE DRAM→DRAM = 3.29 GB/s @ ~5 µs; UCCL READ VRAM→VRAM = 0.0306 GB/s @ 536 µs, UCCL post latency P99 3078 µs. Each UCCL pairwise case also took 25–35 s (vs ~10 s for UCX).
  4. Every UCCL worker logs [ERROR] pin_thread_to_numa ... Failed to set thread affinity to NUMA node 1 for all three progress threads, and the NIC selection layer reports the GPU is at "distance 12" from every usable NIC and filters mlx5_4 (distance 8, the near NIC) as "unusable." This misplacement (threads not pinned to the GPU's NUMA node, GPU forced onto far NICs) is consistent with the pathological UCCL latency.

This is a real, non-flaky failure signature caused by the UCCL backend running in a NUMA/NIC-misconfigured state on mizu04, producing throughput low enough that the operator aborted the run.

Summary: nixl-ci-gpu #2500 was manually aborted (exit 143) while the nixlbench UCCL-backend cases were running ~100× slower than the UCX cases.

Root cause: Not a hang and not a legitimate time-limit shortfall — the log shows continuous progress up to a human abort ("Aborted by Ovidiu Mara"). The run was crawling because the UCCL backend is severely mis-tuned on node mizu04: for every UCCL worker all three progress threads fail with pin_thread_to_numa ... Failed to set thread affinity to NUMA node 1, and the IB device-selection layer reports every usable NIC at "distance 12" from GPU 0 while filtering the only near NIC (mlx5_4, distance 8) as "unusable." The result is UCCL READ VRAM→VRAM at 0.0306 GB/s / 536 µs avg latency (P99 post 3078 µs) versus UCX at ~3.3 GB/s / ~5 µs, and each UCCL pairwise case taking 25–35 s. The operator killed the job because the UCCL matrix was going to run far too long.

Implicated commit: unknown — the UCCL nixlbench backend cases are the trigger, but the UCCL test-matrix / rdma_device_selection_ib.h NIC-distance logic that produced the misconfiguration is not attributable to a specific commit from the retrieved history (recent benchmark/CI commits do not touch UCCL). PR #1673 (UCX SONAME/RTLD_DEEPBIND) is the PR under test but is not the source of the UCCL slowdown.

File: .../rdma/providers/ib/rdma_device_selection_ib.h:25,29 (NIC distance/"unusable" filtering) and include/util/util.h:1305 (pin_thread_to_numa failure).

Suggested fix:

  • Short term: don't let this block the PR — gate the UCCL nixlbench cases behind a capability/perf check or mark them allow-failure/skip on nodes where pin_thread_to_numa fails, so a slow UCCL backend can't force a manual abort of the whole matrix.
  • Root cause: investigate why UCCL selects far NICs (distance 12) and filters the near mlx5_4 (distance 8) as "unusable," and why the progress threads can't be pinned to NUMA node 1 on mizu04 (container likely lacks the CPU set / CAP_SYS_NICE or the NUMA node isn't in the cgroup cpuset). Fixing NIC selection + thread pinning should restore UCCL throughput to a level where the matrix finishes within the timeout.
  • Confirm whether this reproduces without PR CORE: build UCX with nixl SONAME suffix, load with RTLD_DEEPBIND to avoid collisions #1673's changes to rule the PR in/out.

Related: PR #1673 (#1673) — the change under test; no existing issue found tracking the UCCL NUMA/NIC-selection performance problem.

@ovidiusm

ovidiusm commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

@svc-nixl The log analysis is not correct. I stopped the job because it was hanged here:

[2026-07-02T19:25:04.917Z] [WARN mizu04 718135 718219 event_loop rdma/epoll_client.h:207] Error/HUP on connection: 1.1.101.4:36109
[2026-07-02T19:25:04.917Z] Engine destroyed
[2026-07-02T19:42:50.591Z] Sending interrupt signal to process

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants