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