Files
versitygw/cuwrapper/rc/v2_data_phase.cpp
T
Jihyeon Gim efa0309da4 rdma: port the hipObject v2 RC session core
Port the hipObject v2 reliable-connection session core into
cuwrapper/rc: the session table and state machine, the wire
codec for the hipobj-rc-v2 headers, request parsing, the
injectable clock and randomness sources, the transport layer
(QP/CQ lifecycle, RTR/RTS transitions, staging registration),
the data phase (RDMA write with immediate for GET, receive with
immediate for PUT), the RDMA token codec, and the dynamically
loaded ibverbs shim (ibv-core.h plus the dlopen host binding).

The sources are a port of the upstream hipObject v2 core, kept
close to the original so the two trees can be diffed during
review. Nothing links against them yet; a Makefile rule builds
the objects into rdma/librcserver.a for the ABI layer that
follows.

Signed-off-by: Jihyeon Gim <potatogim@potatogim.net>
2026-08-30 00:00:29 +09:00

229 lines
7.1 KiB
C++

/* Copyright (c) Advanced Micro Devices, Inc. All rights reserved.
* Copyright (c) Gluesys Inc. and Jihyeon Gim. All rights reserved.
*
* SPDX-License-Identifier: MIT
*/
#include "v2_data_phase.h"
#include <cstdlib>
#include <cstring>
#include <ctime>
#include <arpa/inet.h>
#include "rc_ibv_host.h"
namespace hipObj {
namespace v2 {
namespace {
constexpr int kAccess = IBV_ACCESS_LOCAL_WRITE | IBV_ACCESS_REMOTE_WRITE |
IBV_ACCESS_REMOTE_READ;
/* Completion markers posted with every work request. */
constexpr uint64_t kWrRecv = 0x5245435632494d4dULL; /* RECV2IMM */
constexpr uint64_t kWrWrite = 0x57524954454d4d47ULL; /* WRITEMM */
uint64_t clockNowMs() {
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return static_cast<uint64_t>(ts.tv_sec) * 1000 +
static_cast<uint64_t>(ts.tv_nsec) / 1000000;
}
} // namespace
bool stagePutBuffer(V2Session& s, size_t size, struct ibv_pd* pd) {
if (s.staging != nullptr) {
return s.stagingMr != nullptr || pd == nullptr;
}
void* buf = std::malloc(size ? size : 1);
if (buf == nullptr) {
return false;
}
/* Without a PD (transport-free host) the buffer stages without
* an MR; the data phase is a no-op there anyway.
*
* Prefer the device-registered path: providers register the
* buffer with the device (dmabuf on GPU hosts, plain
* ibv_reg_mr otherwise) so peers can reach it via rkey. The
* host-only registration is a fallback for wrappers whose
* device path needs an unavailable GPU runtime. */
struct ibv_mr* mr = nullptr;
if (pd != nullptr) {
mr = ibv.reg_mr(pd, buf, size, kAccess);
if (mr == nullptr) {
mr = ibv.reg_mr_host(pd, buf, size, kAccess);
}
if (mr == nullptr) {
std::free(buf);
return false;
}
}
s.staging = buf;
s.stagingMr = mr;
return true;
}
void releaseStaging(V2Session& s) {
/* The caller must have quiesced or destroyed the session QP
* first: a posted work request can still reference the MR
* until the QP is gone. dereg failures leave the MR leaked
* (and logged) rather than freeing memory the NIC may touch. */
if (s.stagingMr != nullptr) {
if (ibv.dereg_mr(s.stagingMr) != 0) {
fprintf(stderr, "v2: staging dereg failed; leaking buffer\n");
s.staging = nullptr; /* MR is dead to us either way */
}
s.stagingMr = nullptr;
}
if (s.staging != nullptr) {
std::free(s.staging);
s.staging = nullptr;
}
}
bool postRecvForImm(struct ibv_qp* qp, struct ibv_mr* mr, size_t len) {
struct ibv_sge sge;
std::memset(&sge, 0, sizeof(sge));
sge.addr = reinterpret_cast<uintptr_t>(mr->addr);
sge.length = static_cast<uint32_t>(len);
sge.lkey = mr->lkey;
struct ibv_recv_wr wr;
std::memset(&wr, 0, sizeof(wr));
wr.wr_id = kWrRecv;
wr.sg_list = &sge;
wr.num_sge = 1;
struct ibv_recv_wr* bad = nullptr;
return ibv.post_recv(qp, &wr, &bad) == 0;
}
bool postWriteWithImm(struct ibv_qp* qp, struct ibv_mr* src,
uint64_t remoteAddr, uint32_t rkey, size_t len,
uint32_t immData) {
/* GET delivery: server pushes the object into the client MR
* with the session cookie as the immediate. */
struct ibv_sge sge;
std::memset(&sge, 0, sizeof(sge));
sge.addr = reinterpret_cast<uintptr_t>(src->addr);
sge.length = static_cast<uint32_t>(len);
sge.lkey = src->lkey;
struct ibv_send_wr wr;
std::memset(&wr, 0, sizeof(wr));
wr.wr_id = kWrWrite;
wr.opcode = IBV_WR_RDMA_WRITE_WITH_IMM;
wr.send_flags = IBV_SEND_SIGNALED;
wr.imm_data = htonl(immData);
wr.wr.rdma.remote_addr = remoteAddr;
wr.wr.rdma.rkey = rkey;
wr.sg_list = &sge;
wr.num_sge = 1;
struct ibv_send_wr* bad = nullptr;
return ibv.post_send(qp, &wr, &bad) == 0;
}
/* Polls the CQ for one completion matching `expectWr`, bounded by
* an absolute deadline on the monotonic clock. */
enum class PollOutcome { Ok, Timeout, Error, Mismatch };
PollOutcome pollCqUntil(struct ibv_cq* cq, uint64_t deadlineMs,
uint64_t expectWr, struct ibv_wc* out) {
for (;;) {
int n = ibv.poll_cq(cq, 1, out);
if (n > 0) {
/* Providers may rewrite the wr_id on emulated paths; the
* opcode + immediate + length identify the completion. */
(void)expectWr;
return PollOutcome::Ok;
}
if (n < 0) {
return PollOutcome::Error;
}
if (clockNowMs() >= deadlineMs) {
return PollOutcome::Timeout;
}
struct timespec ts = {0, 2 * 1000 * 1000};
nanosleep(&ts, nullptr);
}
}
DataPhaseResult runDataPhase(V2Session& s, uint64_t deadlineMs,
DataPhaseStats& stats) {
const bool noTransport = s.qp == nullptr && s.cq == nullptr;
if (noTransport || s.clientQpn == 0) {
/* Control-plane-only session (unit tests, reference
* harness): both objects absent or the client advertised no
* QP. A half-wired session is not accepted here. */
stats.bytes = s.size;
stats.cookie = s.cookie;
return DataPhaseResult::Ok;
}
if (s.qp == nullptr || s.cq == nullptr || s.stagingMr == nullptr) {
return DataPhaseResult::WireFail;
}
struct ibv_wc wc;
PollOutcome po;
if (s.op == "PUT") {
/* The client writes into the server staging MR and signals
* the session cookie. The server is the responder here, so
* requester-side retry exhaustion never surfaces in this CQ;
* any completion error or mismatch is a wire defect. */
if (!postRecvForImm(s.qp, s.stagingMr, static_cast<size_t>(s.size))) {
return DataPhaseResult::WireFail;
}
po = pollCqUntil(s.cq, deadlineMs, kWrRecv, &wc);
if (po == PollOutcome::Timeout) {
return DataPhaseResult::Timeout;
}
if (po != PollOutcome::Ok || wc.status != IBV_WC_SUCCESS ||
wc.opcode != IBV_WC_RECV_RDMA_WITH_IMM ||
(wc.wc_flags & IBV_WC_WITH_IMM) == 0 ||
ntohl(wc.imm_data) != s.cookie || wc.byte_len != s.size) {
return DataPhaseResult::VerifyFail;
}
stats.bytes = wc.byte_len;
stats.cookie = s.cookie;
return DataPhaseResult::Ok;
}
/* GET: push the staged object to the client MR with the
* cookie as the immediate; the client's receive consumes it. */
if (s.clientMrAddr == 0 || s.clientMrRkey == 0) {
return DataPhaseResult::WireFail;
}
if (!postWriteWithImm(s.qp, s.stagingMr, s.clientMrAddr, s.clientMrRkey,
static_cast<size_t>(s.size), s.cookie)) {
return DataPhaseResult::WireFail;
}
po = pollCqUntil(s.cq, deadlineMs, kWrWrite, &wc);
if (po == PollOutcome::Timeout) {
return DataPhaseResult::Timeout;
}
if (po == PollOutcome::Ok &&
(wc.status == IBV_WC_RNR_RETRY_EXC_ERR ||
wc.status == IBV_WC_RETRY_EXC_ERR)) {
/* The server is the requester for the RDMA write: these mean
* the peer's receive queue was not armed or the peer did not
* answer, which is retryable from a fresh pairing rather
* than a wire defect. */
return DataPhaseResult::Busy;
}
if (po != PollOutcome::Ok || wc.status != IBV_WC_SUCCESS ||
wc.opcode != IBV_WC_RDMA_WRITE) {
return DataPhaseResult::VerifyFail;
}
stats.bytes = s.size;
stats.cookie = s.cookie;
return DataPhaseResult::Ok;
}
} // namespace v2
} // namespace hipObj