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