Skip to content

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

Open
yosefe wants to merge 1 commit into
openucx:masterfrom
yosefe:topic/ucp-proto-initialize-short-thresholds-from-protocol
Open

UCP/PROTO: Initialize short thresholds from protocol selection#11817
yosefe wants to merge 1 commit 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 AgentUCX 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 AgentUCX 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_slowucp_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 AgentUCX 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)

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.

4 participants