Skip to content

UCP/RMA: Sample multi-rail zcopy throughput - #12002

Merged
evgeny-leksikov merged 8 commits into
openucx:masterfrom
gleon99:rma-measured-sample-step1
Oct 2, 2026
Merged

evgeny-leksikov merged 8 commits into
openucx:masterfrom
gleon99:rma-measured-sample-step1

Conversation

@gleon99

@gleon99 gleon99 commented Sep 30, 2026

Copy link
Copy Markdown
Contributor

What?

Add opt-in per-rail completed-payload sampling for multi-rail PUT/GET zcopy.

Why?

Foundation for dynamic load-balancing, based on actual port performance measurement.

How?

Track accepted fragments through UCT completion.

@gleon99 gleon99 added the WIP-DNM Work in progress / Do not review label Sep 30, 2026
@svc-nvidia-pr-review

Copy link
Copy Markdown

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

Comment thread src/ucp/rma/rma_bw.inl Resolved
Comment thread src/ucp/rma/rma_bw.inl Resolved
Comment thread src/ucp/rma/rma_bw.inl Resolved
Comment thread src/ucp/rma/put_offload.c Resolved
Comment thread src/ucp/core/ucp_worker.h Resolved
Comment thread test/gtest/Makefile.am Resolved
@svc-nvidia-pr-review

Copy link
Copy Markdown

Residual coverage gap: no end-to-end test enables UCX_RMA_BW_MEASURE, so the config option, the 2-lane/256KB admission conditions and the 1-second rate limit are untested; multi-rail PUT/GET zcopy also needs RDMA hardware, which the current unit test avoids by bypassing the protocol entirely.

Residual coverage gap: the restart/failover interaction described above is exercised by test_ucp_ep_reconfig/test_ucp_fault_tolerance only with the option disabled.

@svc-ucx

svc-ucx commented Sep 30, 2026

Copy link
Copy Markdown

🤖 CI Triage Agent — UCX PR (Build build_aarch64 sles15sp7_aarch64) · commit 136cb21b

TL;DR: The make distcheck build failed because the new header src/ucp/rma/rma_bw.inl (added by this PR) is not listed in noinst_HEADERS in src/ucp/Makefile.am, so it is missing from the distribution tarball; add it to the list.

Full analysis

Summary: build_aarch64 / sles15sp7_aarch64 distcheck build failed compiling src/ucp/rma/get_offload.c and put_offload.c with fatal error: rma_bw.inl: No such file or directory.

Root cause: Commit 136cb21 ("UCP/RMA: Sample multi-rail zcopy throughput") adds a new inline header src/ucp/rma/rma_bw.inl and #include "rma_bw.inl" from rma/get_offload.c:13 and rma/put_offload.c:13, but the file was never added to noinst_HEADERS in src/ucp/Makefile.am (the list contains rma/rma.h, rma/rma.inl, rma/rma_rndv.h only). An in-tree build works, but this job runs make distcheck, which builds from the generated tarball — the un-listed .inl is not distributed, so the compile of the two offload sources fails and make[2]: all-recursive Error 1 / distcheck Error 1 follows. This is purely a packaging/build-file omission, not a code defect; the failure is deterministic and would hit every distcheck-style job, not just aarch64.

Implicated commit: [REDACTED:Hex High Entropy String] — Leonid Genkin, "UCP/RMA: Sample multi-rail zcopy throughput"

File: src/ucp/Makefile.am:72-74 (missing entry); consumer at src/ucp/rma/get_offload.c:13 and src/ucp/rma/put_offload.c:13

Suggested fix: Add the new header to the distributed-source list in src/ucp/Makefile.am, keeping alphabetical order within the rma/ group:

	rma/rma.h \
	rma/rma.inl \
	rma/rma_bw.inl \
	rma/rma_rndv.h \

Then re-run ./autogen.sh && ./contrib/configure-release && make distcheck locally to confirm the tarball includes rma_bw.inl. As a general guard, any new .h/.inl added under src/ must be registered in the corresponding Makefile.am (noinst_HEADERS or nobase_dist_..._HEADERS), since only distcheck jobs catch the omission.

Related: PR #12002 (this PR); the include sites were introduced alongside fe0ea85 "UCP/RMA: Make put/rndv and get/rndv a fallback of zcopy (#11940)" refactor of the offload protocols.

🛡️ This comment had 1 potential secret(s) redacted (Hex High Entropy String). See request_id 8d5b0703-b4b5-41ce-839a-bd84c1e96483 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.

Comment thread src/ucp/core/ucp_request.h Resolved
Comment thread src/ucp/rma/put_offload.c Resolved
Comment thread src/ucp/rma/rma_bw.inl Resolved
Comment thread src/ucp/rma/rma_bw.inl Resolved
Comment thread test/gtest/Makefile.am Resolved
@svc-nvidia-pr-review

Copy link
Copy Markdown

This PR adds ~680 lines, which is over the 500 added-line limit, and the measurement result currently only ends up in a ucs_trace(). Can you split it (sampling infrastructure + its own test, separately from the PUT/GET integration), or describe in the PR what is going to consume the samples?

Residual coverage gaps (not posted, already reported previously): no test verifies that a sample is actually admitted/validated on a real 2-rail zcopy path, and the reset/restart (failover, wireup replay) interaction with ucp_rma_bw_sample_detach() is only exercised with the option disabled.

@svc-ucx

svc-ucx commented Sep 30, 2026

Copy link
Copy Markdown

🤖 CI Triage Agent — UCX PR (Coverity coverity release on coverity_rh7) · commit 94249a33

TL;DR: Coverity flagged 4 new TAINTED_SCALAR defects in ucp_proto_{get,put}_offload_zcopy_progress because this PR hoisted req->send.proto_config->priv into a local mpriv variable, moving the tainted_data_downcast event away from the line that the existing /* coverity[tainted_data_downcast] */ annotation suppresses. Move the annotation to directly above the mpriv declaration.

Full analysis

Summary: The coverity release job failed the defect gate — the Coverity report contains new TAINTED_SCALAR ("Untrusted value as argument" / "Untrusted loop bound") defects at src/ucp/rma/get_offload.c:224,233 and src/ucp/rma/put_offload.c:314,323, so the stage exited non-zero (+ [[ 1 -eq 0 ]]) after publishing cov_build_release/output/errors.

Root cause: Not a real bug — a broken Coverity suppression annotation. Upstream (see src/ucp/tag/eager_multi.c:284-286) the downcast of void const *priv to ucp_proto_multi_priv_t * happens inline in the ucp_proto_multi_zcopy_progress() call argument, and /* coverity[tainted_data_downcast] */ sits immediately above that statement, so the taint is suppressed at its source.

This PR refactored both RMA offload progress functions to hoist the downcast into a local:

  • get_offload.c:216 — const ucp_proto_multi_priv_t *mpriv = req->send.proto_config->priv;
  • put_offload.c:306 — same

The tainted_data_downcast event is now reported on line 216/306, but the annotations were left where they were (lines 223/232 and 313/322, above the return). A coverity[...] comment only suppresses events on the immediately following line, so nothing is suppressed anymore. Coverity then propagates the taint through mpriv->lanes → lpriv->super.max_iov / md_index into loop bounds and array indices in datatype_iter.inl:590/datatype_iter.c:229, producing two defects per call site. Because the new UCP_REQUEST_FLAG_RMA_BW_SAMPLE branch adds a second ucp_proto_multi_zcopy_progress() call in each function, the count doubled to 4.

Implicated commit: 136cb21b "UCP/RMA: Sample multi-rail zcopy throughput" (Leonid Genkin) introduced the hoisted mpriv + sample branch; head commit 94249a33 "UCP/RMA: Make throughput sampling restart-safe and allocation-free" (Leonid Genkin) is what CI built.

File: src/ucp/rma/get_offload.c:216 and src/ucp/rma/put_offload.c:306 (annotations misplaced at get_offload.c:223,232, put_offload.c:313,322)

Suggested fix: Move the single suppression to the taint source and drop the two now-dead ones in each file:

static ucs_status_t ucp_proto_put_offload_zcopy_progress(uct_pending_req_t *self)
{
    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;

    if (!(req->flags & UCP_REQUEST_FLAG_PROTO_INITIALIZED)) {
        ucp_rma_bw_sample_start(req, mpriv);
    }

    if (req->flags & UCP_REQUEST_FLAG_RMA_BW_SAMPLE) {
        return ucp_proto_multi_zcopy_progress(req, mpriv, ...);
    }

    return ucp_proto_multi_zcopy_progress(req, mpriv, ...);
}

Apply the identical change in get_offload.c (annotation above line 216, delete the comments at 223 and 232). If suppressing at the declaration still leaves the call sites flagged, use /* coverity[tainted_data] */ above each return ucp_proto_multi_zcopy_progress(...) instead, since the defect type reported at those lines is TAINTED_SCALAR/tainted_data, not tainted_data_downcast. No functional/runtime change is needed — mpriv is internally generated protocol config, not untrusted input.

Related: PR #12002 (branch rma-measured-sample-step1); precedent pattern in src/ucp/tag/eager_multi.c:284.

@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

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

Comment thread src/ucp/rma/put_offload.c Resolved
Comment thread src/ucp/rma/rma_bw.c Resolved
Comment thread src/ucp/rma/rma_bw.inl Resolved
Comment thread src/ucp/rma/rma_bw.inl Resolved
Comment thread src/ucp/rma/get_offload.c Resolved
Comment thread src/ucp/rma/rma_bw.c Resolved
Comment thread src/ucp/core/ucp_context.c Outdated
ucs_offsetof(ucp_context_config_t, prefer_offload), UCS_CONFIG_TYPE_BOOL},

{"RMA_BW_MEASURE", "n",
"Sample completed payload throughput of two-rail PUT/GET zcopy operations. "

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.

maybe "multi-lane"?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed

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.

could you please point me where? I would suggest to unify everywhere if we are limited by 2 rails at this phase

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed now

Comment thread src/ucp/rma/get_offload.c
Comment on lines +182 to +191
if (measure) {
comp = ucp_rma_bw_frag_start(req, req->send.multi_lane_idx, &frag);
}

status = uct_ep_get_zcopy(ucp_ep_get_lane(req->send.ep, lpriv->super.lane),
&iov, 1, req->send.rma.remote_addr + offset,
tl_rkey, comp);
if (measure) {
ucp_rma_bw_frag_posted(frag, iov.length, status);
}

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.

why do we need 2 timestamps? should not doorbell of zcopy be constant?
I think we need it only if status == INPROGRESS
if status is err, then sampling probably does not make sense

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The return-time clock now runs only for sync UCS_OK. An async frag still needs the pre-post time and later callback time. Err contributes no bytes.

Comment thread src/ucp/rma/rma_bw.inl Resolved
Comment thread src/ucp/rma/rma_bw.inl Outdated
Comment on lines +48 to +56
for (i = 0; i < UCP_RMA_BW_MAX_ACTIVE; ++i) {
sample = &worker->rma_bw_samples[i];
if (ucp_rma_bw_sample_is_free(sample)) {
break;
}
}
if (i == UCP_RMA_BW_MAX_ACTIVE) {
return;
}

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.

maybe organize lifo stack with limited depth?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Completions can release slots out of order. The current scan is bounded to 4 slots and runs only for a new sample.

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 meant free list as a stack, see uct_rc_iface_send_op_t *free_ops for example if we need > 4 elements.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

IMO keep it simple as now, cause only 4 are needed.

Comment thread src/ucp/rma/rma_bw.inl Outdated
Comment on lines +101 to +104
if ((frag == NULL) || ((status != UCS_OK) &&
(status != UCS_INPROGRESS))) {
return;
}

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.

Suggested change
if ((frag == NULL) || ((status != UCS_OK) &&
(status != UCS_INPROGRESS))) {
return;
}
if ((frag == NULL) || UCS_STATUS_IS_ERR(status)) {
return;
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed

Comment thread src/ucp/rma/rma_bw.inl
++sample->pending;
} else if (now > lane->last_comp) {
lane->last_comp = now;
}

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.

ucs_get_time() is the most expensive part here, let's call it only if needed

Suggested change
}
if (lane->num_frags == 0) {
lane->first_post = frag->post_time;
}
lane->bytes += bytes;
++lane->num_frags;
if (ucs_likely(status == UCS_INPROGRESS)) {
++lane->num_async;
++sample->num_frags;
++sample->pending;
return;
}
/* measure sync completion */
ucs_assert(status == UCS_OK);
now = ucs_get_time();
if (now > lane->last_comp) {
lane->last_comp = now;
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good

Comment thread src/ucp/rma/rma_bw.inl Outdated
Comment on lines +48 to +56
for (i = 0; i < UCP_RMA_BW_MAX_ACTIVE; ++i) {
sample = &worker->rma_bw_samples[i];
if (ucp_rma_bw_sample_is_free(sample)) {
break;
}
}
if (i == UCP_RMA_BW_MAX_ACTIVE) {
return;
}

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 meant free list as a stack, see uct_rc_iface_send_op_t *free_ops for example if we need > 4 elements.

Comment thread src/ucp/core/ucp_context.c Outdated
ucs_offsetof(ucp_context_config_t, prefer_offload), UCS_CONFIG_TYPE_BOOL},

{"RMA_BW_MEASURE", "n",
"Sample completed payload throughput of two-rail PUT/GET zcopy operations. "

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.

could you please point me where? I would suggest to unify everywhere if we are limited by 2 rails at this phase

@svc-nvidia-pr-review

Copy link
Copy Markdown

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

Comment thread src/ucp/rma/get_offload.c Resolved
Comment thread src/ucp/rma/rma_bw.c Resolved
Comment thread src/ucp/rma/rma_bw.inl Resolved
Comment thread src/ucp/core/ucp_worker.h Resolved
@svc-ucx

svc-ucx commented Oct 1, 2026

Copy link
Copy Markdown

🤖 CI Triage Agent — UCX PR (Static_check Static checks) · commit 39221b57

TL;DR: The Static_check job failed because clang's static analyzer flagged a dead store at src/ucp/rma/rma_bw.c:75 (lane = &sample->lanes[i]; whose only consumer is a ucp_trace_req() that compiles out in this build config). Drop the lane temporary in that second loop and dereference sample->lanes[i] directly inside the trace call.

Full analysis

Summary: Static_check / cstools reports step exited non-zero: static checkers found errors: CLANG_WARNING: /src/ucp/rma/rma_bw.c:75:17: warning: Value stored to 'lane' is never read [deadcode.DeadStores].

Root cause: The build itself succeeded (all CC/CCLD steps completed, jucx BUILD SUCCESS). The failure is purely the static-analysis gate. In ucp_rma_bw_sample_complete() the second loop (lines 74–80) assigns lane = &sample->lanes[i]; and the only use of lane there is inside ucp_trace_req(...). ucp_trace_req expands to ucs_log(UCS_LOG_LEVEL_TRACE_REQ, ...), which is guarded by ucs_log_component_is_enabled() → (_level) <= UCS_MAX_LOG_LEVEL (src/ucs/debug/log_def.h:24-26,50). In the static-check configuration UCS_MAX_LOG_LEVEL is a compile-time constant below TRACE_REQ, so the analyzer statically prunes that branch, leaving the assignment with no reader — hence deadcode.DeadStores. The warning appears twice because the file is analyzed in two passes, and cslinker/csgrep turns any surviving warning into a hard error ([ -s cs.err ]).

This is new code: the loop at lines 73–81 was introduced by the head commit of this PR branch, which split the single-lane logic into a per-lane trace loop. The follow-on ##[error]Path does not exist: .../static_check from PublishPipelineArtifact is a secondary artifact-publish failure caused by the step aborting before the report directory was created — not an independent problem.

Implicated commit: [REDACTED:Hex High Entropy String] — Leonid Genkin, "UCP/RMA: Support multi-lane sample"

File: src/ucp/rma/rma_bw.c:75 (variable declared at src/ucp/rma/rma_bw.c:61)

Suggested fix: Remove the redundant temporary in the trace-only loop so there is no store for the analyzer to flag:

        if (i == sample->num_lanes) {
            for (i = 0; i < sample->num_lanes; ++i) {
                ucp_trace_req(req, "rma bw sample lane %u/%u: %zu bytes / "
                              "%.3f us", i, sample->num_lanes,
                              sample->lanes[i].bytes,
                              ucs_time_to_sec(sample->lanes[i].last_comp -
                                              sample->lanes[i].first_post) * 1e6);
            }
        }

lane is still legitimately read in the first validation loop (lines 66–70), so keep the declaration at line 61 — only the second loop's assignment needs to go. Alternatives if you prefer to keep the temporary: declare it inside the second loop as ucp_rma_bw_lane_t *lane = &sample->lanes[i]; won't help (same dead store), so inlining is the correct remedy. Avoid adding a (void)lane; suppression — it would hide genuinely unused values later.

Related: PR #12002 (this build). Prior example of the same static-check gate blocking a merge: #11857

🛡️ This comment had 1 potential secret(s) redacted (Hex High Entropy String). See request_id fd30b518-28ac-4e22-b278-0fca8bc87492 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.

@evgeny-leksikov evgeny-leksikov added Ready for Review and removed WIP-DNM Work in progress / Do not review labels Oct 2, 2026
Comment thread src/ucp/rma/rma_bw.inl
ucp_rma_bw_sample_try_start(ucp_request_t *req,
ucp_lane_index_t num_lanes)
{
if (ucs_unlikely(req->send.ep->worker->context->config.ext.rma_bw_measure)) {

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.

ucp_rma_bw_sample_start already has this condition
AFAIU currently ucp_rma_bw_sample_try_start is needed only for inlining, but ucp_rma_bw_sample_startrequires defactoringrequires refactoring. @gleon99 minor for now, pls update in separate PR as well as code style - alignments by /*and=` in few places

@evgeny-leksikov
evgeny-leksikov merged commit bbf1387 into openucx:master Oct 2, 2026
162 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants