Skip to content

Commit 7175f8c

Browse files
sbates130272claude
andcommitted
fix(rdma): move real bytes on the staging and GPU-direct paths
The RDMA data plane reported success for transfers that moved nothing. Four defects, each of which independently made a green run meaningless. buildRdmaToken advertised mr->addr. ibv_reg_dmabuf_mr leaves that NULL, and on the bounce path it is the staging buffer rather than the caller's pointer, so the token described memory the peer could not use. BufferMap now records the address to advertise at registration time. Nothing copied between the caller's buffer and the staging buffer. When the NIC cannot reach GPU memory directly, hipObject registers host staging instead of the caller's pointer -- and then left it empty. Adds hipObjBufSync: callers driving a transfer through hipObjGetRdmaToken call it with HIPOBJ_SYNC_TO_HOST before a PUT and HIPOBJ_SYNC_TO_DEVICE after a GET. It is a no-op for a directly registered buffer. The minio-cpp bridge sent the caller's raw pointer in the token's ":<addr>:<len>" suffix, which rocm-ernic treats as an override of the address in the token. On the staging path that address is not covered by the MR, giving LOCAL_PROT_ERR and a zero-byte transfer that the bridge reported as success. Send zeros -- "no override" -- and let the token speak. rdmaPutWithRetry now stages to host before the request and rdmaGetWithRetry stages back to device after a successful reply. dmabuf support was predicted rather than attempted. init_dmabuf_support_flag() grepped /boot/config-$(uname -r) for both CONFIG_DMABUF_MOVE_NOTIFY=y and CONFIG_PCI_P2PDMA=y, and switched the whole GPU-direct path off when either was missing. Measured on 7.2.4-070204-generic: PCI_P2PDMA is set, ibv_reg_dmabuf_mr resolves in rdma-core 61.0, hsa_amd_portable_export_dmabuf succeeds -- and DMABUF_MOVE_NOTIFY is simply absent from the config. One missing line turned off a path that works. The gate is gone; the call is tried and its result believed. With that, a GPU-direct transfer that cannot be done directly must not be quietly done some other way. HIPOBJ_REQUIRE_GPU_DIRECT=1 makes the client refuse to fall back to host staging for a device pointer, and say so, rather than return success for a transfer the caller did not ask for. This is what turns the GPU Direct CI lane red: it was passing by faking. Failures also say which failure. Every RDMA error used to surface as one anonymous -1, which the HTTP layer rendered as "500 RDMA PUT failed" -- six distinct faults, from an unparseable token to a completion error, indistinguishable from a server with no RDMA at all. pollCompletion now reports the work-completion status, opcode and vendor_err, and each failure path names itself. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: Stephen Bates <sbates@raithlin.com>
1 parent b3b2a4a commit 7175f8c

14 files changed

Lines changed: 702 additions & 105 deletions

File tree

‎include/hipobj.h‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,29 @@ HIPOBJ_API hipObjError_t hipObjPutV2(const char* bucket, const char* key,
403403

404404
#endif /* HIPOBJECT_V2_API */
405405

406+
/*! @brief hipObjBufSync direction: device -> staging buffer @ingroup io */
407+
#define HIPOBJ_SYNC_TO_HOST 0
408+
/*! @brief hipObjBufSync direction: staging buffer -> device @ingroup io */
409+
#define HIPOBJ_SYNC_TO_DEVICE 1
410+
411+
/*!
412+
* @brief Stage a registered buffer between device and host memory
413+
*
414+
* When a device pointer cannot be registered with the NIC directly (no
415+
* dmabuf support, or a fabric with no peer-to-peer path to the GPU),
416+
* hipObjBufRegister() registers a host staging buffer instead and the
417+
* peer's RDMA reads and writes land there. Callers driving a transfer
418+
* through hipObjGetRdmaToken() must therefore call this with
419+
* HIPOBJ_SYNC_TO_HOST before a PUT and HIPOBJ_SYNC_TO_DEVICE after a
420+
* GET. It is a no-op, reporting success, for a directly registered
421+
* buffer; hipObjGet()/hipObjPut() and the V2 entry points do it
422+
* themselves.
423+
*
424+
* @ingroup io
425+
*/
426+
HIPOBJ_API hipObjError_t hipObjBufSync(void* devPtr, size_t size, off_t offset,
427+
int direction);
428+
406429
/*!
407430
* @brief Mint a hex-encoded RC RDMA token for a registered buffer
408431
*

‎integrations/minio-cpp/examples/getput-rdma.cpp‎

Lines changed: 78 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,62 @@
55

66
/* AMD port of minio-cpp GetPutRDMA: PUT + GET over hipObject RDMA */
77

8+
#include <cstdint>
89
#include <cstdio>
910
#include <cstdlib>
1011
#include <cstring>
12+
#include <ctime>
1113
#include <fstream>
1214
#include <iostream>
1315
#include <string>
16+
#include <vector>
1417

1518
#include <hip/hip_runtime.h>
1619

1720
#include <miniocpp/client.h>
21+
#include <unistd.h>
1822

1923
#include "hipobj_minio/client.h"
2024

25+
namespace {
26+
27+
/* A position-dependent pattern, not a constant fill.
28+
*
29+
* This used to memset the buffer to 'A' and check afterwards that every byte
30+
* was still 'A', which cannot distinguish a correct transfer from a
31+
* byte-swapped, misaligned, short, duplicated or scattered one -- every one
32+
* of those returns a buffer full of 'A'. A cheap LCG keyed on the offset
33+
* makes each byte depend on where it is, so any of those failures shows up
34+
* as a mismatch at a specific offset, which is also the first thing you want
35+
* to know when it does. */
36+
uint8_t PatternByte(size_t offset, uint32_t seed) {
37+
uint64_t x = (static_cast<uint64_t>(offset) + 1) * 6364136223846793005ULL +
38+
seed;
39+
x ^= x >> 33;
40+
x *= 0xff51afd7ed558ccdULL;
41+
x ^= x >> 33;
42+
return static_cast<uint8_t>(x & 0xff);
43+
}
44+
45+
void FillPattern(char* buf, size_t size, uint32_t seed) {
46+
for (size_t i = 0; i < size; ++i) {
47+
buf[i] = static_cast<char>(PatternByte(i, seed));
48+
}
49+
}
50+
51+
/* Returns the offset of the first mismatch, or size if the blob read back
52+
* matches the blob written byte for byte. */
53+
size_t FirstMismatch(const char* buf, size_t size, uint32_t seed) {
54+
for (size_t i = 0; i < size; ++i) {
55+
if (static_cast<uint8_t>(buf[i]) != PatternByte(i, seed)) {
56+
return i;
57+
}
58+
}
59+
return size;
60+
}
61+
62+
} // namespace
63+
2164
int main(int argc, char* argv[]) {
2265
if (argc < 4) {
2366
std::cerr << "usage: " << argv[0]
@@ -38,6 +81,15 @@ int main(int argc, char* argv[]) {
3881
gpu_enabled = std::string(argv[5]) == "gpu";
3982
}
4083

84+
/* Seeded per run, and printed, so a stale object left in the store by an
85+
* earlier run cannot satisfy the readback. */
86+
const uint32_t seed = static_cast<uint32_t>(::getpid()) ^
87+
static_cast<uint32_t>(std::time(nullptr));
88+
std::cout << "Pattern seed " << seed << "\n";
89+
90+
std::vector<char> expected(bufsize);
91+
FillPattern(expected.data(), bufsize, seed);
92+
4193
minio::s3::BaseUrl base_url(host, false, "us-east-1");
4294
minio::creds::StaticProvider provider(access_key, secret_key);
4395
hipobj::minio::Client client(base_url, &provider);
@@ -51,9 +103,9 @@ int main(int argc, char* argv[]) {
51103
std::cerr << "hipMalloc failed: " << err << std::endl;
52104
return 1;
53105
}
54-
err = hipMemset(dev_ptr, 'A', bufsize);
106+
err = hipMemcpy(dev_ptr, expected.data(), bufsize, hipMemcpyHostToDevice);
55107
if (err != hipSuccess) {
56-
std::cerr << "hipMemset failed: " << err << std::endl;
108+
std::cerr << "hipMemcpy H2D failed: " << err << std::endl;
57109
(void)hipFree(dev_ptr);
58110
return 1;
59111
}
@@ -67,7 +119,7 @@ int main(int argc, char* argv[]) {
67119
std::cerr << "posix_memalign failed\n";
68120
return 1;
69121
}
70-
std::memset(bufptr, 'A', bufsize);
122+
std::memcpy(bufptr, expected.data(), bufsize);
71123
std::cout << "Host buffer " << bufsize << " bytes\n";
72124
}
73125

@@ -89,11 +141,14 @@ int main(int argc, char* argv[]) {
89141
}
90142
std::cout << "PUT ok etag=" << presp.etag << std::endl;
91143

144+
/* Clobber the buffer before the GET. Without this the readback could be
145+
* satisfied by whatever the PUT left behind, and a GET that transferred
146+
* nothing at all would still verify. */
92147
if (gpu_enabled) {
93-
(void)hipMemset(dev_ptr, 'U', bufsize);
148+
(void)hipMemset(dev_ptr, 0x55, bufsize);
94149
(void)hipDeviceSynchronize();
95150
} else {
96-
std::memset(bufptr, 'U', bufsize);
151+
std::memset(bufptr, 0x55, bufsize);
97152
}
98153

99154
minio::s3::GetObjectArgs gargs;
@@ -153,15 +208,25 @@ int main(int argc, char* argv[]) {
153208
out.close();
154209
std::cout << "Wrote output.bin (" << bufsize << " bytes)\n";
155210

156-
bool ok = true;
157-
for (size_t i = 0; i < bufsize; ++i) {
158-
if (hostptr[i] != 'A') {
159-
ok = false;
160-
break;
161-
}
211+
size_t bad = FirstMismatch(hostptr, bufsize, seed);
212+
bool ok = bad == bufsize;
213+
if (ok) {
214+
std::cout << "Data integrity check passed (" << bufsize << " bytes, seed "
215+
<< seed << ")\n";
216+
} else {
217+
std::cout << "Data integrity check FAILED at offset " << bad
218+
<< ": expected 0x" << std::hex
219+
<< static_cast<int>(PatternByte(bad, seed)) << " got 0x"
220+
<< static_cast<int>(static_cast<uint8_t>(hostptr[bad]))
221+
<< std::dec << "\n";
162222
}
163-
std::cout << (ok ? "Data integrity check passed\n"
164-
: "Data integrity check FAILED\n");
223+
224+
/* Whether the transfers actually used RDMA. The client falls back to
225+
* ordinary HTTP silently when no NIC is available, so without this a lane
226+
* that is meant to be exercising the RDMA data path passes over TCP. */
227+
std::cout << hipobj::minio::TransferStatsLine(
228+
hipobj::minio::TransferStatsSnapshot())
229+
<< "\n";
165230

166231
free(hostptr);
167232
if (gpu_enabled) {

‎integrations/minio-cpp/include/hipobj_minio/client.h‎

Lines changed: 47 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,26 +5,67 @@
55

66
#pragma once
77

8+
#include <cstddef>
9+
#include <cstdint>
10+
#include <string>
11+
812
#include <miniocpp/args.h>
913
#include <miniocpp/client.h>
1014
#include <miniocpp/credentials.h>
1115
#include <miniocpp/response.h>
1216

1317
namespace hipobj::minio {
1418

19+
/* How the last transfers actually went out.
20+
*
21+
* Client::PutObject and Client::GetObject fall back to ordinary HTTP in two
22+
* places -- when hipObjInit cannot find a NIC, and when an RDMA transfer
23+
* returns an error -- and the caller sees a successful response either way.
24+
* That is the right behaviour for a client and the wrong behaviour for a
25+
* test: an integration lane that is supposed to be proving the RDMA data
26+
* path will pass just as happily over TCP. These counters are how a caller
27+
* tells the two apart after the fact.
28+
*
29+
* Process-wide and monotonic. Snapshot with TransferStatsSnapshot() before
30+
* the transfers you care about and subtract. */
31+
struct TransferStats {
32+
uint64_t rdmaPuts = 0;
33+
uint64_t rdmaGets = 0;
34+
uint64_t httpPuts = 0;
35+
uint64_t httpGets = 0;
36+
uint64_t rdmaBytes = 0;
37+
uint64_t httpBytes = 0;
38+
};
39+
40+
/* A snapshot of the process-wide counters. */
41+
TransferStats TransferStatsSnapshot();
42+
43+
/* Reset the counters to zero. */
44+
void TransferStatsReset();
45+
46+
/* One line, stable and greppable, for a CI step to assert on:
47+
*
48+
* hipobj-stats: rdma_put=1 rdma_get=1 http_put=0 http_get=0
49+
* rdma_bytes=131072 http_bytes=0
50+
*
51+
* A lane that requires RDMA asserts http_put=0 and http_get=0; a lane that
52+
* cannot have it (no verbs device, e.g. a plain container) asserts the
53+
* reverse, so the expectation is written down either way. */
54+
std::string TransferStatsLine(const TransferStats& stats);
55+
1556
class Client {
1657
public:
17-
Client(minio::s3::BaseUrl base_url, minio::creds::Provider* provider);
58+
Client(::minio::s3::BaseUrl base_url, ::minio::creds::Provider* provider);
1859

19-
minio::s3::PutObjectResponse PutObject(minio::s3::PutObjectArgs args);
20-
minio::s3::GetObjectResponse GetObject(minio::s3::GetObjectArgs args);
60+
::minio::s3::PutObjectResponse PutObject(::minio::s3::PutObjectArgs args);
61+
::minio::s3::GetObjectResponse GetObject(::minio::s3::GetObjectArgs args);
2162

2263
bool RdmaAvailable() const;
2364

2465
private:
25-
minio::s3::BaseUrl base_url_;
26-
minio::creds::Provider* provider_ = nullptr;
27-
minio::s3::Client s3_client_;
66+
::minio::s3::BaseUrl base_url_;
67+
::minio::creds::Provider* provider_ = nullptr;
68+
::minio::s3::Client s3_client_;
2869
};
2970

3071
} // namespace hipobj::minio

‎integrations/minio-cpp/include/hipobj_minio/context.h‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@
1414
namespace hipobj::minio {
1515

1616
struct S3RdmaContext {
17-
minio::creds::Provider* provider = nullptr;
17+
::minio::creds::Provider* provider = nullptr;
1818
std::string bucket;
1919
std::string object;
20-
minio::s3::BaseUrl url;
20+
::minio::s3::BaseUrl url;
2121
std::string region;
2222
std::string uploadId;
2323
unsigned int partNumber = 0;

0 commit comments

Comments
 (0)