Skip to main content

monarch_rdma/backend/ibverbs/
primitives.rs

1/*
2 * Portions 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/*
10 * Sections of code adapted from
11 * Copyright (c) 2016 Jon Gjengset under MIT License (MIT)
12*/
13
14//! This file contains primitive data structures for interacting with ibverbs.
15//!
16//! Primitives:
17//! - `IbvConfig`: Represents ibverbs specific configurations, holding parameters required to establish and
18//!   manage an RDMA connection, including settings for the RDMA device, queue pair attributes, and other
19//!   connection-specific parameters.
20//! - `IbvDeviceInfo`: Represents an RDMA device, i.e. 'mlx5_0'. Contains information about the device, such as:
21//!   its name, vendor ID, vendor part ID, hardware version, firmware version, node GUID, and capabilities.
22//! - `IbvPort`: Represents information about the port of an RDMA device, including state, physical state,
23//!   LID (Local Identifier), and GID (Global Identifier) information.
24//! - `IbvOperation`: Represents the type of RDMA operation to perform (Read or Write).
25//! - `IbvQpInfo`: Contains connection information needed to establish an RDMA connection with a remote endpoint.
26//! - `IbvWc`: Wrapper around ibverbs work completion structure, used to track the status of RDMA operations.
27use std::collections::BTreeMap;
28use std::ffi::CStr;
29use std::fmt;
30use std::io::Error;
31use std::net::Ipv6Addr;
32use std::sync::Arc;
33use std::sync::OnceLock;
34
35use anyhow::Context;
36use serde::Deserialize;
37use serde::Serialize;
38use typeuri::Named;
39
40use crate::backend::ibverbs::device::IbvDeviceImpl;
41use crate::backend::ibverbs::device::list_all_devices;
42use crate::backend::ibverbs::device_selection::IbvDeviceTarget;
43use crate::backend::ibverbs::device_selection::resolve_target;
44use crate::device_selection::MemoryLocation;
45
46#[derive(
47    Copy,
48    Clone,
49    Debug,
50    Eq,
51    PartialEq,
52    Hash,
53    serde::Serialize,
54    serde::Deserialize
55)]
56// `AsRef`/`AsMut`/`From<Gid>` reinterpret the leading `raw` bytes in place as an
57// `ibv_gid` (which is 8-aligned, holding `__be64`s). `repr(C, align(8))` keeps
58// `raw` first and 8-aligned so that pointer cast is well-defined; without it the
59// derived layout can place `raw` at a misaligned offset, faulting on that read.
60#[repr(C, align(8))]
61pub struct Gid {
62    raw: [u8; 16],
63    /// The GID's index in its port's GID table.
64    index: u8,
65    /// Address scope, classified from `raw` at construction.
66    scope: GidScope,
67    /// RoCE type, from the port's `gid_attrs/types` sysfs entry.
68    gid_type: GidType,
69}
70
71impl Gid {
72    /// Builds a GID from its IPv6 address, RoCE type, and GID-table index,
73    /// classifying the address's scope.
74    fn new(addr: Ipv6Addr, gid_type: GidType, index: u8) -> Self {
75        Self {
76            raw: addr.octets(),
77            index,
78            scope: GidScope::of(addr),
79            gid_type,
80        }
81    }
82
83    /// The GID's index in its port's GID table.
84    pub(crate) fn index(&self) -> u8 {
85        self.index
86    }
87
88    /// The GID's address scope.
89    fn scope(&self) -> GidScope {
90        self.scope
91    }
92
93    /// The GID's RoCE type.
94    fn gid_type(&self) -> GidType {
95        self.gid_type
96    }
97
98    #[allow(dead_code)]
99    fn subnet_prefix(&self) -> u64 {
100        u64::from_be_bytes(self.raw[..8].try_into().unwrap())
101    }
102
103    #[allow(dead_code)]
104    fn interface_id(&self) -> u64 {
105        u64::from_be_bytes(self.raw[8..].try_into().unwrap())
106    }
107}
108
109impl From<Gid> for rdmaxcel_sys::ibv_gid {
110    fn from(mut gid: Gid) -> Self {
111        *gid.as_mut()
112    }
113}
114
115impl AsRef<rdmaxcel_sys::ibv_gid> for Gid {
116    fn as_ref(&self) -> &rdmaxcel_sys::ibv_gid {
117        unsafe { &*self.raw.as_ptr().cast::<rdmaxcel_sys::ibv_gid>() }
118    }
119}
120
121impl AsMut<rdmaxcel_sys::ibv_gid> for Gid {
122    fn as_mut(&mut self) -> &mut rdmaxcel_sys::ibv_gid {
123        unsafe { &mut *self.raw.as_mut_ptr().cast::<rdmaxcel_sys::ibv_gid>() }
124    }
125}
126
127impl fmt::Display for Gid {
128    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
129        f.write_str(&format_gid(&self.raw))
130    }
131}
132
133/// Scope of an IPv6-form GID. RoCE encodes the source GID as an IPv6 address, so
134/// the usual IPv6 scopes apply.
135///
136/// - [`Loopback`](GidScope::Loopback): `::1`, or IPv4-mapped loopback
137///   (`::ffff:127.0.0.0/8`).
138/// - [`LinkLocal`](GidScope::LinkLocal): `fe80::/10` (the default GID prefix), or
139///   IPv4-mapped link-local (`::ffff:169.254.0.0/16`).
140/// - [`SiteLocal`](GidScope::SiteLocal): `fec0::/10`, deprecated by RFC 3879.
141/// - [`Global`](GidScope::Global): everything else, including globally-routable
142///   IPv4-mapped, global, and ULA IPv6.
143#[derive(
144    Debug,
145    Clone,
146    Copy,
147    PartialEq,
148    Eq,
149    Hash,
150    serde::Serialize,
151    serde::Deserialize
152)]
153pub(crate) enum GidScope {
154    Loopback,
155    LinkLocal,
156    SiteLocal,
157    Global,
158}
159
160impl GidScope {
161    /// Classifies an IPv6 address by scope. IPv4-mapped addresses
162    /// (`::ffff:a.b.c.d`) are classified by their embedded IPv4 address, so an
163    /// IPv4 loopback/link-local GID is not mistaken for a global one.
164    fn of(addr: Ipv6Addr) -> Self {
165        if addr.is_loopback() {
166            return GidScope::Loopback;
167        }
168        if let Some(v4) = addr.to_ipv4_mapped() {
169            return if v4.is_loopback() {
170                GidScope::Loopback
171            } else if v4.is_link_local() {
172                GidScope::LinkLocal
173            } else {
174                GidScope::Global
175            };
176        }
177        let [a, b, ..] = addr.octets();
178        if a == 0xfe && b & 0xc0 == 0x80 {
179            GidScope::LinkLocal
180        } else if a == 0xfe && b & 0xc0 == 0xc0 {
181            GidScope::SiteLocal
182        } else {
183            GidScope::Global
184        }
185    }
186}
187
188/// RoCE type of a GID, from its `gid_attrs/types` sysfs entry.
189#[derive(
190    Debug,
191    Clone,
192    Copy,
193    PartialEq,
194    Eq,
195    Hash,
196    serde::Serialize,
197    serde::Deserialize
198)]
199pub(crate) enum GidType {
200    /// `IB/RoCE v1`.
201    RoCEv1,
202    /// `RoCE v2`.
203    RoCEv2,
204    /// Any other or unrecognized type.
205    Unknown,
206}
207
208impl GidType {
209    /// Classifies a `gid_attrs/types` sysfs value.
210    fn of(gid_type: &str) -> Self {
211        match gid_type.trim() {
212            "RoCE v2" => GidType::RoCEv2,
213            "IB/RoCE v1" => GidType::RoCEv1,
214            _ => GidType::Unknown,
215        }
216    }
217}
218
219/// Queue pair type for RDMA operations.
220///
221/// Controls whether to use standard ibverbs queue pairs, mlx5dv extended queue pairs,
222/// or EFA SRD queue pairs. Auto mode automatically selects based on device capabilities.
223#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
224pub enum IbvQpType {
225    /// Auto-detect based on device capabilities
226    Auto,
227    /// Force standard ibverbs queue pair
228    Standard,
229    /// Force mlx5dv extended queue pair
230    Mlx5dv,
231    /// Force EFA SRD queue pair
232    Efa,
233}
234
235/// Converts `IbvQpType` to the corresponding integer enum value in rdmaxcel_sys.
236pub fn resolve_qp_type(qp_type: IbvQpType) -> u32 {
237    match qp_type {
238        IbvQpType::Auto => {
239            if crate::efa::is_efa_device() {
240                rdmaxcel_sys::RDMA_QP_TYPE_EFA
241            } else if mlx5dv_supported() {
242                rdmaxcel_sys::RDMA_QP_TYPE_MLX5DV
243            } else {
244                rdmaxcel_sys::RDMA_QP_TYPE_STANDARD
245            }
246        }
247        IbvQpType::Standard => rdmaxcel_sys::RDMA_QP_TYPE_STANDARD,
248        IbvQpType::Mlx5dv => rdmaxcel_sys::RDMA_QP_TYPE_MLX5DV,
249        IbvQpType::Efa => rdmaxcel_sys::RDMA_QP_TYPE_EFA,
250    }
251}
252
253/// Represents ibverbs specific configurations.
254///
255/// This struct holds various parameters required to establish and manage an RDMA connection.
256/// It includes settings for the RDMA device, queue pair attributes, and other connection-specific
257/// parameters.
258#[derive(Debug, Named, Clone, Serialize, Deserialize)]
259pub struct IbvConfig {
260    /// `target` - An explicit RDMA device target, resolved to a concrete
261    /// device via [`resolve_target`]. When `None`, the consumer picks the
262    /// device itself (the co-located NIC for GPU memory, or a hash-assigned
263    /// NIC for host memory).
264    pub target: Option<IbvDeviceTarget>,
265    /// `cq_entries` - The number of completion queue entries.
266    pub cq_entries: i32,
267    /// `port_num` - The physical port number on the device.
268    pub port_num: u8,
269    /// `max_send_wr` - The maximum number of outstanding send work requests.
270    pub max_send_wr: u32,
271    /// `max_recv_wr` - The maximum number of outstanding receive work requests.
272    pub max_recv_wr: u32,
273    /// `max_send_sge` - Te maximum number of scatter/gather elements in a send work request.
274    pub max_send_sge: u32,
275    /// `max_recv_sge` - The maximum number of scatter/gather elements in a receive work request.
276    pub max_recv_sge: u32,
277    /// `path_mtu` - The path MTU (Maximum Transmission Unit) for the connection.
278    pub path_mtu: u32,
279    /// `retry_cnt` - The number of retry attempts for a connection request.
280    pub retry_cnt: u8,
281    /// `rnr_retry` - The number of retry attempts for a receiver not ready (RNR) condition.
282    pub rnr_retry: u8,
283    /// `qp_timeout` - The timeout for a queue pair operation.
284    pub qp_timeout: u8,
285    /// `min_rnr_timer` - The minimum RNR timer value.
286    pub min_rnr_timer: u8,
287    /// `max_dest_rd_atomic` - The maximum number of outstanding RDMA read operations at the destination.
288    pub max_dest_rd_atomic: u8,
289    /// `max_rd_atomic` - The maximum number of outstanding RDMA read operations at the initiator.
290    pub max_rd_atomic: u8,
291    /// `pkey_index` - The partition key index.
292    pub pkey_index: u16,
293    /// `psn` - The packet sequence number.
294    pub psn: u32,
295    /// `use_gpu_direct` - Whether to enable GPU Direct RDMA support on init.
296    pub use_gpu_direct: bool,
297    /// `hw_init_delay_ms` - The delay in milliseconds before initializing the hardware.
298    /// This is used to allow the hardware to settle before starting the first transmission.
299    pub hw_init_delay_ms: u64,
300    /// `qp_type` - The type of queue pair to create (Auto, Standard, or Mlx5dv).
301    pub qp_type: IbvQpType,
302    /// Test-only override for `register_segments`'s `max_sge`. `<= 0`
303    /// (default) uses `ibv_query_device`; small positive values force
304    /// `RDMAXCEL_MKEY_REG_LIMIT` to exercise the dmabuf fallback.
305    pub max_sge_override: i32,
306}
307wirevalue::register_type!(IbvConfig);
308
309/// rdma-core defaults below come from common rdma-core examples; tune for
310/// production based on `ibv_query_device()` results and workload
311/// characteristics. The default target is `None`, leaving device selection to
312/// the consumer.
313impl Default for IbvConfig {
314    fn default() -> Self {
315        Self {
316            target: None,
317            cq_entries: 1024,
318            port_num: 1,
319            max_send_wr: 512,
320            max_recv_wr: 512,
321            max_send_sge: 30,
322            max_recv_sge: 30,
323            path_mtu: rdmaxcel_sys::IBV_MTU_4096,
324            retry_cnt: 7,
325            rnr_retry: 7,
326            qp_timeout: 14, // 4.096 μs * 2^14 = ~67 ms
327            min_rnr_timer: 12,
328            max_dest_rd_atomic: 16,
329            max_rd_atomic: 16,
330            pkey_index: 0,
331            psn: rand::random::<u32>() & 0xffffff,
332            use_gpu_direct: false, // nv_peermem enabled for cuda
333            hw_init_delay_ms: 2,
334            qp_type: IbvQpType::Auto,
335            max_sge_override: 0,
336        }
337    }
338}
339
340impl IbvConfig {
341    /// An [`IbvConfig`] with default parameters whose device
342    /// [`target`](Self::target) is `target` (see [`IbvDeviceTarget`]).
343    pub fn targeting(target: IbvDeviceTarget) -> Self {
344        Self {
345            target: Some(target),
346            ..Default::default()
347        }
348    }
349}
350
351impl std::fmt::Display for IbvConfig {
352    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
353        write!(
354            f,
355            "IbvConfig {{ target: {:?}, port_num: {}, max_send_wr: {}, max_recv_wr: {}, max_send_sge: {}, max_recv_sge: {}, path_mtu: {:?}, retry_cnt: {}, rnr_retry: {}, qp_timeout: {}, min_rnr_timer: {}, max_dest_rd_atomic: {}, max_rd_atomic: {}, pkey_index: {}, psn: 0x{:x} }}",
356            self.target,
357            self.port_num,
358            self.max_send_wr,
359            self.max_recv_wr,
360            self.max_send_sge,
361            self.max_recv_sge,
362            self.path_mtu,
363            self.retry_cnt,
364            self.rnr_retry,
365            self.qp_timeout,
366            self.min_rnr_timer,
367            self.max_dest_rd_atomic,
368            self.max_rd_atomic,
369            self.pkey_index,
370            self.psn,
371        )
372    }
373}
374
375/// Represents an RDMA device in the system.
376///
377/// This struct encapsulates information about an RDMA device, including its hardware
378/// characteristics, capabilities, and port information. It provides access to device
379/// attributes such as vendor information, firmware version, and supported features.
380///
381/// # Examples
382///
383/// ```
384/// use monarch_rdma::backend::ibverbs::device::list_all_devices;
385///
386/// let devices = list_all_devices();
387/// if let Some(device) = devices.first() {
388///     // Access device name and firmware version
389///     let device_name = device.name();
390///     let firmware_version = device.fw_ver();
391/// }
392/// ```
393#[derive(Debug, Clone, Serialize, Deserialize)]
394pub struct IbvDeviceInfo {
395    /// `name` - The name of the RDMA device (e.g., "mlx5_0").
396    pub name: String,
397    /// `vendor_id` - The vendor ID of the device.
398    vendor_id: u32,
399    /// `vendor_part_id` - The vendor part ID of the device.
400    vendor_part_id: u32,
401    /// `hw_ver` - Hardware version of the device.
402    hw_ver: u32,
403    /// `fw_ver` - Firmware version of the device.
404    fw_ver: String,
405    /// `node_guid` - Node GUID (Globally Unique Identifier) of the device.
406    node_guid: u64,
407    /// `ports` - Vector of ports available on this device.
408    ports: Vec<IbvPort>,
409    /// `max_qp` - Maximum number of queue pairs supported.
410    max_qp: i32,
411    /// `max_cq` - Maximum number of completion queues supported.
412    max_cq: i32,
413    /// `max_mr` - Maximum number of memory regions supported.
414    max_mr: i32,
415    /// `max_pd` - Maximum number of protection domains supported.
416    max_pd: i32,
417    /// `max_qp_wr` - Maximum number of work requests per queue pair.
418    max_qp_wr: i32,
419    /// `max_sge` - Maximum number of scatter/gather elements per work request.
420    max_sge: i32,
421}
422
423impl IbvDeviceInfo {
424    /// Returns the name of the RDMA device.
425    pub fn name(&self) -> &String {
426        &self.name
427    }
428
429    /// Returns the first available RDMA device, if any.
430    pub fn first_available() -> Option<IbvDeviceInfo> {
431        list_all_devices().into_iter().next()
432    }
433
434    /// Returns the vendor ID of the RDMA device.
435    pub fn vendor_id(&self) -> u32 {
436        self.vendor_id
437    }
438
439    /// Returns the vendor part ID of the RDMA device.
440    pub fn vendor_part_id(&self) -> u32 {
441        self.vendor_part_id
442    }
443
444    /// Returns the hardware version of the RDMA device.
445    pub fn hw_ver(&self) -> u32 {
446        self.hw_ver
447    }
448
449    /// Returns the firmware version of the RDMA device.
450    pub fn fw_ver(&self) -> &String {
451        &self.fw_ver
452    }
453
454    /// Returns the node GUID of the RDMA device.
455    pub fn node_guid(&self) -> u64 {
456        self.node_guid
457    }
458
459    /// Returns a reference to the vector of ports available on the RDMA device.
460    pub fn ports(&self) -> &Vec<IbvPort> {
461        &self.ports
462    }
463
464    /// Returns the port with the given `port_num`, if present.
465    pub fn port(&self, port_num: u8) -> Option<&IbvPort> {
466        self.ports.iter().find(|port| port.port_num == port_num)
467    }
468
469    /// The lowest-indexed GID on `port_num` matching `scope` (if `Some`) and
470    /// `gid_type` (if `Some`); a `None` filter matches any value. Errors if the
471    /// device has no such port or no GID matches.
472    pub(crate) fn select_gid(
473        &self,
474        port_num: u8,
475        scope: Option<GidScope>,
476        gid_type: Option<GidType>,
477    ) -> Result<Gid, anyhow::Error> {
478        let port = self
479            .port(port_num)
480            .ok_or_else(|| anyhow::anyhow!("device {} has no port {}", self.name, port_num))?;
481        port.gids
482            .values()
483            .find(|gid| {
484                scope.is_none_or(|s| gid.scope() == s)
485                    && gid_type.is_none_or(|t| gid.gid_type() == t)
486            })
487            .copied()
488            .ok_or_else(|| {
489                anyhow::anyhow!(
490                    "device {} port {} has no GID with scope {:?} and type {:?}",
491                    self.name,
492                    port_num,
493                    scope,
494                    gid_type
495                )
496            })
497    }
498
499    /// The GID at `index` on `port_num`. Errors if the device has no such port
500    /// or the port has no GID at that index.
501    pub(crate) fn gid_at(&self, port_num: u8, index: u8) -> Result<Gid, anyhow::Error> {
502        let port = self
503            .port(port_num)
504            .ok_or_else(|| anyhow::anyhow!("device {} has no port {}", self.name, port_num))?;
505        port.gids.get(&index).copied().ok_or_else(|| {
506            anyhow::anyhow!(
507                "device {} port {} has no GID at index {}",
508                self.name,
509                port_num,
510                index
511            )
512        })
513    }
514
515    /// Aggregate bandwidth (MB/s) of the device's fastest active port,
516    /// derived from its IB `active_speed` / `active_width`. 0 if no port
517    /// is active, which ranks the device at the worst case.
518    pub fn port_speed_mbytes_per_sec(&self) -> u32 {
519        self.ports
520            .iter()
521            .filter(|port| port.state == rdmaxcel_sys::ibv_port_state::IBV_PORT_ACTIVE)
522            .map(|port| {
523                ib_width_lanes(port.active_width) * ib_speed_mbits_per_lane(port.active_speed) / 8
524            })
525            .max()
526            .unwrap_or(0)
527    }
528
529    /// Returns the maximum number of queue pairs supported by the RDMA device.
530    pub fn max_qp(&self) -> i32 {
531        self.max_qp
532    }
533
534    /// Returns the maximum number of completion queues supported by the RDMA device.
535    pub fn max_cq(&self) -> i32 {
536        self.max_cq
537    }
538
539    /// Returns the maximum number of memory regions supported by the RDMA device.
540    pub fn max_mr(&self) -> i32 {
541        self.max_mr
542    }
543
544    /// Returns the maximum number of protection domains supported by the RDMA device.
545    pub fn max_pd(&self) -> i32 {
546        self.max_pd
547    }
548
549    /// Returns the maximum number of work requests per queue pair supported by the RDMA device.
550    pub fn max_qp_wr(&self) -> i32 {
551        self.max_qp_wr
552    }
553
554    /// Returns the maximum number of scatter/gather elements per work request supported by the RDMA device.
555    pub fn max_sge(&self) -> i32 {
556        self.max_sge
557    }
558}
559
560impl IbvDeviceInfo {
561    /// The optimal default device of backend `I`: the best NIC for CPU
562    /// memory on any NUMA node. Panics if `I` has no devices.
563    #[expect(
564        clippy::should_implement_trait,
565        reason = "generic over the backend impl, so it cannot be the parameterless Default::default"
566    )]
567    pub fn default<I: IbvDeviceImpl>() -> Self {
568        resolve_target::<I>(&IbvDeviceTarget::MemoryLocation(MemoryLocation::Cpu(None)))
569            .unwrap_or_else(|| panic!("no RDMA device for backend {}", I::backend_name()))
570    }
571
572    /// Construct an [`IbvDeviceInfo`] with only `name` set (all other
573    /// fields zeroed/empty), for tests that need a named device without
574    /// touching hardware.
575    #[cfg(test)]
576    pub(crate) fn for_test_named(name: &str) -> Self {
577        Self {
578            name: name.to_string(),
579            vendor_id: 0,
580            vendor_part_id: 0,
581            hw_ver: 0,
582            fw_ver: String::new(),
583            node_guid: 0,
584            ports: Vec::new(),
585            max_qp: 0,
586            max_cq: 0,
587            max_mr: 0,
588            max_pd: 0,
589            max_qp_wr: 0,
590            max_sge: 0,
591        }
592    }
593}
594
595#[derive(Debug, Clone, Serialize, Deserialize)]
596pub struct IbvPort {
597    /// `port_num` - The physical port number on the device.
598    port_num: u8,
599    /// `state` - The raw `ibv_port_state` of the port.
600    state: rdmaxcel_sys::ibv_port_state::Type,
601    /// `physical_state` - The physical state of the port.
602    physical_state: String,
603    /// `base_lid` - Base Local Identifier for the port.
604    base_lid: u16,
605    /// `lmc` - LID Mask Control.
606    lmc: u8,
607    /// `sm_lid` - Subnet Manager Local Identifier.
608    sm_lid: u16,
609    /// `capability_mask` - Capability mask of the port.
610    capability_mask: u32,
611    /// `link_layer` - The link layer type (e.g., InfiniBand, Ethernet).
612    link_layer: String,
613    /// `gids` - The port's populated GID-table entries, keyed by table index
614    /// (empty slots omitted).
615    gids: BTreeMap<u8, Gid>,
616    /// `gid_tbl_len` - Length of the GID table.
617    gid_tbl_len: i32,
618    /// `active_speed` - IB active speed bitmask (one bit set).
619    active_speed: u8,
620    /// `active_width` - IB active width bitmask (one bit set).
621    active_width: u8,
622}
623
624impl fmt::Display for IbvDeviceInfo {
625    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
626        writeln!(f, "{}", self.name)?;
627        writeln!(f, "\tNumber of ports: {}", self.ports.len())?;
628        writeln!(f, "\tFirmware version: {}", self.fw_ver)?;
629        writeln!(f, "\tHardware version: {}", self.hw_ver)?;
630        writeln!(f, "\tNode GUID: 0x{:016x}", self.node_guid)?;
631        writeln!(f, "\tVendor ID: 0x{:x}", self.vendor_id)?;
632        writeln!(f, "\tVendor part ID: {}", self.vendor_part_id)?;
633        writeln!(f, "\tMax QPs: {}", self.max_qp)?;
634        writeln!(f, "\tMax CQs: {}", self.max_cq)?;
635        writeln!(f, "\tMax MRs: {}", self.max_mr)?;
636        writeln!(f, "\tMax PDs: {}", self.max_pd)?;
637        writeln!(f, "\tMax QP WRs: {}", self.max_qp_wr)?;
638        writeln!(f, "\tMax SGE: {}", self.max_sge)?;
639
640        for port in &self.ports {
641            write!(f, "{}", port)?;
642        }
643
644        Ok(())
645    }
646}
647
648impl IbvPort {
649    /// The port's populated GID-table entries, keyed by table index.
650    pub fn gids(&self) -> &BTreeMap<u8, Gid> {
651        &self.gids
652    }
653}
654
655impl fmt::Display for IbvPort {
656    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
657        writeln!(f, "\tPort {}:", self.port_num)?;
658        writeln!(f, "\t\tState: {}", get_port_state_str(self.state))?;
659        writeln!(f, "\t\tPhysical state: {}", self.physical_state)?;
660        writeln!(f, "\t\tBase lid: {}", self.base_lid)?;
661        writeln!(f, "\t\tLMC: {}", self.lmc)?;
662        writeln!(f, "\t\tSM lid: {}", self.sm_lid)?;
663        writeln!(f, "\t\tCapability mask: 0x{:08x}", self.capability_mask)?;
664        writeln!(f, "\t\tLink layer: {}", self.link_layer)?;
665        writeln!(f, "\t\tGID table length: {}", self.gid_tbl_len)?;
666        for (index, gid) in &self.gids {
667            writeln!(
668                f,
669                "\t\tGID[{}]: {} ({:?}, {:?})",
670                index,
671                gid,
672                gid.scope(),
673                gid.gid_type()
674            )?;
675        }
676        Ok(())
677    }
678}
679
680/// Per-lane IB bandwidth (Mbit/s) for an `active_speed` bitmask,
681/// indexed by its lowest set bit (SDR, DDR, QDR, QDR, FDR, EDR, HDR, NDR).
682/// Values match NCCL's `ibvSpeeds` (`transport/net_ib/init.cc`). The
683/// `active_speed` field is a `u8`, so NCCL's 9th rate (XDR, bit 8) is not
684/// representable here; an unset value yields 0.
685fn ib_speed_mbits_per_lane(active_speed: u8) -> u32 {
686    const RATES: [u32; 8] = [2500, 5000, 10000, 10000, 14000, 25000, 50000, 100000];
687    first_set_bit(active_speed)
688        .and_then(|bit| RATES.get(bit).copied())
689        .unwrap_or(0)
690}
691
692/// IB link width in lanes for an `active_width` bitmask, indexed by its
693/// lowest set bit (1x, 4x, 8x, 12x, 2x); values match NCCL's `ibvWidths`
694/// (`transport/net_ib/init.cc`). An unset value yields 0.
695fn ib_width_lanes(active_width: u8) -> u32 {
696    const WIDTHS: [u32; 5] = [1, 4, 8, 12, 2];
697    first_set_bit(active_width)
698        .and_then(|bit| WIDTHS.get(bit).copied())
699        .unwrap_or(0)
700}
701
702/// Index of the lowest set bit, or `None` if `v` is 0.
703fn first_set_bit(v: u8) -> Option<usize> {
704    (v != 0).then(|| v.trailing_zeros() as usize)
705}
706
707/// Converts the given port state to a human-readable string.
708///
709/// # Arguments
710///
711/// * `state` - The port state as defined by `ffi::ibv_port_state::Type`.
712///
713/// # Returns
714///
715/// A string representation of the port state.
716pub fn get_port_state_str(state: rdmaxcel_sys::ibv_port_state::Type) -> String {
717    // SAFETY: We are calling a C function that returns a C string.
718    unsafe {
719        let c_str = rdmaxcel_sys::ibv_port_state_str(state);
720        if c_str.is_null() {
721            return "Unknown".to_string();
722        }
723        CStr::from_ptr(c_str).to_string_lossy().into_owned()
724    }
725}
726
727/// Converts the given physical state to a human-readable string.
728///
729/// # Arguments
730///
731/// * `phys_state` - The physical state as a `u8`.
732///
733/// # Returns
734///
735/// A string representation of the physical state.
736pub fn get_port_phy_state_str(phys_state: u8) -> String {
737    match phys_state {
738        1 => "Sleep".to_string(),
739        2 => "Polling".to_string(),
740        3 => "Disabled".to_string(),
741        4 => "PortConfigurationTraining".to_string(),
742        5 => "LinkUp".to_string(),
743        6 => "LinkErrorRecovery".to_string(),
744        7 => "PhyTest".to_string(),
745        _ => "No state change".to_string(),
746    }
747}
748
749/// Converts the given link layer type to a human-readable string.
750///
751/// # Arguments
752///
753/// * `link_layer` - The link layer type as a `u8`.
754///
755/// # Returns
756///
757/// A string representation of the link layer type.
758pub fn get_link_layer_str(link_layer: u8) -> String {
759    match link_layer {
760        1 => "InfiniBand".to_string(),
761        2 => "Ethernet".to_string(),
762        _ => "Unknown".to_string(),
763    }
764}
765
766/// Formats a GID (Global Identifier) into a human-readable string.
767///
768/// # Arguments
769///
770/// * `gid` - A reference to a 16-byte array representing the GID.
771///
772/// # Returns
773///
774/// A formatted string representation of the GID.
775pub fn format_gid(gid: &[u8; 16]) -> String {
776    format!(
777        "{:02x}{:02x}:{:02x}{:02x}:{:02x}{:02x}:{:02x}{:02x}:{:02x}{:02x}:{:02x}{:02x}:{:02x}{:02x}:{:02x}{:02x}",
778        gid[0],
779        gid[1],
780        gid[2],
781        gid[3],
782        gid[4],
783        gid[5],
784        gid[6],
785        gid[7],
786        gid[8],
787        gid[9],
788        gid[10],
789        gid[11],
790        gid[12],
791        gid[13],
792        gid[14],
793        gid[15]
794    )
795}
796
797/// Reads the populated GID-table entries under
798/// `/sys/class/infiniband/{device}/ports/{port}/gids/`, keyed by table index.
799/// Scans indices `0..gid_tbl_len` (capped at the `u8` GID-index range). Empty
800/// slots read as the all-zero GID and have no readable `gid_attrs/types`
801/// attribute, so they are skipped before that file is touched. Errors if a
802/// populated entry's GID or type sysfs file cannot be read.
803fn read_port_gids(
804    device: &str,
805    port: u8,
806    gid_tbl_len: i32,
807) -> Result<BTreeMap<u8, Gid>, anyhow::Error> {
808    let mut gids = BTreeMap::new();
809    for index in 0..gid_tbl_len.min(u8::MAX as i32 + 1) {
810        let gid_path = format!("/sys/class/infiniband/{device}/ports/{port}/gids/{index}");
811        let gid_str =
812            std::fs::read_to_string(&gid_path).with_context(|| format!("reading {gid_path}"))?;
813        // Skip empty (all-zero) or malformed slots before reading the type
814        // attribute, which the kernel leaves unreadable for empty slots.
815        let Ok(addr) = gid_str.trim().parse::<Ipv6Addr>() else {
816            continue;
817        };
818        if addr.is_unspecified() {
819            continue;
820        }
821        let type_path =
822            format!("/sys/class/infiniband/{device}/ports/{port}/gid_attrs/types/{index}");
823        let type_str =
824            std::fs::read_to_string(&type_path).with_context(|| format!("reading {type_path}"))?;
825        let index = index as u8;
826        gids.insert(index, Gid::new(addr, GidType::of(&type_str), index));
827    }
828    Ok(gids)
829}
830
831/// Builds an [`IbvDeviceInfo`] from an already-open `ibv_context`. Errors if
832/// `ibv_query_device` or any GID sysfs read fails.
833///
834/// # Safety
835///
836/// `device` and `context` must both be non-null and valid for
837/// the duration of the call; `context` must be the result of
838/// `ibv_open_device(device)`.
839pub(super) unsafe fn query_device_info(
840    device: *mut rdmaxcel_sys::ibv_device,
841    context: *mut rdmaxcel_sys::ibv_context,
842) -> Result<IbvDeviceInfo, anyhow::Error> {
843    // SAFETY: `device` is non-null per the caller's contract;
844    // `ibv_get_device_name` returns a null-terminated C string
845    // owned by the device list.
846    let device_name = unsafe { CStr::from_ptr(rdmaxcel_sys::ibv_get_device_name(device)) }
847        .to_string_lossy()
848        .into_owned();
849    let mut device_attr = rdmaxcel_sys::ibv_device_attr::default();
850    // SAFETY: `context` is a non-null context per the caller's
851    // contract; `&mut device_attr` is a writable, properly
852    // aligned `ibv_device_attr`.
853    let rc = unsafe { rdmaxcel_sys::ibv_query_device(context, &mut device_attr) };
854    if rc != 0 {
855        anyhow::bail!("ibv_query_device failed for device {device_name}: {rc}");
856    }
857    // SAFETY: `device_attr.fw_ver` is a null-terminated C buffer
858    // populated by `ibv_query_device`.
859    let fw_ver = unsafe { CStr::from_ptr(device_attr.fw_ver.as_ptr()) }
860        .to_string_lossy()
861        .into_owned();
862    let mut info = IbvDeviceInfo {
863        name: device_name,
864        vendor_id: device_attr.vendor_id,
865        vendor_part_id: device_attr.vendor_part_id,
866        hw_ver: device_attr.hw_ver,
867        fw_ver,
868        node_guid: device_attr.node_guid,
869        ports: Vec::new(),
870        max_qp: device_attr.max_qp,
871        max_cq: device_attr.max_cq,
872        max_mr: device_attr.max_mr,
873        max_pd: device_attr.max_pd,
874        max_qp_wr: device_attr.max_qp_wr,
875        max_sge: device_attr.max_sge,
876    };
877    for port_num in 1..=device_attr.phys_port_cnt {
878        let mut port_attr = rdmaxcel_sys::ibv_port_attr::default();
879        // SAFETY: `context` is a valid context; `port_attr` is
880        // a writable, properly aligned `ibv_port_attr`.
881        if unsafe {
882            rdmaxcel_sys::ibv_query_port(
883                context,
884                port_num,
885                &mut port_attr as *mut rdmaxcel_sys::ibv_port_attr as *mut _,
886            )
887        } != 0
888        {
889            continue;
890        }
891        let physical_state = get_port_phy_state_str(port_attr.phys_state);
892        let link_layer = get_link_layer_str(port_attr.link_layer);
893        let gids = read_port_gids(&info.name, port_num, port_attr.gid_tbl_len)?;
894        info.ports.push(IbvPort {
895            port_num,
896            state: port_attr.state,
897            physical_state,
898            base_lid: port_attr.lid,
899            lmc: port_attr.lmc,
900            sm_lid: port_attr.sm_lid,
901            capability_mask: port_attr.port_cap_flags,
902            link_layer,
903            gids,
904            gid_tbl_len: port_attr.gid_tbl_len,
905            active_speed: port_attr.active_speed,
906            active_width: port_attr.active_width,
907        });
908    }
909    Ok(info)
910}
911
912/// Cached result of mlx5dv support check.
913static MLX5DV_SUPPORTED_CACHE: OnceLock<bool> = OnceLock::new();
914
915/// Checks if mlx5dv (Mellanox device-specific verbs extension) is supported.
916///
917/// This function attempts to open the first available RDMA device and check if
918/// mlx5dv extensions can be initialized. The mlx5dv extensions are required for
919/// advanced features like GPU Direct RDMA and direct queue pair manipulation.
920///
921/// The result is cached after the first call, making subsequent calls essentially free.
922///
923/// # Returns
924///
925/// `true` if mlx5dv extensions are supported, `false` otherwise.
926pub fn mlx5dv_supported() -> bool {
927    *MLX5DV_SUPPORTED_CACHE.get_or_init(mlx5dv_supported_impl)
928}
929
930fn mlx5dv_supported_impl() -> bool {
931    // SAFETY: We are calling C functions from libibverbs and libmlx5.
932    unsafe {
933        let mut mlx5dv_supported = false;
934        let mut num_devices = 0;
935        let device_list = rdmaxcel_sys::ibv_get_device_list(&mut num_devices);
936        if !device_list.is_null() && num_devices > 0 {
937            let device = *device_list;
938            if !device.is_null() {
939                mlx5dv_supported = rdmaxcel_sys::mlx5dv_is_supported(device);
940            }
941            rdmaxcel_sys::ibv_free_device_list(device_list);
942        }
943        mlx5dv_supported
944    }
945}
946
947/// Cached result of ibverbs support check.
948static IBVERBS_SUPPORTED_CACHE: OnceLock<bool> = OnceLock::new();
949
950/// Checks if ibverbs devices can be retrieved successfully.
951///
952/// This function attempts to retrieve the list of RDMA devices using the
953/// `ibv_get_device_list` function from the ibverbs library. It returns `true`
954/// if devices are found, and `false` otherwise.
955///
956/// The result is cached after the first call, making subsequent calls essentially free.
957///
958/// # Returns
959///
960/// `true` if devices are successfully retrieved, `false` otherwise.
961pub fn ibverbs_supported() -> bool {
962    *IBVERBS_SUPPORTED_CACHE.get_or_init(ibverbs_supported_impl)
963}
964
965fn ibverbs_supported_impl() -> bool {
966    // SAFETY: We are calling a C function from libibverbs.
967    unsafe {
968        let mut num_devices = 0;
969        let device_list = rdmaxcel_sys::ibv_get_device_list(&mut num_devices);
970        if !device_list.is_null() {
971            rdmaxcel_sys::ibv_free_device_list(device_list);
972        }
973        num_devices > 0
974    }
975}
976
977/// Enum representing the common RDMA operations.
978///
979/// This provides a more ergonomic interface to the underlying ibv_wr_opcode types.
980/// RDMA operations allow for direct memory access between two machines without
981/// involving the CPU of the target machine.
982///
983/// # Variants
984///
985/// * `Write` - Represents an RDMA write operation where data is written from the local
986///   memory to a remote memory region.
987/// * `Read` - Represents an RDMA read operation where data is read from a remote memory
988///   region into the local memory.
989#[derive(Debug, Clone, Copy, PartialEq, Eq)]
990pub enum IbvOperation {
991    /// RDMA write operations
992    Write,
993    WriteWithImm,
994    /// RDMA read operation
995    Read,
996    /// RDMA recv operation
997    Recv,
998}
999
1000impl From<IbvOperation> for rdmaxcel_sys::ibv_wr_opcode::Type {
1001    fn from(op: IbvOperation) -> Self {
1002        match op {
1003            IbvOperation::Write => rdmaxcel_sys::ibv_wr_opcode::IBV_WR_RDMA_WRITE,
1004            IbvOperation::WriteWithImm => rdmaxcel_sys::ibv_wr_opcode::IBV_WR_RDMA_WRITE_WITH_IMM,
1005            IbvOperation::Read => rdmaxcel_sys::ibv_wr_opcode::IBV_WR_RDMA_READ,
1006            IbvOperation::Recv => panic!("Invalid wr opcode"),
1007        }
1008    }
1009}
1010
1011impl From<rdmaxcel_sys::ibv_wc_opcode::Type> for IbvOperation {
1012    fn from(op: rdmaxcel_sys::ibv_wc_opcode::Type) -> Self {
1013        match op {
1014            rdmaxcel_sys::ibv_wc_opcode::IBV_WC_RDMA_WRITE => IbvOperation::Write,
1015            rdmaxcel_sys::ibv_wc_opcode::IBV_WC_RDMA_READ => IbvOperation::Read,
1016            _ => panic!("Unsupported operation type"),
1017        }
1018    }
1019}
1020
1021/// Contains information needed to establish an RDMA queue pair with a remote endpoint.
1022///
1023/// `IbvQpInfo` encapsulates all the necessary information to establish a queue pair
1024/// with a remote RDMA device. This includes queue pair number, LID (Local Identifier),
1025/// GID (Global Identifier), remote memory address, remote key, and packet sequence number.
1026#[derive(Default, Named, Clone, serde::Serialize, serde::Deserialize)]
1027pub struct IbvQpInfo {
1028    /// `qp_num` - Queue Pair Number, uniquely identifies a queue pair on the remote device
1029    pub qp_num: u32,
1030    /// `lid` - Local Identifier, used for addressing in InfiniBand subnet
1031    pub lid: u16,
1032    /// `gid` - Global Identifier, used for routing across subnets (similar to IPv6 address)
1033    pub gid: Option<Gid>,
1034    /// `psn` - Packet Sequence Number, used for ordering packets
1035    pub psn: u32,
1036}
1037wirevalue::register_type!(IbvQpInfo);
1038
1039impl std::fmt::Debug for IbvQpInfo {
1040    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1041        write!(
1042            f,
1043            "IbvQpInfo {{ qp_num: {}, lid: {}, gid: {:?}, psn: 0x{:x} }}",
1044            self.qp_num, self.lid, self.gid, self.psn
1045        )
1046    }
1047}
1048
1049/// Wrapper around ibv_wc (ibverbs work completion).
1050///
1051/// This exposes only the public fields of rdmaxcel_sys::ibv_wc, allowing us to more easily
1052/// interact with it from Rust. Work completions are used to track the status of
1053/// RDMA operations and are generated when an operation completes.
1054#[derive(Debug, Named, Clone, serde::Serialize, serde::Deserialize)]
1055pub struct IbvWc {
1056    /// `wr_id` - Work Request ID, used to identify the completed operation
1057    wr_id: u64,
1058    /// `len` - Length of the data transferred
1059    len: usize,
1060    /// `valid` - Whether the work completion is valid
1061    valid: bool,
1062    /// `error` - Error information if the operation failed
1063    error: Option<(rdmaxcel_sys::ibv_wc_status::Type, u32)>,
1064    /// `opcode` - Type of operation that completed (read, write, etc.)
1065    opcode: rdmaxcel_sys::ibv_wc_opcode::Type,
1066    /// `bytes` - Immediate data (if any)
1067    bytes: Option<u32>,
1068    /// `qp_num` - Queue Pair Number
1069    qp_num: u32,
1070    /// `src_qp` - Source Queue Pair Number
1071    src_qp: u32,
1072    /// `pkey_index` - Partition Key Index
1073    pkey_index: u16,
1074    /// `slid` - Source LID
1075    slid: u16,
1076    /// `sl` - Service Level
1077    sl: u8,
1078    /// `dlid_path_bits` - Destination LID Path Bits
1079    dlid_path_bits: u8,
1080}
1081wirevalue::register_type!(IbvWc);
1082
1083impl From<rdmaxcel_sys::ibv_wc> for IbvWc {
1084    fn from(wc: rdmaxcel_sys::ibv_wc) -> Self {
1085        IbvWc {
1086            wr_id: wc.wr_id(),
1087            len: wc.len(),
1088            valid: wc.is_valid(),
1089            error: wc.error(),
1090            opcode: wc.opcode(),
1091            bytes: wc.imm_data(),
1092            qp_num: wc.qp_num,
1093            src_qp: wc.src_qp,
1094            pkey_index: wc.pkey_index,
1095            slid: wc.slid,
1096            sl: wc.sl,
1097            dlid_path_bits: wc.dlid_path_bits,
1098        }
1099    }
1100}
1101
1102impl IbvWc {
1103    /// Returns the Work Request ID associated with this work completion.
1104    ///
1105    /// The Work Request ID is used to identify the specific operation that completed.
1106    /// It is set by the application when posting the work request and is returned
1107    /// unchanged in the work completion.
1108    pub fn wr_id(&self) -> u64 {
1109        self.wr_id
1110    }
1111
1112    /// Returns whether this work completion is valid.
1113    ///
1114    /// A valid work completion indicates that the operation completed successfully.
1115    /// If false, the `error` field may contain additional information about the failure.
1116    pub fn is_valid(&self) -> bool {
1117        self.valid
1118    }
1119
1120    #[cfg(test)]
1121    pub(super) fn for_test(wr_id: u64, valid: bool) -> Self {
1122        Self {
1123            wr_id,
1124            len: 0,
1125            valid,
1126            error: None,
1127            opcode: rdmaxcel_sys::ibv_wc_opcode::IBV_WC_RDMA_WRITE,
1128            bytes: None,
1129            qp_num: 0,
1130            src_qp: 0,
1131            pkey_index: 0,
1132            slid: 0,
1133            sl: 0,
1134            dlid_path_bits: 0,
1135        }
1136    }
1137}
1138
1139/// Owns an `ibv_cq` together with the `Arc<IbvContext>` it was created on,
1140/// destroying the CQ on drop (a no-op if null) before releasing the context.
1141/// Holding the context keeps it open across `ibv_destroy_cq`.
1142#[derive(Debug)]
1143pub(super) struct IbvCq {
1144    cq: *mut rdmaxcel_sys::ibv_cq,
1145    /// Keeps the context open until after `ibv_destroy_cq`. Never read.
1146    _context: Arc<IbvContext>,
1147}
1148
1149// SAFETY: the only raw member is the `ibv_cq` pointer. The ibverbs CQ it names is
1150// not thread-affine — it may be created on one thread and used or destroyed on
1151// another (`Send`) — and `IbvCq` exposes no operation that mutates the CQ through
1152// a shared `&` (`as_ptr` only hands back the pointer value), so sharing a
1153// `&IbvCq` cannot race (`Sync`).
1154unsafe impl Send for IbvCq {}
1155// SAFETY: as for `Send` above.
1156unsafe impl Sync for IbvCq {}
1157
1158impl IbvCq {
1159    /// Creates a completion queue with `cq_entries` entries on `context`,
1160    /// retaining `context` for the CQ's lifetime.
1161    ///
1162    /// # Safety
1163    ///
1164    /// `context` must wrap a live `ibv_context`; a null context yields `Err`.
1165    pub(super) unsafe fn create(
1166        context: Arc<IbvContext>,
1167        cq_entries: i32,
1168    ) -> Result<Self, anyhow::Error> {
1169        if context.as_ptr().is_null() {
1170            anyhow::bail!("cannot create a completion queue on a null context");
1171        }
1172        // SAFETY: `context.as_ptr()` is non-null (checked above) and live (caller
1173        // contract); `ibv_create_cq` returns null on failure.
1174        let cq = unsafe {
1175            rdmaxcel_sys::ibv_create_cq(
1176                context.as_ptr(),
1177                cq_entries,
1178                std::ptr::null_mut(),
1179                std::ptr::null_mut(),
1180                0,
1181            )
1182        };
1183        if cq.is_null() {
1184            anyhow::bail!(
1185                "failed to create completion queue: {}",
1186                Error::last_os_error()
1187            );
1188        }
1189        Ok(Self {
1190            cq,
1191            _context: context,
1192        })
1193    }
1194
1195    /// The raw `ibv_cq`; null for a placeholder that holds no queue.
1196    pub(super) fn as_ptr(&self) -> *mut rdmaxcel_sys::ibv_cq {
1197        self.cq
1198    }
1199
1200    /// A placeholder holding no completion queue: `as_ptr` returns null and
1201    /// `Drop` is a no-op.
1202    #[cfg(test)]
1203    pub(super) fn null() -> Self {
1204        Self {
1205            cq: std::ptr::null_mut(),
1206            _context: Arc::new(IbvContext::null()),
1207        }
1208    }
1209}
1210
1211impl Drop for IbvCq {
1212    fn drop(&mut self) {
1213        if self.cq.is_null() {
1214            return;
1215        }
1216        // SAFETY: a non-null `self.cq` was returned by `ibv_create_cq` and, since
1217        // `IbvCq` is not `Clone`, is destroyed exactly once. `_context` drops only
1218        // after this returns, so the device is still open here.
1219        let ret = unsafe { rdmaxcel_sys::ibv_destroy_cq(self.cq) };
1220        if ret != 0 {
1221            tracing::error!(
1222                "failed to destroy completion queue {:p}: error code {}",
1223                self.cq,
1224                ret
1225            );
1226        }
1227    }
1228}
1229
1230/// Owns an `ibv_qp` together with the resources it is built against: its two
1231/// completion queues and the protection domain. The QP is destroyed on drop (a
1232/// no-op if null) before the CQs and PD, so the destruction order is correct by
1233/// construction and holders need not track the CQs or PD separately.
1234#[derive(Debug)]
1235pub(super) struct IbvQp {
1236    qp: *mut rdmaxcel_sys::ibv_qp,
1237    /// Declared after `qp` so the QP is destroyed before its completion queues.
1238    send_cq: IbvCq,
1239    recv_cq: IbvCq,
1240    /// Keeps the PD alive for the QP's lifetime and is the source of the QP's
1241    /// device context (via [`IbvPd::context`]).
1242    pd: Arc<IbvPd>,
1243}
1244
1245// SAFETY: the only raw member is the `ibv_qp` pointer (the other fields are
1246// already `Send`/`Sync`). The ibverbs QP it names is not thread-affine — it may
1247// be created on one thread and used or destroyed on another (`Send`) — and
1248// `IbvQp` exposes no operation that mutates the QP through a shared `&`, so
1249// sharing a `&IbvQp` cannot race (`Sync`).
1250unsafe impl Send for IbvQp {}
1251// SAFETY: as for `Send` above.
1252unsafe impl Sync for IbvQp {}
1253
1254impl IbvQp {
1255    /// Takes ownership of a raw `ibv_qp` and the `send_cq`/`recv_cq`/`pd` it was
1256    /// built against, destroying the QP on drop.
1257    ///
1258    /// # Safety
1259    ///
1260    /// `qp`, if non-null, must be a live `ibv_qp` owned solely by the returned
1261    /// value (its `Drop` calls `ibv_destroy_qp` once), built against `pd` with
1262    /// `send_cq`/`recv_cq` as its completion queues.
1263    pub(super) unsafe fn from_raw(
1264        qp: *mut rdmaxcel_sys::ibv_qp,
1265        send_cq: IbvCq,
1266        recv_cq: IbvCq,
1267        pd: Arc<IbvPd>,
1268    ) -> Self {
1269        Self {
1270            qp,
1271            send_cq,
1272            recv_cq,
1273            pd,
1274        }
1275    }
1276
1277    /// The raw `ibv_qp`; null for a placeholder that holds no queue pair.
1278    pub(super) fn as_ptr(&self) -> *mut rdmaxcel_sys::ibv_qp {
1279        self.qp
1280    }
1281
1282    /// The send completion queue.
1283    pub(super) fn send_cq(&self) -> &IbvCq {
1284        &self.send_cq
1285    }
1286
1287    /// The receive completion queue.
1288    pub(super) fn recv_cq(&self) -> &IbvCq {
1289        &self.recv_cq
1290    }
1291
1292    /// The device context this QP was created on, sourced from its PD.
1293    pub(super) fn context(&self) -> &IbvContext {
1294        self.pd.context()
1295    }
1296
1297    /// A placeholder holding no queue pair: `as_ptr` returns null and `Drop` is
1298    /// a no-op.
1299    #[cfg(test)]
1300    pub(super) fn null() -> Self {
1301        Self {
1302            qp: std::ptr::null_mut(),
1303            send_cq: IbvCq::null(),
1304            recv_cq: IbvCq::null(),
1305            pd: Arc::new(IbvPd::null()),
1306        }
1307    }
1308}
1309
1310impl Drop for IbvQp {
1311    fn drop(&mut self) {
1312        if self.qp.is_null() {
1313            return;
1314        }
1315        // SAFETY: a non-null `self.qp` was handed to `from_raw` as a live `ibv_qp`
1316        // and, since `IbvQp` is not `Clone`, is destroyed exactly once. Its CQs
1317        // and PD drop only after this returns, so they outlive the destruction.
1318        let ret = unsafe { rdmaxcel_sys::ibv_destroy_qp(self.qp) };
1319        if ret != 0 {
1320            tracing::error!(
1321                "failed to destroy queue pair {:p}: error code {}",
1322                self.qp,
1323                ret
1324            );
1325        }
1326    }
1327}
1328
1329/// RAII owner of a raw `ibv_context*`, closing it in [`Drop`] (a no-op if
1330/// null).
1331#[derive(Debug)]
1332pub struct IbvContext(*mut rdmaxcel_sys::ibv_context);
1333
1334// SAFETY: libibverbs treats `ibv_context*` as thread-safe for the
1335// operations we perform.
1336unsafe impl Send for IbvContext {}
1337unsafe impl Sync for IbvContext {}
1338
1339impl IbvContext {
1340    /// Wraps a raw `ibv_context*`. A null pointer yields a no-op context
1341    /// (its `Drop` does nothing).
1342    ///
1343    /// # Safety
1344    ///
1345    /// `context` must be either null or a pointer returned by
1346    /// `ibv_open_device` that has not been (and will not be) closed elsewhere:
1347    /// the resulting `IbvContext` takes sole ownership and its `Drop` calls
1348    /// `ibv_close_device` exactly once.
1349    pub(super) unsafe fn from_raw(context: *mut rdmaxcel_sys::ibv_context) -> Self {
1350        Self(context)
1351    }
1352
1353    /// Returns the raw `ibv_context*`. The pointer is valid for
1354    /// the lifetime of `&self`.
1355    pub fn as_ptr(&self) -> *mut rdmaxcel_sys::ibv_context {
1356        self.0
1357    }
1358
1359    /// A placeholder holding no context: `as_ptr` returns null and `Drop` is a
1360    /// no-op. Used by the test-only `null()` constructors of the pointer
1361    /// wrappers that hold a context.
1362    #[cfg(test)]
1363    pub(super) fn null() -> Self {
1364        // SAFETY: a null context is explicitly allowed; `Drop` skips
1365        // `ibv_close_device` for null.
1366        unsafe { Self::from_raw(std::ptr::null_mut()) }
1367    }
1368}
1369
1370impl Drop for IbvContext {
1371    fn drop(&mut self) {
1372        if self.0.is_null() {
1373            return;
1374        }
1375        // SAFETY: `self.0` was returned by `ibv_open_device` and
1376        // has not been closed elsewhere.
1377        let result = unsafe { rdmaxcel_sys::ibv_close_device(self.0) };
1378        if result != 0 {
1379            tracing::error!(
1380                "ibv_close_device failed for context {:p}: error code {}",
1381                self.0,
1382                result
1383            );
1384        }
1385    }
1386}
1387
1388/// Owns an `ibv_pd` together with the `Arc<IbvContext>` it was allocated
1389/// against, deallocating the PD on drop (a no-op if null) before releasing the
1390/// context.
1391#[derive(Debug)]
1392pub(super) struct IbvPd {
1393    pd: *mut rdmaxcel_sys::ibv_pd,
1394    /// The context the PD was allocated against, kept open until after
1395    /// `ibv_dealloc_pd` and reached by holders via [`Self::context`].
1396    context: Arc<IbvContext>,
1397}
1398
1399// SAFETY: the only raw member is the `ibv_pd` pointer. The ibverbs PD it names is
1400// not thread-affine — it may be allocated on one thread and used or deallocated
1401// on another (`Send`) — and `IbvPd` exposes no operation that mutates the PD
1402// through a shared `&` (`as_ptr` only hands back the pointer value), so sharing a
1403// `&IbvPd` cannot race (`Sync`).
1404unsafe impl Send for IbvPd {}
1405// SAFETY: as for `Send` above.
1406unsafe impl Sync for IbvPd {}
1407
1408impl IbvPd {
1409    /// Allocates a protection domain against `context`.
1410    ///
1411    /// # Safety
1412    ///
1413    /// `context` must wrap a live `ibv_context`; a null context yields `Err`.
1414    pub(super) unsafe fn create(context: Arc<IbvContext>) -> Result<Self, anyhow::Error> {
1415        if context.as_ptr().is_null() {
1416            anyhow::bail!("cannot allocate a protection domain on a null context");
1417        }
1418        // SAFETY: `context.as_ptr()` is non-null (checked above) and live (caller
1419        // contract); `ibv_alloc_pd` returns null on failure.
1420        let pd = unsafe { rdmaxcel_sys::ibv_alloc_pd(context.as_ptr()) };
1421        if pd.is_null() {
1422            anyhow::bail!("ibv_alloc_pd failed: {}", Error::last_os_error());
1423        }
1424        Ok(Self { pd, context })
1425    }
1426
1427    /// The raw `ibv_pd`; null for a placeholder that holds no protection domain.
1428    pub(super) fn as_ptr(&self) -> *mut rdmaxcel_sys::ibv_pd {
1429        self.pd
1430    }
1431
1432    /// The context this PD was allocated against.
1433    pub(super) fn context(&self) -> &Arc<IbvContext> {
1434        &self.context
1435    }
1436
1437    /// A placeholder holding no protection domain (and a no-op context): both
1438    /// `Drop`s are no-ops.
1439    #[cfg(test)]
1440    pub(super) fn null() -> Self {
1441        Self {
1442            pd: std::ptr::null_mut(),
1443            context: Arc::new(IbvContext::null()),
1444        }
1445    }
1446}
1447
1448impl Drop for IbvPd {
1449    fn drop(&mut self) {
1450        if self.pd.is_null() {
1451            return;
1452        }
1453        // SAFETY: a non-null `self.pd` was returned by `ibv_alloc_pd` and, since
1454        // `IbvPd` is not `Clone`, is deallocated exactly once. `context` is
1455        // dropped only after this returns, so the device is still open here.
1456        let ret = unsafe { rdmaxcel_sys::ibv_dealloc_pd(self.pd) };
1457        if ret != 0 {
1458            tracing::error!(
1459                "failed to deallocate protection domain {:p}: error code {}",
1460                self.pd,
1461                ret
1462            );
1463        }
1464    }
1465}
1466
1467/// Owns an `ibv_mr` together with the `Arc<IbvPd>` it was registered against,
1468/// deregistering the MR on drop (a no-op if null) before releasing the PD.
1469/// Holding the PD keeps it (and, through it, the context) alive across
1470/// `ibv_dereg_mr`, so an `IbvMr` is a self-contained registration callers can
1471/// keep alive on its own.
1472#[derive(Debug)]
1473pub(super) struct IbvMr {
1474    mr: *mut rdmaxcel_sys::ibv_mr,
1475    /// Keeps the PD open until after `ibv_dereg_mr`. Never read.
1476    _pd: Arc<IbvPd>,
1477}
1478
1479// SAFETY: the only raw member is the `ibv_mr` pointer. The ibverbs MR it names is
1480// not thread-affine — it may be registered on one thread and used or
1481// deregistered on another (`Send`) — and `IbvMr` exposes no operation that
1482// mutates the MR through a shared `&` (`as_ptr` only hands back the pointer
1483// value), so sharing a `&IbvMr` cannot race (`Sync`).
1484unsafe impl Send for IbvMr {}
1485// SAFETY: as for `Send` above.
1486unsafe impl Sync for IbvMr {}
1487
1488impl IbvMr {
1489    /// Takes ownership of a raw `ibv_mr` and the `pd` it was registered against,
1490    /// deregistering the MR on drop.
1491    ///
1492    /// # Safety
1493    ///
1494    /// `mr`, if non-null, must be a live `ibv_mr` owned solely by the returned
1495    /// value (its `Drop` calls `ibv_dereg_mr` once), registered against `pd`.
1496    pub(super) unsafe fn from_raw(mr: *mut rdmaxcel_sys::ibv_mr, pd: Arc<IbvPd>) -> Self {
1497        Self { mr, _pd: pd }
1498    }
1499
1500    /// The raw `ibv_mr`; null for a placeholder that holds no region.
1501    pub(super) fn as_ptr(&self) -> *mut rdmaxcel_sys::ibv_mr {
1502        self.mr
1503    }
1504
1505    /// A placeholder holding no memory region (and a no-op PD): both `Drop`s are
1506    /// no-ops.
1507    #[cfg(test)]
1508    pub(super) fn null() -> Self {
1509        Self {
1510            mr: std::ptr::null_mut(),
1511            _pd: Arc::new(IbvPd::null()),
1512        }
1513    }
1514}
1515
1516impl Drop for IbvMr {
1517    fn drop(&mut self) {
1518        if self.mr.is_null() {
1519            return;
1520        }
1521        // SAFETY: a non-null `self.mr` was handed to `from_raw` as a live
1522        // `ibv_mr` and, since `IbvMr` is not `Clone`, is deregistered exactly
1523        // once. `_pd` drops only after this returns, so the PD is still alive.
1524        let ret = unsafe { rdmaxcel_sys::ibv_dereg_mr(self.mr) };
1525        if ret != 0 {
1526            tracing::error!("failed to deregister MR {:p}: error code {}", self.mr, ret);
1527        }
1528    }
1529}
1530
1531#[cfg(test)]
1532mod tests {
1533    use super::*;
1534
1535    #[test]
1536    fn test_list_all_devices() {
1537        // Skip test if RDMA devices are not available
1538        let devices = list_all_devices();
1539        if devices.is_empty() {
1540            println!("Skipping test: RDMA devices not available");
1541            return;
1542        }
1543        // Basic validation of first device
1544        let device = &devices[0];
1545        assert!(!device.name().is_empty(), "device name should not be empty");
1546        assert!(
1547            !device.ports().is_empty(),
1548            "device should have at least one port"
1549        );
1550    }
1551
1552    #[test]
1553    fn test_first_available() {
1554        // Skip test if RDMA is not available
1555        let devices = list_all_devices();
1556        if devices.is_empty() {
1557            println!("Skipping test: RDMA devices not available");
1558            return;
1559        }
1560        // Basic validation of first device
1561        let device = &devices[0];
1562
1563        let dev = device;
1564        // Verify getters return expected values
1565        assert_eq!(dev.vendor_id(), dev.vendor_id);
1566        assert_eq!(dev.vendor_part_id(), dev.vendor_part_id);
1567        assert_eq!(dev.hw_ver(), dev.hw_ver);
1568        assert_eq!(dev.fw_ver(), &dev.fw_ver);
1569        assert_eq!(dev.node_guid(), dev.node_guid);
1570        assert_eq!(dev.max_qp(), dev.max_qp);
1571        assert_eq!(dev.max_cq(), dev.max_cq);
1572        assert_eq!(dev.max_mr(), dev.max_mr);
1573        assert_eq!(dev.max_pd(), dev.max_pd);
1574        assert_eq!(dev.max_qp_wr(), dev.max_qp_wr);
1575        assert_eq!(dev.max_sge(), dev.max_sge);
1576    }
1577
1578    #[test]
1579    fn test_device_display() {
1580        if let Some(device) = IbvDeviceInfo::first_available() {
1581            let display_output = format!("{}", device);
1582            assert!(
1583                display_output.contains(&device.name),
1584                "display should include device name"
1585            );
1586            assert!(
1587                display_output.contains(&device.fw_ver),
1588                "display should include firmware version"
1589            );
1590        }
1591    }
1592
1593    #[test]
1594    fn test_port_display() {
1595        if let Some(device) = IbvDeviceInfo::first_available()
1596            && !device.ports().is_empty()
1597        {
1598            let port = &device.ports()[0];
1599            let display_output = format!("{}", port);
1600            assert!(
1601                display_output.contains(&get_port_state_str(port.state)),
1602                "display should include port state"
1603            );
1604            assert!(
1605                display_output.contains(&port.link_layer),
1606                "display should include link layer"
1607            );
1608        }
1609    }
1610
1611    #[test]
1612    fn test_ib_speed_mbits_per_lane() {
1613        // `active_speed` is a one-hot bitmask, indexed by its lowest set
1614        // bit. Values mirror NCCL's `ibvSpeeds` (SDR..NDR).
1615        assert_eq!(ib_speed_mbits_per_lane(1), 2500); // SDR
1616        assert_eq!(ib_speed_mbits_per_lane(2), 5000); // DDR
1617        assert_eq!(ib_speed_mbits_per_lane(4), 10000); // QDR
1618        assert_eq!(ib_speed_mbits_per_lane(8), 10000); // QDR / FDR10
1619        assert_eq!(ib_speed_mbits_per_lane(16), 14000); // FDR
1620        assert_eq!(ib_speed_mbits_per_lane(32), 25000); // EDR
1621        assert_eq!(ib_speed_mbits_per_lane(64), 50000); // HDR
1622        assert_eq!(ib_speed_mbits_per_lane(128), 100000); // NDR
1623        assert_eq!(ib_speed_mbits_per_lane(0), 0); // unset → unknown
1624    }
1625
1626    #[test]
1627    fn test_ib_width_lanes() {
1628        assert_eq!(ib_width_lanes(1), 1); // 1x
1629        assert_eq!(ib_width_lanes(2), 4); // 4x
1630        assert_eq!(ib_width_lanes(4), 8); // 8x
1631        assert_eq!(ib_width_lanes(8), 12); // 12x
1632        assert_eq!(ib_width_lanes(16), 2); // 2x
1633        assert_eq!(ib_width_lanes(0), 0); // unset → unknown
1634    }
1635
1636    #[test]
1637    fn test_port_speed_mbytes_per_sec() {
1638        use rdmaxcel_sys::ibv_port_state::IBV_PORT_ACTIVE;
1639        use rdmaxcel_sys::ibv_port_state::IBV_PORT_DOWN;
1640        fn mk_port(
1641            state: rdmaxcel_sys::ibv_port_state::Type,
1642            active_speed: u8,
1643            active_width: u8,
1644        ) -> IbvPort {
1645            IbvPort {
1646                port_num: 1,
1647                state,
1648                physical_state: String::new(),
1649                base_lid: 0,
1650                lmc: 0,
1651                sm_lid: 0,
1652                capability_mask: 0,
1653                link_layer: String::new(),
1654                gids: BTreeMap::new(),
1655                gid_tbl_len: 0,
1656                active_speed,
1657                active_width,
1658            }
1659        }
1660        fn mk_device(ports: Vec<IbvPort>) -> IbvDeviceInfo {
1661            IbvDeviceInfo {
1662                name: "test".to_string(),
1663                vendor_id: 0,
1664                vendor_part_id: 0,
1665                hw_ver: 0,
1666                fw_ver: String::new(),
1667                node_guid: 0,
1668                ports,
1669                max_qp: 0,
1670                max_cq: 0,
1671                max_mr: 0,
1672                max_pd: 0,
1673                max_qp_wr: 0,
1674                max_sge: 0,
1675            }
1676        }
1677
1678        // NDR (128) x4 (width bit 2): 100000 * 4 / 8 = 50000 MB/s.
1679        assert_eq!(
1680            mk_device(vec![mk_port(IBV_PORT_ACTIVE, 128, 2)]).port_speed_mbytes_per_sec(),
1681            50000
1682        );
1683
1684        // The fastest port is DOWN, so it is ignored; the result is the
1685        // fastest ACTIVE port, not the (faster) down one.
1686        let mixed = mk_device(vec![
1687            mk_port(IBV_PORT_ACTIVE, 32, 2), // EDR x4 = 12500
1688            mk_port(IBV_PORT_ACTIVE, 64, 2), // HDR x4 = 25000 (fastest ACTIVE)
1689            mk_port(IBV_PORT_DOWN, 128, 2),  // NDR x4 = 50000, but DOWN → ignored
1690        ]);
1691        assert_eq!(mixed.port_speed_mbytes_per_sec(), 25000);
1692
1693        // No ACTIVE port → 0 (treated as unknown, no cap).
1694        assert_eq!(
1695            mk_device(vec![mk_port(IBV_PORT_DOWN, 128, 2)]).port_speed_mbytes_per_sec(),
1696            0
1697        );
1698    }
1699
1700    #[test]
1701    fn test_rdma_operation_conversion() {
1702        assert_eq!(
1703            rdmaxcel_sys::ibv_wr_opcode::IBV_WR_RDMA_WRITE,
1704            rdmaxcel_sys::ibv_wr_opcode::Type::from(IbvOperation::Write)
1705        );
1706        assert_eq!(
1707            rdmaxcel_sys::ibv_wr_opcode::IBV_WR_RDMA_READ,
1708            rdmaxcel_sys::ibv_wr_opcode::Type::from(IbvOperation::Read)
1709        );
1710
1711        assert_eq!(
1712            IbvOperation::Write,
1713            IbvOperation::from(rdmaxcel_sys::ibv_wc_opcode::IBV_WC_RDMA_WRITE)
1714        );
1715        assert_eq!(
1716            IbvOperation::Read,
1717            IbvOperation::from(rdmaxcel_sys::ibv_wc_opcode::IBV_WC_RDMA_READ)
1718        );
1719    }
1720
1721    #[test]
1722    fn test_rdma_endpoint() {
1723        let endpoint = IbvQpInfo {
1724            qp_num: 42,
1725            lid: 123,
1726            gid: None,
1727            psn: 0x5678,
1728        };
1729
1730        let debug_str = format!("{:?}", endpoint);
1731        assert!(debug_str.contains("qp_num: 42"));
1732        assert!(debug_str.contains("lid: 123"));
1733        assert!(debug_str.contains("psn: 0x5678"));
1734    }
1735
1736    #[test]
1737    fn test_ibv_wc() {
1738        let mut wc = rdmaxcel_sys::ibv_wc::default();
1739
1740        // SAFETY: modifies private fields through pointer manipulation
1741        unsafe {
1742            // Cast to pointer and modify the fields directly
1743            let wc_ptr = &mut wc as *mut rdmaxcel_sys::ibv_wc as *mut u8;
1744
1745            // Set wr_id (at offset 0, u64)
1746            *(wc_ptr as *mut u64) = 42;
1747
1748            // Set status to SUCCESS (at offset 8, u32)
1749            *(wc_ptr.add(8) as *mut i32) = rdmaxcel_sys::ibv_wc_status::IBV_WC_SUCCESS as i32;
1750        }
1751        let ibv_wc = IbvWc::from(wc);
1752        assert_eq!(ibv_wc.wr_id(), 42);
1753        assert!(ibv_wc.is_valid());
1754    }
1755
1756    #[test]
1757    fn test_gid_scope_of() {
1758        fn scope(addr: &str) -> GidScope {
1759            GidScope::of(addr.parse().expect("valid IPv6"))
1760        }
1761
1762        // Loopback: `::1` and IPv4-mapped loopback (`::ffff:127.0.0.0/8`) — the
1763        // latter must not be classified as global.
1764        assert_eq!(scope("::1"), GidScope::Loopback);
1765        assert_eq!(scope("::ffff:127.0.0.1"), GidScope::Loopback);
1766
1767        // Link-local: `fe80::/10` (the default GID prefix) and IPv4-mapped
1768        // link-local (`::ffff:169.254.0.0/16`).
1769        assert_eq!(scope("fe80::1"), GidScope::LinkLocal);
1770        assert_eq!(scope("::ffff:169.254.1.1"), GidScope::LinkLocal);
1771
1772        // Site-local `fec0::/10`.
1773        assert_eq!(scope("fec0::1"), GidScope::SiteLocal);
1774
1775        // Global: IPv4-mapped (the common RoCE v2 form, in both sysfs hextet and
1776        // dotted-quad forms), ULA, and global IPv6.
1777        assert_eq!(
1778            scope("0000:0000:0000:0000:0000:ffff:0a1e:0f44"),
1779            GidScope::Global
1780        );
1781        assert_eq!(scope("::ffff:10.30.15.68"), GidScope::Global);
1782        assert_eq!(scope("fd00::1"), GidScope::Global);
1783        assert_eq!(scope("2001:db8::1"), GidScope::Global);
1784    }
1785
1786    #[test]
1787    fn test_gid_type_of() {
1788        // Matches the sysfs `gid_attrs/types` strings (trailing newline and all);
1789        // anything else is `Unknown`.
1790        assert_eq!(GidType::of("RoCE v2\n"), GidType::RoCEv2);
1791        assert_eq!(GidType::of("IB/RoCE v1\n"), GidType::RoCEv1);
1792        assert_eq!(GidType::of(""), GidType::Unknown);
1793        assert_eq!(GidType::of("something else"), GidType::Unknown);
1794    }
1795
1796    #[test]
1797    fn test_format_gid() {
1798        let gid = [
1799            0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66,
1800            0x77, 0x88,
1801        ];
1802
1803        let formatted = format_gid(&gid);
1804        assert_eq!(formatted, "1234:5678:9abc:def0:1122:3344:5566:7788");
1805    }
1806
1807    #[test]
1808    fn test_mlx5dv_supported_basic() {
1809        // The test just verifies the function doesn't panic
1810        let mlx5dv_support = mlx5dv_supported();
1811        println!("mlx5dv_supported: {}", mlx5dv_support);
1812    }
1813}