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}