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}