Skip to content

UCP/RMA: Make EP-based fence non-blocking and failover-safe - #11871

Open
nbellalou wants to merge 9 commits into
openucx:masterfrom
nbellalou:rmaStrongFence_v2
Open

nbellalou wants to merge 9 commits into
openucx:masterfrom
nbellalou:rmaStrongFence_v2

Conversation

@nbellalou

Copy link
Copy Markdown
Contributor

What?

This PR is an updated replacement for PR #11199, not an incremental follow-up to the old branch. It carries the original endpoint-based, non-blocking fence design forward and adapts it to the current UCP protocol, flush-pipeline, and endpoint-failover behavior.

It makes the strong path of UCX_FENCE_MODE=ep_based non-blocking for one-sided RMA and atomic operations:

  • Treat ucp_worker_fence() as an epoch increment.
  • Track the lanes that contain pre-fence operations on each endpoint.
  • Enforce a multi-lane boundary with an asynchronous, lane-selective remote flush.
  • Queue post-fence requests per endpoint and replay them after the required epoch completes.
  • Keep the single-lane weak-fence and no-preceding-work fast paths.
  • Use a bounded inline spin to avoid asynchronous queueing overhead when the flush completes quickly.
  • Reconcile lane-selective flush state when endpoint lanes disappear or are replaced during failover.

Why?

The worker-wide strong fence is a synchronous remote flush across active endpoints and rails. It blocks the caller and over-serializes multi-rail traffic, since unrelated endpoints and faster rails must wait for the slowest outstanding work.

Endpoint-based enforcement keeps ucp_worker_fence() lightweight and pays the ordering cost only on endpoints that have both pre-fence and post-fence work. The strong operation is further limited to the lanes that carried the preceding epoch whenever the endpoint topology remains stable.

The original implementation in PR #11199 did not account for the current flush pipeline and endpoint failover model. This updated version ensures that:

  • Endpoint and worker flushes wait for API-visible requests held in the fence pending queue.
  • A flush only waits for fence epochs that existed when that flush started.
  • Lane removal, addition, and same-index replacement cannot make an in-flight selective flush miss required work.
  • Fence-blocked protocol requests are restarted against the current endpoint configuration after failover.

The behavior remains opt-in with UCX_FENCE_MODE=ep_based and requires Proto v2.

How?

The worker and each endpoint maintain fence sequence numbers. A post-fence request snapshots its required epoch before protocol-specific progress. The endpoint then takes one of three paths:

  1. Advance immediately when there is no pre-fence work.
  2. Apply a weak UCT fence when all relevant work shares one lane.
  3. Start a lane-selective remote flush for multi-lane or RMA rendezvous work. If it does not complete within the bounded spin interval, queue the request and resume it asynchronously from the endpoint progress callback.

Each in-flight flush snapshots the endpoint lane generation and tracks its known, started, and selected lanes. If failover changes the topology, the completion accounting is reconciled and current live lanes are conservatively included so that a same-index replacement cannot be mistaken for the original transport endpoint.

Testing

  • make -j64
  • ASAN_OPTIONS=protect_shadow_gap=0 ./test/gtest/gtest --gtest_filter='*fence*:*flush*' — 1222/1222 passed
  • git diff --check master...HEAD

@svc-nvidia-pr-review

Copy link
Copy Markdown

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

Comment thread src/ucp/core/ucp_request.h Outdated
#define ucp_trace_req(_sreq, _message, ...) \
ucs_trace_req("req %p: " _message, (_sreq), ## __VA_ARGS__)

#define UCP_STATUS_FENCE_DEFER ((ucs_status_t)(UCS_ERR_LAST - 2))

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_STATUS_FENCE_DEFER is outside the valid ucs_status_t range. UCS_ERR_LAST is -100, so UCP_STATUS_FENCE_DEFER = UCS_ERR_LAST - 2 = -102, which is more negative than UCS_ERR_LAST. Any accidental flow into UCS_STATUS_PTR/UCS_PTR_IS_ERR would misclassify it as a valid pointer (since UCS_PTR_IS_ERR tests >= UCS_ERR_LAST), and it is out of range for ucs_status_string. All current paths appear to intercept it explicitly, but this is fragile. Can we use a value inside the reserved error range, or a dedicated non-status sentinel that can't be confused with a status pointer?

@svc-nvidia-pr-review

Copy link
Copy Markdown

PR exceeds the 500 added-line hard limit set in REVIEW.md. It mixes several separable concerns: the send.rma/send.amo → fenced_req struct refactor, the RMA-RNDV remote-op tracking (rma_rndv_ops), the flush lane-generation/lane_mask rework, and the new fence-pending queue engine. Please split — at minimum the mechanical fenced_req field rename and the flush lane-tracking refactor should be their own PRs with no functional change, leaving the fence engine as a focused feature PR.

src/ucp/rma/rma.h

Adding uint8_t flags to the packed ucp_cmpl_hdr_t changes the on-wire UCP_AM_ID_CMPL header from 8 to 9 bytes. ucp_rma_cmpl_handler reads putackh->flags with no length check, so a completion sent by an older peer (8-byte header) makes the new receiver read flags out of bounds. If that stray byte has UCP_CMPL_FLAG_RMA_RNDV set, it calls ucp_ep_rma_rndv_remote_request_completed, which asserts rma_rndv_ops > 0 and decrements it — an assert failure / underflow across mixed-version peers. Please confirm how this is version-negotiated, or carry the RNDV distinction without growing the wire header. Question: does this break wire compatibility with peers that don't send the new flags byte?

src/ucp/core/ucp_ep.c

ucp_ep_fence_dispatch_request: when req->send.uct.func returns UCS_INPROGRESS after the batch loop, the request is pushed back to the head of the fence-pending queue and the loop breaks. A normal multi-fragment send that is legitimately in progress is now only re-driven when the fence queue is next resumed (flush completion / signal), not by the usual pending/progress path. Please confirm an in-progress (non-fence-blocked) request cannot get stuck waiting only on ucp_ep_fence_pending_resume.

src/ucp/core/ucp_ep.c

ucp_ep_fence_pending_add: the epoch-ordered insert scans the whole queue on every add. Under many deferred fenced ops this is O(n²). Given the stated goal is to avoid blocking, worth a short note on expected queue depth, or use a structure that keeps ordering cheaply.

Test coverage: the new synthetic tests (fresh_defer_without_uct_lane, pending_queue_orders_fence_epochs, lane-state units) are good, but they use stack requests and defer_once. There is no end-to-end test that exercises the real strong-fence-nb + fence_inflight_req + spin + resume path across multiple lanes, nor the mixed-version CMPL header path. Please add coverage for the multi-lane strong-fence drain, or identify the CI job that covers it.

Rebase risk: the diff moves send.rma into send.fenced_req.rma (dropping the rma.sgl sub-struct from the new layout) but does not touch the req->send.rma.sgl.* uses in rma_send.c and put_offload.c. If those SGL uses exist on this PR's target base, this will not compile — please rebase and confirm the SGL PUT path still builds and works.

@svc-nvidia-pr-review

Copy link
Copy Markdown

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

@nbellalou

Copy link
Copy Markdown
Contributor Author

@svc-nvidia-pr-review

Comment thread src/ucp/core/ucp_request.h Outdated
#define ucp_trace_req(_sreq, _message, ...) \
ucs_trace_req("req %p: " _message, (_sreq), ## __VA_ARGS__)

#define UCP_STATUS_FENCE_DEFER ((ucs_status_t)(UCS_ERR_LAST - 2))

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_STATUS_FENCE_DEFER value is fragile w.r.t. ucs_status_ptr_t classification. It is defined as UCS_ERR_LAST - 2 (= -102), which is outside the error range that UCS_PTR_IS_ERR recognizes: UCS_PTR_IS_ERR(-102) is false (because (uintptr_t)(-102) < (uintptr_t)UCS_ERR_LAST), while UCS_STATUS_IS_ERR(-102) is true and ucp_request_send_state_advance treats any UCS_STATUS_IS_ERR as fatal-fastforward. The current call paths keep this code inside uct_pending_callback_t returns that are explicitly filtered (ucp_request_try_send, ucp_proto_multi_*, ucp_ep_fence_dispatch_request), so it does not leak into a UCS_STATUS_PTR()/ucp_request_send_state_advance path today. But this is a landmine: any future progress path that forwards an unfiltered status into UCS_STATUS_PTR or ucp_request_send_state_ff will silently misbehave. Suggest defining it inside the reserved error range (e.g. between UCS_ERR_LAST and the endpoint-failure band, or a dedicated internal sentinel) and/or adding a compile-time assert that UCS_PTR_IS_ERR(UCP_STATUS_FENCE_DEFER) holds, so the invariant is enforced.

Comment thread src/ucp/core/ucp_ep.inl
{
ucs_assert(lane_index != UCP_NULL_LANE);

++ep->ext->lane_generation;

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_set_lane marks fence tracking dirty even for initial/NULL lane sets. It now unconditionally does ++lane_generation; fence_lanes_dirty = 1;, but it is called during normal wireup and teardown with uct_ep == NULL (e.g. ucp_ep.c allocation, cleanup, several wireup.c NULL sets). Setting an initially-empty lane or clearing a lane on teardown flips fence_lanes_dirty, which forces the next fence to conservatively flush all live lanes (ucp_ep_fence_lane_map_normalize). Correctness is preserved (normalize resets the flag), but this over-eagerly widens the fence flush set for benign topology events. Consider only marking dirty when replacing a non-NULL lane with a different non-NULL EP, which is the case the dirty flag is meant to guard.

@svc-nvidia-pr-review

Copy link
Copy Markdown

Observation on ucp_ep_fence_pending_purge: it clears fence_pending_scheduled = 0 but does not remove any already-queued ucp_ep_fence_pending_progress oneshot from the progress callbackq. The dedup guard is only the flag, and ucs_callbackq_add_oneshot does not dedup by (cb,arg). After a purge that empties the queue, a subsequent ucp_ep_fence_pending_add can enqueue a second oneshot while the stale one is still pending; both fire harmlessly against an empty/idempotent queue, but the invariant "at most one scheduled callback per ep" is not actually held by the flag alone. This is not a crash today (ucp_ep_remove_filter cleans up on destroy, and the progress callback is empty-safe), but worth confirming there's no path where the double-schedule interacts badly with ep refcount/teardown ordering.

Coverage gap: the multi-lane strong-fence-nb drain, the fence_inflight_req refcount/ucp_ep_fence_try_spin path, and the mixed-version CMPL header on hardware could not be exercised from this checkout, so those coverage gaps remain open.

@svc-ucx

svc-ucx commented Sep 1, 2026

Copy link
Copy Markdown

🤖 CI Triage Agent — UCX PR (Coverity coverity devel on coverity_rh7) · commit bf3246cd

TL;DR: The Coverity devel job failed with 4 new TAINTED_SCALAR defects because this PR hoisted req->send.proto_config->priv into a mpriv local declaration (so it could pass mpriv->lane_map to the new ucp_proto_rma_fence_progress()), which moved the void const * → ucp_proto_multi_priv_t downcast away from the line covered by the existing /* coverity[tainted_data_downcast] */ annotation. Move the annotation to the line immediately above each const ucp_proto_multi_priv_t *mpriv = req->send.proto_config->priv; declaration.

Full analysis

Summary: Azure Pipelines job "coverity devel on coverity_rh7" failed with ##[error]Coverity found 4 issues — all TAINTED_SCALAR (Untrusted value as argument) in src/ucp/rma/get_offload.c and src/ucp/rma/put_offload.c. The build itself compiled cleanly (739/739 TUs, cov-build completed successfully, cov-analyze ran 13:36) and the script returned exit 4 = number of Coverity errors.

Root cause: Not a build/test/timeout failure — a static-analysis gate. In UCX the void const *priv → ucp_proto_multi_priv_t * downcast is a known false positive that is suppressed with an inline /* coverity[tainted_data_downcast] */ annotation placed directly above the ucp_proto_multi_zcopy_progress() call, where the downcast used to be written inline.

This PR (rmaStrongFence_v2) refactored all four *_progress() functions so the downcast now happens in a hoisted local declaration, because mpriv->lane_map is needed for the new fence check:

const ucp_proto_multi_priv_t *mpriv = req->send.proto_config->priv;   /* <-- downcast moved HERE */
...
if (!(req->flags & UCP_REQUEST_FLAG_PROTO_INITIALIZED) &&
    !ucp_proto_rma_fence_progress(req, mpriv->lane_map, &status)) {
    return status;
}

/* coverity[tainted_data_downcast] */                                 /* <-- annotation stranded */
return ucp_proto_multi_zcopy_progress(req, mpriv, ...);

Coverity's event 1. tainted_data_downcast is now reported at the declaration line (186 / 278 / 520 / 617), which the annotation no longer covers. The taint then propagates: mpriv → mpriv->lanes → lpriv = &mpriv->lanes[lane_idx] (proto_multi.inl:180) → lpriv->super.lane → ucp_ep_md_index() → array index into ucp_ep_config(ep)->md_index (ucp_ep.inl:159), producing four identical TAINTED_SCALAR reports.

Implicated commit: [REDACTED:Hex High Entropy String] — Nathan Bellalou, "UCP/RMA: Harden EP-based fence across failover" (together with 4ca79f7b, "UCP/RMA: Create non-blocking worker fence for one-sided RMA multi-lane ops", same PR #11871)

File: src/ucp/rma/get_offload.c:186 (defect reported at 195), src/ucp/rma/put_offload.c:278 (287), src/ucp/rma/put_offload.c:520 (529), src/ucp/rma/put_offload.c:617 (626)

Suggested fix: Move the existing suppression comment to immediately precede the declaration that now performs the downcast, in all four functions, and drop it from the (no longer downcasting) call site. E.g. in ucp_proto_get_offload_zcopy_progress():

 ucp_request_t *req = ucs_container_of(self, ucp_request_t, send.uct);
+/* coverity[tainted_data_downcast] */
 const ucp_proto_multi_priv_t *mpriv = req->send.proto_config->priv;
 ucs_status_t status;

 if (!(req->flags & UCP_REQUEST_FLAG_PROTO_INITIALIZED) &&
     !ucp_proto_rma_fence_progress(req, mpriv->lane_map, &status)) {
     return status;
 }

-/* coverity[tainted_data_downcast] */
 return ucp_proto_multi_zcopy_progress(req, mpriv, ...);

Apply the same edit at put_offload.c:278, :520, :617. If Coverity still flags the propagation path after moving the annotation (the sink is in proto_multi.inl/ucp_ep.inl, not on the annotated line), the alternative is to keep the downcast in the declaration but restore an explicit annotated cast, e.g. a small helper/macro so the annotation and the cast are on the same statement, or add TAINTED_SCALAR to the Coverity --disable/model list in buildlib/ for this call chain. Do not raise any timeout — there is no hang here; timestamps show continuous progress and the job finished normally.

Related: PR #11871 (this PR). Prior refactors of the same functions that preserved the annotation on the call-site line: efcc8340 (#11647), 0a5fba2c (#11765), df65ebdd (#11577). No existing issue tracking this Coverity suppression pattern was found.

🛡️ This comment had 1 potential secret(s) redacted (Hex High Entropy String). See request_id da573231-d202-434c-9b30-6dd72ea354bd in the triage console for the audit trail.

@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

src/ucp/rma/rma.inl

ucp_ep_fence_try_spin calls uct_iface_progress directly from ucp_ep_rma_handle_fence, which runs in the send-submission path (ucp_put_nbx/ucp_atomic_op_nbx). Elsewhere UCP only progresses ifaces from the worker progress loop. Can we confirm this reentrant progress is safe — e.g. completion callbacks fired here (including ucp_ep_fence_strong_nb_flushed_cb → ucp_ep_fence_pending_resume) won't recurse into request processing or violate the caller's async-blocked assumptions?

@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.

…e ops

Post-fence requests are queued in per-EP pending queue and drain
asynchronously using callback once the preceding epoch's flush completes.
Add lane mask to flush only needed lanes.
Make the private fence-defer status compatible with UCS error pointers.

Track lane generations only for real endpoint changes, initialize new and recycled lane storage, and mark fence lane state dirty only when replacing a lane with outstanding work.

Preserve ownership of an already scheduled fence-pending callback when purging its request queue.
Decode both legacy 8-byte and current 9-byte RMA completion headers, treating a missing flags byte as zero and rejecting truncated endpoint IDs.

Require a peer version that understands the flagged completion header and keep RMA/RNDV disabled for failover mode, where completion accounting cannot identify individual operations.
Keep a request-owned visibility reference while an RMA rendezvous operation is being prepared, retried, transferred to an internal request, or aborted.

Move the operation into remote-completion accounting only after the wire send starts, and restore the request-owned reference if the send must be retried. This keeps strong fence and flush from completing while the operation is still active.
Add deterministic coverage for a strong fence that owns one in-flight request across two asynchronous lane flushes, including reference lifetime and final fence state.

Place Coverity tainted-downcast annotations directly on the protocol-private-data reads they describe.
@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 (Tests gpu on worker 0) · commit 4727d13c

TL;DR: The only failing test is the newly-added shm_ib/test_ucp_proto_am_rndv.am_explicit_scheme_disables_force_provenance/0, which hard-asserts that am/rndv is selected at 1 MB while running with RNDV_SCHEME=get_zcopy — on this GPU worker the AM eager protocol (am/egr/multi/bcopy) wins instead, because with an explicit RNDV scheme all non-matching rndv variants are registered with UCS_MEMUNITS_INF (last-resort) thresholds. Fix the test: restrict PROTOS (as the sibling test_ucp_proto_rma_rndv class already does) or skip gracefully when am/rndv is not the selected protocol.

Full analysis

Summary: gtest shm_ib/test_ucp_proto_am_rndv.am_explicit_scheme_disables_force_provenance/0 failed (make: *** [Makefile:4714: test] Error 1); 10278/10299 tests passed, 1 failed.

Root cause: Log evidence:

test/gtest/ucp/test_ucp_proto.cc:439: Expected equality of these values:
  "am/rndv"  vs  thresh->proto_config.proto->name  Which is: "am/egr/multi/bcopy"
test/gtest/ucp/test_ucp_proto.cc:455: Expected: (nullptr) != (remote_proto_config), actual: (nullptr) vs NULL

select_am_rndv_remote_proto_config() performs a slow protocol lookup for UCP_OP_ID_AM_SEND with CUDA memory at 1 MB and requires the winner to be am/rndv (test_ucp_proto.cc:432-442). The test runs with RNDV_PIPELINE_SHM_CUDA_STAGING_FORCE=y and RNDV_SCHEME=get_zcopy (test_ucp_proto.cc:796-804). With RNDV_SCHEME set, rndv_mode != UCP_RNDV_MODE_AUTO, so ucp_proto_rndv_shm_pipeline_force_enabled() returns 0 (proto_rndv.inl:20-28) and every rndv-ctrl variant whose remote proto doesn't match the forced scheme is added with remote_proto->cfg_thresh == UCS_MEMUNITS_INF — "used only as last resort" (proto_rndv.inl:145-181, and the explicit assertion/comment at proto_rndv.c:476-483). On this worker (variant shm,ib,cuda_copy,rocm_copy, no gdr_copy) no get_zcopy remote variant survives for the estimated CUDA rkey config, so all am/rndv variants remain last-resort and am/egr/multi/bcopy wins the 1 MB threshold; the subsequent ASSERT_NE(nullptr, remote_proto_config) then fails because the helper returned nullptr.

The two sibling tests without RNDV_SCHEME (am_default_remote_model_preserves_legacy_key, am_force_remote_model_remains_untagged) passed, and the analogous RMA tests passed because test_ucp_proto_rma_rndv::init() pins modify_config("PROTOS", "put/rndv,get/rndv,rndv/*") (test_ucp_proto.cc:314-318) — test_ucp_proto_am_rndv has no such guard, so it competes against eager protocols and is therefore configuration/environment dependent. This is a test-side defect, not a product regression from the strong-fence changes.

Implicated commit: 8e881fc "UCP/RMA: Preserve rendezvous completion compatibility" — Nathan Bellalou (most recent commit touching test/gtest/ucp/test_ucp_proto.cc, part of this PR series; branch head 4727d13 "UCP/RMA: Remove inline progress from strong fence")

File: test/gtest/ucp/test_ucp_proto.cc:439 (helper at 404-447; failing test at 796-804); related selection logic in src/ucp/rndv/proto_rndv.inl:168-182 and src/ucp/rndv/proto_rndv.c:474-499

Suggested fix: Make test_ucp_proto_am_rndv deterministic, mirroring the RMA class:

  1. Add an init() override to the class: modify_config("PROTOS", "am/rndv,rndv/*"); test_ucp_proto::init(); so eager protocols can't win the 1 MB threshold; and/or
  2. Turn the hard EXPECT_STREQ("am/rndv", ...) into a graceful UCS_TEST_SKIP_R("AM/RNDV protocol is not selected in this configuration") (as select_rma_rndv_remote_proto_config() does at line 353-355) so the test doesn't fail on workers where no get_zcopy rndv variant is available for CUDA (no gdr_copy in the shm,ib,cuda_copy,rocm_copy variant).
    Optionally also force RNDV_THRESH=0 for this test to remove threshold sensitivity.

Related: PR #11871 (this PR); prior protocol-selection triage PRs #11858, #11861, #11864; #11902 ("UCP/PROTO: Keep direct-access protocols eligible for CPU-accessible memory")

Signed-off-by: Nathan Bellalou <nbellalou@nvidia.com>
@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.

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