Skip to content

Commit e0cd444

Browse files
obj: add generic GPU-direct S3-over-RDMA path (gds=true)
Add a vendor-neutral S3-over-RDMA data path to the object plugin's standard S3 client, implementing the published `x-amz-rdma-*` protocol (NVIDIA cuObject / aws-c-s3 convention). Any compliant endpoint (MinIO AIStor today, and AWS S3 if it adopts it) works with no per-vendor code. What it does: - New s3/rdma_protocol.h (dependency-free wire helpers; unit tested) and s3/rdma.{h,cpp} (cuObject token API: cuMemObjGetRDMAToken/PutRDMAToken, plus a SigV4 UNSIGNED-PAYLOAD control plane that carries x-amz-rdma-token and reads x-amz-rdma-reply / x-amz-rdma-bytes-transferred). - RDMA is an explicit opt-in via the `gds=true` backend param. The standard client attempts RDMA for registered DRAM/VRAM buffers; VRAM (GPU-direct) is advertised only under gds + an available fabric. - No automatic HTTP fallback: an RDMA decline/failure under gds is a hard error. Auto-fallback relies on the server returning x-amz-rdma-reply:501 for unsupported RDMA; a server that silently ignores the token would accept a body-less PUT as a 0-byte object. Until that contract is universal we require the explicit opt-in. The future fallback path is documented in code/README. - Fail-safe PUT: success is reported only when the server returns x-amz-rdma-reply:200/204, so a misconfigured (non-RDMA) endpoint errors instead of silently writing a 0-byte object. Why not s3_accel/dell: the legacy Dell engine uses a non-standard x-rdma-info header, a static whole-backend engine selection, and a PUT that does not check any reply header. It is kept for backward compatibility (accelerated=true, type=dell) but is deprecated and superseded by this path; see s3_accel/TODO.md for the removal plan. Docs: RDMA_PROTOCOL.md documents the server-side wire contract and a vendor compliance checklist; README gains a GPU-Direct section and marks accelerated/type as deprecated. nixl_object_test gains -g/--gds (and -v works with --gds); -a/--accelerated is marked deprecated. Note: the AWS SDK control-plane code in s3/rdma.cpp compiles only with cuobjclient + aws-sdk-cpp present and is validated on RDMA hardware; the protocol layer is covered by a unit test (test/gtest/unit/obj/rdma_protocol.cpp). Signed-off-by: Harshavardhana <harsha@minio.io>
1 parent 77d79d8 commit e0cd444

20 files changed

Lines changed: 1309 additions & 45 deletions

File tree

src/plugins/obj/RDMA_PROTOCOL.md

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
<!--
2+
SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
3+
SPDX-License-Identifier: Apache-2.0
4+
-->
5+
6+
# S3-over-RDMA Wire Protocol
7+
8+
This document specifies the GPU-direct S3-over-RDMA protocol the object plugin
9+
speaks, so that **any S3-compatible storage vendor can comply on the server side
10+
without NIXL-specific code**. It is a vendor-neutral AWS S3 convention (the same
11+
one used by NVIDIA cuObject / `aws-c-s3`): the control plane stays standard
12+
HTTP+SigV4, while the object payload moves out-of-band over RDMA. A server that
13+
implements the contract below works with this client when the caller opts in via
14+
`gds=true`.
15+
16+
> The client treats RDMA as an explicit opt-in and does **not** auto-fall-back to
17+
> HTTP today (see the README's "No automatic fallback (yet)"). A compliant server
18+
> signals an unsupported request with `x-amz-rdma-reply: 501`, but a
19+
> *non*-compliant server may silently ignore the token and accept a body-less PUT
20+
> as a 0-byte object — so auto-fallback becomes safe only once the `501` contract
21+
> below is universal.
22+
23+
The reference server implementation is MinIO AIStor (`internal/rdma`, repo at
24+
`../aistor`).
25+
26+
## Transport
27+
28+
- RDMA **Dynamically Connected (DC)** transport over **InfiniBand** or
29+
**RoCEv2** (cuObject protocol `CUOBJ_PROTO_RDMA_DC_V1`).
30+
- **GET**: the server issues `RDMA_WRITE`, pushing object bytes directly into the
31+
client's registered buffer (host DRAM or GPU VRAM).
32+
- **PUT** / **UploadPart**: the server issues `RDMA_READ`, pulling bytes directly
33+
out of the client's registered buffer.
34+
- The HTTP request/response body is **empty**; payload never traverses it.
35+
36+
## Headers
37+
38+
| Header | Direction | Meaning |
39+
|--------|-----------|---------|
40+
| `x-amz-rdma-token` | request | RDMA negotiation token (see format below). Its presence is what marks a request as RDMA. |
41+
| `x-amz-content-sha256: UNSIGNED-PAYLOAD` | request | Body is not signed (there is no body). |
42+
| `Content-Length: 0` | request (PUT) | No HTTP body; data is delivered via `RDMA_READ`. |
43+
| `Range: bytes=a-b` | request (GET) | Optional; selects a byte range (server replies `206`). |
44+
| `x-amz-checksum-crc64nvme` | request/response | Optional CRC64NVME checksum (PUT/UploadPart). |
45+
| `x-amz-rdma-reply` | response | Outcome marker. **Required on success.** `200`/`204`/`206` = success; `501` (or absent) = server declined RDMA. Under `gds=true` the client treats a decline as a hard error (no automatic HTTP fallback today — see README). |
46+
| `x-amz-rdma-bytes-transferred` | response (GET) | Actual bytes moved over RDMA (may be `< requested` for ranged GETs). |
47+
| `ETag`, `x-amz-checksum-crc64nvme` | response (PUT) | Standard S3 result metadata. |
48+
49+
### Token format
50+
51+
```text
52+
x-amz-rdma-token: <descriptor>:<start_addr_hex>:<size_hex>
53+
```
54+
55+
- `<descriptor>` — opaque, fixed-length RDMA memory descriptor minted by the
56+
client's RDMA provider (cuObject). The server forwards it to its RDMA provider
57+
(cuObjServer); it must not interpret the bytes.
58+
- `<start_addr_hex>` — client buffer start address, 16-digit lowercase hex.
59+
- `<size_hex>` — transfer size in bytes, 16-digit lowercase hex.
60+
61+
The two trailing fields are `:`-separated and base-16; the server splits them off
62+
the end and treats the remaining prefix as the descriptor. Trailing `;` (some
63+
clients append one) must be tolerated.
64+
65+
## Request flow (server obligations)
66+
67+
1. **Authenticate** the request with standard SigV4. `x-amz-rdma-token` is part
68+
of the signed headers; `x-amz-content-sha256` is `UNSIGNED-PAYLOAD`.
69+
2. **Detect** an RDMA request by the presence of `x-amz-rdma-token`; parse it
70+
into `(descriptor, start_addr, size)`.
71+
3. **Decide** whether RDMA can be honored (fabric reachable, buffer registrable,
72+
object/permissions valid).
73+
- **If not** → respond `x-amz-rdma-reply: 501`. You MAY include a normal HTTP
74+
error body (do not force `Content-Length: 0`). The `501` signal is what lets
75+
a client safely decide what to do; today this client treats it as an error
76+
under `gds=true` (it does not auto-retry over HTTP — see README), but the
77+
signal is required so future clients can fall back transparently.
78+
4. **Transfer** using the cuObject server APIs (register a local buffer, then):
79+
- GET / RANGE_GET → `RDMA_WRITE` the requested bytes into the client buffer.
80+
- PUT / UploadPart → `RDMA_READ` the bytes from the client buffer and persist
81+
them as the object/part.
82+
5. **Respond** on success:
83+
- `x-amz-rdma-reply: 200` (full GET/PUT), `204` (PUT, no content), or `206`
84+
(ranged GET partial).
85+
- `x-amz-rdma-bytes-transferred: <n>` for GET.
86+
- `Content-Length: 0` and **no HTTP body** (the data went over RDMA — without
87+
this the client blocks waiting for body bytes that never arrive).
88+
- `ETag` (and `x-amz-checksum-crc64nvme` if requested) for PUT.
89+
90+
## Fail-safe contract (why the reply header is mandatory on success)
91+
92+
A compliant PUT carries `Content-Length: 0` and no body. A server that does
93+
**not** implement this protocol would accept that request as a *legitimate
94+
zero-byte object* and return `200 OK` + an `ETag`. To prevent that silent data
95+
loss, the client treats a PUT as successful **only** when `x-amz-rdma-reply` is
96+
present and `200`/`204`. Therefore a compliant server MUST set
97+
`x-amz-rdma-reply` on every successful RDMA response. (GET is inherently safe:
98+
an absent reply header is read as "declined".)
99+
100+
## Multipart uploads
101+
102+
UploadPart uses the same body-less, token-carrying PUT with the standard
103+
`?uploadId=<id>&partNumber=<n>` query parameters (`1 ≤ n ≤ 10000`). The part's
104+
`ETag`/checksum are returned as usual; CompleteMultipartUpload is an ordinary
105+
HTTP call.
106+
107+
## Compliance checklist
108+
109+
- [ ] Parse `x-amz-rdma-token` as `<descriptor>:<addr16>:<size16>` (tolerate a
110+
trailing `;`).
111+
- [ ] Authenticate with SigV4 treating the body as `UNSIGNED-PAYLOAD`.
112+
- [ ] GET ⇒ `RDMA_WRITE` into the client buffer; PUT ⇒ `RDMA_READ` from it.
113+
- [ ] Always set `x-amz-rdma-reply` (`200`/`204`/`206`) on success, `501` to decline.
114+
- [ ] Set `x-amz-rdma-bytes-transferred` on GET; force `Content-Length: 0` with
115+
no body on every success.
116+
- [ ] Return `ETag`/checksum for PUT; honor `Range` for GET (`206`).
117+
- [ ] Fall through to normal HTTP semantics for non-RDMA requests.
118+
119+
A vendor that satisfies this checklist is supported by the NIXL object plugin's
120+
default S3 client automatically — no NIXL-side engine or plugin is required.

src/plugins/obj/README.md

Lines changed: 107 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,9 @@ Backend parameters are passed as a key-value map (`nixl_b_params_t`) when creati
7070
| `resp_checksum` | Response checksum validation (`required`/`supported`) | - | No |
7171
| `ca_bundle` | path to a custom certificate bundle | - | No |
7272
| `crtMinLimit` | Minimum object size (bytes) to use S3 CRT client for high-performance transfers | Disabled**** | No |
73-
| `accelerated` | Enable S3 Accelerated engine (`true`/`false`) | `false` | No |
74-
| `type` | Accelerated engine type (`dell`, etc.) | - | No |
73+
| `gds` | Enable GPU-direct S3-over-RDMA on the standard client (`true`/`false`). Opt-in; requires an endpoint that implements the protocol. See [GPU-Direct (S3 over RDMA)](#gpu-direct-s3-over-rdma). | `false` | No |
74+
| `accelerated` | **(Deprecated/Legacy)** Enable a registry-provided vendor accel engine (`true`/`false`). Superseded by `gds`; kept for legacy Dell. | `false` | No |
75+
| `type` | **(Deprecated/Legacy)** Vendor accel engine type (`dell`). Superseded by `gds`. | - | No |
7576

7677
\* If `access_key` and `secret_key` are not provided, the AWS SDK will attempt to use default credential providers (IAM roles, environment variables, credential files, etc.)
7778

@@ -215,6 +216,102 @@ This configuration automatically uses the high-performance S3 CRT client for obj
215216

216217
> **Part size and the 5 MiB floor**: AWS S3 requires a minimum part size of 5 MiB for all parts except the last. If `crtMinLimit` is set below 5 MiB, the CRT SDK silently clamps the part size to 5 MiB (logging a warning) while still triggering multipart at `crtMinLimit`. Objects smaller than 5 MiB are uploaded as a single-part multipart request, which S3 permits. The 5 MiB minimum is an AWS service constraint and cannot be bypassed. To avoid the silent clamp, use `crtMinLimit >= 5242880` (5 MiB).
217218
219+
## GPU-Direct (S3 over RDMA)
220+
221+
When built against the `cuobjclient` library, run on a host with an RDMA fabric
222+
(InfiniBand or RoCEv2), and **explicitly enabled with `gds=true`**, the standard
223+
S3 client accelerates GET/PUT using **GPU-direct S3 over RDMA**. This is the
224+
idiomatic path; enable it only against an endpoint you know implements the
225+
protocol:
226+
227+
```cpp
228+
nixl_b_params_t params = {
229+
{"bucket", "models"}, {"endpoint_override", "http://aistor:9000"},
230+
{"gds", "true"}
231+
};
232+
agent.createBackend("obj", params);
233+
```
234+
235+
> **Why opt-in and not auto-detected?** See
236+
> [No automatic fallback (yet)](#no-automatic-fallback-yet) — auto-probing is
237+
> unsafe until servers reliably signal `501` for unsupported RDMA.
238+
239+
### How it works
240+
241+
The implementation follows the published, vendor-neutral S3 RDMA convention
242+
(NVIDIA cuObject / `aws-c-s3`), so it works against any compliant endpoint
243+
(MinIO AIStor today; AWS S3 transparently if it ever adopts it):
244+
245+
1. On `registerMem`, DRAM/VRAM buffers are pinned for RDMA via cuObject
246+
(`cuMemObjGetDescriptor`). VRAM requires this; DRAM registration is
247+
best-effort.
248+
2. On transfer, the client mints a per-op RDMA token and issues a **body-less**,
249+
SigV4 `UNSIGNED-PAYLOAD` GET/PUT carrying `x-amz-rdma-token:
250+
<descriptor>:<addr>:<size>`.
251+
3. The server performs an `RDMA_WRITE` (GET) or `RDMA_READ` (PUT) **directly
252+
to/from the registered buffer** — including GPU VRAM — bypassing the host CPU
253+
and the HTTP body.
254+
4. The server confirms with `x-amz-rdma-reply: 200/204/206` and reports the
255+
moved byte count via `x-amz-rdma-bytes-transferred`.
256+
257+
### No automatic fallback (yet)
258+
259+
RDMA is an **explicit opt-in** (`gds=true`), not an auto-probe, and when enabled
260+
an RDMA decline/failure is a **hard error** — the client does *not* silently fall
261+
back to HTTP. This is deliberate:
262+
263+
- The fallback handshake relies on the server returning `x-amz-rdma-reply: 501`
264+
(or omitting the reply header) when it cannot honor RDMA. A server that instead
265+
**silently ignores** `x-amz-rdma-token` would accept our `Content-Length: 0`
266+
body-less PUT as a legitimate **zero-byte object** and return `200`+ETag —
267+
silent data loss. The client cannot safely distinguish that case by probing.
268+
- So the caller asserts RDMA support via `gds=true`; if the endpoint then
269+
declines, that is a misconfiguration worth surfacing, not papering over.
270+
271+
> **Fail-safe PUT:** even under `gds=true`, a PUT is reported successful **only**
272+
> when the server returns `x-amz-rdma-reply: 200/204`. A bare HTTP `200` is never
273+
> trusted, so a mistaken endpoint surfaces as an error rather than a silent
274+
> zero-byte write. (GET is inherently safe: an absent reply header reads as a
275+
> decline.)
276+
>
277+
> **FUTURE — transparent fallback.** Once server implementations universally
278+
> return `x-amz-rdma-reply: 501` for unsupported RDMA, the client can safely
279+
> auto-detect RDMA and fall back to HTTP for host (DRAM) memory — making `gds`
280+
> unnecessary. The building blocks (reply parsing, the synchronous HTTP path) are
281+
> in place; the fallback is intentionally disabled until that contract is
282+
> universal. VRAM can never fall back (the SDK cannot stream a GPU pointer).
283+
284+
### Why this supersedes the legacy `s3_accel` vendor engines
285+
286+
The earlier `s3_accel/` path (e.g. `s3_accel/dell`, selected via
287+
`accelerated=true, type=dell`) is **retained only for backward compatibility**.
288+
It is not the recommended approach and is protocol-incorrect for general use:
289+
290+
- It uses a **non-standard `x-rdma-info`** header instead of the established
291+
`x-amz-rdma-token` / `x-amz-rdma-reply` convention, so it only works against
292+
one vendor's server.
293+
- It sends `Content-Length: 0` PUTs **without checking any RDMA reply header**,
294+
so against a non-RDMA endpoint it silently writes 0-byte objects.
295+
- It has **no fallback**: RDMA selection is a static, whole-backend choice made
296+
at `createBackend` time, not a per-transfer negotiation.
297+
- It models RDMA as a **separate vendor engine** behind a registry, requiring
298+
bespoke per-vendor code, when RDMA is really just a transparent client
299+
capability.
300+
301+
The generic path above fixes all four: it is a per-transfer capability of the
302+
standard client, follows the public protocol, fails safely on a non-RDMA
303+
response instead of writing a 0-byte object (no silent fallback — see [No
304+
automatic fallback (yet)](#no-automatic-fallback-yet)), and needs **no per-vendor
305+
code** — any vendor that complies with the protocol is supported automatically.
306+
307+
### Protocol specification (for storage vendors)
308+
309+
The full wire contract — headers, token format, transport, server obligations,
310+
the fail-safe success rule, and a compliance checklist — is documented in
311+
[**RDMA_PROTOCOL.md**](RDMA_PROTOCOL.md). A storage vendor that implements that
312+
contract on its S3 server is supported by this client out of the box, with no
313+
NIXL-side changes.
314+
218315
## Transfer Operations
219316

220317
The Object Storage backend supports read and write operations between local memory and S3 objects. Here are the key aspects of transfer operations:
@@ -259,7 +356,14 @@ The client selection happens transparently during transfer operations, requiring
259356
- The `checkXfer` function can be used to poll for operation completion
260357
- The request handle must be released using `releaseReqH` after the operation is complete
261358

262-
## Extending with Vendor-Specific Implementations
359+
## Extending with Vendor-Specific Implementations (Legacy)
360+
361+
> **⚠️ Legacy.** This vendor-engine + registry mechanism is retained for
362+
> backward compatibility only. For GPU-direct/RDMA acceleration, prefer the
363+
> built-in [GPU-Direct (S3 over RDMA)](#gpu-direct-s3-over-rdma) path, which is
364+
> transparent, protocol-compliant, and requires no per-vendor code. A new vendor
365+
> only needs to implement the standard `x-amz-rdma-*` protocol on its server to
366+
> be supported automatically; no NIXL-side engine is required.
263367
264368
The object plugin uses a modular, inheritance-based architecture that makes it easy to add vendor-specific backends without modifying core engine logic.
265369

src/plugins/obj/meson.build

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,12 @@ obj_sources = [
2424
's3/client.h',
2525
's3/engine_impl.cpp',
2626
's3/engine_impl.h',
27+
# Generic, vendor-neutral S3-over-RDMA (x-amz-rdma-*) data path. rdma.cpp is
28+
# internally guarded by HAVE_CUOBJ_CLIENT and compiles to nothing when the
29+
# cuObjClient library is absent; rdma_protocol.h is dependency-free.
30+
's3/rdma_protocol.h',
31+
's3/rdma.cpp',
32+
's3/rdma.h',
2733
's3_crt/engine_impl.cpp',
2834
's3_crt/engine_impl.h',
2935
's3_crt/client.cpp',

src/plugins/obj/obj_backend.cpp

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,24 @@
2626
// -----------------------------------------------------------------------------
2727
// Obj Engine Implementation
2828
// -----------------------------------------------------------------------------
29+
//
30+
// Engine selection:
31+
// * Default (no params) -> DefaultObjEngineImpl
32+
// * crtMinLimit > 0 -> S3CrtObjEngineImpl
33+
// * accelerated=true, type=... -> registry-provided vendor engine (LEGACY)
34+
//
35+
// The idiomatic GPU-direct path is the S3-over-RDMA capability built into the
36+
// standard S3 client (see s3/rdma.h): it follows the published `x-amz-rdma-*`
37+
// protocol and requires NO per-vendor code — any S3 endpoint that implements the
38+
// protocol (e.g. MinIO AIStor) works, and so would AWS S3 if it adopted it. It is
39+
// an explicit opt-in via `gds=true` (not auto-probed, and no silent HTTP fallback
40+
// — see s3/client.cpp for the rationale and the future auto-fallback path).
41+
//
42+
// The `objAccelEngineRegistry` + `accelerated`/`type` path below is RETAINED ONLY
43+
// FOR LEGACY vendor engines (e.g. s3_accel/dell, which predates the protocol and
44+
// uses a non-standard `x-rdma-info` header). New deployments should use the
45+
// default engine; the generic client-level RDMA path supersedes the vendor
46+
// engines.
2947

3048
namespace {
3149

src/plugins/obj/obj_backend.h

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,17 @@ class iS3Client {
8585
*/
8686
virtual void
8787
checkObjectExistsAsync(std::string_view key, check_object_callback_t callback) = 0;
88+
89+
/**
90+
* Whether this client's GPU-direct S3-over-RDMA fast path is fully ready
91+
* (cuObject fabric connected AND control plane + executor available). The
92+
* engine uses this to decide whether VRAM_SEG can be safely advertised:
93+
* a GPU pointer must never reach the HTTP path. Defaults to false.
94+
*/
95+
virtual bool
96+
supportsRdma() const {
97+
return false;
98+
}
8899
};
89100

90101
/**

0 commit comments

Comments
 (0)