Skip to content

UCP/RMA: Apply ep_based weak fence once per worker fence - #12029

Open
michael-beebe wants to merge 2 commits into
openucx:masterfrom
michael-beebe:topic/ucp-rma-ep-based-fence-once
Open

michael-beebe wants to merge 2 commits into
openucx:masterfrom
michael-beebe:topic/ucp-rma-ep-based-fence-once

Conversation

@michael-beebe

Copy link
Copy Markdown

What

Apply an ep_based weak fence once per worker fence instead of on every
subsequent RMA/AMO operation on the endpoint.

Why

With UCX_FENCE_MODE=ep_based, ucp_ep_rma_handle_fence() applies a weak
fence when only one lane has unflushed operations, but it does not advance
ep->ext->fence_seq. Every later operation on that endpoint therefore still
sees the fence as required, calls uct_ep_fence() again, and is excluded
from the put_short fast path. This lasts until a strong fence or a new
worker fence.

This hurts fence-heavy workloads such as OpenSHMEM shmem_fence() followed
by many small puts. On an InfiniBand (rc_mlx5) system, an OpenSHMEM
alltoalls benchmark built from per-element puts ran about 4.4x faster at
4 KiB per PE with this change.

How

  • Advance the endpoint fence sequence after a successful weak fence, or when
    there are no earlier operations to order.
  • Track the weakly fenced lane in a new ucp_ep_ext_t::fenced_lanes. A weak
    fence orders only that lane, so a later operation on a different lane still
    applies a strong fence, as before. fenced_lanes is cleared by the strong
    fence (it is always a subset of unflushed_lanes).
  • ucp_put_send_short() falls back to the protocol path when another lane
    was weakly fenced, so that path applies the strong fence.
  • Add test_ep_based_fence_applied_once_{put,get,atomic} gtests that check
    the endpoint fence sequence after a fenced operation. They fail without the
    fix (the endpoint sequence stays behind the worker sequence).

Tested locally with gtest filters
*test_ucp_ep_based_fence*:*test_ucp_fence*:*test_ucp_rma_order*:*test_ucp_rma.*
(116 tests) and *amo*:*rma*:*flush*:*fence*:-*test_ucp_rma.* (764 tests).

With UCX_FENCE_MODE=ep_based, an operation that needs a fence on a
single-lane endpoint applies a weak fence but does not advance the
endpoint fence sequence. Every later operation on that endpoint therefore
keeps requesting the fence, applies another weak fence, and is excluded
from the put short fast path until a strong fence or new worker fence.

Advance the endpoint sequence after a successful weak fence, or when
there are no earlier operations to order. Record the weakly fenced lane;
a weak fence orders only that lane, so a later operation on another lane
still applies a strong fence, as before. The put short fast path falls
back to the protocol path for that case.

Add gtests that check the endpoint sequence after a fenced put, get, and
atomic operation.
@svc-nvidia-pr-review

Copy link
Copy Markdown

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

Comment thread src/ucp/rma/rma.inl Outdated
} else {
status = ucp_ep_fence_strong(ep);
}
} else if (ucs_unlikely(lane_map & ~ep->ext->fenced_lanes) &&

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 test fenced_lanes != 0 first: in the common case it is 0, so lane_map & ~fenced_lanes is non-zero and the ucs_unlikely hint is on the wrong sub-expression.

Suggested change
} else if (ucs_unlikely(lane_map & ~ep->ext->fenced_lanes) &&
} else if (ucs_unlikely(ep->ext->fenced_lanes != 0) &&
(lane_map & ~ep->ext->fenced_lanes)) {

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done in ca6fc76. The NULL check now comes first, inside the new ucp_ep_rma_is_strong_fence_required() helper, so the ucs_unlikely hint applies to the rare case. The field is now a single fenced_lane index, so the check is ucs_unlikely(fenced_lane != UCP_NULL_LANE) && (lane_map & ~UCS_BIT(fenced_lane)). Testing for NULL first also avoids computing UCS_BIT(UCP_NULL_LANE).

Comment thread src/ucp/rma/rma.inl
if (ucs_unlikely(req->flags & UCP_REQUEST_FLAG_FENCE_REQUIRED)) {
if (ucs_unlikely(ep->ext->unflushed_lanes == 0)) {
status = UCS_OK;
/* No earlier operation to order */

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 unflushed_lanes == 0 branch: fenced_lanes is always a subset of unflushed_lanes, so this store is redundant. maybe replace it with an assert to document the invariant that ucp_ep_fence_strong() and the new branch rely on?

Suggested change
/* No earlier operation to order */
/* No earlier operation to order */
ucs_assert(ep->ext->fenced_lanes == 0);
ep->ext->fence_seq = ep->worker->fence_seq;
status = UCS_OK;

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done in ca6fc76. The store is replaced with ucs_assert(ep->ext->fenced_lane == UCP_NULL_LANE).

Comment thread src/ucp/rma/rma.inl
} else if (ucs_likely(
ucs_is_pow2_or_zero(ep->ext->unflushed_lanes | lane_map))) {
status = ucp_ep_fence_weak(ep);
if (status == UCS_OK) {

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 weak fence branch: why set fence_seq to the current worker->fence_seq and not to the value that was pending when the request was created? if the user calls ucp_worker_fence() again after this request was submitted but before it is progressed, the EP is marked as satisfying the newer fence too, and a later operation on the same lane will not be fenced against this one. (same assumption already exists in ucp_ep_fence_strong(), so maybe worth a follow-up rather than this PR.)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Agreed, but I would keep this out of this PR. The weak branch makes the same assumption that ucp_ep_fence_strong() already makes, so this change is consistent with the current code. Recording the sequence number pending at request creation would have to change both paths, plus the request state, so it seems better as a separate follow-up.

Comment thread src/ucp/rma/rma_send.c
tl_rkey = ucp_rkey_get_tl_rkey(rkey, rkey_config->put_short.rkey_index);

if (ucs_unlikely(ucp_ep_rma_is_fence_required(ep))) {
if (ucs_unlikely(ucp_ep_rma_is_fence_required(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: can we hide this in an inline helper in rma.inl next to ucp_ep_rma_is_fence_required()? the fence bookkeeping is otherwise fully contained there, and put_send_short now open-codes part of it.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done in ca6fc76. Added ucp_ep_rma_is_strong_fence_required(ep, lane_map) next to ucp_ep_rma_is_fence_required() in rma.inl. Both ucp_ep_rma_handle_fence() and ucp_put_send_short() use it now.

Comment thread test/gtest/ucp/test_ucp_rma.cc Outdated
sender().ep()->ext->fenced_lanes = 0;
}

void do_rma_op_with_fence_applied_once(op_type_t op, void *sbuf,

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 second EXPECT_EQ(worker_fence_seq(), ep_fence_seq()) cannot fail — re-applying a fence does not change ep_fence_seq, so the test does not actually verify that the fence is applied only once. pls also cover the new path: weak fence on one lane followed by an operation on another lane (should trigger the strong fence), e.g. small op + fence + small op, then a large multi-lane op.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done in ca6fc76.

  • The *_once tests now mock ep_fence on each lane's iface. They check that the fence count does not change on the next op after the fence, which is a real check. I confirmed they fail without the fence_seq update.
  • Added test_ep_based_fence_other_lane_{put,get}. Each does op + fence + op, then a 1-byte put (which can take the short path) and a large multi-lane put, and expects a strong fence (fenced_lane reset) whenever an op lands on a lane other than the fenced one.
  • I left out an atomic variant. When AMOs are emulated in software, the strong fence blocks until the receiver makes progress, which a single-threaded test cannot provide.
  • On a single-lane config (e.g. local cma) the other-lane check is trivially satisfied. It takes effect with MAX_RMA_LANES=2 on multi-lane transports such as rc.

@svc-nvidia-pr-review

Copy link
Copy Markdown

Residual coverage gap: ucp_put_send_short() bail-out on fenced_lanes is not exercised by any test.

Residual coverage gap: flush_sender_cleanup() clears both unflushed_lanes and fenced_lanes between iterations, which hides the new else if branch from the existing test_ucp_ep_based_fence cases; test_ucp_rma_order does not reach it either (it always takes the strong-fence path after the multi-lane put).

When the planned flush_worker() reset of unflushed_lanes lands, it must reset fenced_lanes as well, otherwise ucp_ep_fence_strong()'s unflushed_lanes != 0 assertion can fire from the new branch.

@svc-ucx

svc-ucx commented Oct 5, 2026

Copy link
Copy Markdown

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

TL;DR: test_obj_size.size failed because the PR's new fence_seq field grew ucp_ep_ext_t from 216 to 224 bytes while the hard-coded expected size in test/gtest/common/test_obj_size.cc:53 was not updated. Either update the expected size to 224 or shrink/repack the new field so the struct stays at 216 bytes.

Full analysis

Summary: Release-config gtest test_obj_size.size failed: EXPECT_EQ((size_t)216, sizeof(ucp_ep_ext_t)) reported Which is: 224.

Root cause: The only failing assertion is the object-size guard on ucp_ep_ext_t. The branch commit [REDACTED:Hex High Entropy String] ("UCP/RMA: Apply ep_based weak fence once per worker fence") is the most recent change to src/ucp/core/ucp_ep.h and added the 8-byte uint64_t fence_seq member to ucp_ep_ext_t (src/ucp/core/ucp_ep.h:613, right after fenced_lanes). Summing the struct's members gives exactly 216 bytes without fence_seq and 224 with it, which matches the reported delta precisely. The test's hard-coded expectation (216) was not updated along with the struct, so the size-regression guard tripped. This is a deterministic build-config failure, not flakiness — note the test only runs in release config (it is skipped when debug data / stats / asserts are enabled), which is why it only surfaced in this "althca" release job.

Implicated commit: [REDACTED:Hex High Entropy String] — Michael Beebe, "UCP/RMA: Apply ep_based weak fence once per worker fence" (PR #12029)

File: test/gtest/common/test_obj_size.cc:53 (expected value) / src/ucp/core/ucp_ep.h:613 (new fence_seq member)

Suggested fix: Preferred: avoid growing the hot ucp_ep_ext_t structure — a 64-bit sequence counter is not needed for fence-once detection. Declare fence_seq as uint32_t and move it up next to ucp_rsc_index_t cm_idx (offset 32), which currently has 7 bytes of trailing padding before the 8-byte-aligned local_ep_id; this makes the field free and keeps sizeof(ucp_ep_ext_t) == 216, so no test change is needed. Make sure the matching worker-side counter uses the same width and that wraparound is handled with UCS_CIRCULAR_COMPARE32-style comparison rather than </>. If the field genuinely must be 64-bit and the extra 8 bytes are acceptable, instead update test/gtest/common/test_obj_size.cc:53 to EXPECTED_SIZE(ucp_ep_ext_t, 224) in the same commit and call out the per-endpoint memory increase in the PR description for reviewer sign-off.

Related: PR #12029 (this change); prior fence work in PR #10474 / commit 0ccdfbec "UCP/RMA/FLUSH: Dynamic selection of strong vs. weak fence" (added fenced_lanes); PR #11871 "UCP/RMA: Make EP-based fence non-blocking and failover-safe" (overlapping area). Unrelated cosmetic noise in the same log: gcc: error: test_ucp_config-test_ucp_config.o: No such file or directory from the test_ucp_config step, which did not fail the job but is worth a separate look.

🛡️ This comment had 1 potential secret(s) redacted (Hex High Entropy String). See request_id 7d26600a-dd66-4b84-a7ce-a0408757ac17 in the triage console for the audit trail.

Track the weakly fenced lane in a one-byte field placed in existing
padding, keeping ucp_ep_ext_t at its previous size. Move the cross-lane
fence check into an inline helper used by both the request and put-short
paths, assert the fenced-lane invariant when no lane is unflushed, and
count transport fences in the test to verify a fence is applied once.
Add tests for an operation on another lane after a weak fence.
@svc-nvidia-pr-review

Copy link
Copy Markdown

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

@michael-beebe

Copy link
Copy Markdown
Author

Pushed ca6fc76 to address the review:

  • CI test_obj_size failure: this came from the new 64-bit fenced_lanes field, not from fence_seq. It is replaced with a one-byte ucp_lane_index_t fenced_lane placed in the existing padding after cm_idx, so sizeof(ucp_ep_ext_t) returns to its previous value. A weak fence only applies when exactly one lane is unflushed, so a single index is enough.
  • put_short bail-out coverage: the new other_lane tests issue a short put after a weak fence on another lane. On multi-lane configs this takes the bail-out.
  • flush_sender_cleanup(): the other_lane tests check the cross-lane path before cleanup runs. Cleanup also resets fenced_lane along with unflushed_lanes.
  • Future flush_worker() reset: agreed. Any reset of unflushed_lanes must also reset fenced_lane, as ucp_ep_fence_strong() already does.
  • Request-time fence sequence: proposed as a follow-up (see the inline reply).

@michael-beebe
michael-beebe marked this pull request as ready for review October 6, 2026 04:54
@svc-ucx

svc-ucx commented Oct 6, 2026

Copy link
Copy Markdown

🤖 CI Triage Agent — UCX PR (AddressSanitizer new on worker 3) · commit ca6fc760

TL;DR: The ASAN gtest job aborted in rc_mlx5/test_uct_ib_sl.check_ib_sl_config_without_ddp/0 with a fatal "Transport retry count exceeded ... sl=4" — all SL=4 traffic on mlx5_0:1 is being dropped on that host, which matches the leaked drop rules installed by the CI fault-tolerance test (sl = EXECUTOR_NUMBER+1 = 4) that is torn down with kill -9. This is unrelated to PR #12029; fix the FT test's cleanup (graceful kill / explicit unblock) rather than the UCX code.

Full analysis

Summary: rc_mlx5/test_uct_ib_sl.check_ib_sl_config_without_ddp/0 aborted (core dumped) during send_recv_short() on SL 4 with Transport retry count exceeded on mlx5_0:1/IB (synd 0x15 vend 0x81), failing make -C test/gtest test (Bash exited with code '2').

Root cause: Environmental, not a code regression. The log shows the failing WQE was RC QP 0x1aed4 wqe[0]: SEND --e [inl len 18] [rqpn 0x1aed0 dlid=1 sl=4 port=1] — i.e. a loopback RC send on service level 4 timed out after exhausting RC retries, while every other IB/mlx5 test in the same run (all using the default SL 0) passed. test_check_ib_sl_config() walks SL 0..UCT_IB_SL_NUM and does a real send/recv per SL (test/gtest/uct/ib/test_ib.cc:728-742); the test ran 14s (06:24:45 → 06:24:59), consistent with SLs 0‑3 succeeding quickly and SL 4 silently dropping until the RC retry counter expired. On this CI host that exact SL is what the fault-tolerance job blackholes: contrib/test_jenkins.sh:494 sets sl=$(( EXECUTOR_NUMBER + 1 )) (worker/executor 3 → SL 4) and contrib/test_jenkins.sh:549 runs ${drop_tool} -d ${dev} -s ${sl} -t ${tc} & for every active IB device, never unblocking a device between iterations and tearing the tool down with kill -9 (kill_background_processes, lines 471-476, 539/559/566). A SIGKILLed drop tool cannot remove its SL/TC drop rules, so SL 4 stays blackholed on the adapter and poisons any later job on the same machine — here the ASAN gtest run. The failure is deterministic-fatal because uct_ib_mlx5_completion_with_err logs at FATAL level, aborting the whole gtest binary (hence the backtrace through ucs_fatal_error_message).

Implicated commit: [REDACTED:Hex High Entropy String] "CI/FT: Adds fault tolerance CI test (#11570)" — william-gallagher-nv (introduced run_ucx_perftest_fault_tolerance and its kill -9 teardown). The PR under test (ca6fc76, topic/ucp-rma-ep-based-fence-once, UCP/RMA fence) touches no IB/SL code and is not implicated.

File: contrib/test_jenkins.sh:471-476 and 494, 546-566 (drop-rule teardown); symptom surfaces at test/gtest/uct/ib/test_ib.cc:728-742

Suggested fix:

  1. Make the drop tool teardown graceful and idempotent in contrib/test_jenkins.sh: send SIGTERM (and wait) instead of kill -9 so the tool removes its rules, add a trap 'kill_background_processes "${background_pids[@]}"' EXIT ERR around run_ucx_perftest_fault_tolerance, and explicitly unblock each device (${drop_tool} -d ${dev} --unblock/equivalent) after each loop iteration and in the error paths at lines 539 and 559.
  2. Add a pre-test sanity/cleanup step (e.g. in check_machine() in buildlib/tools/common.sh) that clears any leftover SL/TC drop rules on all active IB devices, so a crashed FT run can't poison subsequent jobs on the same executor.
  3. Re-run build 138129 after clearing the stale rules on swx-rdmz-ucx-new-01; the PR itself should be re-tested, not changed.

Related: PR #12029 (build under test, unrelated to the failure); PR #11570 / commit [REDACTED:Hex High Entropy String] (added the FT test with the kill -9 teardown); PR #10262 "UCT/IB/TEST: Configure available SLs in gtest" (added GTEST_MAX_IB_SL, which could be used as a stopgap to cap the SL range on affected machines).

🛡️ This comment had 1 potential secret(s) redacted (Hex High Entropy String). See request_id ed847232-4103-4231-9aab-52083be775d9 in the triage console for the audit trail.

@michael-beebe

Copy link
Copy Markdown
Author

/azp run UCX PR

@azure-pipelines

Copy link
Copy Markdown
Commenter does not have sufficient privileges for PR 12029 in repo openucx/ucx

This branch has not been deployed

No deployments
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.

3 participants