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}