mirror of
https://github.com/versity/versitygw.git
synced 2026-09-22 16:04:15 +00:00
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>
457 lines
13 KiB
C++
457 lines
13 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-transport.h"
|
|
|
|
#include <atomic>
|
|
#include <cerrno>
|
|
#include <cstring>
|
|
|
|
#include <arpa/inet.h>
|
|
|
|
#include "rc_ibv_host.h"
|
|
#include "transport.h"
|
|
#include "vendor-ops.h"
|
|
|
|
namespace hipObj {
|
|
namespace v2 {
|
|
|
|
namespace {
|
|
|
|
constexpr int kCqSize = 16;
|
|
constexpr int kMaxSendWr = 16;
|
|
constexpr int kMaxRecvWr = 16;
|
|
|
|
/* TU-private cancel for a claim that never reached a destroy verb.
|
|
* Called from exactly one place in releaseConnection(), before the
|
|
* destroy_qp branch; after any destroy attempt the procedure must
|
|
* end in commitDestroy() instead. The entry lands back in Poisoned
|
|
* so a retry can claim it again. */
|
|
bool abortClaim(ConnId id) {
|
|
ConnectionRegistry& reg = registry();
|
|
bool ok = false;
|
|
reg.withEntry(id, [&](ConnectionEntryV2& entry) {
|
|
if (entry.destroying) {
|
|
entry.destroying = false;
|
|
entry.poisoned = true;
|
|
ok = true;
|
|
}
|
|
});
|
|
return ok;
|
|
}
|
|
|
|
/* Vendor QP attributes depend only on the device, not on the
|
|
* connection struct, so both v1 and v2 transitions share the same
|
|
* helpers below (mirrors transport.cpp's applyVendorQpAttrs). */
|
|
void applyVendorQpAttrs(struct ibv_context* ctx, struct ibv_qp_attr* attr) {
|
|
if (!ctx || !attr) {
|
|
return;
|
|
}
|
|
struct ibv_device_attr devAttr;
|
|
std::memset(&devAttr, 0, sizeof(devAttr));
|
|
if (ibv.query_device(ctx, &devAttr) != 0) {
|
|
return;
|
|
}
|
|
#ifdef HIPOBJ_BNXT
|
|
if (isBnxtDevice(devAttr.vendor_id)) {
|
|
configureBnxtQp(attr);
|
|
}
|
|
#endif
|
|
#ifdef HIPOBJ_IONIC
|
|
if (isIonicDevice(devAttr.vendor_id)) {
|
|
configureIonicQp(attr);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
int modifyQpToInit(DeviceHandle* dh, struct ibv_qp* qp) {
|
|
struct ibv_qp_attr attr;
|
|
std::memset(&attr, 0, sizeof(attr));
|
|
attr.qp_state = IBV_QPS_INIT;
|
|
attr.pkey_index = 0;
|
|
attr.port_num = dh->portNum;
|
|
attr.qp_access_flags = IBV_ACCESS_REMOTE_READ | IBV_ACCESS_REMOTE_WRITE;
|
|
applyVendorQpAttrs(dh->ctx, &attr);
|
|
int mask = IBV_QP_STATE | IBV_QP_PKEY_INDEX | IBV_QP_PORT |
|
|
IBV_QP_ACCESS_FLAGS;
|
|
return ibv.modify_qp(qp, &attr, mask);
|
|
}
|
|
|
|
int modifyQpToRtr(struct ibv_context* ctx, struct ibv_qp* qp,
|
|
uint32_t destQpNum, uint16_t destLid, union ibv_gid destGid,
|
|
int gidIndex, uint32_t rqPsn, uint8_t portNum) {
|
|
struct ibv_qp_attr attr;
|
|
std::memset(&attr, 0, sizeof(attr));
|
|
attr.qp_state = IBV_QPS_RTR;
|
|
/* The path MTU must not exceed the port's active MTU: emulated
|
|
* devices commonly report 1024 while HCAs run 4096, and a
|
|
* larger value fails the transition with EINVAL. */
|
|
{
|
|
struct ibv_port_attr pa;
|
|
std::memset(&pa, 0, sizeof(pa));
|
|
if (ibv.query_port(ctx, portNum, &pa) == 0 &&
|
|
pa.active_mtu >= IBV_MTU_512) {
|
|
attr.path_mtu = static_cast<enum ibv_mtu>(pa.active_mtu);
|
|
} else {
|
|
attr.path_mtu = IBV_MTU_1024;
|
|
}
|
|
}
|
|
applyVendorQpAttrs(ctx, &attr);
|
|
attr.dest_qp_num = destQpNum;
|
|
attr.rq_psn = rqPsn;
|
|
attr.max_dest_rd_atomic = 1;
|
|
attr.min_rnr_timer = 12;
|
|
/* hipObject targets RoCEv2: the GRH with the peer GID is
|
|
* the routing path; the LID stays unused on RoCE links. */
|
|
attr.ah_attr.is_global = 1;
|
|
attr.ah_attr.dlid = 0;
|
|
attr.ah_attr.grh.dgid = destGid;
|
|
attr.ah_attr.grh.hop_limit = 64;
|
|
attr.ah_attr.grh.sgid_index = gidIndex;
|
|
attr.ah_attr.grh.traffic_class = 0;
|
|
attr.ah_attr.sl = 0;
|
|
attr.ah_attr.src_path_bits = 0;
|
|
/* Service type needs the port in the address handle; leaving
|
|
* it zero fails the transition on providers that validate it
|
|
* (the emulated NIC rejects port 0 with EINVAL). */
|
|
attr.ah_attr.port_num = portNum;
|
|
int mask = IBV_QP_STATE | IBV_QP_AV | IBV_QP_PATH_MTU | IBV_QP_DEST_QPN |
|
|
IBV_QP_RQ_PSN | IBV_QP_MAX_DEST_RD_ATOMIC | IBV_QP_MIN_RNR_TIMER;
|
|
return ibv.modify_qp(qp, &attr, mask);
|
|
}
|
|
|
|
int modifyQpToRts(struct ibv_context* ctx, struct ibv_qp* qp, uint32_t sqPsn) {
|
|
struct ibv_qp_attr attr;
|
|
std::memset(&attr, 0, sizeof(attr));
|
|
attr.qp_state = IBV_QPS_RTS;
|
|
applyVendorQpAttrs(ctx, &attr);
|
|
attr.timeout = 14;
|
|
attr.retry_cnt = 7;
|
|
attr.rnr_retry = 7;
|
|
attr.sq_psn = sqPsn;
|
|
attr.max_rd_atomic = 1;
|
|
int mask = IBV_QP_STATE | IBV_QP_TIMEOUT | IBV_QP_RETRY_CNT |
|
|
IBV_QP_RNR_RETRY | IBV_QP_SQ_PSN | IBV_QP_MAX_QP_RD_ATOMIC;
|
|
return ibv.modify_qp(qp, &attr, mask);
|
|
}
|
|
|
|
} // namespace
|
|
|
|
int createRcConnV2(DeviceHandle* dh, RcConnV2& conn, bool* rollbackFailed) {
|
|
if (rollbackFailed) {
|
|
*rollbackFailed = false;
|
|
}
|
|
bool sawCq = false;
|
|
conn.cq = ibv.create_cq(dh->ctx, kCqSize, nullptr, nullptr, 0);
|
|
if (!conn.cq) {
|
|
return -1;
|
|
}
|
|
sawCq = true;
|
|
struct ibv_qp_init_attr init;
|
|
std::memset(&init, 0, sizeof(init));
|
|
init.qp_type = IBV_QPT_RC;
|
|
init.send_cq = conn.cq;
|
|
init.recv_cq = conn.cq;
|
|
init.cap.max_send_wr = kMaxSendWr;
|
|
init.cap.max_recv_wr = kMaxRecvWr;
|
|
init.cap.max_send_sge = 1;
|
|
init.cap.max_recv_sge = 1;
|
|
conn.qp = ibv.create_qp(dh->pd, &init);
|
|
if (!conn.qp) {
|
|
bool cqOk = true;
|
|
if (ibv.destroy_cq(conn.cq) != 0) {
|
|
cqOk = false;
|
|
/* Keep conn.cq when the destroy failed: clearing it would
|
|
* lose the handle the caller needs for the reaper retry. */
|
|
} else {
|
|
conn.cq = nullptr;
|
|
}
|
|
if (!cqOk) {
|
|
if (rollbackFailed) {
|
|
*rollbackFailed = true;
|
|
}
|
|
return -1;
|
|
}
|
|
return -1;
|
|
}
|
|
conn.qpNum = conn.qp->qp_num;
|
|
/* Threaded callers share the device handle; the counter must
|
|
* be atomic to avoid a data race on concurrent PREPAREs. */
|
|
dh->connRefs.fetch_add(1, std::memory_order_relaxed);
|
|
return 0;
|
|
}
|
|
|
|
void destroyRcConnV2(RcConnV2& conn, bool* qpOk, bool* cqOk) {
|
|
if (qpOk) {
|
|
*qpOk = true;
|
|
}
|
|
if (cqOk) {
|
|
*cqOk = true;
|
|
}
|
|
/* A successful destroy clears the handle so callers storing
|
|
* the surviving pointers never re-destroy them. */
|
|
if (conn.qp) {
|
|
if (ibv.destroy_qp(conn.qp) == 0) {
|
|
conn.qp = nullptr;
|
|
} else if (qpOk) {
|
|
*qpOk = false;
|
|
}
|
|
}
|
|
if (conn.cq) {
|
|
if (ibv.destroy_cq(conn.cq) == 0) {
|
|
conn.cq = nullptr;
|
|
} else if (cqOk) {
|
|
*cqOk = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
int createRcQpOnly(DeviceHandle* dh, struct ibv_cq* cq, RcConnV2& conn) {
|
|
if (!cq) {
|
|
return -1;
|
|
}
|
|
struct ibv_qp_init_attr init;
|
|
std::memset(&init, 0, sizeof(init));
|
|
init.qp_type = IBV_QPT_RC;
|
|
init.send_cq = cq;
|
|
init.recv_cq = cq;
|
|
init.cap.max_send_wr = kMaxSendWr;
|
|
init.cap.max_recv_wr = kMaxRecvWr;
|
|
init.cap.max_send_sge = 1;
|
|
init.cap.max_recv_sge = 1;
|
|
conn.qp = ibv.create_qp(dh->pd, &init);
|
|
if (!conn.qp) {
|
|
return -1;
|
|
}
|
|
conn.qpNum = conn.qp->qp_num;
|
|
return 0;
|
|
}
|
|
|
|
int transitionQpToInitV2(DeviceHandle* dh, RcConnV2& conn) {
|
|
return modifyQpToInit(dh, conn.qp);
|
|
}
|
|
|
|
int rearmQpToInitV2(DeviceHandle* dh, RcConnV2& conn) {
|
|
/* RESET is the only transition valid from every QP state. */
|
|
struct ibv_qp_attr attr;
|
|
std::memset(&attr, 0, sizeof(attr));
|
|
attr.qp_state = IBV_QPS_RESET;
|
|
if (ibv.modify_qp(conn.qp, &attr, IBV_QP_STATE) != 0) {
|
|
return -1;
|
|
}
|
|
return modifyQpToInit(dh, conn.qp);
|
|
}
|
|
|
|
int transitionQpToRtrV2(DeviceHandle* dh, RcConnV2& conn, uint32_t destQpNum,
|
|
uint16_t destLid, union ibv_gid destGid,
|
|
uint32_t rqPsn) {
|
|
return modifyQpToRtr(dh->ctx, conn.qp, destQpNum, destLid, destGid,
|
|
dh->gidIndex, rqPsn, dh->portNum);
|
|
}
|
|
|
|
int transitionQpToRtsV2(RcConnV2& conn, DeviceHandle* dh, uint32_t sqPsn) {
|
|
return modifyQpToRts(dh->ctx, conn.qp, sqPsn);
|
|
}
|
|
|
|
void releaseDevice(DeviceHandle* dh) {
|
|
if (!dh) {
|
|
return;
|
|
}
|
|
uint32_t cur = dh->connRefs.load(std::memory_order_relaxed);
|
|
while (cur > 0 &&
|
|
!dh->connRefs.compare_exchange_weak(cur, cur - 1,
|
|
std::memory_order_relaxed)) {
|
|
}
|
|
/* PD/context close happens only when the buffer map is empty too;
|
|
* the caller (hipObjShutdown stage) checks both. */
|
|
}
|
|
|
|
int postRecvImm(DeviceHandle* dh, RcConnV2& conn) {
|
|
(void)dh;
|
|
struct ibv_recv_wr wr;
|
|
struct ibv_recv_wr* bad = nullptr;
|
|
struct ibv_sge sge; /* zero-SGE: no scatter entry */
|
|
std::memset(&wr, 0, sizeof(wr));
|
|
std::memset(&sge, 0, sizeof(sge));
|
|
wr.wr_id = kRecvImm;
|
|
wr.sg_list = nullptr;
|
|
wr.num_sge = 0;
|
|
return ibv.post_recv(conn.qp, &wr, &bad);
|
|
}
|
|
|
|
int releaseConnection(ConnId id) {
|
|
ConnectionRegistry& reg = registry();
|
|
if (!reg.claimDestroy(id)) {
|
|
return 0; /* claimed elsewhere or already gone: idempotent */
|
|
}
|
|
bool hasQp = false;
|
|
uint64_t rid = 0;
|
|
uint32_t qpn = 0;
|
|
reg.withEntry(id, [&](ConnectionEntryV2& entry) {
|
|
hasQp = entry.conn.qp != nullptr;
|
|
rid = entry.reservationId;
|
|
qpn = entry.conn.qpNum;
|
|
entry.reservationId = 0; /* local ownership during destroy */
|
|
});
|
|
if (hasQp && rid == 0) {
|
|
/* Defensive: the normal creation sequence always inserts an
|
|
* entry with a live reservation, so a live qp without one is
|
|
* not expected. Keep the handling anyway so the teardown path
|
|
* stays complete: reserve a slot now, and report busy when the
|
|
* retired ring is exhausted so the caller can retry later. */
|
|
rid = reg.retired().reserve();
|
|
if (rid == 0) {
|
|
abortClaim(id); /* pre-destroy cancel; stays reclaimable */
|
|
return kReleaseBusy;
|
|
}
|
|
reg.withEntry(id, [&](ConnectionEntryV2& entry) {
|
|
entry.reservationId = rid;
|
|
});
|
|
}
|
|
bool qpOk = true;
|
|
bool cqOk = true;
|
|
uint32_t psn = 0;
|
|
reg.withEntry(id, [&](ConnectionEntryV2& entry) {
|
|
psn = entry.clientPsn; /* recorded into the retired ring below */
|
|
if (entry.conn.qp) {
|
|
qpOk = ibv.destroy_qp(entry.conn.qp) == 0;
|
|
if (qpOk) {
|
|
if (rid != 0) {
|
|
/* Record the destroyed pair immediately; the tuple tracks
|
|
* the QP lifetime, independent of the CQ result below. */
|
|
reg.retired().record(rid, qpn, psn);
|
|
}
|
|
entry.conn.qp = nullptr;
|
|
entry.conn.qpNum = 0;
|
|
} else {
|
|
entry.reservationId = rid; /* keep for the retry */
|
|
}
|
|
} else if (rid != 0) {
|
|
/* No live qp: the reservation serves no future destroy. */
|
|
reg.retired().unreserve(rid);
|
|
}
|
|
if (entry.conn.cq) {
|
|
cqOk = ibv.destroy_cq(entry.conn.cq) == 0;
|
|
if (cqOk) {
|
|
entry.conn.cq = nullptr;
|
|
}
|
|
}
|
|
});
|
|
reg.commitDestroy(id, qpOk, cqOk);
|
|
if (qpOk && cqOk) {
|
|
reg.eraseDestroyed(id);
|
|
return 0;
|
|
}
|
|
return kReleaseLeftover;
|
|
}
|
|
|
|
int discardAndRecreateQp(ConnId id) {
|
|
ConnectionRegistry& reg = registry();
|
|
|
|
/* Local slot B guards the tuple we are about to retire. */
|
|
uint64_t slotB = reg.retired().reserve();
|
|
if (slotB == 0) {
|
|
return kReleaseBusy;
|
|
}
|
|
|
|
struct Captured {
|
|
struct ibv_qp* qp = nullptr;
|
|
struct ibv_cq* cq = nullptr;
|
|
DeviceHandle* device = nullptr;
|
|
uint64_t rid = 0;
|
|
uint32_t qpn = 0;
|
|
uint32_t psn = 0;
|
|
} cap;
|
|
|
|
bool found = reg.withEntry(id, [&](ConnectionEntryV2& entry) {
|
|
cap.qp = entry.conn.qp;
|
|
cap.cq = entry.conn.cq;
|
|
cap.device = entry.device;
|
|
cap.rid = entry.reservationId;
|
|
cap.qpn = entry.conn.qpNum;
|
|
cap.psn = entry.clientPsn;
|
|
});
|
|
if (!found || !cap.qp || !cap.cq || cap.rid == 0) {
|
|
/* Nothing to discard or the entry is mid-teardown. */
|
|
reg.retired().unreserve(slotB);
|
|
return kReleaseLeftover;
|
|
}
|
|
|
|
/* Destroy the old qp; on failure keep the live qp and return. */
|
|
if (ibv.destroy_qp(cap.qp) != 0) {
|
|
reg.retired().unreserve(slotB);
|
|
return kReleaseLeftover;
|
|
}
|
|
reg.retired().record(slotB, cap.qpn, cap.psn);
|
|
|
|
/* The old reservation A is released back: the tuple it guarded is
|
|
* now recorded through slot B, tied to the destroyed qp. */
|
|
reg.retired().unreserve(cap.rid);
|
|
|
|
/* Move ownership of A's slot to the caller and clear the fields
|
|
* before the recreate attempt, so the entry never holds a live
|
|
* reservationId without a qp. */
|
|
reg.withEntry(id, [&](ConnectionEntryV2& entry) {
|
|
entry.reservationId = 0;
|
|
entry.conn.qp = nullptr;
|
|
entry.conn.qpNum = 0;
|
|
});
|
|
|
|
RcConnV2 fresh;
|
|
if (createRcQpOnly(cap.device, cap.cq, fresh) != 0) {
|
|
/* No qp: release through the normal teardown path (the cq is
|
|
* destroyed there) and report the failure. */
|
|
int rc = releaseConnection(id);
|
|
if (rc != 0) {
|
|
return rc;
|
|
}
|
|
return kReleaseLeftover;
|
|
}
|
|
|
|
reg.withEntry(id, [&](ConnectionEntryV2& entry) {
|
|
entry.conn.qp = fresh.qp;
|
|
entry.conn.qpNum = fresh.qpNum;
|
|
entry.reservationId = cap.rid;
|
|
});
|
|
return 0;
|
|
}
|
|
|
|
bool validateDataCompletion(const struct ibv_wc& wc,
|
|
const WcExpectation& expect, const char** reason) {
|
|
if (wc.status != IBV_WC_SUCCESS) {
|
|
*reason = "completion status is not success";
|
|
return false;
|
|
}
|
|
if (wc.wr_id != expect.wrId) {
|
|
*reason = "unexpected wr_id";
|
|
return false;
|
|
}
|
|
if (expect.kind == WcKind::kGet) {
|
|
if (wc.opcode != IBV_WC_RECV_RDMA_WITH_IMM) {
|
|
*reason = "expected RECV_RDMA_WITH_IMM";
|
|
return false;
|
|
}
|
|
} else {
|
|
if (wc.opcode != IBV_WC_RECV) {
|
|
*reason = "expected RECV";
|
|
return false;
|
|
}
|
|
if (!(wc.wc_flags & IBV_WC_WITH_IMM)) {
|
|
*reason = "immediate flag missing";
|
|
return false;
|
|
}
|
|
}
|
|
if (ntohl(wc.imm_data) != expect.cookie) {
|
|
*reason = "immediate cookie mismatch";
|
|
return false;
|
|
}
|
|
*reason = "";
|
|
return true;
|
|
}
|
|
|
|
} // namespace v2
|
|
} // namespace hipObj
|