Skip to content

fix(rdma): stage device memory through the host in the HTTP fallbacks - #239

Merged
harshavardhana merged 4 commits into
minio:mainfrom
harshavardhana:fix/rdma-fallback-device-memory
Jul 29, 2026
Merged

fix(rdma): stage device memory through the host in the HTTP fallbacks#239
harshavardhana merged 4 commits into
minio:mainfrom
harshavardhana:fix/rdma-fallback-device-memory

Conversation

@harshavardhana

@harshavardhana harshavardhana commented Jul 29, 2026

Copy link
Copy Markdown
Member

Problem

Client::GetObject and Client::PutObject attempt RDMA and, when the server declines or retries are exhausted, fall through
to an HTTP path that does:

ss.rdbuf()->pubsetbuf(args.buf, size);   // + stream reads/writes

Those are ordinary host loads and stores. With a GPU-Direct caller args.buf is a CUDA device pointer, so the fallback
faults:

SIGSEGV: segmentation violation
PC=... sigcode=2 addr=0x746d9f000000        <- exactly the buffer argument
signal arrived during cgo execution
minio-go/v7._Cfunc_miniocpp_get_object(..., 0x746d9f000000, 0x1000000, 0x0, 0x0)

sigcode=2 is SEGV_ACCERR, at exactly the buffer address. Concurrency is not the cause — it only controls how often a
transfer declines and therefore how often the fallback is reached. The same file already guards host access elsewhere with
cuObjClient::getMemoryType(buf) == CUOBJ_MEMORY_SYSTEM; the two fallbacks just never got that treatment.

Fix

Stage through a host buffer whenever the caller's memory is not CUOBJ_MEMORY_SYSTEM: GET streams the body into host memory
and copies it up afterwards, PUT copies down before the upload reads it.

The two driver-API copies are resolved with dlopen("libcuda.so.1") rather than linked, so libminiocpp gains no
link-time CUDA dependency
readelf -d shows the same NEEDED set as before:

libcufile.so.0  libcuobjclient.so.1  libstdc++.so.6  libm.so.6  libgcc_s.so.1  libc.so.6  ld-linux-x86-64.so.2

A host without the driver gets a clear error instead of a crash.

Testing

Built with -DMINIO_CPP_ENABLE_RDMA=ON against a 4-node AIStor cluster over RoCE where RDMA PUT declines with
IBV_WC_REM_OP_ERR (remote RDMA READ out of GPU VRAM is unsupported on that platform, so the fallback is always taken):

before after
warp put --rdma=gpu every operation fails 496 MiB/s
warp get --rdma=gpu SIGSEGV past low concurrency 8264 MiB/s

Verified the dlopen path is actually wired (strings shows libcuda.so.1, cuMemcpyDtoH_v2, cuMemcpyHtoD_v2) and that
CPU-buffer callers are unaffected — getMemoryType short-circuits them to the original code path.

One residual error per run remains, but it is caller-side rather than here: cuMemcpyDtoH needs a current CUDA context on
the calling thread, and the Go caller does not yet pin its worker to one OS thread on the PUT path (fixed separately in
minio/warp#499 for GET).

Summary by CodeRabbit

  • Bug Fixes
    • Improved HTTP fallback behavior for RDMA GET and PUT when the caller uses CUDA device-memory buffers.
    • Added safe staging via host buffers so fallback transfers write to the correct memory location.
    • When required CUDA runtime capabilities aren’t available, the system now returns clearer, more actionable error messages instead of failing silently or producing incorrect results.

Client::GetObject and Client::PutObject attempt RDMA and, when the server declines or retries are exhausted, fall through to
an HTTP path that does

    ss.rdbuf()->pubsetbuf(args.buf, size);

plus stream reads/writes. Those are ordinary host loads and stores. With a GPU-Direct caller args.buf is a CUDA device
pointer, so the fallback faults: SIGSEGV with sigcode 2 (SEGV_ACCERR) at exactly the buffer address, from inside
miniocpp_get_object/miniocpp_put_object. Concurrency only controls how often a transfer declines and therefore how often the
fallback is reached.

Stage through a host buffer when the caller's memory is not CUDA_MEMORY_SYSTEM: GET streams the body into host memory and
copies it up afterwards, PUT copies down before the upload reads it. The two driver-API copies are resolved with dlopen so
libminiocpp gains no link-time CUDA dependency — readelf shows the same NEEDED set as before, and a host without the driver
gets a clear error instead of a crash.

Measured against a 4-node AIStor cluster where RDMA PUT declines with IBV_WC_REM_OP_ERR (remote RDMA READ out of GPU VRAM is
unsupported on that platform): warp put --rdma=gpu went from every operation failing to 512 MiB/s over the fallback.
@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@harshavardhana, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 25 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 18b47fd2-3dae-4b0a-9a07-3227dbc7d4b7

📥 Commits

Reviewing files that changed from the base of the PR and between b8394d4 and 46392fc.

📒 Files selected for processing (1)
  • src/client.cc
📝 Walkthrough

Walkthrough

client.cc adds optional runtime CUDA driver loading and host staging for CUDA device buffers in the RDMA-to-HTTP fallback paths of GetObject and direct-buffer PutObject.

Changes

CUDA-aware HTTP fallback

Layer / File(s) Summary
Runtime CUDA copy support
src/client.cc
Lazily resolves CUDA driver copy and context functions from libcuda.so.1, manages CUDA contexts, and detects device buffers without a hard CUDA link dependency.
GET and PUT buffer staging
src/client.cc
Stages device-buffer data through host memory during HTTP fallback transfers and reports CUDA, context, staging, and copy failures.

Estimated code review effort: 4 (Complex) | ~45 minutes

Suggested reviewers: jiuker

Poem

I’m a bunny with bytes in my paws,
Staging buffers by CUDA’s laws.
GET hops down, PUT hops near,
Host memory makes the path clear—
Errors thump softly when drivers aren’t here.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: staging CUDA device memory through host buffers in RDMA HTTP fallbacks.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/client.cc (1)

627-664: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Wrap the device-buffer staging allocations so an OOM reports an error instead of throwing uncaught. Both fallback paths size a std::vector<char> to the full transfer size with no exception handling, unlike the rest of the file's allocation code which reports failure via return codes.

  • src/client.cc#L627-L664: guard stage.resize(size) in the GET fallback (Line 641) with a try/catch (or a nothrow-checked allocation) and return error::make<GetObjectResponse>(...) on failure instead of letting std::bad_alloc propagate.
  • src/client.cc#L1140-L1164: apply the same guard to stage.resize(size) in the PUT fallback (Line 1153), returning error::make<PutObjectResponse>(...) on failure.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/client.cc` around lines 627 - 664, Guard the staging allocation in the
GET fallback around stage.resize(size) within the device_buf path, catching
allocation failure and returning an error::make<GetObjectResponse> result
instead of propagating std::bad_alloc. Apply the same allocation-failure
handling to stage.resize(size) in the PUT fallback, returning
error::make<PutObjectResponse>; both affected sites are in src/client.cc ranges
627-664 and 1140-1164.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/client.cc`:
- Around line 96-132: Ensure the RDMA fallback device-buffer copy path
establishes a valid CUDA context on the std::async worker before invoking the
functions resolved by GetCudaHostCopy. Update Client::GetObjectAsync and
Client::PutObjectAsync to retain and set the originating device context for
their staging copies, or explicitly detect and reject device buffers without a
current context while preserving the existing host-buffer behavior.

---

Outside diff comments:
In `@src/client.cc`:
- Around line 627-664: Guard the staging allocation in the GET fallback around
stage.resize(size) within the device_buf path, catching allocation failure and
returning an error::make<GetObjectResponse> result instead of propagating
std::bad_alloc. Apply the same allocation-failure handling to stage.resize(size)
in the PUT fallback, returning error::make<PutObjectResponse>; both affected
sites are in src/client.cc ranges 627-664 and 1140-1164.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 80f728f7-1af3-4a39-878b-07fa1d638437

📥 Commits

Reviewing files that changed from the base of the PR and between 3d21189 and d9eb66f.

📒 Files selected for processing (1)
  • src/client.cc

Comment thread src/client.cc
The driver-API copies act on the calling thread's current context. GetObjectAsync/PutObjectAsync dispatch through
std::async, so a fallback can run on a pool thread that never touched CUDA, where every copy fails with
CUDA_ERROR_INVALID_CONTEXT. Borrow the primary context of the device the buffer belongs to for the duration of the copy and
restore the thread afterwards; a thread that already has a context is left untouched.
@harshavardhana

harshavardhana commented Jul 29, 2026

Copy link
Copy Markdown
Member Author

Fixes Applied Successfully

Fixed 1 file based on 1 CodeRabbit feedback item.

Files modified:

  • src/client.cc

Commit: 5820abe

Context-protect the RDMA fallback copies before async dispatch

Valid, and I had a live symptom for it: every warp put --rdma=gpu run was leaving exactly one
unable to stage device memory to host for the HTTP fallback, which is cuMemcpyDtoH returning
CUDA_ERROR_INVALID_CONTEXT. Confirmed the mechanism in-tree — GetObjectAsync/PutObjectAsync dispatch through
std::async(std::launch::async, ...) (src/client.cc:1578), so a fallback can land on a pool thread that never touched CUDA.

Rather than documenting a caller constraint, the library now makes itself safe: both copies run under a ScopedCudaContext
that borrows the primary context of the device the buffer belongs to (via cuPointerGetAttribute
cuDevicePrimaryCtxRetaincuCtxSetCurrent) and restores the thread afterwards. A thread that already has a current
context is left untouched, so the common case costs one cuCtxGetCurrent. The new entry points are resolved through the same
dlopen handle, so there is still no link-time CUDA dependency:

NEEDED: libcufile.so.0 libcuobjclient.so.1 libstdc++.so.6 libm.so.6 libgcc_s.so.1 libc.so.6

Measured on the same 4-node cluster, where RDMA PUT always declines with IBV_WC_REM_OP_ERR so the fallback is on the
critical path:

before this commit after
put --rdma=gpu 496 MiB/s, 1 error 611 MiB/s, 0 errors
get --rdma=gpu 7127 MiB/s 7127 MiB/s (unchanged)
put --rdma=cpu 961 MiB/s 961 MiB/s (unchanged)

The latest autofix changes are on the fix/rdma-fallback-device-memory branch.

check-style.sh runs clang-format-20 --style=Google; the two reinterpret_cast lines wrapped after the '=' rather than before
it. No behaviour change.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/client.cc`:
- Line 193: Replace the -1 sentinel used by retained_ordinal_ with
std::optional<int>, using an empty optional to represent “nothing retained.”
Update all reads, comparisons, assignments, and resets of retained_ordinal_ in
the surrounding client implementation to use optional presence and value access
while preserving the existing retained-ordinal behavior.
- Around line 183-188: Update the ScopedCudaContext destructor to check the
return codes from ctx_set(nullptr) and ctx_release(retained_ordinal_), logging
warnings for failures consistent with ScopedRDMARegistration::Release().
Preserve the existing cleanup order and only release when retained_ordinal_ is
valid.
- Around line 157-195: Update ScopedCudaContext so a null current context is
bootstrapped before calling fns_.ptr_attr: obtain and set a usable device
context first, then query the pointer’s device ordinal and switch to the correct
context when necessary. Track whether the class owns a retained context
separately from retained_ordinal_ so the no-ownership path never relies on
retaining or releasing ordinal -1, while preserving cleanup in the destructor.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 982d2bcf-c502-45ae-b360-02680363199d

📥 Commits

Reviewing files that changed from the base of the PR and between d9eb66f and 5820abe.

📒 Files selected for processing (1)
  • src/client.cc

Comment thread src/client.cc
Comment thread src/client.cc
Comment thread src/client.cc Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
src/client.cc (3)

716-718: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Replace the implementation-defined stringstream::pubsetbuf() staging contract.

basic_stringbuf::setbuf() effects are implementation-defined, so this fallback is non-portable; MSVC’s default does not make stringstream write into the user-supplied buffer. Use a bound check + explicit chunk copy for the GET fallback, and construct an input stream from the staged host data (or use an explicit span-backed buffer) for the PUT fallback.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/client.cc` around lines 716 - 718, Replace the implementation-defined
stringstream::pubsetbuf staging in both GET and PUT fallback paths in
src/client.cc:716-718 and src/client.cc:1245-1246. For GET, validate the
requested size and explicitly copy chunks into the caller-provided buffer; for
PUT, construct the input stream from the staged host data or use an explicit
span-backed buffer. Do not rely on basic_stringbuf::setbuf behavior.

137-142: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Load the unsuffixed primary-context release symbol as a fallback.

GetCudaHostCopy().Ok() returns false when cuDevicePrimaryCtxRelease_v2 is missing, even though the driver may support all the other loaded symbols. Try cuDevicePrimaryCtxRelease_v2 first, then fall back to cuDevicePrimaryCtxRelease, or document/enforce the minimum CUDA driver version that requires _v2.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/client.cc` around lines 137 - 142, Update the CUDA symbol initialization
in GetCudaHostCopy() to load cuDevicePrimaryCtxRelease_v2 first and fall back to
cuDevicePrimaryCtxRelease when unavailable, so CudaHostCopy::Ok() remains true
with older compatible drivers. Keep the existing function-pointer type and other
symbol loading unchanged.

160-165: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Validate that an existing context supports the device copy.

When cuCtxGetCurrent() returns a non-null context, the thread already has a current CUDA context, but it is not checked against the allocation/device context. For multi-GPU callers, ensure that existing current contexts can cover the devptr allocation; otherwise the Driver API copy may use an inappropriate context. Check the pointer’s owning context/device, or document that copies are only supported when the current context is compatible.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/client.cc` around lines 160 - 165, Update the existing-context branch in
the relevant initialization method around fns.ctx_get and devptr so it verifies
that the current CUDA context is compatible with the allocation’s owning
context/device before setting ok_ and returning. Reject or otherwise handle
incompatible contexts rather than treating every non-null current context as
valid; preserve the existing success path for compatible contexts.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/client.cc`:
- Around line 716-718: Replace the implementation-defined
stringstream::pubsetbuf staging in both GET and PUT fallback paths in
src/client.cc:716-718 and src/client.cc:1245-1246. For GET, validate the
requested size and explicitly copy chunks into the caller-provided buffer; for
PUT, construct the input stream from the staged host data or use an explicit
span-backed buffer. Do not rely on basic_stringbuf::setbuf behavior.
- Around line 137-142: Update the CUDA symbol initialization in
GetCudaHostCopy() to load cuDevicePrimaryCtxRelease_v2 first and fall back to
cuDevicePrimaryCtxRelease when unavailable, so CudaHostCopy::Ok() remains true
with older compatible drivers. Keep the existing function-pointer type and other
symbol loading unchanged.
- Around line 160-165: Update the existing-context branch in the relevant
initialization method around fns.ctx_get and devptr so it verifies that the
current CUDA context is compatible with the allocation’s owning context/device
before setting ok_ and returning. Reject or otherwise handle incompatible
contexts rather than treating every non-null current context as valid; preserve
the existing success path for compatible contexts.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 15a39408-5f08-4b7c-9d9f-c8c57f2829da

📥 Commits

Reviewing files that changed from the base of the PR and between 5820abe and b8394d4.

📒 Files selected for processing (1)
  • src/client.cc

cuPointerGetAttribute can itself require a current context, so querying the buffer's device ordinal before establishing one
could fail on a fresh worker thread and report 'unable to establish a CUDA context' instead of using the primary context.
Borrow device 0's primary context to make the query legal, then move to the buffer's own device when it differs.

Also track ownership with std::optional<int> rather than a -1 sentinel, and log ctx_set/ctx_release failures during teardown
the way ScopedRDMARegistration already does, so a leaked primary-context reference leaves a trail.
@harshavardhana

Copy link
Copy Markdown
Member Author

Fixes Applied Successfully

Fixed 1 file based on 3 CodeRabbit feedback items (all three inline threads).

Files modified:

  • src/client.cc

Commit: 46392fc

🔴 Bootstrap a context before querying ptr_attr (client.cc:195)

Valid, and self-inflicted: ScopedCudaContext called cuPointerGetAttribute to learn the buffer's device before
establishing any context, while that query can itself require one. It happens to succeed on this driver — which is exactly
why it deserved fixing rather than relying on. Now device 0's primary context is retained purely to make the ordinal query
legal, then the guard moves to the buffer's own device when it differs, releasing the bootstrap either way.

🔵 Destructor silently swallows ctx_set/ctx_release failures (client.cc:188)

Valid. Both now log a warning, matching ScopedRDMARegistration::Release(), so a leaked primary-context reference leaves a
trail instead of vanishing.

🔵 Prefer std::optional<int> over the -1 sentinel (client.cc:193)

Valid per the repo guidelines. retained_ordinal_ is std::optional<int>, which also made the bootstrap→device handoff
above express ownership explicitly rather than juggling a sentinel.

Verification

Rebuilt with -DMINIO_CPP_ENABLE_RDMA=ON and re-run against the 4-node cluster, on the GPU that shares a PCIe switch with
the NIC:

result
put --rdma=gpu 1603 MiB/s, 0 errors, 0 warnings
server-side rdma_write_ops delta 1047 — every transfer took the RDMA path, none fell back
get --rdma=gpu 7513 MiB/s (unchanged)
check-style.sh exit 0

Worth noting for context: the RDMA PUT declines that made this fallback hot turned out to be PCIe ACS Redirect left
enabled on the switch above that GPU, not a platform limitation. With ACS cleared these PUTs now go over RDMA directly, so
this guard is back to being the safety net it was meant to be.

@harshavardhana
harshavardhana merged commit 313486d into minio:main Jul 29, 2026
12 checks passed
@harshavardhana
harshavardhana deleted the fix/rdma-fallback-device-memory branch July 29, 2026 13:08
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.

1 participant