Skip to content

feat(s3): add optional S3-over-RDMA (cuObject) data plane#87

Open
harshavardhana wants to merge 7 commits into
NVIDIA:mainfrom
harshavardhana:fea-s3-rdma-cuobject
Open

feat(s3): add optional S3-over-RDMA (cuObject) data plane#87
harshavardhana wants to merge 7 commits into
NVIDIA:mainfrom
harshavardhana:fea-s3-rdma-cuobject

Conversation

@harshavardhana

@harshavardhana harshavardhana commented Jun 26, 2026

Copy link
Copy Markdown

What

Optional S3-over-RDMA data plane for the s3 provider via NVIDIA cuObject. Set rdma: {} and payloads move over RDMA (signed x-amz-rdma-token header, empty HTTP body) instead of the HTTP/TLS path — offloading bulk transfer from the CPU on RDMA-capable endpoints like MinIO AIStor.

How

  • Swaps only the data plane (like rust_client); mutually exclusive with it; type: s3 unchanged.
  • Single-shot for small objects and all GETs; RDMA multipart past rdma.multipart_chunksize (default 512 MiB).
  • Thin extern "C" shim over a process-wide cuObjClient, loaded via ctypes; import-safe without the native lib.
  • Zero-copy write for writable buffers; a per-PUT CRC64NVME guards against silent 0-byte saves.
  • Host-staged (GPUDirect device path out of scope).

Review fixes

  • Register before-sign.s3.UploadPart so multipart parts carry the token.
  • Size buffers by memoryview.nbytes, not len().
  • Release the token before dropping the registry entry; always deregister the buffer.
  • Reject rdma.multipart_chunksize < 1 (runtime + schema).
  • Bind the full-object GET to the HEAD version via IfMatch.

Validated

  • H200 → MinIO AIStor, 400G RoCE, dual-rail; real RDMA confirmed on the wire.
  • msc-benchmark 1 MiB–1 GiB: RDMA PUT beats TCP at every size (+6–33%); GET on par to +13% (numbers in comment below).
  • 15 unit tests pass (native engine mocked).

Summary by CodeRabbit

  • New Features
    • Added optional S3-over-RDMA transfers using NVIDIA cuObject for RDMA-capable endpoints.
    • Introduced the s3_cuobject storage provider for accelerated GET/PUT and multipart uploads.
    • Added configuration support for RDMA multipart chunk sizing (rdma.multipart_chunksize).
  • Documentation
    • Added an end-to-end RDMA roundtrip example against an S3-compatible endpoint (reads/writes multiple payload sizes).

@copy-pr-bot

copy-pr-bot Bot commented Jun 26, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@coderabbitai

coderabbitai Bot commented Jun 26, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds optional NVIDIA cuObject S3-over-RDMA support with native Rust/C++ bindings, Python transfer orchestration, a registered storage provider, schema and unit-test coverage, and an executable roundtrip example.

Changes

S3-over-RDMA data plane

Layer / File(s) Summary
Native cuObject build and shim
multi-storage-client/rust/Cargo.toml, multi-storage-client/rust/build.rs, multi-storage-client/rust/csrc/*, .gitignore
Adds the optional RDMA build feature, SDK discovery, native compilation, and C ABI wrappers for cuObject client, descriptor, and token operations.
Rust cuObject bindings
multi-storage-client/rust/src/cuobj.rs, multi-storage-client/rust/src/lib.rs
Adds singleton cuObject client management, token lifetime tracking, PyO3 bindings, and feature-gated module registration.
Python RDMA engine and provider
multi-storage-client/src/multistorageclient/providers/_cuobj.py, multi-storage-client/src/multistorageclient/providers/s3_cuobject.py, multi-storage-client/src/multistorageclient/{config.py,schema.py}, multi-storage-client/src/multistorageclient/providers/__init__.py
Adds RDMA token hooks and cleanup, empty-body checksum handling, RDMA GET/PUT and multipart transfers, provider registration, and configuration validation.
RDMA provider validation
multi-storage-client/tests/test_multistorageclient/unit/providers/test_s3_rdma.py
Tests provider wiring, checksums, buffer handling, GET sizing, multipart completion and abort behavior, hooks, cleanup, and invalid options.
Roundtrip example and release note
multi-storage-client/examples/rdma_roundtrip.py, .release_notes/.unreleased.md
Adds an environment-configured end-to-end roundtrip script and documents the new capability.

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

Sequence Diagram(s)

sequenceDiagram
  participant S3CuObjectStorageProvider
  participant CuObjEngine
  participant multistorageclient_rust
  participant S3Endpoint
  S3CuObjectStorageProvider->>CuObjEngine: register buffer and request token
  CuObjEngine->>multistorageclient_rust: call cuObject token API
  multistorageclient_rust-->>CuObjEngine: return RDMA token
  CuObjEngine->>S3Endpoint: sign request with x-amz-rdma-token
  S3CuObjectStorageProvider->>S3Endpoint: send empty-body S3 request
  S3Endpoint-->>CuObjEngine: return x-amz-rdma-reply
  CuObjEngine->>multistorageclient_rust: release token and deregister buffer
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly states the main change: adding an optional cuObject-based S3-over-RDMA data plane.
Description check ✅ Passed The description is detailed and covers the change, implementation, fixes, and validation, though it doesn't follow the template verbatim.
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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

@shunjiad

Copy link
Copy Markdown
Contributor

/ok to test 72f8703

@harshavardhana
harshavardhana marked this pull request as ready for review July 18, 2026 03:43
@harshavardhana
harshavardhana requested a review from a team July 18, 2026 03:43

@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: 6

🧹 Nitpick comments (1)
multi-storage-client/src/multistorageclient/providers/_cuobj.py (1)

222-222: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add the required public-method docstring.

As per coding guidelines, “All public Python functions must have docstrings (Sphinx autodoc extracts them).”

🤖 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 `@multi-storage-client/src/multistorageclient/providers/_cuobj.py` at line 222,
Add a Sphinx-compatible docstring directly to the public install_hooks method,
documenting its purpose and s3_client parameter; leave the method’s behavior
unchanged.

Source: Coding guidelines

🤖 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 `@multi-storage-client/src/multistorageclient/providers/_cuobj.py`:
- Around line 223-225: The RDMA token hook is not registered for multipart
uploads. In multi-storage-client/src/multistorageclient/providers/_cuobj.py
lines 223-225, update install_hooks to register self._inject_token for
before-sign.s3.UploadPart alongside the existing PutObject and GetObject hooks;
verify the related handling in
multi-storage-client/src/multistorageclient/providers/s3.py lines 680-688
requires no direct change, and extend the multipart RDMA coverage in
multi-storage-client/tests/test_multistorageclient/unit/providers/test_s3_rdma.py
lines 195-221 to assert UploadPart receives the x-amz-rdma-token header.
- Around line 180-188: The buffer sizing logic in _buffer_address and the
corresponding RDMA registration path must use the full byte size of writable
memoryviews. Replace len(buffer) with memoryview(buffer).nbytes in both
locations, and add a regression test covering a writable multi-byte-format
memoryview to verify the complete region is registered/tokenized.

In `@multi-storage-client/src/multistorageclient/providers/cuobj_shim.cpp`:
- Around line 106-120: Keep tokenRegistry() entries available until
getClient()->cuMemObjPutRDMAToken succeeds in cuobj_put_rdma_token; only erase
the entry after successful release so failures remain retryable. In
multi-storage-client/src/multistorageclient/providers/_cuobj.py lines 268-271,
place deregister_buffer(addr) in its own finally block so it executes even when
put_rdma_token(token) raises.

In `@multi-storage-client/src/multistorageclient/providers/s3.py`:
- Around line 275-277: Reject non-positive values when initializing
_rdma_multipart_chunksize in
multi-storage-client/src/multistorageclient/providers/s3.py:275-277, and
constrain rdma.multipart_chunksize to a positive integer in
multi-storage-client/src/multistorageclient/schema.py:119-119. In
multi-storage-client/src/multistorageclient/providers/s3.py:671-672, retain the
multipart loop only under the guarantee that n is greater than zero.
- Around line 589-604: Update the full-object path in the surrounding read
method and _invoke_api so the metadata lookup and allocation are bound to the
same object version: capture the metadata identity/version, pass the
corresponding conditional version constraint to get_object, and retry the
metadata/allocation flow or fail cleanly when the GET detects a mismatch.
Preserve ranged-read behavior unless it also relies on the same metadata. Add a
unit test that replaces the object between metadata lookup and GET and verifies
the mismatch is retried or reported without returning a partially populated
buffer.

In
`@multi-storage-client/tests/test_multistorageclient/unit/providers/test_s3_rdma.py`:
- Around line 195-221: Update the RDMA multipart upload setup used by
test_rdma_upload_multipart_splits_and_completes and the corresponding
CuObjEngine.install_hooks configuration to register the UploadPart operation
alongside PutObject and GetObject, ensuring multipart upload_part calls receive
RDMA token injection.

---

Nitpick comments:
In `@multi-storage-client/src/multistorageclient/providers/_cuobj.py`:
- Line 222: Add a Sphinx-compatible docstring directly to the public
install_hooks method, documenting its purpose and s3_client parameter; leave the
method’s behavior unchanged.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: b5947bcd-0b89-4c5f-b2d1-945173ef4304

📥 Commits

Reviewing files that changed from the base of the PR and between d6e7890 and d95d553.

📒 Files selected for processing (7)
  • .release_notes/.unreleased.md
  • multi-storage-client/examples/rdma_roundtrip.py
  • multi-storage-client/src/multistorageclient/providers/_cuobj.py
  • multi-storage-client/src/multistorageclient/providers/cuobj_shim.cpp
  • multi-storage-client/src/multistorageclient/providers/s3.py
  • multi-storage-client/src/multistorageclient/schema.py
  • multi-storage-client/tests/test_multistorageclient/unit/providers/test_s3_rdma.py

Comment thread multi-storage-client/src/multistorageclient/providers/_cuobj.py Outdated
Comment thread multi-storage-client/src/multistorageclient/providers/_cuobj.py
Comment thread multi-storage-client/src/multistorageclient/providers/cuobj_shim.cpp Outdated
Comment thread multi-storage-client/src/multistorageclient/providers/s3.py Outdated
Comment thread multi-storage-client/src/multistorageclient/providers/s3.py Outdated
Comment thread multi-storage-client/tests/test_multistorageclient/unit/providers/test_s3_rdma.py Outdated
Add an `rdma` option to the `s3` storage provider that routes object
transfers through NVIDIA cuObject (libcuobjclient) instead of the HTTP
body. When enabled, a contiguous host buffer is registered with cuObject
and its RDMA descriptor is carried to the endpoint as the signed
`x-amz-rdma-token` header; the RDMA-capable endpoint transfers the
payload directly into or out of the buffer, leaving the HTTP body empty.
This offloads the bulk transfer from the CPU and the HTTP/TLS path.

The option mirrors the existing `rust_client` sub-option: it swaps only
the data plane and is mutually exclusive with it. Enabling RDMA forces
the empty-body wire contract (unsigned payload, checksums only
when_required) and single-shot put/get, since multipart does not apply
to a single registered-buffer transfer. cuObject is a C++ library, so a
thin extern "C" shim (providers/cuobj_shim.cpp) wraps a process-wide
cuObjClient and is loaded over ctypes (providers/_cuobj.py); the module
is import-safe without the native library present, mirroring the
torch.cuda.cuobj / BotoCuObjClient split in the PyTorch checkpoint
backend.

Test Plan:
- Unit (native engine mocked), run with the boto3 extra:
  `uv run --extra boto3 pytest tests/test_multistorageclient/unit/providers/test_s3_rdma.py`
- Regression on existing S3/schema unit tests: passing.
- End-to-end against a live RDMA endpoint:
  `python examples/rdma_roundtrip.py` (see the script header for the
  required cuObject runtime and environment).
Two refinements to the S3-over-RDMA data plane:

Reuse the caller's buffer for an RDMA PUT whenever it is writable
(gating on `memoryview(body).readonly` instead of an `isinstance`
bytearray check), so writable memoryviews are registered in place and
only a genuinely read-only body (e.g. immutable `bytes`) is copied.

Compute a CRC64NVME checksum of the payload on the client and send it as
`x-amz-checksum-crc64nvme`. An RDMA-capable endpoint validates it against
the bytes delivered over RDMA; an endpoint that ignores the RDMA token
validates it against the empty HTTP body and rejects the upload with a
checksum mismatch, instead of silently storing a 0-byte object. CRC64NVME
is hardware-accelerated and computed via awscrt.

Test Plan:
  uv run --extra boto3 pytest tests/test_multistorageclient/unit/providers/test_s3_rdma.py
Verified the empty-body/RDMA-reader/checksum ordering against the MinIO
AIStor PutObjectHandler (rr.Reader feeds the hash reader on the RDMA
path; r.Body feeds it otherwise).
Split RDMA uploads larger than the part size (default 512 MiB,
`rdma.multipart_chunksize`) into an RDMA multipart upload instead of a
single PutObject. Each part is transferred as its own registered cuObject
buffer with an empty HTTP body, its RDMA token, and a per-part CRC64NVME
that the endpoint validates against the RDMA-delivered bytes, so the
0-byte-save guard holds per part. Parts are aborted on any failure.

This is required past the single-PutObject size limit (5 GiB) that AI
checkpoint and model shards routinely exceed, bounds the size of any one
registered buffer / RDMA transfer, and lets AIStor spread parts across
nodes. Small uploads and every GET still take the single-shot path.

Verified the protocol against the MinIO AIStor multipart handlers:
PutObjectPartHandler routes the RDMA reader through the same per-part
hash/checksum reader (AddChecksum) as a normal part, and
CompleteMultipartUploadHandler assembles by part ETag without requiring
per-part checksums at completion.

Test Plan:
  uv run --extra boto3 pytest tests/test_multistorageclient/unit/providers/test_s3_rdma.py
…ards

- register before-sign.s3.UploadPart so multipart RDMA parts carry the token
- size RDMA buffers by memoryview.nbytes, not len() (multi-byte formats)
- release the RDMA token before dropping the shim registry entry (and
  deregister the buffer even if token release raises)
- reject rdma.multipart_chunksize < 1 (runtime + schema) to avoid a
  non-terminating multipart loop
- bind the full-object RDMA GET to the HEAD'd version via IfMatch
- add unit tests for each

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

🧹 Nitpick comments (1)
multi-storage-client/src/multistorageclient/providers/_cuobj.py (1)

223-227: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a docstring to the public install_hooks.

UploadPart is now correctly wired alongside PutObject/GetObject (resolves the earlier multipart-token gap), but this public method lacks a docstring.

As per coding guidelines, "All public Python functions must have docstrings (Sphinx autodoc extracts them)".

📝 Proposed docstring
     def install_hooks(self, s3_client) -> None:
+        """Register the RDMA token injector on the S3 client's ``before-sign`` events.
+
+        Wires ``PutObject``, ``GetObject``, and ``UploadPart`` so the
+        ``x-amz-rdma-token`` header is present before SigV4 signing.
+        """
         events = s3_client.meta.events
🤖 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 `@multi-storage-client/src/multistorageclient/providers/_cuobj.py` around lines
223 - 227, Add a concise docstring to the public install_hooks method describing
that it registers token-injection hooks for S3 PutObject, GetObject, and
UploadPart operations. Preserve the existing event registrations and behavior.

Source: Coding guidelines

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

Nitpick comments:
In `@multi-storage-client/src/multistorageclient/providers/_cuobj.py`:
- Around line 223-227: Add a concise docstring to the public install_hooks
method describing that it registers token-injection hooks for S3 PutObject,
GetObject, and UploadPart operations. Preserve the existing event registrations
and behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: fcfcb1a0-e6cb-4052-a510-f27b80c2633f

📥 Commits

Reviewing files that changed from the base of the PR and between d95d553 and ad1080c.

📒 Files selected for processing (7)
  • .release_notes/.unreleased.md
  • multi-storage-client/examples/rdma_roundtrip.py
  • multi-storage-client/src/multistorageclient/providers/_cuobj.py
  • multi-storage-client/src/multistorageclient/providers/cuobj_shim.cpp
  • multi-storage-client/src/multistorageclient/providers/s3.py
  • multi-storage-client/src/multistorageclient/schema.py
  • multi-storage-client/tests/test_multistorageclient/unit/providers/test_s3_rdma.py
🚧 Files skipped from review as they are similar to previous changes (5)
  • .release_notes/.unreleased.md
  • multi-storage-client/src/multistorageclient/schema.py
  • multi-storage-client/examples/rdma_roundtrip.py
  • multi-storage-client/src/multistorageclient/providers/cuobj_shim.cpp
  • multi-storage-client/tests/test_multistorageclient/unit/providers/test_s3_rdma.py

@harshavardhana

harshavardhana commented Jul 20, 2026

Copy link
Copy Markdown
Author

Re-ran with the review fixes in — msc-benchmark against an 8-node MinIO AIStor cluster (cuObject RDMA) from an H200 client, 400G RoCE, dual-rail. RDMA vs plain TCP, real CRC64NVME on both.

msc-benchmark (MB/s, 8 proc × 4 threads):

Size RDMA PUT TCP PUT RDMA GET TCP GET
1 MB 126 119 120 125
16 MB 527 409 527 472
64 MB 776 646 790 701
256 MB 1030 859 1020 980
1 GB 1180 884 1200 1100

RDMA PUT beats TCP everywhere (+6–33%); GET is on par to +13%. Real RDMA confirmed on the wire (x-amz-rdma-reply: 200, empty body).

These absolutes are client-capped, not fabric. A single MSC process is GIL/boto3-bound (~1.3 GB/s RDMA vs ~0.29 GB/s TCP, ~4.6×) and scales with processes, not threads. For the fabric ceiling, warp (Go, all 8 hosts, 256 MiB, c64–128) hits ~44 GiB/s GET / ~22 GiB/s PUT. So RDMA's real win here is CPU offload — exactly what a Python client like MSC needs.

@edmc-ss

edmc-ss commented Jul 20, 2026 via email

Copy link
Copy Markdown
Contributor

@shunjiad

Copy link
Copy Markdown
Contributor

/ok to test 188a37a

@copy-pr-bot

copy-pr-bot Bot commented Jul 20, 2026

Copy link
Copy Markdown

/ok to test 188a37a

@shunjiad, there was an error processing your request: E2

See the following link for more information: https://docs.gha-runners.nvidia.com/cpr/e/2/

@commiterate

commiterate commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

The main concern I have right now is the inability to test in CI/CD to ensure nothing breaks over time. There's 2 main blockers here:

  1. libcuobjclient header files.
  2. Some locally-runnable S3 emulator with RDMA support.

The primary blocker for both of these is licensing + Nixpkgs availability (for the portable development/build shell).

I believe NVIDIA is working on giving permission to the NixOS foundation to redistribute CUDA packages in the official Nixpkgs binary cache.

MinIO we migrated away from for local testing (b94c5db) due to it being deprecated. MinIO AIStor is under a non-free license so even if it were in Nixpkgs, its pre-built binaries likely wouldn't be cached in the official Nixpkgs binary cache.

Since we can't have full confidence without continuous testing, I think it's better if we create a new s3_cuobject storage provider. That way, we don't need to touch the existing s3 storage provider and worry about broken RDMA-related code paths affecting it.

multi-storage-client/src/multistorageclient/providers/s3_cuobject.py

from .s3 import S3StorageProvider

PROVIDER = "s3_cuobject"

class S3CuObjectStorageProvider(S3StorageProvider):
    """
    A concrete implementation of the :py:class:`multistorageclient.types.StorageProvider` for interacting with
    S3 via NVIDIA cuObject.

    https://docs.nvidia.com/gpudirect-storage/cuobject
    """

    # Only override methods related to object GET/PUT (single and ranged/multi-part).

This approach is already used for the s8k, ais_s3, and gcs_s3 storage providers.


The other thing I'd like to see changed is moving the C++ shim to Rust since:

  1. Rust has C/C++ interop.
  2. We can piggyback off Maturin to handle Rust + Python interop and packaging (already used for our Rust client parts. It's cumbersome to manage a separate C++ compiler toolchain + build recipes).

Granted, we need to import cuobjclient.h so we'll need to wait until cudaPackages.libcuobjclient is present in the official Nixpkgs binary cache (it's too expensive to rebuild its entire closure in every CI/CD job and I believe it requires build hosts with NVIDIA GPUs).

@harshavardhana

Copy link
Copy Markdown
Author

Good data here...I had a curiosity about whether or not these were encrypted measurements (TCP was the column head...but was this TLS?).

This was on non-TLS, but TLS won't change anything anyway since the RDMA control channel has no data transfer, so keep the cost the same to some extent. TLS was not used.

If you expect the TLS stack to be slower, I can do those tests, but it will only matter for TCP/HTTP-based runs; RDMA numbers won't change in any meaningful manner.

@harshavardhana

Copy link
Copy Markdown
Author

MinIO we migrated away from for local testing (b94c5db) due to it being deprecated. MinIO AIStor is under a non-free license so even if it were in Nixpkgs, its pre-built binaries likely wouldn't be cached in the official Nixpkgs binary cache.

For local testing, MinIO does provide an AIStor Free Tier license if that is something that can be incorporated.

However, even beyond that, to test the cuObject client, you do need an RDMA-capable runner; I have tried over soft_roce, but it doesn't work.

So yes, if you want this to be part of the CI to be tested, I personally don't have a good solution.

Granted, we need to import cuobjclient.h so we'll need to wait until cudaPackages.libcuobjclient is present in the official Nixpkgs binary cache (it's too expensive to rebuild its entire closure in every CI/CD job and I believe it requires build hosts with NVIDIA GPUs).

Not really; cuobjClient is fairly simple to incorporate. The newer CUDA release already has the relevant bits, and no, it doesn't need a GPU server to build this- the build itself can happen even on your laptops, and you can test it without having access to GPUs. The main testing infrastructure requirement is an RDMA RoCEv2-capable runner.

Since we can't have full confidence without continuous testing, I think it's better if we create a new s3_cuobject storage provider. That way, we don't need to touch the existing s3 storage provider and worry about broken RDMA-related code paths affecting it.

multi-storage-client/src/multistorageclient/providers/s3_cuobject.py

from .s3 import S3StorageProvider

PROVIDER = "s3_cuobject"

class S3CuObjectStorageProvider(S3StorageProvider):
    """
    A concrete implementation of the :py:class:`multistorageclient.types.StorageProvider` for interacting with
    S3 via NVIDIA cuObject.

    https://docs.nvidia.com/gpudirect-storage/cuobject
    """

    # Only override methods related to object GET/PUT (single and ranged/multi-part).

This approach is already used for the s8k, ais_s3, and gcs_s3 storage providers.

Sure I am okay with this.

@commiterate

commiterate commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

For local testing, MinIO does provide an AIStor Free Tier license if that is something that can be incorporated.

However, even beyond that, to test the cuObject client, you do need an RDMA-capable runner; I have tried over soft_roce, but it doesn't work.

So yes, if you want this to be part of the CI to be tested, I personally don't have a good solution.

Does NVIDIA qualify for the free tier? My impression was that we would need an enterprise license, unless there's some special exception for enterprise open source projects. We'll need sign-off from legal for this arrangement and would like MinIO to package AIStor in Nixpkgs with cached builds in the official Nixpkgs binary cache.

For an RDMA-capable system, we can try a QEMU VM down the road (since for unit tests in CI/CD, we primarily care about functionality instead of performance). That would enable local testing as well without special hardware since it's a mix of Macs and Linux machines for local development. The main question is if we can use iWARP instead (like Soft-RoCE) since it looks like a VirtIO RDMA network device doesn't exist yet.

Not really; cuobjClient is fairly simple to incorporate. The newer CUDA release already has the relevant bits, and no, it doesn't need a GPU server to build this- the build itself can happen even on your laptops, and you can test it without having access to GPUs. The main testing infrastructure requirement is an RDMA RoCEv2-capable runner.

The issue isn't during the MSC build step, but rather sourcing libcuobjclient from Nixpkgs for our Nix shell (used for local development + CI/CD) which is before MSC build can even happen.

Since none of the CUDA packages in Nixpkgs have pre-built copies cached in the offical Nixpkgs binary cache due to licensing issues, we need to build all the ones in libcuobjclient's closure from source (which I believe requires GPUs for some unit tests). For the Python portions of this repository, we source all non-PyPI dependencies (e.g. Python interpreters, macOS SDK, Rust toolchain, object storage emulators) from Nixpkgs for reproducibility.

…C++ build

Replaces the standalone hand-built libmsc_cuobj.so with a cuObject binding
inside the existing multistorageclient_rust crate, gated behind an optional
`rdma` cargo feature (default off, so the normal wheel needs no C++ toolchain
or cuObject SDK). build.rs compiles a thin extern-C shim via cc and resolves
the cuObject SDK from the environment (MSC_CUOBJ_HOME / CUDA_HOME); nothing is
vendored into the repo. _cuobj.py now binds to the Rust extension and stays
import-safe when built without the feature. Removes cuobj_shim.cpp and the
MSC_CUOBJ_SHIM env handling.
Moves the entire S3-over-RDMA (cuObject) data plane out of the s3 provider
into a new s3_cuobject provider that subclasses S3StorageProvider and overrides
only object GET/PUT. The base s3 provider is now byte-identical to main again,
so the RDMA path -- which cannot be exercised in CI without an RDMA NIC + an
RDMA-capable endpoint -- can never affect it. Mirrors the s8k/ais_s3/gcs_s3
subclass pattern. Enable with `type: s3_cuobject` instead of `type: s3` +
`rdma: {}`. Registered in config/schema/providers; RDMA unit tests migrated.

@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: 4

🤖 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 `@multi-storage-client/rust/build.rs`:
- Around line 23-25: Add CUDA_ROOT to the cargo:rerun-if-env-changed
declarations in build.rs alongside MSC_CUOBJ_HOME, CUDA_HOME, and CUDA_PATH,
ensuring changes to every environment variable read by resolve_cuobj_dirs
trigger the build script to rerun.

In `@multi-storage-client/rust/src/cuobj.rs`:
- Around line 222-233: Update cuobj_put_rdma_token to atomically remove the
token and obtain its pointer in a single TOKEN_REGISTRY lock acquisition before
calling client.put_rdma_token. Return Ok(()) when the atomic removal finds no
entry, ensuring concurrent calls cannot release the same descriptor twice and
preserving already-released-token no-op behavior.

In `@multi-storage-client/src/multistorageclient/providers/s3_cuobject.py`:
- Around line 237-246: Update the multipart-selection logic around
_rdma_upload_multipart to only use RDMA multipart for binary-mode streams,
excluding all text-mode file-like objects rather than checking only io.StringIO.
Preserve the existing size threshold and normal _put_object path, including
UTF-8 encoding for text data.
- Line 66: Update the upload-path validation around _rdma_upload_multipart so
all text-mode streams, not only io.StringIO, are detected before multipart RDMA
handling. Encode string chunks appropriately or route text streams through the
existing single-shot upload path, ensuring chunk.extend receives bytes and
binary-stream behavior remains unchanged.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: c36e9f6e-42c5-45fc-99e9-ed6f0898f56d

📥 Commits

Reviewing files that changed from the base of the PR and between ad1080c and b166351.

⛔ Files ignored due to path filters (1)
  • multi-storage-client/rust/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (14)
  • .gitignore
  • multi-storage-client/examples/rdma_roundtrip.py
  • multi-storage-client/rust/Cargo.toml
  • multi-storage-client/rust/build.rs
  • multi-storage-client/rust/csrc/cuobj_shim.cc
  • multi-storage-client/rust/csrc/cuobj_shim.h
  • multi-storage-client/rust/src/cuobj.rs
  • multi-storage-client/rust/src/lib.rs
  • multi-storage-client/src/multistorageclient/config.py
  • multi-storage-client/src/multistorageclient/providers/__init__.py
  • multi-storage-client/src/multistorageclient/providers/_cuobj.py
  • multi-storage-client/src/multistorageclient/providers/s3_cuobject.py
  • multi-storage-client/src/multistorageclient/schema.py
  • multi-storage-client/tests/test_multistorageclient/unit/providers/test_s3_rdma.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • multi-storage-client/src/multistorageclient/schema.py
  • multi-storage-client/tests/test_multistorageclient/unit/providers/test_s3_rdma.py

Comment thread multi-storage-client/rust/build.rs
Comment thread multi-storage-client/rust/src/cuobj.rs
@harshavardhana

Copy link
Copy Markdown
Author

Thanks, submitted the new changes as per your review @commiterate

- cuobj_put_rdma_token: remove the registry entry under the lock before
  releasing, so concurrent releases of the same token can't double-free the
  descriptor (TOCTOU)
- route any text-mode stream (not just StringIO) to the single-shot encode
  path; multipart reads raw bytes and would crash on str chunks
- build.rs: declare CUDA_ROOT as a rerun-if-env-changed trigger
- test: text-mode stream over the part size takes single-shot

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

♻️ Duplicate comments (1)
multi-storage-client/rust/src/cuobj.rs (1)

226-231: 🩺 Stability & Availability | 🟠 Major

Restore the token when native release fails.

The atomic removal fixes the TOCTOU race, but a failed put_rdma_token now permanently loses the registry entry, preventing retries and potentially leaking the cuObject/RDMA resource. Reinsert (token, ptr) before returning the error, or maintain an equivalent retryable release state. This failure-path issue was also noted in the prior review fix.

#!/bin/bash
set -euo pipefail
rg -n -A16 -B4 'miniors_cuobj_put_rdma_token|put_rdma_token' \
  multi-storage-client/rust/csrc \
  multi-storage-client/rust/src
🤖 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 `@multi-storage-client/rust/src/cuobj.rs` around lines 226 - 231, Update the
token release flow around TOKEN_REGISTRY removal and client.put_rdma_token so
that when native release fails, the removed (token, ptr) entry is reinserted
before returning the error, preserving retryability while keeping the atomic
removal behavior.
🤖 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.

Duplicate comments:
In `@multi-storage-client/rust/src/cuobj.rs`:
- Around line 226-231: Update the token release flow around TOKEN_REGISTRY
removal and client.put_rdma_token so that when native release fails, the removed
(token, ptr) entry is reinserted before returning the error, preserving
retryability while keeping the atomic removal behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 8fd920b9-ed7b-485a-9aca-95e50763cbae

📥 Commits

Reviewing files that changed from the base of the PR and between b166351 and 28c18e2.

📒 Files selected for processing (4)
  • multi-storage-client/rust/build.rs
  • multi-storage-client/rust/src/cuobj.rs
  • multi-storage-client/src/multistorageclient/providers/s3_cuobject.py
  • multi-storage-client/tests/test_multistorageclient/unit/providers/test_s3_rdma.py
🚧 Files skipped from review as they are similar to previous changes (3)
  • multi-storage-client/rust/build.rs
  • multi-storage-client/tests/test_multistorageclient/unit/providers/test_s3_rdma.py
  • multi-storage-client/src/multistorageclient/providers/s3_cuobject.py

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.

4 participants