EFA examples and best practices#
EFA uses reliable, connectionless SRD queue pairs. Applications exchange a
small endpoint description and create an address handle instead of driving an
RC QP through a connection handshake. Every work request names its destination
with dest=peer.
SRD bootstrap with torchrun#
The following fragment creates one QP per rank and exchanges endpoint metadata through a Gloo control plane:
import os
os.environ.setdefault(
"PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True"
)
import torch
import torch.distributed as dist
import efa
import efa.cuda as efa_cuda
dist.init_process_group("gloo")
rank = dist.get_rank()
world_size = dist.get_world_size()
local_rank = int(os.environ["LOCAL_RANK"])
torch.cuda.set_device(local_rank)
gpu = torch.device("cuda", local_rank)
peer_rank = 1 - rank
qkey = 0x1234
port = int(os.getenv("RDMA_PORT", "1"))
gid_index = int(os.getenv("RDMA_GID_INDEX", "0"))
device = efa.get_efa_device_list()[0]
context = device.open()
pd = context.alloc_pd()
cq = context.create_cq(32)
qp = pd.create_qp(
efa.QPInitAttr(
send_cq=cq,
recv_cq=cq,
max_send_wr=16,
max_recv_wr=16,
)
).prepare(qkey=qkey)
local_info = efa.local_endpoint_info(
qp,
qkey=qkey,
gid_index=gid_index,
port=port,
)
local_wire = torch.tensor(list(local_info.to_bytes()), dtype=torch.uint8)
wires = [torch.empty_like(local_wire) for _ in range(world_size)]
dist.all_gather(wires, local_wire)
remote_info = efa.EndpointInfo.from_bytes(bytes(wires[peer_rank].tolist()))
peer = remote_info.peer(pd, sgid_index=gid_index, port=port)
Resolve peer on both ranks, even for one-way data. EFA responders require
a reverse address handle before accepting RDMA reads or writes from that peer.
Close peer before its protection domain.
One-sided GPU write with notification#
This is the one-QP data path. Rank 0 writes a CUDA tensor into rank 1’s registered memory and attaches an immediate value for receiver notification.
WRITE_COMPLETE = 0xC0DE
def wait(cq):
while True:
completions = cq.poll(1)
if completions:
completions[0].raise_for_status()
return completions[0]
tensor = (
torch.arange(1024, dtype=torch.int32, device=gpu)
if rank == 0
else torch.zeros(1024, dtype=torch.int32, device=gpu)
)
mr_access = efa.AccessFlags.LOCAL_WRITE
if rank == 1:
mr_access |= efa.AccessFlags.REMOTE_WRITE
with efa_cuda.register_tensor(pd, tensor, mr_access) as mr:
target = torch.zeros(2, dtype=torch.int64)
if rank == 1:
target[0] = mr.addr
target[1] = mr.rkey
qp.post_recv(efa.RecvWR(wr_id=1, sg_list=[mr.sge()]))
# Rank 1 publishes its address/rkey after posting the notification receive.
dist.broadcast(target, src=1)
if rank == 0:
torch.cuda.current_stream(gpu).synchronize()
qp.post_send(
efa.SendWR(
wr_id=2,
sg_list=[mr.sge()],
opcode=efa.WROpcode.RDMA_WRITE_WITH_IMM,
send_flags=efa.SendFlags.SIGNALED,
remote_addr=int(target[0]),
rkey=int(target[1]),
imm_data=WRITE_COMPLETE,
dest=peer,
)
)
wait(cq)
else:
notification = wait(cq)
assert notification.opcode == efa.WCOpcode.RECV_RDMA_WITH_IMM
assert notification.wc_flags & efa.WCFlags.WITH_IMM
assert notification.imm_data == WRITE_COMPLETE
with torch.cuda.device(gpu):
efa_cuda.flush_gpudirect_writes()
The receive SGE is not the write payload path; the remote address and rkey
place the data. The receive work request supplies the notification credit.
One-sided GPU read#
Rank 0 can instead pull a tensor exposed by rank 1. No receive work request is needed, and rank 1 receives no verbs completion for the read.
tensor = (
torch.zeros(1024, dtype=torch.int32, device=gpu)
if rank == 0
else torch.arange(1024, dtype=torch.int32, device=gpu)
)
mr_access = efa.AccessFlags.LOCAL_WRITE
if rank == 1:
mr_access |= efa.AccessFlags.REMOTE_READ
with efa_cuda.register_tensor(pd, tensor, mr_access) as mr:
source = torch.zeros(2, dtype=torch.int64)
if rank == 1:
torch.cuda.current_stream(gpu).synchronize()
source[0] = mr.addr
source[1] = mr.rkey
dist.broadcast(source, src=1)
if rank == 0:
qp.post_send(
efa.SendWR(
wr_id=3,
sg_list=[mr.sge()],
opcode=efa.WROpcode.RDMA_READ,
send_flags=efa.SendFlags.SIGNALED,
remote_addr=int(source[0]),
rkey=int(source[1]),
dest=peer,
)
)
wait(cq)
with torch.cuda.device(gpu):
efa_cuda.flush_gpudirect_writes()
Use a control-plane message if rank 1 needs to know that rank 0 has finished reading its memory.
QPs and physical lanes#
EFA has two independent scaling dimensions:
Dimension |
Resources |
Purpose |
|---|---|---|
More QPs on one EFA |
Shared context, PD, CQ, and MR |
More outstanding work, especially for small transfers |
More physical EFA lanes |
One context, PD, CQ, peer, and MR registration per EFA |
Aggregate bandwidth across physical links |
Within one lane, divide the tensor into disjoint regions and post work to every QP before polling completions:
for qp_index, lane_qp in enumerate(qps):
offset = qp_index * chunk_size
lane_qp.post_send(
efa.SendWR(
wr_id=qp_index,
sg_list=[lane_mr.sge(chunk_size, offset=offset)],
opcode=efa.WROpcode.RDMA_WRITE,
send_flags=efa.SendFlags.SIGNALED,
remote_addr=remote_addr + offset,
rkey=remote_rkey,
dest=peers[qp_index],
)
)
# Drain one successful completion for each posted request.
...
For multiple physical lanes, repeat that resource set for each selected EFA.
Register the shared torch allocation in every lane’s PD, publish the address
and lane-specific rkey, post all lanes, and only then begin polling. Pair
each GPU with PCIe-local EFAs instead of selecting devices by enumeration
order.
One QP is generally enough to saturate a lane for large messages. Multiple QPs primarily improve the number of small operations in flight; multiple physical lanes are what increase the large-transfer bandwidth ceiling.
Signalling choices#
Every EFA RDMA read/write work request should be signaled. Drain successful sender completions before reusing local ranges.
A plain
RDMA_WRITEproduces no receiver completion.RDMA_WRITE_WITH_IMMnotifies the receiver after consuming a posted receive.Unsolicited write-with-immediate removes that posted-receive requirement, but every peer must enable
unsolicited_write_recvand use an extended CQ with unsolicited metadata enabled.An RDMA read produces a completion only on the requester.
Checklist#
Query
Context.query_efa_device()and require the matchingEfaDeviceCaps.RDMA_WRITEorRDMA_READbit before selecting one-sided I/O.Split a transfer at
EfaDeviceAttr.max_rdma_size; thewrite_wrsandread_wrshelpers perform this chunking.Resolve endpoint metadata in both directions before one-sided traffic.
Register long-lived buffers once per PD and keep the owning tensor alive.
Synchronize the CUDA stream that produced an outbound buffer before the NIC reads it.
Flush inbound GPUDirect writes in the destination CUDA context after the matching completion or notification.
Check every completion with
raise_for_status()and retain vendor status in error logs.Keep each GPU, EFA, and polling CPU within the same PCIe/NUMA domain when possible.
Launch a local two-rank smoke test with:
torchrun --standalone --nproc-per-node=2 example.py
Production EFA traffic is normally cross-host; use one rank per host and the
standard multi-node torchrun rendezvous options.