Skip to content

UCP/PROTO: Initialize short thresholds from protocol selection - #11817

Merged
yosefe merged 3 commits into
openucx:masterfrom
yosefe:topic/ucp-proto-initialize-short-thresholds-from-protocol
Sep 8, 2026
Merged

yosefe merged 3 commits into
openucx:masterfrom
yosefe:topic/ucp-proto-initialize-short-thresholds-from-protocol

Conversation

@yosefe

@yosefe yosefe commented Aug 23, 2026

Copy link
Copy Markdown
Member

What

Avoid protocols initialization from async thread; delay initialization to communication operations slow-path one-shot lazy init on main thread

Why

Fix race condition and segfault - Internal issue 5216089

@svc-ucx

svc-ucx commented Aug 23, 2026

Copy link
Copy Markdown

🤖 CI Triage Agent — UCX PR (Tests BlueField on worker 1) · commit b81d5f16

TL;DR: The BlueField gtest aborted on a new debug assertion !ucs_async_is_from_async(&worker->async) in ucp_proto_select_elem_init() (added by this PR's commit b81d5f1), because protocol selection legitimately runs on the async progress thread via the UD wireup message handler → ucp_wireup_init_lanes() → pending-request replay. The assert (or the lazy short-threshold init it guards) must be removed/reworked to be async-safe.

Full analysis

Summary: ud/test_ucp_sockaddr_protocols_err.tag_rndv_unexp_get_scheme/2 aborted with proto_select.c:486 Assertion '!ucs_async_is_from_async(&worker->async)' failed (core dumped), failing make test.

Root cause: The PR commit adds, in ucp_proto_select_elem_init(), an assertion that protocol selection is never entered from async context, plus a lazy call to ucp_ep_config_proto_short_init() (proto_select.c:514-518) which mutates shared ucp_ep_config_t state and therefore must not run on the async thread. That assumption is wrong: the backtrace shows the path
uct_ud_verbs_iface_async_handler() → uct_ud_ep_process_rx() → ucp_wireup_msg_handler() (wireup.c:1168) → ucp_wireup_process_pre_request() (wireup.c:653) → ucp_wireup_init_lanes() (wireup.c:2194) → ucp_wireup_replay_pending_requests() → ucp_wireup_replay_pending_request() (wireup.c:1225) → ucp_proto_request_restart() → ucp_proto_request_init() → ucp_proto_select_lookup_slow() → ucp_proto_select_elem_init().
So a wireup pre-request handled on the UD async progress thread re-selects protocols for replayed pending requests, i.e. slow-path protocol selection (and now the short-threshold init) genuinely executes from async context. This is deterministic for any UD/async wireup with pending requests, not a flake — it reproduces on the aarch64 BlueField worker where UD async progress is used.

Implicated commit: b81d5f1 "UCP/PROTO: Initialize short thresholds from protocol selection" — Yossi Itigin (HEAD of PR #11817)

File: src/ucp/proto/proto_select.c:486 (and the guarded init at src/ucp/proto/proto_select.c:514-518)

Suggested fix: Do not assume non-async context in ucp_proto_select_elem_init(). Either:

  1. Drop the ucs_assert(!ucs_async_is_from_async(&worker->async)) and make the new short-threshold initialization async-safe — e.g. compute the short thresholds eagerly when the EP config is created (ucp_worker_get_ep_config() / ucp_ep_config_init()), so no lazy mutation of ep_config->proto_init_flags / max_short values happens during a lookup that can run on the async thread; or
  2. Keep it lazy but only perform ucp_ep_config_proto_short_init() when !ucs_async_is_from_async(&worker->async) (and otherwise leave the short path disabled / defer it to the next main-thread selection), instead of asserting.

Option 1 is preferable since it also removes the UCP_EP_PROTO_SHORT_INITIALIZED re-check on every selection. If the assert is intended as a real invariant, then ucp_wireup_replay_pending_requests() would have to defer request restart to the main thread (e.g. via a worker progress callback), which is a much larger change.

Related: PR #11817 (this PR); protocol selection re-entrancy note added in the same file at proto_select.c:575-581

@yosefe
yosefe force-pushed the topic/ucp-proto-initialize-short-thresholds-from-protocol branch from b81d5f1 to bafc4ec Compare August 23, 2026 17:09
@yosefe

yosefe commented Aug 23, 2026

Copy link
Copy Markdown
Member Author

@svc-nvidia-pr-review

@svc-nvidia-pr-review

Copy link
Copy Markdown

🤖 Starting review — findings will be posted here when done.

@yosefe
yosefe force-pushed the topic/ucp-proto-initialize-short-thresholds-from-protocol branch from bafc4ec to 554bec5 Compare August 23, 2026 17:15
Comment thread src/ucp/wireup/wireup.c Outdated

/* If we are still in the wireup phase, add the request to the pending queue
* of the AM lane. This avoids full protocol lookup from async thread. */
if (ucp_wireup_ep_test(ucp_ep_get_lane(ucp_ep, am_lane))) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

AM lane redirect assumes the AM lane exists and is a wireup EP; verify the non-proto case.

The new block runs for all replayed requests, including non-proto (protov1) requests:

if (ucp_wireup_ep_test(ucp_ep_get_lane(ucp_ep, am_lane))) {
    req->send.lane = am_lane;
    ucp_request_pending_add(req);
    return;
}

A request originally pending on a non-AM lane (e.g. an RMA/AMO lane wireup EP) gets its send.lane rewritten to am_lane and re-queued there. For protov1 requests whose send.uct.func targets a specific lane's resource, moving them to the AM lane can send on the wrong lane once replayed. Can we confirm this path is only reachable for requests that are lane-agnostic at this point, or restrict the redirect to protov2 (proto_request_reset/PROTO_SEND) requests? A one-line justification in the comment would help.

Comment thread src/ucp/core/ucp_ep.c
@@ -4793,7 +4799,6 @@ void ucp_ep_set_cfg_index(ucp_ep_h ep, ucp_worker_cfg_index_t cfg_index,
ucs_trace("ep %p: set cfg_index %u -> %u", ep, ep->cfg_index, cfg_index);
ep->cfg_index = cfg_index;
ep->am_lane = ucp_ep_config(ep)->key.am_lane;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Removing the proto-init call from ucp_ep_set_cfg_index changes when short thresholds are ready.

Previously short thresholds were initialized synchronously when the cfg index was set. Now they are only initialized on the first ucp_proto_select_lookup_slow. Any protov2 code path that reads config->tag.max_eager_short / am_u.max_eager_short before a proto-select lookup has occurred will observe the uninitialized/default value (set by ucp_ep_config_init_short_thresh). The new test only covers the tag-send-then-check ordering. Is there a path (e.g. ucp_ep_print_info, or a first send that hits the short fast-path before any slow lookup) that reads these thresholds without first triggering ucp_proto_select_lookup_slow? Worth confirming the short fast-path in tag_send.c/ucp_am.c cannot fire before the lazy init runs.

Comment thread test/gtest/ucp/test_ucp_proto.cc Outdated
tag, &param);

/* Expect short-circuit path initialization to be done after first send */
ep_config = &ucs_array_elem(&worker()->ep_config, sender().ep()->cfg_index);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Test reads cfg_index right after send without guarding against immediate completion.

ep_config is fetched via sender().ep()->cfg_index after ucp_tag_send_nbx. This is fine, but the test asserts SHORT_INITIALIZED is set purely as a side effect of the send. If the send is satisfied by a path that does not go through ucp_proto_select_lookup_slow (e.g. served from the proto-select cache populated elsewhere), the assertion could become flaky. Minor: consider asserting via an explicit lookup path or documenting why the first send is guaranteed to hit the slow lookup.

@svc-ucx

svc-ucx commented Aug 23, 2026

Copy link
Copy Markdown

🤖 CI Triage Agent — UCX PR (Tests althca on worker 0) · commit 554bec5a

TL;DR: tcp/test_ucp_sockaddr_iface_activate.iface_activate_count/1 failed because PR #11817 (commit 5e115f0d) moved short-threshold initialization from eager ep-config creation to a lazy path inside ucp_proto_select_lookup_slow(); since worker interfaces are activated as a side effect of protocol selection (ucp_proto_select_wiface_activate), a receive-only endpoint that never performs a send no longer activates any interface. Restore the eager short-threshold init (or activate the ep-config lanes' wifaces at ep-config/wireup time, independently of protocol selection).

Full analysis

Summary: Azure job "althca on worker 0" — gtest tcp/test_ucp_sockaddr_iface_activate.iface_activate_count/1 <tcp/tag,mt> failed twice on EXPECT_TRUE(is_any_interface_activated(receiver())) (test/gtest/ucp/test_ucp_sockaddr.cc:3333), 1 failed of 3923; make: *** [test] Error 1.

Root cause: In proto-v2, UCP worker ifaces are activated lazily from protocol selection: ucp_proto_select_elem_init() calls ucp_proto_select_wiface_activate() (src/ucp/proto/proto_select.c:456-471, invoked at :514), which does ucp_worker_iface_progress_ep for the lanes used by the newly selected protocols. Previously, ucp_worker_get_ep_config() initialized the short (fast-path) thresholds eagerly right after ucp_ep_config_init(), which forced a protocol selection for every new non-internal ep config and therefore activated the ifaces of that config's lanes — even for an endpoint that only receives.

This PR replaced that with a lazy call: ucp_ep_config_proto_short_lazy_init(worker, ep_cfg_index) at the top of ucp_proto_select_lookup_slow() (src/ucp/proto/proto_select.c:555-556), and the eager call was dropped from ucp_worker_get_ep_config() — the stale comment "Do not initialize short protocol thresholds for internal endpoints…" at src/ucp/core/ucp_worker.c:2232-2237 now guards nothing. Consequently, on the passive/receiver side of the sockaddr connection (which never issues a send, so never triggers a protocol lookup), no protocol selection happens, no ucp_proto_select_wiface_activate() runs, and wiface->activate_count stays 0 — exactly what the test observes (is_any_interface_activated(receiver()) == false, while the sender, which does select protocols, is activated). Note the test only runs with proto-v2 (UCS_TEST_SKIP_COND_P(..., !is_proto_enabled())), which is why this regression shows up only here; the /0 (non-mt) variant simply ran on a different gtest worker shard.

Implicated commit: 5e115f0d — "UCP/PROTO: Initialize short thresholds from protocol selection", Yossi Itigin (PR #11817, head [REDACTED:Hex High Entropy String])

File: src/ucp/proto/proto_select.c:555-556 (lazy init call site) and src/ucp/core/ucp_worker.c:2225-2241 (removed eager init); failing assertion at test/gtest/ucp/test_ucp_sockaddr.cc:3333

Suggested fix: Don't rely on protocol selection for iface activation. Either:

  1. Keep the lazy threshold computation but re-add an eager activation step in ucp_worker_get_ep_config() for non-internal ep configs (e.g. call ucp_ep_config_proto_short_lazy_init(worker, ep_cfg_index) there as before, keeping the UCP_EP_INIT_FLAG_INTERNAL early-out so the existing comment stays accurate), or
  2. Decouple the two concerns: activate the wifaces of all lanes in the new ep config (ucp_wiface_process_for_each_lane(worker, ep_config, all_lanes, ucp_worker_iface_progress_ep) and set ep_config->proto_lane_map accordingly) at ep-config creation/ucp_ep_set_cfg_index() time, so receive-only endpoints keep their ifaces progressing.

Also update/remove the now-misleading comment in ucp_worker_get_ep_config(). If the new lazy behavior is intentional, the test expectation for the receiver must be revisited explicitly (not silently), since it changes when receive-side ifaces start being progressed.

Related: PR #11817 (this change); prior art on the activation counter: PR #9650, PR #7819, issue #7964.

🛡️ This comment had 1 potential secret(s) redacted (Hex High Entropy String). See request_id 8c4ad5e2-e67b-44cd-92d9-861f2d702dfa in the triage console for the audit trail.

@yosefe

yosefe commented Aug 24, 2026

Copy link
Copy Markdown
Member Author

@svc-nvidia-pr-review

@svc-nvidia-pr-review

Copy link
Copy Markdown

🤖 Starting review — findings will be posted here when done.

ucp_proto_select_key_t key;
khiter_t khiter;

ucs_assert(!ucs_async_is_from_async(&worker->async));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

blocker: The assertion ucs_assert(!ucs_async_is_from_async(&worker->async)) is placed at the top of the inline ucp_proto_select_lookup, so it fires on every lookup, including the common cache/hash-hit fast path that only reads an already-initialized entry (which is safe from any thread). The intent is to guarantee protocol initialization (ucp_proto_select_lookup_slow → ucp_proto_select_elem_init, which recursively modifies the hash) never runs from the async thread — and that is correctly guarded by the separate assert in proto_select.c:486. Sends can legitimately reach this inline lookup from an async-thread context in signal mode with the protocol already selected, e.g. ucp_proto_request_restart() → ucp_proto_request_init() → ucp_proto_request_lookup_proto() on a failover/completion callback (ucp_proto_request_zcopy_complete_cb). Those will hit the cache/hash and trip this assertion in assert-enabled builds. The inline assert should only cover the case where the slow path is actually taken (i.e. move it into the else branch just before calling ucp_proto_select_lookup_slow), matching the .c guard.

Suggested change
ucs_assert(!ucs_async_is_from_async(&worker->async));
UCS_STATIC_ASSERT(sizeof(key.param) == sizeof(key.u64));
key.param = *select_param;

(remove the top-level assert; the slow-path assert in ucp_proto_select_lookup_slow/ucp_proto_select_elem_init already guards actual initialization)

Comment thread src/ucp/wireup/wireup.c Outdated
ucs_debug("ep %p: adding request %p to pending of lane %d", ucp_ep, req,
am_lane);
req->send.lane = am_lane;
ucp_request_pending_add(req);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

In the new protov2 replay branch, the return value of ucp_request_pending_add(req) is ignored. For a wireup-EP lane ucp_wireup_ep_pending_add() always queues and returns UCS_OK for non-ucp_wireup_msg_progress requests, so today this is safe, but the pattern is fragile: if pending_add ever returns 0 (BUSY) here, the extracted request is silently dropped (it was already removed from the queue by ucs_queue_for_each_extract). Consider asserting the success (e.g. ucs_assert(... == 1)) to document and enforce this invariant.

Comment thread AGENTS.md Outdated

- For any file you touch, read the nearest `AGENTS.md` and each parent guide up
to this root guide before editing.
- For tasks that inspect, diagnose, review, or modify UCX source code, use

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The added guidance mandates using a specific external tool (codegraph_explore / codegraph sync) for all source inspection/review/edit tasks. This is a tool-specific workflow that, per this repo's own rule ("add tool-specific adapters only when a tool requires them" and keep the root guide tool-agnostic), reads as an ownership/tool-coupling concern in the canonical guide. Consider whether this belongs in a tool adapter/skill rather than the root AGENTS.md.

@yosefe
yosefe force-pushed the topic/ucp-proto-initialize-short-thresholds-from-protocol branch from b012851 to 766fe79 Compare August 24, 2026 17:10
@yosefe

yosefe commented Aug 24, 2026

Copy link
Copy Markdown
Member Author

@svc-nvidia-pr-review

@svc-nvidia-pr-review

Copy link
Copy Markdown

🤖 Starting review — findings will be posted here when done.

Comment thread src/ucp/proto/proto_select.c Outdated
ucp_proto_select_init_protocols_t proto_init;
ucs_status_t status;

ucs_assert(!ucs_async_is_from_async(&worker->async));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The same ucs_assert(!ucs_async_is_from_async(&worker->async)) guard is added in both the inline ucp_proto_select_lookup() and inside ucp_proto_select_elem_init() (reached via ucp_proto_select_lookup_slow). The inline one already covers the common send path before it reaches the slow path. The elem_init copy is only non-redundant for direct ucp_proto_select_lookup_slow callers (tests, short-init). Not wrong, but worth a one-line comment on why the guard exists in two places, or dropping the redundant one. Non-blocking.

Comment thread src/ucp/wireup/wireup.c Outdated

/* With protov2, requests are lane-agnostic before protocol lookup, so keep
* them pending on the AM lane to avoid lookup from async thread. */
if (ucp_ep->worker->context->config.ext.proto_enable &&

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The new replay guard ucp_ep->worker->context->config.ext.proto_enable && ucp_wireup_ep_test(ucp_ep_get_lane(ucp_ep, am_lane)) also re-queues non-proto requests (e.g. wireup messages) onto the AM lane's wireup_ep pending queue and rewrites req->send.lane = am_lane. For wireup-msg requests the lane is re-selected in ucp_wireup_msg_progress, so this is harmless, but the comment only mentions "requests are lane-agnostic before protocol lookup". Consider narrowing the condition to proto requests or clarifying the comment that non-proto requests are also intentionally handled here.

@yosefe
yosefe force-pushed the topic/ucp-proto-initialize-short-thresholds-from-protocol branch from 766fe79 to 94f6b7e Compare August 24, 2026 17:45
@yosefe

yosefe commented Aug 24, 2026

Copy link
Copy Markdown
Member Author

@svc-nvidia-pr-review

@svc-nvidia-pr-review

Copy link
Copy Markdown

🤖 Starting review — findings will be posted here when done.

Comment thread src/ucp/wireup/wireup.c
}

/* Replay pending requests */
ucs_trace("ep %p: replaying pending requests", ucp_ep);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This added ucs_trace("ep %p: replaying pending requests", ...) duplicates information already logged per-request inside ucp_wireup_replay_pending_request ("replay proto ..."). It's harmless but adds little; can drop it or keep as-is.

ucp_proto_select_key_t key;
khiter_t khiter;

/* Protocol selection should not be triggered from async context, since

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The assertion ucs_assert(!ucs_async_is_from_async(&worker->async)) is only on the inlined fast-path ucp_proto_select_lookup. Direct callers of ucp_proto_select_lookup_slow (rndv_ppln.c, proto_rndv.c, rma_rndv.c) bypass it, but those are internal recursive selections on the main thread. Was it intentional to leave the guard off ucp_proto_select_lookup_slow (which now also runs lazy_init)? It seems acceptable since the top-level entry is guarded, just confirming.

@yosefe
yosefe force-pushed the topic/ucp-proto-initialize-short-thresholds-from-protocol branch from 94f6b7e to ef0ed42 Compare August 24, 2026 18:09
@yosefe

yosefe commented Aug 24, 2026

Copy link
Copy Markdown
Member Author

@svc-nvidia-pr-review

@svc-nvidia-pr-review

Copy link
Copy Markdown

🤖 Starting review — findings will be posted here when done.

khiter_t khiter;
int khret;

ucs_assert(!ucs_async_is_from_async(&worker->async));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The ucs_assert(!ucs_async_is_from_async(...)) here duplicates the identical assert added in ucp_proto_select_lookup (proto_select.inl:87). The inline path always calls into this slow path, so for those callers the check runs twice. It's still reachable directly (e.g. ucp_proto_rndv_ctrl_select_remote_proto), so not fully redundant, but consider adding the same short justification comment that the inline version has, for consistency.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Comment thread src/ucp/wireup/wireup.c
if (ucp_wireup_ep_test(ucp_ep_get_lane(ucp_ep, am_lane))) {
ucs_debug("ep %p: adding request %p to pending of lane %d", ucp_ep,
req, am_lane);
req->send.lane = am_lane;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

req->send.lane = am_lane overwrites the lane of a PROTO_SEND request before adding it to the wireup_ep pending queue. When this request is later replayed from that queue, ucp_wireup_replay_pending_request runs again; if by then cfg_index == proto_config->ep_cfg_index and proto_request_reset is false, it falls to the else branch and calls ucp_request_send(req) — is send.lane guaranteed to be re-derived from the (possibly changed) proto config at that point, or could the stale am_lane be used? Worth a comment clarifying why the overwrite is safe.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

cfg_index == proto_config->ep_cfg_index will not become true by then since proto_config is not updated with the new config

@svc-ucx

svc-ucx commented Aug 24, 2026

Copy link
Copy Markdown

🤖 CI Triage Agent — UCX PR (Tests roce on worker 1) · commit ef0ed424

TL;DR: dcx/test_ucp_perf.envelope/63 <dc_x/am_mr_b/all_to_all> (AM message‑rate test in blocking/WAIT_MODE_SLEEP mode) made zero progress for exactly 900 s and was killed by the gtest watchdog — a hang, not a slow test. The only change in this PR is deferring protocol‑selection/short‑threshold initialization (and the ucp_proto_select_wiface_activate() that comes with it) into the first send inside ucp_proto_select_lookup_slow(), which is the prime suspect for the lost wakeup/re‑entrancy that stalls the wakeup‑mode AM test.

Full analysis

Summary: Azure job "roce on worker 1" failed: gtest watchdog aborted the run (test_helpers.cc:56: Connection timed out - abort testing, SIGABRT) while dcx/test_ucp_perf.envelope/63 <dc_x/am_mr_b/all_to_all> was running; the main thread was stuck in pthread_join at test_perf.cc:300.

Root cause: A hang, not a timeout. Log timestamps show continuous activity (every test 0.1–1.7 s) up to 21:07:56.688 ("[ RUN ] dcx/test_ucp_perf.envelope/63 <dc_x/am_mr_b/all_to_all>"), then a single gap of exactly 900 s to 21:22:56.689 when the watchdog fired. So the last operation to produce output — the AM message‑rate test with UCX_PERF_WAIT_MODE_SLEEP (test/gtest/ucp/test_ucp_perf.cc:308-313, i.e. UCP wakeup/blocking mode over dc_x with UCX_CONNECT_ALL_TO_ALL=y) — never progressed; both perf threads were still alive, so pthread_join blocked forever.

The commit under test changes exactly the code path that runs on the first send of that test. ucp_ep_config_init() no longer initializes the short thresholds; instead ucp_proto_select_lookup_slow() now calls ucp_ep_config_proto_short_lazy_init() on the first send (src/ucp/proto/proto_select.c:553-556, flag UCP_EP_PROTO_SHORT_INITIALIZED, new gtest test_ucp_proto.ep_config_proto_init_on_send at test/gtest/ucp/test_ucp_proto.cc:561). Two consequences, both able to hang a wakeup‑mode test and neither able to hang a polling test (all polling variants in the same shard passed):

  1. Protocol selection now runs inside the send, so ucp_proto_select_wiface_activate() (proto_select.c:512) → ucp_worker_iface_progress_ep() → ucp_worker_iface_activate() (src/ucp/core/ucp_worker.c:578-611) first adds the iface to the wakeup epoll set and to worker->arm_ifaces at data‑transfer time. Only ifaces already on that list are armed/polled (ucp_worker.c:3331-3339, 3376-3387), so an iface that becomes active after the peer/worker has armed and blocked in ucp_worker_wait() yields a lost wakeup and an indefinite sleep — the classic symptom seen here (blocking *_b variant hangs, poll variants fine).
  2. ucp_proto_select_short_init() re‑enters ucp_proto_select_lookup() (proto_select.c:769) from the top of ucp_proto_select_lookup_slow(), before the caller's kh_get. That recursion can create new ep/rkey configs and grow worker->ep_config, invalidating the proto_select/ucp_ep_config_t* pointer the caller already passed in — undefined behaviour that can also manifest as a stall.

Note: the hung thread's backtrace is not in the log (only the aborting main thread), so the exact mechanism between (1) and (2) cannot be confirmed from the log alone.

Implicated commit: [REDACTED:Hex High Entropy String] — "UCP/PROTO: Initialize short thresholds from protocol selection", Yossi Itigin (the same commit touches src/ucp/core/ucp_ep.c, src/ucp/core/ucp_ep.h, src/ucp/proto/proto_select.c, proto_select.inl).

File: src/ucp/proto/proto_select.c:553-556 (lazy init invoked from ucp_proto_select_lookup_slow), with src/ucp/proto/proto_select.c:512 / src/ucp/core/ucp_worker.c:578-611 (deferred wiface activation) and src/ucp/core/ucp_ep.h:792.

Suggested fix:

  • Do not perform the lazy short init (and therefore protocol selection + ucp_proto_select_wiface_activate()) from inside ucp_proto_select_lookup_slow(). Move the ucp_ep_config_proto_short_lazy_init() call to a single well‑defined non‑re‑entrant point (e.g. after the EP is fully wired up / at the top of the send entry point) and guard it so it can never recurse into ucp_proto_select_lookup_slow(); also re‑resolve proto_select from ep_cfg_index after it runs, since worker->ep_config/rkey_config arrays may have been reallocated.
  • Keep eager initialization when wakeup is in use (context->config.features & UCP_FEATURE_WAKEUP) so no interface can be activated after the application armed the worker; alternatively make ucp_worker_iface_activate() arm the new iface and call ucp_worker_signal_internal(worker) when the worker is already armed, so a late activation cannot lose an event.
  • Reproduce locally with UCX_TLS=dc_x UCX_CONNECT_ALL_TO_ALL=y ./gtest --gtest_filter='dcx/test_ucp_perf.envelope/63' and capture all‑thread backtraces (UCX_HANDLE_ERRORS=bt or gdb -p ... thread apply all bt) to confirm whether the hang is in ucp_worker_wait() (lost wakeup) or inside protocol selection.

Related: PR #11817 (the change under test). No pre‑existing issue found for test_ucp_perf.envelope/am_mr_b hangs, so this is not a known flake.

🛡️ This comment had 1 potential secret(s) redacted (Hex High Entropy String). See request_id 203fcdbb-3e2f-4476-b94e-0157811032b8 in the triage console for the audit trail.

khiter_t khiter;

/* Protocol selection should not be triggered from async context, since
it may race with protocol table updates from main thread */

@tvegas1 tvegas1 Aug 25, 2026 •

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.

I understand that the fix makes sure we don't call select_lookup_slow in async, to avoid thread race.
But there is also possibility for rkey_config pointer invalidation due to recursive select_lookup_slow calls, like described in #11802? Or you think we can assume rkey_config array is already fully populated and not subject to such realloc?

was similar: #11676 (async covered by lock and rkey_config with one level of indirection to make it stable)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

rkey configuration can happen only from protocol initialization or rkey_unpack (API or protocol flow)
the recursive rkey is a related but different problem: IMO the callers in protocol init flow should not hold a pointer to rkey config but always lookup using an index.

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.

do we want to keep rkey_config in one contiguous array? there is proposal to not store array of rkey_config, but instead array of rkey_config pointers (#11676).

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.

@tvegas1 @yosefe Yes, this is the remaining design choice. #11676 makes stable rkey-config addresses an invariant, while #11802 keeps contiguous storage and re-fetches configurations by index after potentially recursive operations.

The forced 32→33 relocation confirms that #11817 alone does not solve the synchronous invalidation: it failed 5/5, while #11817 combined with #11676 passed 5/5.

Do you prefer the stable-storage approach in #11676 or the index/re-lookup approach in #11802? I can adapt based on that decision.

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.

Fable5 analysis:
The lazy init makes this worse: when proto_select points into worker->rkey_config(RMA send, or put_short init in ucp_worker_add_rkey_config), the tag/am selection it triggers can add rkey configs via the RTS probe and grow the array, so the caller keeps writing to the old copy (e.g. put_short is lost and short put stays disabled).

Can we run the lazy init only when rkey_cfg_index == UCP_WORKER_CFG_INDEX_NULL?
Short thresholds are only used by tag/am sends, and &ep_config->proto_select is stable.

@svc-nvidia-pr-review

Copy link
Copy Markdown

🤖 Starting review — findings will be posted here when done.

Comment thread src/ucp/wireup/wireup.c
ucp_trace_req(req, "replay proto %s",
req->send.proto_config->proto->name);
ucp_proto_request_restart(req);
if (ucp_wireup_ep_test(ucp_ep_get_lane(ucp_ep, am_lane))) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

ucp_wireup_ep_test(ucp_ep_get_lane(ucp_ep, am_lane)) — if ucp_ep_get_lane returns NULL (lane not yet created), ucp_wireup_ep_test will dereference NULL and crash. In the replay context (ucp_wireup_eps_progress), the ep has just gone through wireup and lanes should be set, but the surrounding code uses NULL-safe helpers (ucp_wireup_ep, ucp_ep_is_lane_failed_stub). If a NULL lane is possible, use ucp_wireup_ep(...) != NULL (NULL-safe) instead. Soft question about reachability.

ucp_proto_select_key_t key;
khiter_t khiter;

/* Protocol selection should not be triggered from async context, since

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

ucs_assert(!ucs_async_is_from_async(&worker->async)) — potential false positive on RNDV receive path. RNDV receive-side handlers (ucp_rndv_rts_handler → ucp_proto_rndv_receive_start → ucp_proto_rndv_send_reply → ucp_proto_request_lookup_proto → ucp_proto_select_lookup, and ucp_proto_rndv_handle_rtr → ucp_proto_rndv_send_start) run from the AM callback, which can be dispatched from the async progress thread (UCT_CB_FLAG_ASYNC). On the first RNDV op for a given select-param this reaches lookup_slow. Confirm that all send-side protocol selection (including RNDV reply/RTR handling) is guaranteed to run only on the main thread; otherwise this assert would be a regression in debug builds.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

rndv callback cannot be dispatch from async thread

khiter_t khiter;
int khret;

ucs_assert(!ucs_async_is_from_async(&worker->async));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

ucs_assert(!ucs_async_is_from_async(&worker->async)) — potential false positive on RNDV receive path. RNDV receive-side handlers can be dispatched from the async progress thread (UCT_CB_FLAG_ASYNC), reaching lookup_slow on the first RNDV op for a given select-param. This selection path already existed before this PR, so the new assert may fire on a path that previously worked. Confirm all send-side protocol selection runs only on the main thread.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

rndv callback cannot be dispatch from async thread

@svc-nvidia-pr-review

Copy link
Copy Markdown

🤖 Starting review — findings will be posted here when done.

Comment thread src/ucp/core/ucp_worker.c
@@ -575,13 +581,44 @@ ucp_worker_iface_error_handler(void *arg, uct_ep_h uct_ep, ucs_status_t status)
return status;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

arm_block_count can leak if a busy-marked iface is deactivated/closed. UCP_WORKER_IFACE_FLAG_ACTIVATE_ARM_BUSY is set in check_events_do when arm returns BUSY, and only cleared in ucp_worker_iface_activate or when arm later succeeds. ucp_worker_iface_deactivate() / ucp_worker_iface_disarm() / iface cleanup do not call ucp_worker_iface_activate_arm_unmark_busy(). If an iface is deactivated or torn down while marked busy, worker->arm_block_count stays > 0 and ucp_worker_arm() returns UCS_ERR_BUSY forever, which can hang a ucp_worker_wait() loop. Confirm the busy flag is always cleared on the deactivate/cleanup paths — can we unmark in ucp_worker_iface_deactivate()/cleanup?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

  • deactivate is called only after activate , which already clears BUSY state
  • ifaces are destroyed only when worker is destroyed so it does not matter

@@ -558,6 +558,33 @@ UCS_TEST_P(test_ucp_proto, dump_protocols) {
ucp_ep_print_info(sender().ep(), stdout);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

test_ucp_proto.ep_config_proto_init_on_send: requests_wait({send_req, recv_req}) is passed pointers that may be UCS_OK (immediate completion, not a valid request pointer) rather than in-progress request handles. Confirm requests_wait tolerates non-pointer status values here; otherwise the test can deref a status code.

Comment thread src/ucp/core/ucp_ep.c
@@ -3767,9 +3767,12 @@ ucs_status_t ucp_ep_config_init(ucp_worker_h worker, ucp_ep_config_t *config,
&config->tag.rndv.am_thresh);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Behavior change under protov2: the four ucp_ep_config_set_memtype_thresh() calls are now skipped when proto_enable. This is consistent with protov2 computing short thresholds lazily via ucp_ep_config_proto_short_lazy_init, and the fields were being overwritten anyway, so this is a reasonable cleanup. Noting for scope awareness.

@svc-nvidia-pr-review

Copy link
Copy Markdown

Scope note: The PR mixes several logically distinct changes — the proto-short lazy-init move, the wireup pending-replay fix, and the worker arm-busy tracking, plus a large number of trace→debug logging changes. These are related (all touch async-safety of protocol selection / iface activation) but the logging churn makes the functional diff harder to review. Not a blocker.

Comment thread src/ucp/core/ucp_worker.c Resolved
Comment thread src/ucp/wireup/wireup.c Resolved
khiter_t khiter;

/* Protocol selection should not be triggered from async context, since
it may race with protocol table updates from main thread */

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.

Fable5 analysis:
The lazy init makes this worse: when proto_select points into worker->rkey_config(RMA send, or put_short init in ucp_worker_add_rkey_config), the tag/am selection it triggers can add rkey configs via the RTS probe and grow the array, so the caller keeps writing to the old copy (e.g. put_short is lost and short put stays disabled).

Can we run the lazy init only when rkey_cfg_index == UCP_WORKER_CFG_INDEX_NULL?
Short thresholds are only used by tag/am sends, and &ep_config->proto_select is stable.

@svc-nvidia-pr-review

Copy link
Copy Markdown

🤖 Starting review — findings will be posted here when done.

Comment thread src/ucp/proto/proto_select.inl Resolved
Comment thread src/ucp/proto/proto_select.c Resolved
@svc-nvidia-pr-review

Copy link
Copy Markdown

src/ucp/core/ucp_worker.h

arm_block_count is a monotonic counter that is only decremented via unmark_busy, reached from ucp_worker_iface_activate and the successful-arm branch of check_events_do. If a busy-marked iface is force-deactivated (cleanup) without going through either path, the count leaks and ucp_worker_arm would return UCS_ERR_BUSY for the rest of the worker's life. In practice this only happens at teardown, so it is harmless — but a short comment or an unmark in the deactivate/cleanup path would make the invariant robust.

PR scope / logging churn: the large volume of trace→debug conversions and format-string reworks are mixed with three distinct functional fixes (proto-short lazy init, wireup pending-replay async-safety, worker arm-busy tracking), making the functional diff hard to isolate. Not a blocker, but splitting the pure logging changes into a separate PR would ease review, per REVIEW.md's refactoring/scope guidance.

@yosefe

yosefe commented Sep 3, 2026

Copy link
Copy Markdown
Member Author

@iyastreb regarding the comment
"
The lazy init makes this worse: when proto_select points into worker->rkey_config(RMA send, or put_short init in ucp_worker_add_rkey_config), the tag/am selection it triggers can add rkey configs via the RTS probe and grow the array, so the caller keeps writing to the old copy (e.g. put_short is lost and short put stays disabled).
"

The issue is real, but not introduced or made worse by this PR, because we still call ucp_proto_select_short_init from ucp_worker_add_rkey_config

@yosefe

yosefe commented Sep 3, 2026

Copy link
Copy Markdown
Member Author

check_events_do

Fixed in 45a0983

@svc-nvidia-pr-review

Copy link
Copy Markdown

🤖 Starting review — findings will be posted here when done.

Comment thread src/ucp/proto/proto_select.c Resolved
Comment thread src/ucp/proto/proto_select.inl Resolved
Comment thread test/gtest/ucp/test_ucp_proto.cc Resolved
@yosefe

yosefe commented Sep 3, 2026

Copy link
Copy Markdown
Member Author

@iyastreb fixed

iyastreb
iyastreb previously approved these changes Sep 3, 2026
If check_events_do could not arm the iface (returned BUSY), we can hang
forever in ucp_worker_wait because one of the inactive ifaces is not
armed to be re-activated and trigger wakeup via worker->eventfd channel.
@yosefe
yosefe force-pushed the topic/ucp-proto-initialize-short-thresholds-from-protocol branch from 45a0983 to 2feb274 Compare September 6, 2026 15:55
@svc-nvidia-pr-review

Copy link
Copy Markdown

🤖 Starting review — findings will be posted here when done.

}
}

/* FIXME proto_select could get invalidated by adding rkey config */

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

New FIXME documents an unfixed latent bug (question): the cached select_elem points into the hash's value storage, which can be reallocated when a later lookup adds an rkey config. Since this PR is about async-safety of protocol selection, please clarify: is caching a raw select_elem pointer here actually unsafe today, or is it covered by ucp_proto_select_cache_reset() on epoch change? If it's a real hazard, a FIXME alone in an async-hardening PR leaves a known crash/UAF path open — please either scope it out explicitly in the description or file/reference a tracking issue.

@svc-nvidia-pr-review

Copy link
Copy Markdown

src/ucp/core/ucp_ep.c

ucp_ep_config_proto_short_lazy_init no longer honors the protov1 early-out (question): the old ucp_ep_config_proto_init returned early for !proto_enable. The new lazy variant drops that guard and relies solely on being called from ucp_proto_select_lookup_slow (proto_enable-only). This load-bearing invariant isn't asserted. Adding ucs_assert(worker->context->config.ext.proto_enable) at the top of ucp_ep_config_proto_short_lazy_init would make the coupling explicit and catch any future protov1 caller.

@svc-nvidia-pr-review

Copy link
Copy Markdown

🤖 Starting review — findings will be posted here when done.

@svc-nvidia-pr-review

Copy link
Copy Markdown

🤖 Review complete — no issues found.

@svc-ucx

svc-ucx commented Sep 6, 2026

Copy link
Copy Markdown

🤖 CI Triage Agent — UCX PR (AddressSanitizer roce on worker 2) · commit 9a7838c4

TL;DR: The ASAN/roce gtest job failed on a single unrelated UCT test — dc_mlx5/uct_atomic_key_reg_rdma_mem_type.fadd64/1 (64-bit fetch-add on on-device "RDMA"/MEMIC memory over mlx5_1:1); 8656/8657 tests passed and nothing in the PR touches UCT atomics, so this is the known-flaky device-memory AMO test, not a regression — re-run the job and, if it reproduces, harden the MEMIC allocation path in the test.

Full analysis

Summary: make test in build-test/test/gtest exited with Error 1 because exactly one test failed: [ FAILED ] dc_mlx5/uct_atomic_key_reg_rdma_mem_type.fadd64/1, where GetParam() = dc_mlx5/mlx5_1:1 (log line at 17:25:30, make: *** [Makefile:4713: test] Error 1).

Root cause: Evidence in the log: the run completed normally (8657 tests from 1216 test suites ran, PASSED 8656), there is no ASAN report, no crash/signal, no timeout and no large gaps before the kill — a single functional test assertion failed. That test (test/gtest/uct/test_atomic_key_reg_rdma_mem_type.cc:29-41) allocates a UCS_MEMORY_TYPE_RDMA (device/MEMIC) buffer with num_retries = 10 and runs 4 concurrent worker threads doing uct_ep_atomic64_fetch(ADD) over dc_mlx5 on the second port (mlx5_1:1). Its failure path is a device-memory allocation exhaustion (UCS_ERR_NO_MEMORY after all retries → ASSERT_UCS_OK at test/gtest/uct/uct_test.cc:998) or a flaky AMO reply validation — this exact test has a documented history of MEMIC-allocation flakiness (commits 69718f6, [REDACTED:Hex High Entropy String], d7d57e1 and PR #10475 all added retries/sleeps/limits for it). The PR under test (topic/ucp-proto-initialize-short-thresholds-from-protocol, commit 9a7838c) changes UCP protocol short-threshold initialization only; it cannot affect UCT-level dc_mlx5 device-memory atomics, and every other dc_mlx5/rc_mlx5 suite in the same run passed. Note: the fetched log is the console tail, so the failing test's own assertion text (printed ~40 min earlier in the run) is not visible — the diagnosis above is based on the failure summary, the test source, and its retry/flake history.

Implicated commit: unknown — not attributable to 9a7838c (PR #11817); the flaky test area was last touched by 69718f6 (Michal Shalev, "TEST/GTEST/UCT: Add retries and rand sleep when it cannot allocate MEMIC memory")

File: test/gtest/uct/test_atomic_key_reg_rdma_mem_type.cc:33 (device-memory buffer alloc) and test/gtest/uct/uct_test.cc:968-998 (retry loop + ASSERT_UCS_OK)

Suggested fix: Re-trigger the "AddressSanitizer roce on worker 2" job — this failure is unrelated to the PR. If it reproduces, capture the full gtest output for --gtest_filter=dc_mlx5/uct_atomic_key_reg_rdma_mem_type.fadd64* (the tail-truncated console log hides the assertion). For a durable fix, make the MEMIC path non-fatal: in uct_test::entity::mem_alloc() convert a persistent UCS_ERR_NO_MEMORY for UCS_MEMORY_TYPE_RDMA into UCS_TEST_SKIP_R("no device memory available") instead of ASSERT_UCS_OK, and/or raise num_retries above 10 at test_atomic_key_reg_rdma_mem_type.cc:34.

Related: PR #10475 ("TEST/GTEST/UCT: Increase retries and add random sleep when MEMIC allocation fails"), PR #11817 (this PR)

🛡️ This comment had 1 potential secret(s) redacted (Hex High Entropy String). See request_id 3d4d76ee-c5ef-4971-a68a-175b0e8d563d in the triage console for the audit trail.

@yosefe
yosefe enabled auto-merge September 8, 2026 10:35
@yosefe
yosefe disabled auto-merge September 8, 2026 12:22
@yosefe
yosefe merged commit 42fc490 into openucx:master Sep 8, 2026
162 checks passed
@yosefe
yosefe deleted the topic/ucp-proto-initialize-short-thresholds-from-protocol branch September 8, 2026 12:23
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.

6 participants