Installed host RDMA qualification#
This gate builds a packaged orbite crate as an ordinary dependency and runs
the public TransferEngine API with only the rdma feature. Fault and
rail-accounting modes add a separate process-local observer library. The crate
is packaged locally, never published.
Build the artifact#
Install the build prerequisites, then build from a clean committed checkout into a directory outside the repository:
python3 tests/hardware/build_installed.py \
--toolchain stable --output /var/tmp/orbite-installed/stable
python3 tests/hardware/build_installed.py \
--toolchain 1.85.0 --output /var/tmp/orbite-installed/msrv
The script creates a .crate, extracts it, copies the packaged rdma_host
fixture into an independent consumer, resolves OrbiTE offline from that package
with locked versions, runs fmt and Clippy, and installs
installed/bin/orbite-rdma-host. The manifest pins the commit, archive, fixture
modules, lockfile, binary, features and native libraries; the test protocol is
orbite-installed-host-v4. CI builds both toolchains, but --protocol only
proves the binary starts. The controller rejects a dirty or mismatched manifest
(--allow-dirty is for local diagnosis only).
Run the two-host profile#
Use the identity-pinned SSH profile and supervisor from the
RDMA guide with
runner: "orbite-installed-host", two rails, a gid_index, distinct source
and requester nodes, and one resource lock per RNIC on each node. Nodes must
have different hostnames and boot IDs; the controller rechecks both at
deployment and inside the lock supervisor.
{
"source": "a", "requester": "b",
"source_kind": "cpu", "requester_kind": "cpu", "transport": "rdma",
"mode": "installed-host", "operation": "read", "rdma_events": false,
"parents": 16, "payload": 6291457, "block": 1048576,
"num_lanes": 3, "mask": 3
}
python3 tests/hardware/run.py --profile /var/tmp/host-profile.json \
--installation /var/tmp/orbite-installed/stable/manifest.json \
--binary /var/tmp/orbite-installed/stable/installed/bin/orbite-rdma-host \
--output /var/tmp/orbite-installed/native-stable
Bounds: 256 MiB of backing per peer, 32 parents, 32 MiB per parent and 1–3 QPs per peer/context/operation. Each of three rounds runs concurrent parent requests with fresh input, checks every byte and guard and waits for source drain. Both contexts publish persistent MRs even under a one-rail mask, and native byte counters, bounded QP creation and reuse must match the workload. TCP carries only rendezvous; the controller checks the round ledger, provider mappings, child exit and released locks.
Physical rail accounting#
Select mode: "installed-rail-accounting" with 16 parents,
payload: 4194305, block: 1048576, num_lanes: 1 and mask: 3; run
READ/polling and WRITE/events, reversing hosts for the second toolchain. Each
round has exactly 64 payload WRs and 67,108,880 payload bytes.
The requester-only observer (accounting.c) forwards native calls and CQEs
unchanged and joins each accepted payload WR to its successful CQE by context,
CQ, QPN and WR ID. The verifier checks both rails’ payload, at least two parents
outstanding on each rail’s shared QP, at most eight payload WRs per QP, and
native bytes equal to the public counters. This measures accepted work, not
RDMA-network saturation, fairness or throughput.
Automatic discovery against an independent inventory#
Add "discovery": "auto" to the rail-accounting cases: no device, port or GID
pins, a whitelist of exactly the two locked RNICs, and an identical blacklist to
check whitelist precedence. A separate engine first requests strict GID 255 and
must get Unavailable with no retained owners.
Under the RNIC locks, the supervisor runs discovery_inventory.py before and
after each child. This read-only tool shares no OrbiTE code; it records sysfs
PCI, NUMA, port, GID, interface and node data. The verifier requires an unchanged
inventory with one active Ethernet port and one RoCEv2 candidate per RNIC, then
compares public NIC IDs, memory priority tiers and strict-pin rejection against
it. This mirrors TENT’s platform/cpu_probe.cpp, runtime/topology.cpp and
context address selection. It does not cover port transitions, link loss,
InfiniBand or hotplug.
Peer restart with a surviving engine#
Select mode: "installed-restart" with fault_role: "source" or "requester"
and generations: 2 or 3. Each generation runs three full READ/WRITE rounds,
drains to zero debt with cached QPs and four persistent MRs still live, then the
supervisor SIGKILLs and reaps only the selected child. This is abrupt death with
idle cached resources, not death during DMA.
The replacement reuses its service address and registration ID with a fresh
epoch. The survivor keeps its PID, engine, registration and MRs. Every earlier
epoch must fail both open_segment and data submission with EpochMismatch,
zero QP or WR creation, zero bytes, RemoteEffect::None and local drain. Each
generation creates at most rails × num_lanes new QPs. installed_restart.py
reconciles the receipts with the supervisor’s actual PIDs and exit statuses.
Interrupt a posted completion RQ#
installed-interruption uses the restart roles with payload <= block (one
slice per parent) and 1–32 parents. Before each nonfinal restart, a source-side
control relay forwards Describe and the RDMA handshake but holds READY/GRANT
after the source has bound its MW and posted its completion receive; payload
still travels over RDMA.
Both peers must show pending grants, unchanged byte counters and no new QPs;
the requester’s batches stay Running, waits time out and free() returns
BatchBusy. Then the exact child is killed. A surviving source must drain the
outstanding RQ; a surviving requester must report failed, locally drained
batches with zero bytes and RemoteEffect::None. The survivor keeps its engine,
PD and MRs, reclaims failed CQ lanes and resumes with fresh-epoch rounds. This
interrupts authorization before payload; it does not prove mid-payload DMA
interruption.
Lost payload completions#
Select mode: "installed-lost-completion" with the restart roles, 2–3
generations, payload <= block, 8 parents with one lane or 16 with three, a
1 MiB payload, both rails, both directions and both progress modes.
build_installed.py also builds the packaged tests/hardware/cq_fault/ tool,
pinned by hash and loaded with LD_PRELOAD only for this profile. Armed on the
requester, it records accepted payload WRs and removes their successful CQEs
from poll_cq; it never fabricates data or completions. At the kill point the
engine still owns SQ debt, source authorization, DMA loans and busy batches. The
surviving requester must fail and drain with zero reported bytes and WRITE
effects Unknown, and successful ibv_destroy_qp receipts must cover every
affected QP before the drain receipt. This proves recovery after lost engine
observation, not physical link failure.
Native cleanup failure#
mode: "installed-cleanup-failure" checks quarantine through the public API:
fault_role: "requester", two generations, one parent, payload <= block, one
lane and mask 1 or 2. The default qp selector withholds one successful CQE,
then makes the real ibv_destroy_qp return EIO by filtering writes and ioctls
on that context’s uverbs descriptor in a helper thread. This is syscall fault
injection, not a spontaneous hardware fault.
The batch must fail with Unquiesced and locally_drained = false; free stays
busy, CPU writes stay excluded and SQ, byte and batch charges stay held. Repeated
close cannot report drain or retry destruction. After the fixture drops its
handles, a weak owner and a real QP query must still show the retained backing
and QP. The replacement process then runs the stale-authority and healthy
checks. See the single-host diagnostic
and two-host cases.
In-flight DMA interruption#
mode: "installed-inflight-dma" uses one parent, one lane, mask 1 or 2 and
payload == block (one WR); the focused profile uses 4 MiB READ/polling and
16 MiB WRITE/events. Only this mode allows blocks up to 16 MiB. Both engines must
survive the fault.
After three healthy rounds, the observer posts the real WR and moves that QP to
IBV_QPS_ERR with ibv_modify_qp, changing no port or device. It joins the WR
to its error completion by CQ, QPN and WR ID, and requires a successful QP
destruction before native drain. The batch must fail with zero bytes, proven
local drain and WRITE effects unknown. After drain, the destination must contain
both copied and untouched sentinel bytes (zero-progress or fully completed copies
fail), with every other byte unchanged. Both processes then run three more
healthy rounds. The trigger corresponds to TENT’s
beginDestroyNoLock
ERR transition.
MR/MW cleanup and engine replacement#
installed-memory-cleanup uses one parent, one QP and one slice, with both
contexts registered and one rail active. Select cleanup_kind: "mr" with
fault_role requester or source, or cleanup_kind: "mw" with source. The
frozen cases use 64 KiB READ/polling and WRITE/events.
After three healthy rounds, the real ibv_dereg_mr or dealloc_mw returns
EIO through the same scoped filter, and any retry is rejected. MR failure must
keep the registration charge and backing; MW failure must keep its parent MR
borrow, grant, DMA loan, quota and key domain. A real PD destroy must still
return EBUSY. Both processes then create fresh engines at the same addresses:
old epochs fail before QP creation, three rounds pass, and the old quarantined
allocation stays retained until process exit. See the
six two-host cases.
CQ, completion-channel and PD dependency failures#
Use installed-cleanup-failure with cleanup_kind set to cq, channel or
pd; omitting it keeps the qp case. Channel failure requires event progress.
Owner |
Native failure |
Must stay retained after the fixture drops its owners |
|---|---|---|
CQ |
A dependent RESET QP blocks CQ destruction |
CQ, PD and the unresolved grant, SQ, batch and allocation |
Completion channel |
An extra CQ blocks channel destruction |
Channel, context and unresolved grant and allocation; QPs and engine CQs are released |
PD |
A Type-2 MW blocks idle PD destruction |
PD and device only; MRs, grants, batches and the allocation are released |
Each destructor returns EBUSY once, and retries or premature device close fail
the run. The CQ and channel cases reuse a withheld CQE and require unquiesced
batches; the PD case is idle. All three then run the live-peer restart checks.
See qualification.
TENT correspondence and limits#
TENT responsibility |
Exercised OrbiTE behavior |
|---|---|
Context/PD and registered buffers outlive endpoints |
Persistent context MRs survive peer process replacement |
|
Connection generations fence cached QPs; data uses the fresh authority |
|
The surviving worker pool keeps serving the same contexts |
|
Exact WR completion drives grants; missing observation fences QPs until native drain |
OrbiTE also makes the source epoch explicit, so an old segment fails instead of
adopting a new process’s authority; this gate is not a TENT wire or restart
oracle. Fixture owners: rdma_host/resources.rs (registration and drain),
restart.rs, interruption.rs, gate.rs (test relay), completion.rs,
inflight.rs and memory.rs, with installed_*.py verifiers. Physical port or
device faults, per-rail fairness, InfiniBand and GPU transfers are outside this
profile; see qualification and the
P4 queue.