monarch_rdma/backend/ibverbs/efa_queue_pair.rs
1/*
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 * All rights reserved.
4 *
5 * This source code is licensed under the BSD-style license found in the
6 * LICENSE file in the root directory of this source tree.
7 */
8
9//! EFA SRD queue pair built on the efadv and extended-verbs work-request
10//! builder.
11
12use std::io::Error;
13use std::result::Result;
14use std::sync::Arc;
15
16use super::domain::IbvDomain;
17use super::domain::IbvDomainImpl;
18use super::memory_region::IbvMemoryRegionView;
19use super::memory_region::IbvRemoteMemoryRegionView;
20use super::primitives::Gid;
21use super::primitives::IbvAh;
22use super::primitives::IbvConfig;
23use super::primitives::IbvCq;
24use super::primitives::IbvQp;
25use super::primitives::IbvQpInfo;
26use super::queue_pair::IbvQueuePair;
27
28/// Queue key for EFA SRD traffic. Both peers must present the same value or the
29/// responder drops the traffic silently; [`IbvQpInfo`] carries no queue key, so
30/// this is a shared constant rather than something negotiated at connect time.
31const EFA_QKEY: u32 = 0x4242;
32
33/// The RDMA operations an EFA SRD queue pair posts.
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35enum EfaOp {
36 Write,
37 Read,
38}
39
40/// One work request within a posting session: the byte offset into both the
41/// local and the remote buffer, the length, and the id its completion carries.
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43struct Chunk {
44 offset: usize,
45 len: usize,
46 wr_id: u64,
47}
48
49/// Splits a `total_size`-byte transfer into `chunk_max`-bound work requests,
50/// numbering them from `first_wr_id`. A zero-byte transfer still yields one
51/// work request, so every posted operation produces a completion.
52fn chunks(total_size: usize, chunk_max: usize, first_wr_id: u64) -> Vec<Chunk> {
53 assert!(chunk_max > 0, "chunk size must be positive");
54 let mut chunks = Vec::with_capacity(total_size.div_ceil(chunk_max).max(1));
55 let mut offset = 0;
56 let mut remaining = total_size;
57 loop {
58 let len = std::cmp::min(remaining, chunk_max);
59 let wr_id = first_wr_id + chunks.len() as u64;
60 chunks.push(Chunk { offset, len, wr_id });
61 offset += len;
62 remaining -= len;
63 if remaining == 0 {
64 break;
65 }
66 }
67 chunks
68}
69
70/// The address-handle attributes describing a peer reachable at `dgid`, sent
71/// from `port_num` using local GID-table entry `sgid_index`.
72///
73/// Only the destination GID reaches the EFA device: it resolves the handle from
74/// `grh.dgid` alone and returns an opaque routing token. The remaining GRH
75/// fields — `hop_limit`, `traffic_class`, `sl` — carry IP header values that
76/// matter on RoCE v2 and never reach the wire here, so they stay zero.
77/// `sgid_index` must still name a populated GID-table entry, because the kernel
78/// validates it whenever the handle is global.
79fn ah_attr(port_num: u8, dgid: Gid, sgid_index: u8) -> rdmaxcel_sys::ibv_ah_attr {
80 rdmaxcel_sys::ibv_ah_attr {
81 port_num,
82 is_global: 1,
83 grh: rdmaxcel_sys::ibv_global_route {
84 dgid: rdmaxcel_sys::ibv_gid::from(dgid),
85 sgid_index,
86 ..Default::default()
87 },
88 ..Default::default()
89 }
90}
91
92/// One RDMA work request to add to a [`WrSession`]. Addresses and lengths are
93/// resolved by the caller, so building it cannot fail.
94#[derive(Debug, Clone, Copy)]
95struct Wr {
96 op: EfaOp,
97 wr_id: u64,
98 laddr: u64,
99 lkey: u32,
100 raddr: u64,
101 rkey: u32,
102 len: u32,
103}
104
105/// An open work-request session on one queue pair's extended builder, borrowing
106/// both that queue pair and the peer it is routed to for as long as the session
107/// lives.
108///
109/// `ibv_wr_start` takes the send queue's lock, which only `ibv_wr_complete` or
110/// `ibv_wr_abort` releases. This guard makes that pairing unconditional: the
111/// session opens on construction and, if the guard is dropped without
112/// [`Self::post`] — an unwind out of the caller, say — `Drop` aborts it. Leaving
113/// it open would strand the lock and deadlock every later post on the queue
114/// pair, and `ibv_destroy_qp` would then destroy a held lock.
115struct WrSession<'a> {
116 qpex: *mut rdmaxcel_sys::ibv_qp_ex,
117 rdma_write: unsafe extern "C" fn(*mut rdmaxcel_sys::ibv_qp_ex, u32, u64),
118 rdma_read: unsafe extern "C" fn(*mut rdmaxcel_sys::ibv_qp_ex, u32, u64),
119 set_sge: unsafe extern "C" fn(*mut rdmaxcel_sys::ibv_qp_ex, u32, u64, u32),
120 set_ud_addr:
121 unsafe extern "C" fn(*mut rdmaxcel_sys::ibv_qp_ex, *mut rdmaxcel_sys::ibv_ah, u32, u32),
122 complete: unsafe extern "C" fn(*mut rdmaxcel_sys::ibv_qp_ex) -> std::os::raw::c_int,
123 abort: unsafe extern "C" fn(*mut rdmaxcel_sys::ibv_qp_ex),
124 /// The queue pair `qpex` points into. Never read: it is held so the borrow
125 /// checker keeps the queue pair alive for as long as the builder pointer
126 /// derived from it.
127 _qp: &'a IbvQp,
128 /// The destination every request in this session is routed to. Borrowed, so
129 /// the address handle cannot be destroyed while the session is open.
130 peer: &'a EfaPeer,
131 /// Set once `wr_complete` has run. It releases the lock even when it reports
132 /// failure, so past that point [`Drop`] must not abort.
133 completed: bool,
134}
135
136impl<'a> WrSession<'a> {
137 /// Resolves `qp`'s extended work-request builder and opens a session on it,
138 /// routed to `peer`.
139 ///
140 /// Every entry point is resolved before the session opens, so a queue pair
141 /// missing one fails here rather than partway through building requests.
142 ///
143 /// # Safety
144 ///
145 /// `qp` must hold a non-null, live `ibv_qp`, and `peer`'s address handle must
146 /// be a live handle created on the same protection domain as `qp`. Borrowing
147 /// `peer` keeps that handle from being destroyed for the life of the session,
148 /// but says nothing about which device or domain it addresses.
149 unsafe fn start(qp: &'a IbvQp, peer: &'a EfaPeer) -> Result<Self, anyhow::Error> {
150 // SAFETY: `qp` holds a live `ibv_qp` (caller contract).
151 // `ibv_qp_to_qp_ex` returns null unless the QP was created with
152 // `IBV_QP_INIT_ATTR_SEND_OPS_FLAGS`.
153 let qpex = unsafe { rdmaxcel_sys::ibv_qp_to_qp_ex(qp.as_ptr()) };
154 if qpex.is_null() {
155 anyhow::bail!(
156 "queue pair has no extended work-request builder; it was not created with IBV_QP_INIT_ATTR_SEND_OPS_FLAGS"
157 );
158 }
159
160 // SAFETY: `qpex` is non-null (checked above) and points into the live
161 // `qp`, so reading its function-pointer fields is sound.
162 let ops = unsafe { &*qpex };
163 let session = Self {
164 qpex,
165 rdma_write: ops.wr_rdma_write.ok_or_else(|| missing("wr_rdma_write"))?,
166 rdma_read: ops.wr_rdma_read.ok_or_else(|| missing("wr_rdma_read"))?,
167 set_sge: ops.wr_set_sge.ok_or_else(|| missing("wr_set_sge"))?,
168 set_ud_addr: ops
169 .wr_set_ud_addr
170 .ok_or_else(|| missing("wr_set_ud_addr"))?,
171 complete: ops.wr_complete.ok_or_else(|| missing("wr_complete"))?,
172 abort: ops.wr_abort.ok_or_else(|| missing("wr_abort"))?,
173 _qp: qp,
174 peer,
175 completed: false,
176 };
177 let start = ops.wr_start.ok_or_else(|| missing("wr_start"))?;
178 // SAFETY: `qpex` is the live builder resolved above. From here the
179 // send-queue lock is held, and `session`'s `Drop` releases it on any
180 // path that does not reach `post`.
181 unsafe { start(qpex) };
182 Ok(session)
183 }
184
185 /// Adds one signaled work request to the session.
186 fn add(&mut self, wr: Wr) {
187 // SAFETY: `self.qpex` is the live builder this open session was started
188 // on, and `self.peer`'s address handle is borrowed for the session, so it
189 // is still live. Each request is fully specified — `wr_id`/`wr_flags`,
190 // then the opcode builder, then the scatter/gather entry, then the
191 // destination — before the next one begins.
192 unsafe {
193 // The builder call below reads both of these, so they are set per
194 // request rather than once per session.
195 (*self.qpex).wr_id = wr.wr_id;
196 (*self.qpex).wr_flags = rdmaxcel_sys::ibv_send_flags::IBV_SEND_SIGNALED.0;
197 match wr.op {
198 EfaOp::Write => (self.rdma_write)(self.qpex, wr.rkey, wr.raddr),
199 EfaOp::Read => (self.rdma_read)(self.qpex, wr.rkey, wr.raddr),
200 }
201 // Order matters: `wr_set_sge` dispatches on the opcode the builder
202 // just wrote.
203 (self.set_sge)(self.qpex, wr.lkey, wr.laddr, wr.len);
204 (self.set_ud_addr)(
205 self.qpex,
206 self.peer.ah.as_ptr(),
207 self.peer.remote_qpn,
208 self.peer.qkey,
209 );
210 }
211 }
212
213 /// Closes the session, posting every request added to it.
214 ///
215 /// All-or-nothing: on failure the provider rolls the whole session back, so
216 /// no completion arrives for any of its requests.
217 fn post(mut self) -> Result<(), anyhow::Error> {
218 // SAFETY: `self.qpex` is the live builder this open session was started
219 // on.
220 let errno = unsafe { (self.complete)(self.qpex) };
221 // `wr_complete` releases the send-queue lock whether or not it
222 // succeeded, so the session is closed either way and `Drop` must not
223 // abort it. Nothing may be inserted above this line: `Drop` would then
224 // be reachable for a session that is already unlocked.
225 self.completed = true;
226 if errno != 0 {
227 return Err(anyhow::anyhow!(
228 "failed to post work-request session: {}",
229 Error::from_raw_os_error(errno)
230 ));
231 }
232 Ok(())
233 }
234}
235
236impl Drop for WrSession<'_> {
237 fn drop(&mut self) {
238 if self.completed {
239 return;
240 }
241 // Abandoned without posting — an unwind out of the caller, say. Roll the
242 // session back so the send-queue lock is released; leaving it held would
243 // deadlock every later post on this queue pair.
244 // SAFETY: the session is still open (`completed` is false) and
245 // `self.qpex` is the live builder it was started on.
246 unsafe { (self.abort)(self.qpex) };
247 }
248}
249
250fn missing(verb: &str) -> anyhow::Error {
251 anyhow::anyhow!("EFA queue pair is missing the {} extended verb", verb)
252}
253
254/// Transitions `qp` to the state in `attr`, reporting a failure using
255/// the string in `target`.
256///
257/// # Safety
258///
259/// `qp` must be a live `ibv_qp` (non-null).
260unsafe fn modify_qp(
261 qp: *mut rdmaxcel_sys::ibv_qp,
262 attr: &mut rdmaxcel_sys::ibv_qp_attr,
263 mask: rdmaxcel_sys::ibv_qp_attr_mask,
264 target: &str,
265) -> Result<(), anyhow::Error> {
266 // SAFETY: `qp` is a live `ibv_qp` (caller contract); `attr` is a valid
267 // `ibv_qp_attr` whose populated fields match `mask`. `ibv_modify_qp` returns
268 // the errno.
269 let errno = unsafe { rdmaxcel_sys::ibv_modify_qp(qp, attr, mask.0 as i32) };
270 if errno != 0 {
271 return Err(anyhow::anyhow!(
272 "failed to transition EFA queue pair to {}: {}",
273 target,
274 Error::from_raw_os_error(errno)
275 ));
276 }
277 Ok(())
278}
279
280/// Queries the EFA-specific attributes of the device behind `context`.
281///
282/// # Safety
283///
284/// `context` must be a live `ibv_context` belonging to an EFA device.
285unsafe fn query_device(
286 context: *mut rdmaxcel_sys::ibv_context,
287) -> Result<rdmaxcel_sys::efadv_device_attr, anyhow::Error> {
288 let mut attr = rdmaxcel_sys::efadv_device_attr::default();
289 // SAFETY: `context` is a live EFA device context (caller contract); the
290 // out-param is a writable, properly aligned `efadv_device_attr` whose size
291 // we pass as `inlen`. `efadv_query_device` returns the errno.
292 let errno = unsafe {
293 rdmaxcel_sys::efadv_query_device(
294 context,
295 &mut attr,
296 std::mem::size_of::<rdmaxcel_sys::efadv_device_attr>() as u32,
297 )
298 };
299 if errno != 0 {
300 return Err(anyhow::anyhow!(
301 "failed to query EFA device attributes: {}",
302 Error::from_raw_os_error(errno)
303 ));
304 }
305 Ok(attr)
306}
307
308/// The remote endpoint of a connected [`EfaQueuePair`].
309///
310/// EFA SRD is UD-addressed: the destination is not a queue-pair attribute, so
311/// every work request names it explicitly through an address handle, the peer's
312/// queue-pair number, and the queue key.
313#[derive(Debug)]
314struct EfaPeer {
315 ah: IbvAh,
316 remote_qpn: u32,
317 qkey: u32,
318}
319
320/// An EFA SRD queue pair, created through `efadv_create_qp_ex` and driven by the
321/// extended work-request builder reached via `ibv_qp_to_qp_ex`.
322///
323/// SRD is a driver queue-pair type that the device runs through the unreliable
324/// datagram state machine, so this shares almost nothing with the
325/// reliable-connected path: the connection handshake carries a queue key rather
326/// than access flags, and each work request supplies its own destination. Only
327/// endpoint discovery, state queries, and completion polling are common, and
328/// those delegate to the shared helpers in [`super::queue_pair`].
329///
330/// Single-owner: it owns the [`IbvQp`] — which in turn owns its two completion
331/// queues and the protection domain — and destroys them on drop, so the type is
332/// intentionally `!Clone`.
333#[derive(Debug)]
334pub struct EfaQueuePair {
335 qp: IbvQp,
336 /// Declared after `qp` so the queue pair is destroyed first. Every work
337 /// request embeds the address handle's device token, so the handle has to
338 /// outlive any request still referencing it.
339 peer: Option<EfaPeer>,
340 config: IbvConfig,
341 /// The source GID, always table entry 0: EFA is not RoCE, so its
342 /// `gid_attrs/types` never reports "RoCE v2" and its table holds one entry.
343 gid: Gid,
344 /// Largest transfer issued as a single work request, from the device's
345 /// reported `max_rdma_size`.
346 max_msg_size: usize,
347 /// Monotonic work-request id, handed out one per posted WR. The extended
348 /// verbs carry no internal counter, so the queue pair tracks its own.
349 next_wr_id: u64,
350}
351
352impl EfaQueuePair {
353 /// Posts `op` over `total_size` bytes from `laddr` to `raddr` as a single
354 /// work-request session, split into [`Self::max_msg_size`]-bound chunks, and
355 /// returns one work-request id per chunk.
356 ///
357 /// The session is all-or-nothing: on failure `ibv_wr_complete` rolls every
358 /// request in it back, so an `Err` means nothing was posted and no
359 /// completion will arrive for any of the ids.
360 fn post_chunked(
361 &mut self,
362 op: EfaOp,
363 laddr: usize,
364 lkey: u32,
365 raddr: usize,
366 rkey: u32,
367 total_size: usize,
368 ) -> Result<Vec<u64>, anyhow::Error> {
369 if self.peer.is_none() {
370 anyhow::bail!("cannot post on an EfaQueuePair that has not been connected");
371 }
372
373 // Resolve every request before opening the session, so the window where
374 // the send-queue lock is held does no arithmetic and no allocation.
375 // `WrSession` makes an unwind there recoverable.
376 let plan = chunks(total_size, self.max_msg_size, self.next_wr_id);
377 self.next_wr_id += plan.len() as u64;
378 let wrs: Vec<Wr> = plan
379 .iter()
380 .map(|chunk| Wr {
381 op,
382 wr_id: chunk.wr_id,
383 laddr: (laddr + chunk.offset) as u64,
384 lkey,
385 raddr: (raddr + chunk.offset) as u64,
386 rkey,
387 len: chunk.len as u32,
388 })
389 .collect();
390
391 let peer = self.peer.as_ref().expect("checked above");
392 // SAFETY: `self.qp` holds the live queue pair created in `new`, and
393 // `peer`'s address handle is owned by `self` and outlives the session.
394 let mut session = unsafe { WrSession::start(&self.qp, peer) }?;
395 for wr in wrs {
396 session.add(wr);
397 }
398 session.post().map_err(|e| {
399 anyhow::anyhow!("{:?} session of {} work request(s): {e}", op, plan.len())
400 })?;
401 Ok(plan.into_iter().map(|chunk| chunk.wr_id).collect())
402 }
403}
404
405impl IbvQueuePair for EfaQueuePair {
406 unsafe fn new<I: IbvDomainImpl<QueuePair = Self>>(
407 domain: &IbvDomain<I>,
408 config: IbvConfig,
409 send_cq: Arc<IbvCq>,
410 recv_cq: Arc<IbvCq>,
411 ) -> Result<Self, anyhow::Error> {
412 tracing::debug!("creating an EfaQueuePair from config {}", config);
413 // `IbvDomain`'s `pd` accessor permits null (e.g. a test domain); a real
414 // queue pair needs one, so reject null up front. Everything below then
415 // has a live context too, since a PD is only ever allocated against one.
416 let pd = domain.as_ptr();
417 if pd.is_null() {
418 anyhow::bail!("cannot create an EfaQueuePair on a null protection domain");
419 }
420 let context = domain.context().as_ptr();
421
422 // Resolve the source GID up front (before allocating any FFI resources),
423 // so a port without a usable GID fails cleanly here.
424 let gid = domain.device_info().gid_at(config.port_num, 0)?;
425
426 // SAFETY: `context` is the live context the PD above was allocated
427 // against.
428 let device_attr = unsafe { query_device(context) }?;
429 let required = rdmaxcel_sys::EFADV_DEVICE_ATTR_CAPS_RDMA_READ
430 | rdmaxcel_sys::EFADV_DEVICE_ATTR_CAPS_RDMA_WRITE;
431 anyhow::ensure!(
432 device_attr.device_caps & required == required,
433 "EFA device does not support both RDMA read and RDMA write (device_caps: {:#x})",
434 device_attr.device_caps
435 );
436 let max_msg_size = device_attr.max_rdma_size as usize;
437 anyhow::ensure!(
438 max_msg_size > 0,
439 "EFA device reports a maximum RDMA transfer size of 0"
440 );
441
442 // EFA accepts exactly one scatter/gather entry per RDMA work request.
443 // `EfaDevice::apply_config_defaults` already caps these, but the manager
444 // seeds those defaults only when it spawns without an explicit config,
445 // so enforce it here rather than trust the caller.
446 let mut config = config;
447 config.max_send_sge = 1;
448 config.max_recv_sge = 1;
449 // The queue depths must fit the device. Reject rather than clamp: the
450 // owning `QueuePairActor` budgets send-queue credits against the depth in
451 // its own config, so quietly granting fewer would let it over-commit.
452 anyhow::ensure!(
453 config.max_send_wr <= device_attr.max_sq_wr,
454 "configured max_send_wr ({}) exceeds the EFA device's limit ({})",
455 config.max_send_wr,
456 device_attr.max_sq_wr
457 );
458 anyhow::ensure!(
459 config.max_recv_wr <= device_attr.max_rq_wr,
460 "configured max_recv_wr ({}) exceeds the EFA device's limit ({})",
461 config.max_recv_wr,
462 device_attr.max_rq_wr
463 );
464
465 // An SRD queue pair: a driver queue-pair type selected through
466 // `efadv_qp_init_attr`. The send-ops flags both request the RDMA
467 // builders and are what makes `ibv_qp_to_qp_ex` yield a builder at all.
468 let mut init_attr = rdmaxcel_sys::ibv_qp_init_attr_ex {
469 send_cq: send_cq.as_ptr(),
470 recv_cq: recv_cq.as_ptr(),
471 cap: rdmaxcel_sys::ibv_qp_cap {
472 max_send_wr: config.max_send_wr,
473 max_recv_wr: config.max_recv_wr,
474 max_send_sge: config.max_send_sge,
475 max_recv_sge: config.max_recv_sge,
476 max_inline_data: 0,
477 },
478 qp_type: rdmaxcel_sys::ibv_qp_type::IBV_QPT_DRIVER,
479 sq_sig_all: 0,
480 pd,
481 comp_mask: rdmaxcel_sys::IBV_QP_INIT_ATTR_PD
482 | rdmaxcel_sys::IBV_QP_INIT_ATTR_SEND_OPS_FLAGS,
483 send_ops_flags: (rdmaxcel_sys::IBV_QP_EX_WITH_RDMA_WRITE
484 | rdmaxcel_sys::IBV_QP_EX_WITH_RDMA_READ) as u64,
485 ..Default::default()
486 };
487 let mut efa_attr = rdmaxcel_sys::efadv_qp_init_attr {
488 driver_qp_type: rdmaxcel_sys::EFADV_QP_DRIVER_TYPE_SRD,
489 ..Default::default()
490 };
491 // `efadv_create_qp_ex` writes the granted queue depths back into
492 // `init_attr.cap`, hence the mutable borrow.
493 // SAFETY: `context` and `pd` are non-null (checked above) and live; both
494 // attr structs are fully initialized and outlive the call, and
495 // `init_attr`'s CQ pointers came from the freshly created
496 // `send_cq`/`recv_cq`. `efadv_create_qp_ex` returns null on failure.
497 let qp = unsafe {
498 rdmaxcel_sys::efadv_create_qp_ex(
499 context,
500 &mut init_attr,
501 &mut efa_attr,
502 std::mem::size_of::<rdmaxcel_sys::efadv_qp_init_attr>() as u32,
503 )
504 };
505 if qp.is_null() {
506 anyhow::bail!(
507 "failed to create EFA SRD queue pair (QP): {}",
508 Error::last_os_error()
509 );
510 }
511
512 // SAFETY: `qp` is a live SRD QP just created against `pd` with
513 // `send_cq`/`recv_cq`; `IbvQp` holds a clone of each, keeping them alive
514 // for at least as long as the QP it destroys on drop.
515 let qp = unsafe { IbvQp::from_raw(qp, send_cq, recv_cq, domain.pd().clone()) };
516
517 Ok(Self {
518 qp,
519 peer: None,
520 config,
521 gid,
522 max_msg_size,
523 next_wr_id: 0,
524 })
525 }
526
527 fn connect(&mut self, info: &IbvQpInfo) -> Result<(), anyhow::Error> {
528 let Some(dgid) = info.gid else {
529 anyhow::bail!(
530 "EFA addresses peers by GID, but the peer endpoint {:?} carries none",
531 info
532 );
533 };
534
535 // Build the address handle before the transitions. It is a
536 // protection-domain operation that does not depend on queue-pair state,
537 // so failing here leaves the queue pair in RESET rather than in RTS with
538 // no route — a state in which every post would fail.
539 let mut attr = ah_attr(self.config.port_num, dgid, self.gid.index());
540 // SAFETY: the queue pair was created against this PD, which it keeps
541 // alive; `attr` is fully initialized and outlives the call.
542 let ah = unsafe { IbvAh::create(self.qp.pd().clone(), &mut attr) }?;
543
544 // The device runs an SRD queue pair through the unreliable datagram
545 // state machine: INIT carries the queue key in place of access flags,
546 // RTR carries nothing but the state, and RTS only the send-queue packet
547 // sequence number. None of the reliable-connected path attributes apply,
548 // because the destination travels with each work request instead.
549 let qp = self.qp.as_ptr();
550 let mut attr = rdmaxcel_sys::ibv_qp_attr {
551 qp_state: rdmaxcel_sys::ibv_qp_state::IBV_QPS_INIT,
552 qkey: EFA_QKEY,
553 pkey_index: self.config.pkey_index,
554 port_num: self.config.port_num,
555 ..Default::default()
556 };
557 let mask = rdmaxcel_sys::ibv_qp_attr_mask::IBV_QP_STATE
558 | rdmaxcel_sys::ibv_qp_attr_mask::IBV_QP_PKEY_INDEX
559 | rdmaxcel_sys::ibv_qp_attr_mask::IBV_QP_PORT
560 | rdmaxcel_sys::ibv_qp_attr_mask::IBV_QP_QKEY;
561 // SAFETY: `qp` is the live queue pair, kept alive for `self`'s lifetime.
562 unsafe { modify_qp(qp, &mut attr, mask, "INIT") }?;
563
564 let mut attr = rdmaxcel_sys::ibv_qp_attr {
565 qp_state: rdmaxcel_sys::ibv_qp_state::IBV_QPS_RTR,
566 ..Default::default()
567 };
568 let mask = rdmaxcel_sys::ibv_qp_attr_mask::IBV_QP_STATE;
569 // SAFETY: as for the INIT transition above.
570 unsafe { modify_qp(qp, &mut attr, mask, "RTR") }?;
571
572 let mut attr = rdmaxcel_sys::ibv_qp_attr {
573 qp_state: rdmaxcel_sys::ibv_qp_state::IBV_QPS_RTS,
574 sq_psn: self.config.psn,
575 ..Default::default()
576 };
577 let mask = rdmaxcel_sys::ibv_qp_attr_mask::IBV_QP_STATE
578 | rdmaxcel_sys::ibv_qp_attr_mask::IBV_QP_SQ_PSN;
579 // SAFETY: as for the INIT transition above.
580 unsafe { modify_qp(qp, &mut attr, mask, "RTS") }?;
581
582 self.peer = Some(EfaPeer {
583 ah,
584 remote_qpn: info.qp_num,
585 qkey: EFA_QKEY,
586 });
587 tracing::debug!(
588 "EfaQueuePair reached RTS and is routed to {:?} (qp: {:?})",
589 info,
590 qp
591 );
592 Ok(())
593 }
594
595 fn get_qp_info(&mut self) -> Result<IbvQpInfo, anyhow::Error> {
596 let context = self.qp.context().as_ptr();
597 // SAFETY: `self.qp` is the live queue pair and `context` its non-null
598 // device context (both validated in `new`), valid for `self`'s lifetime.
599 unsafe { super::queue_pair::get_qp_info(self.qp.as_ptr(), context, &self.config, self.gid) }
600 }
601
602 fn state(&mut self) -> Result<u32, anyhow::Error> {
603 // SAFETY: `self.qp` is the live queue pair, kept alive for `self`'s
604 // lifetime.
605 unsafe { super::queue_pair::state(self.qp.as_ptr()) }
606 }
607
608 fn max_msg_size(&self) -> usize {
609 self.max_msg_size
610 }
611
612 fn put(
613 &mut self,
614 remote_dst: IbvRemoteMemoryRegionView,
615 local_src: IbvMemoryRegionView,
616 ) -> Result<Vec<u64>, anyhow::Error> {
617 if remote_dst.size < local_src.size {
618 return Err(anyhow::anyhow!(
619 "remote buffer size ({}) is smaller than local buffer size ({})",
620 remote_dst.size,
621 local_src.size
622 ));
623 }
624 self.post_chunked(
625 EfaOp::Write,
626 local_src.rdma_addr,
627 local_src.lkey,
628 remote_dst.addr,
629 remote_dst.rkey,
630 local_src.size,
631 )
632 }
633
634 fn get(
635 &mut self,
636 local_dst: IbvMemoryRegionView,
637 remote_src: IbvRemoteMemoryRegionView,
638 ) -> Result<Vec<u64>, anyhow::Error> {
639 if local_dst.size < remote_src.size {
640 return Err(anyhow::anyhow!(
641 "local buffer size ({}) is smaller than remote buffer size ({})",
642 local_dst.size,
643 remote_src.size
644 ));
645 }
646 self.post_chunked(
647 EfaOp::Read,
648 local_dst.rdma_addr,
649 local_dst.lkey,
650 remote_src.addr,
651 remote_src.rkey,
652 remote_src.size,
653 )
654 }
655}
656
657#[cfg(test)]
658mod tests {
659 use std::net::Ipv6Addr;
660
661 use super::*;
662
663 // A transfer that fits in one work request still gets exactly one, and it
664 // is numbered from the id handed in.
665 #[test]
666 fn chunks_fitting_transfer_yields_one_wr() {
667 assert_eq!(
668 chunks(1024, 4096, 7),
669 vec![Chunk {
670 offset: 0,
671 len: 1024,
672 wr_id: 7
673 }]
674 );
675 }
676
677 // A zero-byte transfer still yields one work request, so the caller always
678 // receives a completion to match against.
679 #[test]
680 fn chunks_zero_byte_transfer_yields_one_wr() {
681 assert_eq!(
682 chunks(0, 4096, 0),
683 vec![Chunk {
684 offset: 0,
685 len: 0,
686 wr_id: 0
687 }]
688 );
689 }
690
691 // An oversized transfer splits into consecutive, contiguous chunks with a
692 // short final one, each carrying the next id.
693 #[test]
694 fn chunks_oversized_transfer_splits_with_short_tail() {
695 let plan = chunks(2500, 1000, 100);
696 assert_eq!(
697 plan,
698 vec![
699 Chunk {
700 offset: 0,
701 len: 1000,
702 wr_id: 100
703 },
704 Chunk {
705 offset: 1000,
706 len: 1000,
707 wr_id: 101
708 },
709 Chunk {
710 offset: 2000,
711 len: 500,
712 wr_id: 102
713 },
714 ]
715 );
716 let total: usize = plan.iter().map(|chunk| chunk.len).sum();
717 assert_eq!(total, 2500, "chunks must cover the transfer exactly");
718 }
719
720 // An exact multiple of the chunk size produces no trailing empty request.
721 #[test]
722 fn chunks_exact_multiple_has_no_empty_tail() {
723 let plan = chunks(2000, 1000, 0);
724 assert_eq!(plan.len(), 2, "2000 bytes at 1000 per WR is two requests");
725 assert!(plan.iter().all(|chunk| chunk.len == 1000));
726 }
727
728 // The address handle carries the peer's GID and the *local* source-GID
729 // index, and leaves the RoCE-only GRH fields zero.
730 #[test]
731 fn ah_attr_carries_peer_gid_and_local_sgid_index() {
732 let peer = Gid::for_test(Ipv6Addr::new(0xfe80, 0, 0, 0, 0, 0, 0, 2), 3);
733 let attr = ah_attr(1, peer, 0);
734
735 assert_eq!(attr.port_num, 1);
736 assert_eq!(attr.is_global, 1, "EFA requires a global address handle");
737 // SAFETY: `raw` is one arm of the `ibv_gid` union and is always
738 // initialized; reading the 16 address bytes back is sound.
739 let dgid = unsafe { attr.grh.dgid.raw };
740 assert_eq!(
741 dgid,
742 Ipv6Addr::new(0xfe80, 0, 0, 0, 0, 0, 0, 2).octets(),
743 "the handle must address the peer's GID"
744 );
745 assert_eq!(
746 attr.grh.sgid_index, 0,
747 "sgid_index names the local GID table entry, not the peer's index 3"
748 );
749 assert_eq!(attr.grh.hop_limit, 0, "hop_limit is RoCE-only");
750 assert_eq!(attr.grh.traffic_class, 0, "traffic_class is RoCE-only");
751 assert_eq!(attr.sl, 0);
752 assert_eq!(attr.dlid, 0, "EFA has no LID");
753 }
754}