Skip to content

UCP/FT/TF: Integrate lane hold, wireup reply, and token tests - #12195

Closed
evgeny-leksikov wants to merge 4 commits into
openucx:masterfrom
evgeny-leksikov:ucp_ft_integration
Closed

evgeny-leksikov wants to merge 4 commits into
openucx:masterfrom
evgeny-leksikov:ucp_ft_integration

Conversation

@evgeny-leksikov

Copy link
Copy Markdown
Contributor

Summary

Test plan

  • Azure CI on this draft
  • test_ucp_fault_tolerance and test_ucp_fault_tolerance_tf
  • sockaddr tests that share this wireup path

@evgeny-leksikov
evgeny-leksikov marked this pull request as ready for review October 28, 2026 21:24
@evgeny-leksikov evgeny-leksikov added the WIP-DNM Work in progress / Do not review label Oct 28, 2026
@svc-ucx

svc-ucx commented Oct 28, 2026

Copy link
Copy Markdown

🤖 CI Triage Agent — UCX PR (Codestyle AUTHORS file update check) · commit 33f29d29

TL;DR: The "AUTHORS file update check" job failed because contrib/authors_update.sh hard-exits with "provided range … is empty" when git log --first-parent --no-merges <base>..<head> returns nothing — which is the case for this PR since the branch tip's first-parent chain leads straight back into the base. Fix: make the empty-range case non-fatal in CI (skip the check) instead of exit 1.

Full analysis

Summary: Azure job Codestyle / AUTHORS file update check (build 141400) failed at the AUTHORS file check bash step with Error: provided range "1cd4d9f8b…..33f29d298…" is empty and Bash exited with code '1'.

Root cause: The step computes base_revision=$(git rev-parse <merge>^1) / head_revision=$(git rev-parse <merge>^2) and runs ./contrib/authors_update.sh "$base..$head" (buildlib/pr/codestyle.yml:88-91). Inside the script, the author list is built with:

git log --first-parent --no-merges --pretty=format:"%an%x09%ae" "$range" | sort -u

For PR #12195 (source branch ucp_ft_integration, head 33f29d298) this produces zero lines: the --first-parent walk from the PR head immediately enters commits already reachable from the base (i.e. the head is itself a merge/integration commit whose first parent is on the base branch), and --no-merges then drops the merge commit itself. The script treats an empty result as a fatal user error (exit 1) — a guard originally meant to catch a typo'd range when the script is run by hand — so a legitimately "no new first-parent commits" PR is reported as a CI failure. Note the sibling check codestyle_check_commit_title() (buildlib/tools/codestyle.sh:11) uses the exact same --first-parent --no-merges range but simply iterates an empty list and returns success, which is why only the AUTHORS job went red.

The --first-parent restriction (and thus this empty-range possibility) was introduced in cfd9cd1f "BUILD/CI: Limit PR commit range checks (#11527)"; the empty-range exit 1 guard comes from the original f8c7e7a9.

Implicated commit: cfd9cd1f — Yossi Itigin, "BUILD/CI: Limit PR commit range checks (#11527)" (guard itself added in f8c7e7a9, Thomas Vegas, "CONTRIB: Automate AUTHORS file update"). Not a product-code regression from commit 33f29d298.

File: contrib/authors_update.sh:18-24 (triggered from buildlib/pr/codestyle.yml:85-92)

Suggested fix: Make the empty range non-fatal for CI, one of:

  1. Guard in the pipeline step (smallest change), buildlib/pr/codestyle.yml:
set -eEx
base_revision=$(git rev-parse "$(Build.SourceVersion)^1")
head_revision=$(git rev-parse "$(Build.SourceVersion)^2")
if [ -z "$(git log --first-parent --no-merges --format=%h "$base_revision..$head_revision")" ]; then
    echo "No first-parent non-merge commits in range - skipping AUTHORS check"
    exit 0
fi
./contrib/authors_update.sh "$base_revision..$head_revision"
git diff --exit-code
  1. Or add an opt-in flag to the script (e.g. --allow-empty) so interactive use keeps the typo guard while CI exits 0 on an empty range.

Also consider validating the range explicitly (git rev-parse --verify on both ends) so a genuinely bad range is still caught, rather than inferring it from empty output. As a workaround to get this PR green, rebasing ucp_ft_integration onto the target branch (so its own commits appear on the first-parent chain) will also make the range non-empty.

Related: PR #11527 (introduced --first-parent range limiting); PR #12195 (this build). No existing issue found for the empty-range failure.

@svc-nvidia-pr-review

Copy link
Copy Markdown

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

} else {
shuffle_lanes(lanes, lane_type_str);
}

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: get_lanes() is in the shared base class, so this replaces the random shuffle for every PUT-only variant of the already-enabled test_ucp_fault_tolerance on all transports, and order_put_lanes() skips unless there are exactly 6 RMA_BW lanes numbered 0..5. That silently drops the PUT coverage we have today. Pls keep shuffle_lanes() and, if the 4,0,2,1,3,5 order is needed to reproduce a specific failure, add it as a separate dedicated test case.

UCS_TEST_MESSAGE << lane_type << ": " << size_t(lane) << "/"
<< lanes.size();
}
}

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 comment "Replay the rc_x/PUT order 4, 0, 2, 1, 3, then 5" plus the per-lane UCS_TEST_MESSAGE loop reads like leftover debug scaffolding. Can we drop this helper?

(op_info->comp->func == ucp_proto_repost_completion)) {
req = ucs_container_of(op_info->comp, ucp_request_t,
send.state.uct_comp);
ucp_trace_req(req, "repost restart op %s",

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 trace is unrelated to the LANES_ADDR reply change. Pls move it to a separate PR (REVIEW.md: feature PRs must not carry unrelated changes).

ucp_proto_repost_completion);
}

ucp_trace_req(req, "repost submit op %s lane %u",

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 trace is unrelated to the LANES_ADDR reply change. Pls move it to a separate PR (REVIEW.md: feature PRs must not carry unrelated changes).

Comment thread src/ucp/core/ucp_ep.c Outdated

if (ucp_ep_has_cm_lane(ep)) {
return;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggestion:

    ucs_assert(!ucp_ep_has_cm_lane(ep));

The if (ucp_ep_has_cm_lane(ep)) return; right after the assert is dead code — pls keep only one of the two. Same pattern in ucp_ep_tf_accept_peer_req() at L2776-2778.

Comment thread src/ucp/core/ucp_ep.c
if ((ucp_ep_recovery_arg(ep) == NULL) ||
(ucp_ep_recovery_arg(ep)->tf_reply == NULL)) {
ucs_assert(ucp_ep_recovery_arg(ep) == NULL);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggestion:

        /* A peer request moves the queue pair to error and waits for that
         * completion before this lane is failed. Recovery stays idle until
         * the completion holds it. */
        ucs_assert((ucp_ep_recovery_arg(ep) == NULL) ||
                   (ucp_ep_recovery_arg(ep)->tf_reply != NULL));

The if (...) { ucs_assert(...) } form is equivalent but much harder to read.

Comment thread src/ucp/core/ucp_ep.h
const void *tx_token, uint8_t tx_len,
void **rx_token_p, uint8_t *rx_len_p);
int require_open, void **rx_token_p,
uint8_t *rx_len_p);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

require_open is a flag whose only job is to decide whether ucp_ep_tf_derive_rx() applies its ucp_ep_tf_exchange_open() gate, and it is threaded through ucp_wireup_derive_rx_tokens() as well. The root AGENTS.md says not to add a parameter that decides whether the function performs its core operation — can the request path do the ucp_ep_tf_exchange_open() check itself and keep one derive function?

Comment thread src/ucp/core/ucp_ep.h
2) it does not make sense to recover lanes during close protocol */
2) it does not make sense to recover lanes during close protocol
The low bit set means there is no pointer: the remaining bits
are the last accepted 16-bit peer request id. */

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Tagging the close_req/recovery_arg union slot with a low bit to stash a 16-bit peer id is fragile: every reader now has to go through ucp_ep_recovery_arg(), while ucp_ep_set_close_request() still writes the slot directly, and the TODO at L581 shows the field layout is not settled. Since ucp_ep_ext_t already gains a uint16_t tf_request_id, why not add a plain uint16_t tf_seen_peer_id next to it and drop the tagging plus most of the accessor churn?

Comment thread src/ucp/core/ucp_ep.c
ucs_debug("ep %p: lane %d keep uct_ep %p until the ACK token", ep,
lane, tf->uct_ep);
continue;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Which message later releases a lane that was purged (tf->purged = 1) but is not in release_lanes? On the reply path release_lanes is derived from provided_lane_map while the purge walks requested_lane_map, so a lane the peer did not provide stays UCP_EP_TF_LANE_HELD with a live uct_ep. It then stays in ucp_ep_tf_held_lanes() forever, so ucp_ep_recovery_progress() can never clear it and the retries get exhausted. Can you point at the releasing path, or should the mask be intersected with the purged lanes instead?

@svc-nvidia-pr-review

Copy link
Copy Markdown

PR-level: This adds roughly 800 lines. Pls split it: the ucp_ep_recovery_arg()/ucp_ep_close_req() accessor introduction and its call-site churn (ucp_ep.c, wireup_cm.c, gtest) is a pure refactor and can go first, the added ucs_debug/ucp_trace_req lines are a separate change, and the deferred LANES_ADDR reply plus the peer request-id ordering is the actual feature. REVIEW.md caps a PR at 500 added lines.

Residual coverage gap: No test exercises the new deferred-reply path (a LANES_ADDR request parked on uct_ep_invalidate error completions, and the "newer request discards the parked reply" branch). Enabling test_ucp_fault_tolerance_tf on rc_x only covers it incidentally and depends on timing.

Residual coverage gap: ucp_ep_tf_peer_req_is_newer() wrap-around at the 16-bit boundary has no unit coverage; it is cheap to add one since the function is already non-static.

evgeny-leksikov and others added 4 commits November 2, 2026 11:28
Publish the replacement address once the held lane error completion arrives, and ignore a stale peer request.
…hm tls

Co-authored-by: svc-nvidia-pr-review <svc-nvidia-pr-review@nvidia.com>
shm,ib selects the same token lanes as rc_x, so it adds no coverage.
@evgeny-leksikov
evgeny-leksikov requested a review from a team as a code owner November 2, 2026 12:30
@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
}

ucp_ep_set_lanes_failed(ucp_ep, UCS_BIT(lane), status);
ucp_wireup_tf_lane_errored(ucp_ep, 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.

blocker: ucp_ep_set_lanes_failed() can fail the whole ep and destroy it (ucp_ep_set_failed() -> ucp_ep_disconnected() -> ucp_ep_destroy_internal()), so ucp_ep may already be freed here.

Suggested change
ucp_wireup_tf_lane_errored(ucp_ep, lane);
ucp_wireup_tf_lane_errored(ucp_ep, lane);
ucp_ep_set_lanes_failed(ucp_ep, UCS_BIT(lane), status);
return UCS_OK;

Comment thread src/ucp/core/ucp_worker.c
}
}

ucs_debug("worker %p: uct_ep %p is ep %p lane %d slot %p", worker, uct_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.

minor: this looks like leftover debugging and mostly repeats the debug message at the top of the handler, pls drop it.

Comment thread src/ucp/core/ucp_ep.c
/* A peer request moves the queue pair to error and waits for that
* completion before this lane is failed. Recovery stays idle until
* the completion holds it. */
if ((ucp_ep_recovery_arg(ep) == NULL) ||

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

pls write this as one assert, the if + assert form is hard to read:

Suggested change
if ((ucp_ep_recovery_arg(ep) == NULL) ||
/* A peer request moves the queue pair to error and waits for that
* completion before this lane is failed, so a parked reply can keep
* the recovery state alive with no failed lane. */
ucs_assert((ucp_ep_recovery_arg(ep) == NULL) ||
(ucp_ep_recovery_arg(ep)->tf_reply != NULL));
goto done;

Comment thread src/ucp/core/ucp_ep.h
ucs_queue_head_t tf_pending_q;
/* LANES_ADDR reply waiting for error completions. Owned by wireup.
* NULL when no reply is waiting. */
void *tf_reply;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

IMO pls forward-declare ucp_wireup_tf_reply_t and use it here instead of void*, so this pointer keeps type checking.

Comment thread src/ucp/wireup/wireup.c
static ucp_ep_recovery_arg_t *ucp_wireup_tf_recovery_arg(ucp_ep_h ep)
{
if (!ucp_ep_err_mode_eq(ep, UCP_ERR_HANDLING_MODE_FAILOVER) ||
(ucp_ep_get_cm_lane(ep) != UCP_NULL_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_ep_recovery_arg() already returns NULL when the ep has a CM lane, so this check is redundant - pls keep only the err_mode check, like ucp_ep_tf_get() does.

Suggested change
(ucp_ep_get_cm_lane(ep) != UCP_NULL_LANE)) {
if (!ucp_ep_err_mode_eq(ep, UCP_ERR_HANDLING_MODE_FAILOVER)) {

Comment thread src/ucp/wireup/wireup.c
uint8_t scheduled;
ucp_wireup_lane_token_t rx_tokens[UCP_MAX_LANES];
void *address;
size_t address_length;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

minor: address_length is only written, never read (ucp_address_unpack() does not take a length), pls remove it.

Comment thread src/ucp/wireup/wireup.c
if (status == UCS_OK) {
/* Installed before the invalidate, so a completion that arrives
* from it can finish the reply. */
armed = ucp_ep_tf_invalidate_uct_eps(ep, pending);

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 old code put pending & ~armed into peer_unaware so those lanes went through local failover; now they are only cleared from waiting and are also excluded from peer_unaware by skip_lanes, so a lane whose uct_ep_invalidate() failed is never failed over nor rebuilt. can we fail them here, same as the stuck handling above?

shuffle_lanes(lanes, lane_type_str);
if ((op_mask & TEST_OP_PUT) &&
!(op_mask & (TEST_OP_GET | TEST_OP_AM))) {
order_put_lanes(lanes, lane_type_str);

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: this also replaces the random shuffle for every PUT-only variant of the base test_ucp_fault_tolerance, and order_put_lanes() skips the test unless there are exactly 6 RMA_BW lanes numbered 0..5, so existing PUT coverage is lost on most transports. if token failover only passes with this specific order, pls fix the ordering dependency instead of hard-coding the reproducer.

@svc-nvidia-pr-review

Copy link
Copy Markdown

Not re-raised (already reported): PR size/split, missing deferred-reply test, and ucp_ep_tf_peer_req_is_newer() wrap-around coverage are in the previous round; for the unresolved ones a +1 reaction on the existing threads is the right action rather than new comments.

Residual coverage gap: order_put_lanes() removes lane-order randomization from the PUT variants, so the asymmetric-failure ordering that the deferred reply is meant to handle is no longer exercised randomly anywhere — the new path depends on one hard-coded order on rc_x.

@svc-ucx

svc-ucx commented Nov 2, 2026

Copy link
Copy Markdown

🤖 CI Triage Agent — UCX PR (Tests new on worker 0) · commit ecfb222b

TL;DR: The only failure in the whole 8873-test run is test_config.performance, a wall‑clock microbenchmark that asserts UCX config parsing finishes in ≤50 ms with no warm-up and no retry; it timed out due to I/O/CPU contention on the shared Azure worker, not due to anything in PR #12195. Fix by warming up the config-file read outside the timed block and timing the best of N iterations (or scaling the limit on CI).

Full analysis

Summary: make -C build-test/test/gtest test exited 1 (Makefile:4971: test) with [ FAILED ] 1 test ... test_config.performance; all other 8872 tests passed.

Root cause: test_config.performance wraps a single car_opts construction in UCS_TEST_TIME_LIMIT(0.05) (test/gtest/common/test_helpers.h:159) — a hard 50 ms wall-clock budget, with ucs::test_time_multiplier() == 1 in this non-valgrind/non-ASan job (test/gtest/common/test_helpers.cc:326). Inside that timed region the test does far more than the getenv() scan it claims to measure: car_opts::parse() calls ucs_config_parse_config_files() on every construction (test/gtest/ucs/test_config.cc:405), which performs five separate fopen()+ini_parse_file() attempts — UCX_CONFIG_DIR, the library dir's ../etc/ucx/, $HOME, $UCX_CONFIG_DIR, and the current working directory (src/ucs/config/parser.c:2001-2039). The CWD here is the shared build tree /scrap/azure/agent-05/AZP_WORKSPACE/2/s/build-test/test/gtest, so the measurement is filesystem-I/O bound on a build agent shared by multiple parallel workers (log shows worker 0, total run 3,365,488 ms with 51 s single tests and even a transient bind(...) failed: Address already in use). There is also no retry: despite GTEST_EXTRA_ARGS="-p 10 -i 2.0" (contrib/test_jenkins.sh:1296-1300, commented "10 retries"), perf_retry_count is only used as a gate to enable failure reporting in the macro; test_base::run()/TestBodyProxy() execute test_body() exactly once (test/gtest/common/test.cc:465-517). So a single unlucky 50 ms sample fails the build. Nothing in PR #12195 (branch ucp_ft_integration, UCP fault tolerance) touches src/ucs/config/ — the most recent changes there are from April 2026 (597005c) — so this is an environment-induced flake, not a regression from the commit under test.

Implicated commit: No code regression. Contributing CI-topology changes: 390a30e "AZP/TESTS: Build once per GPU test row and share the tree with the workers" and ed5255a "AZP/TESTS: Run MPI tests on a worker instead of the prebuild job" (both Alexey Rivkin, Oct 2026), which increase contention on the shared worker/build tree.

File: test/gtest/ucs/test_config.cc:781-795 (timed block), test/gtest/ucs/test_config.cc:405 (ucs_config_parse_config_files() inside the measurement), test/gtest/common/test_helpers.h:159 (UCS_TEST_TIME_LIMIT)

Suggested fix:

  1. Re-run the job — this single failure is not caused by the PR and should not block it.
  2. Make the test measure what it intends and stop being I/O-sensitive: construct one car_opts before UCS_TEST_TIME_LIMIT to warm the config-file/page cache, then time the second construction; better still, loop N times inside the timed block and assert on the best/median iteration rather than a single sample:
    /* warm up config-file parsing and page cache */
    { car_opts warmup(UCS_DEFAULT_ENV_PREFIX, NULL); }
    
    double best = std::numeric_limits<double>::max();
    for (int i = 0; i < 10; ++i) {
        ucs_time_t t = ucs_get_time();
        { car_opts opts(UCS_DEFAULT_ENV_PREFIX, NULL); }
        best = std::min(best, ucs_time_to_sec(ucs_get_time() - t));
    }
    EXPECT_LT(best, 0.05 * ucs::test_time_multiplier());
  3. Either implement the advertised retry (honour ucs::perf_retry_count / perf_retry_interval around the body) or fix the misleading "10 retries" comment in contrib/test_jenkins.sh:1298, since perf time-limit failures are currently never retried.

Related: PR #12195 (the build under test); CI-topology commits 390a30e and ed5255a. No existing issue tracking test_config.performance flakiness was found — worth opening one.

@evgeny-leksikov

Copy link
Copy Markdown
Contributor Author

all the rest changes are pulled into #12184

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

Labels

WIP-DNM Work in progress / Do not review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants