Skip to main content

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}