Skip to main content

monarch_rdma/backend/ibverbs/
device.rs

1/*
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 * All rights reserved.
4 *
5 * This source code is licensed under the BSD-style license found in the
6 * LICENSE file in the root directory of this source tree.
7 */
8
9//! Backend-agnostic ibverbs device abstraction.
10//!
11//! [`IbvDevice`] owns the per-process state for a single opened RDMA
12//! device: an `Arc<IbvContext>`, an [`IbvConfig`], and a map of named
13//! [`IbvDomain`]s allocated against the device.
14//! Per-backend behavior — claiming a device as the backend's,
15//! allocating a domain, and seeding config defaults — is provided by
16//! an [`IbvDeviceImpl`].
17//!
18//! Each [`IbvDeviceImpl`] registers itself via the `inventory` crate
19//! (using [`register_ibv_device_impl!`]). At first access,
20//! [`DEVICE_NAMES_BY_IMPL`] walks the ibverbs device list once and
21//! assigns each device to the first registered impl whose
22//! [`IbvDeviceImpl::is_instance`] claims it, caching the resulting
23//! `typename() → device-infos` map. [`IbvDevice::open`] consults this
24//! map: it returns `None` when `name` is not advertised under
25//! `I::typename()`, and otherwise opens the device using the cached
26//! [`IbvDeviceInfo`].
27
28use std::collections::HashMap;
29use std::ffi::CStr;
30use std::marker::PhantomData;
31use std::sync::Arc;
32use std::sync::LazyLock;
33
34use typeuri::Named;
35
36use super::domain::IbvDomain;
37use super::domain::IbvDomainImpl;
38use super::primitives::IbvConfig;
39use super::primitives::IbvContext;
40use super::primitives::IbvDeviceInfo;
41use super::primitives::query_device_info;
42
43/// Per-backend driver for [`IbvDevice`]. Concrete impls register
44/// themselves via [`register_ibv_device_impl!`].
45pub trait IbvDeviceImpl: Named + std::fmt::Debug + Send + Sync + 'static {
46    /// The per-domain strategy used by this backend (how memory regions
47    /// are registered and queue pairs built against a PD). [`IbvDomain`] is
48    /// generic over this.
49    type Domain: IbvDomainImpl;
50
51    /// Human-readable display name for the backend this impl
52    /// drives (e.g., `"mellanox"`, `"efa"`). Surfaced in
53    /// diagnostics; not used as a registry key.
54    fn backend_name() -> &'static str;
55
56    /// Returns `true` if `ctx` belongs to this backend. Called
57    /// transiently, once per ibverbs device, while
58    /// [`DEVICE_NAMES_BY_IMPL`] is built.
59    fn is_instance(ctx: Arc<IbvContext>) -> bool;
60
61    /// Seeds an [`IbvConfig`] with backend-appropriate defaults
62    /// (e.g., EFA caps `max_send_sge` at 1).
63    fn apply_config_defaults(config: &mut IbvConfig);
64}
65
66/// Inventory entry submitted by [`register_ibv_device_impl!`] for
67/// each concrete [`IbvDeviceImpl`]. Captures function pointers to
68/// the impl's `typename()`, `backend_name()`, and `is_instance()`,
69/// all erased of the concrete type.
70pub struct IbvDeviceImplRegistration {
71    typename: fn() -> &'static str,
72    backend_name: fn() -> &'static str,
73    is_instance: fn(Arc<IbvContext>) -> bool,
74}
75
76/// Per-impl entry stored in [`DEVICE_NAMES_BY_IMPL`]. Keyed by
77/// `typename()`; carries the human-readable `backend_name` for
78/// diagnostics alongside the impl's device infos.
79#[derive(Debug)]
80struct RegisteredBackend {
81    #[expect(
82        dead_code,
83        reason = "read only via Debug for the IbvDevice::open diagnostic warning"
84    )]
85    backend_name: &'static str,
86    devices: Vec<IbvDeviceInfo>,
87}
88
89inventory::collect!(IbvDeviceImplRegistration);
90
91/// Submits an `inventory` entry for a concrete [`IbvDeviceImpl`].
92///
93/// Place one invocation per impl at module scope. The expansion
94/// references `inventory` and `typeuri` by bare crate name, so the
95/// calling crate must list both as direct dependencies.
96///
97/// ```ignore
98/// register_ibv_device_impl!(StandardImpl);
99/// ```
100#[macro_export]
101macro_rules! register_ibv_device_impl {
102    ($impl_ty:ty) => {
103        inventory::submit! {
104            $crate::backend::ibverbs::device::IbvDeviceImplRegistration::new(
105                <$impl_ty as typeuri::Named>::typename,
106                <$impl_ty as $crate::backend::ibverbs::device::IbvDeviceImpl>::backend_name,
107                <$impl_ty as $crate::backend::ibverbs::device::IbvDeviceImpl>::is_instance,
108            )
109        }
110    };
111}
112
113impl IbvDeviceImplRegistration {
114    /// Construct a registration. Visible to the rest of the crate
115    /// so the [`register_ibv_device_impl!`] macro can call it from
116    /// sibling call sites.
117    pub const fn new(
118        typename: fn() -> &'static str,
119        backend_name: fn() -> &'static str,
120        is_instance: fn(Arc<IbvContext>) -> bool,
121    ) -> Self {
122        Self {
123            typename,
124            backend_name,
125            is_instance,
126        }
127    }
128}
129
130/// Process-wide map from `IbvDeviceImpl::typename()` to a
131/// [`RegisteredBackend`] holding the impl's display name and the
132/// device infos it claims. Built once on first access by a single
133/// walk of the ibverbs device list, assigning each device to the
134/// first registration whose `is_instance` claims it.
135static DEVICE_NAMES_BY_IMPL: LazyLock<HashMap<&'static str, RegisteredBackend>> =
136    LazyLock::new(|| {
137        let registrations: Vec<&IbvDeviceImplRegistration> =
138            inventory::iter::<IbvDeviceImplRegistration>().collect();
139        let mut by_impl: HashMap<&'static str, RegisteredBackend> = registrations
140            .iter()
141            .map(|reg| {
142                (
143                    (reg.typename)(),
144                    RegisteredBackend {
145                        backend_name: (reg.backend_name)(),
146                        devices: Vec::new(),
147                    },
148                )
149            })
150            .collect();
151
152        let mut num_devices = 0i32;
153        // SAFETY: `ibv_get_device_list` populates `num_devices` and
154        // returns either null or a pointer to `num_devices` entries;
155        // we free it before returning.
156        let device_list = unsafe { rdmaxcel_sys::ibv_get_device_list(&mut num_devices) };
157        // A non-null list must be freed even when empty, so only the
158        // null case returns early; the loop below is a no-op at zero
159        // devices and the `ibv_free_device_list` at the end still runs.
160        if device_list.is_null() {
161            return by_impl;
162        }
163        for i in 0..num_devices {
164            // SAFETY: `device_list` is non-null with `num_devices`
165            // valid entries (checked above).
166            let device = unsafe { *device_list.add(i as usize) };
167            if device.is_null() {
168                continue;
169            }
170            // SAFETY: `device` is non-null per the check above.
171            let raw_ctx = unsafe { rdmaxcel_sys::ibv_open_device(device) };
172            if raw_ctx.is_null() {
173                continue;
174            }
175            // SAFETY: `raw_ctx` was just returned by `ibv_open_device` and is
176            // non-null (checked above); this `Arc<IbvContext>` is its sole owner.
177            let ctx = Arc::new(unsafe { IbvContext::from_raw(raw_ctx) });
178            // Assign the device to the first impl that claims it.
179            if let Some(reg) = registrations
180                .iter()
181                .find(|reg| (reg.is_instance)(Arc::clone(&ctx)))
182            {
183                // SAFETY: `device` and `ctx.as_ptr()` are non-null and
184                // `ctx.as_ptr()` was returned by `ibv_open_device(device)`.
185                match unsafe { query_device_info(device, ctx.as_ptr()) } {
186                    Ok(info) => by_impl
187                        .get_mut((reg.typename)())
188                        .expect("registration typename present in map")
189                        .devices
190                        .push(info),
191                    Err(err) => tracing::warn!("failed to query RDMA device info: {err:#}"),
192                }
193            }
194            // `ctx` drops here, closing the transient context.
195        }
196        // SAFETY: `device_list` was returned by `ibv_get_device_list`
197        // above and has not been freed.
198        unsafe { rdmaxcel_sys::ibv_free_device_list(device_list) };
199        by_impl
200    });
201
202/// An opened RDMA device.
203///
204/// Owns an `Arc<IbvContext>`, the queried [`IbvDeviceInfo`]
205/// metadata, a device-scoped [`IbvDeviceConfig`], and a map of
206/// named [`IbvDomain`]s allocated against the device (one PD
207/// per name, created lazily by [`Self::get_or_create_domain`]).
208///
209/// `I` is the backend driver type, parameterizing all per-backend
210/// behavior via [`IbvDeviceImpl`].
211#[derive(Debug)]
212pub(crate) struct IbvDevice<I: IbvDeviceImpl> {
213    domains: HashMap<String, IbvDomain<I::Domain>>,
214    device_info: IbvDeviceInfo,
215    config: IbvConfig,
216    context: Arc<IbvContext>,
217    _marker: PhantomData<I>,
218}
219
220#[expect(dead_code, reason = "called by manager_actor in follow-up commits")]
221impl<I: IbvDeviceImpl> IbvDevice<I> {
222    /// Opens `name` under impl `I`, storing `config` as-is. Returns
223    /// `None` if `name` is not one of the devices registered for
224    /// `I::typename()`; otherwise returns an `IbvDevice<I>` with the
225    /// device's queried [`IbvDeviceInfo`] and the given [`IbvConfig`].
226    ///
227    /// # Panics
228    ///
229    /// Panics if a registered device cannot be opened — e.g. it
230    /// disappeared from the system between registration and open.
231    /// Such a failure indicates a broken driver invariant rather
232    /// than a runtime condition the caller can recover from.
233    pub fn open(name: &str, config: IbvConfig) -> Option<Self> {
234        let device_info = DEVICE_NAMES_BY_IMPL
235            .get(I::typename())
236            .and_then(|entry| entry.devices.iter().find(|d| d.name() == name).cloned());
237        let device_info = match device_info {
238            Some(info) => info,
239            None => {
240                tracing::warn!(
241                    "ibv device {} not found under backend {}; available: {:?}",
242                    name,
243                    I::backend_name(),
244                    *DEVICE_NAMES_BY_IMPL,
245                );
246                return None;
247            }
248        };
249
250        // The registry already confirmed `name` belongs to `I`, so
251        // reopen the device by name; a miss here is a broken invariant.
252        let mut num_devices = 0i32;
253        // SAFETY: `ibv_get_device_list` populates `num_devices` and
254        // returns either null or a pointer to `num_devices` entries;
255        // we free it before returning.
256        let device_list = unsafe { rdmaxcel_sys::ibv_get_device_list(&mut num_devices) };
257        assert!(
258            !device_list.is_null(),
259            "RDMA device list was null while opening registered device {}",
260            name,
261        );
262        if num_devices == 0 {
263            // A non-null list must be freed even when empty, per the
264            // libibverbs contract.
265            // SAFETY: `device_list` is non-null (asserted above) and was
266            // returned by `ibv_get_device_list`.
267            unsafe { rdmaxcel_sys::ibv_free_device_list(device_list) };
268            panic!(
269                "no RDMA devices found while opening registered device {}",
270                name
271            );
272        }
273        let mut target = std::ptr::null_mut();
274        let mut context = None;
275        for i in 0..num_devices {
276            // SAFETY: `device_list` is non-null with `num_devices`
277            // valid entries (checked above).
278            let device = unsafe { *device_list.add(i as usize) };
279            if device.is_null() {
280                continue;
281            }
282            // SAFETY: `device` is non-null per the check above;
283            // `ibv_get_device_name` returns a null-terminated C string
284            // owned by the device list.
285            let dev_name = unsafe { CStr::from_ptr(rdmaxcel_sys::ibv_get_device_name(device)) };
286            if dev_name.to_bytes() == name.as_bytes() {
287                target = device;
288                break;
289            }
290        }
291        let mut failure = None;
292        if target.is_null() {
293            failure = Some(format!(
294                "ibv device with name {} not found by ibv_get_device_list",
295                name
296            ));
297        } else {
298            // Open while `target` still points into `device_list`, then free.
299            // SAFETY: `target`, when non-null, is one of `device_list`'s
300            // entries; `ibv_open_device` returns null on failure.
301            let raw_context = unsafe { rdmaxcel_sys::ibv_open_device(target) };
302            if raw_context.is_null() {
303                failure = Some(format!(
304                    "registered ibv device {} could not be opened: {}",
305                    name,
306                    std::io::Error::last_os_error(),
307                ));
308            } else {
309                // SAFETY: `raw_context` was just returned by `ibv_open_device` and is
310                // non-null (checked above); this `Arc<IbvContext>` is its sole owner.
311                context = Some(Arc::new(unsafe { IbvContext::from_raw(raw_context) }));
312            }
313        }
314
315        // SAFETY: `device_list` was returned by `ibv_get_device_list`
316        // above and has not been freed.
317        unsafe { rdmaxcel_sys::ibv_free_device_list(device_list) };
318
319        if let Some(failure) = failure {
320            panic!("{}", failure);
321        }
322
323        Some(Self {
324            domains: HashMap::new(),
325            device_info,
326            config,
327            context: context.expect("device context opened above"),
328            _marker: PhantomData,
329        })
330    }
331
332    /// Returns the `Arc<IbvContext>` for this device.
333    pub fn context(&self) -> Arc<IbvContext> {
334        Arc::clone(&self.context)
335    }
336
337    /// Returns the queried [`IbvDeviceInfo`] metadata for this
338    /// device.
339    pub fn device_info(&self) -> &IbvDeviceInfo {
340        &self.device_info
341    }
342
343    /// Returns the [`IbvConfig`] this device was opened with.
344    pub fn config(&self) -> &IbvConfig {
345        &self.config
346    }
347
348    /// Returns the [`IbvDomain`] registered under `name`, if one has already
349    /// been created. Read-only lookup; use [`Self::get_or_create_domain`] to
350    /// create on demand.
351    pub fn domain(&self, name: &str) -> Option<&IbvDomain<I::Domain>> {
352        self.domains.get(name)
353    }
354
355    /// Returns the [`IbvDomain`] registered under `name`, creating (and
356    /// caching) a new one via [`IbvDomain::new`] on first access.
357    pub fn get_or_create_domain(&mut self, name: &str) -> anyhow::Result<&IbvDomain<I::Domain>> {
358        if !self.domains.contains_key(name) {
359            // SAFETY: `self.context` wraps the live `ibv_context` opened by
360            // `IbvDevice::open`, valid for this device's lifetime.
361            let domain = unsafe {
362                IbvDomain::<I::Domain>::new(
363                    Arc::clone(&self.context),
364                    self.device_info.clone(),
365                    &self.config,
366                )
367            }?;
368            self.domains.insert(name.to_string(), domain);
369        }
370        Ok(self
371            .domains
372            .get(name)
373            .expect("domain just inserted or already present"))
374    }
375
376    /// Whether any device claimed by impl `I` is present on this
377    /// host. Reads the [`DEVICE_NAMES_BY_IMPL`] registry, built on
378    /// first access by a single walk of the ibverbs device list.
379    pub fn available() -> bool {
380        DEVICE_NAMES_BY_IMPL
381            .get(I::typename())
382            .is_some_and(|backend| !backend.devices.is_empty())
383    }
384
385    /// All devices claimed by impl `I` on this host. Reads the
386    /// [`DEVICE_NAMES_BY_IMPL`] registry, built on first access by a
387    /// single walk of the ibverbs device list.
388    pub fn list() -> Vec<IbvDeviceInfo> {
389        DEVICE_NAMES_BY_IMPL
390            .get(I::typename())
391            .map(|backend| backend.devices.clone())
392            .unwrap_or_default()
393    }
394}
395
396/// All RDMA devices on this host, across every registered backend
397/// impl. Reads the [`DEVICE_NAMES_BY_IMPL`] registry, built on first
398/// access by a single walk of the ibverbs device list.
399///
400/// A device claimed by no registered [`IbvDeviceImpl`] is omitted; in
401/// practice every RDMA NIC is claimed by some backend.
402pub fn list_all_devices() -> Vec<IbvDeviceInfo> {
403    DEVICE_NAMES_BY_IMPL
404        .values()
405        .flat_map(|backend| backend.devices.iter().cloned())
406        .collect()
407}