Skip to main content

monarch_rdma/backend/ibverbs/
mlx_domain.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//! Mellanox (mlx5) domain strategy for [`IbvDomainImpl`].
10//!
11//! Host and non-mlx5dv device memory use the default host/dmabuf
12//! registration. On an mlx5dv-capable device, CUDA device memory is bound
13//! to an indirect mlx5dv memory key so a whole allocator segment is
14//! addressable through a single key.
15//!
16//! The segment scanning + binding bookkeeping lives here in Rust. Resource
17//! creation goes through [`MlxDomainOps`] so the (intricate) scan/bind logic
18//! can be unit-tested against a mock; the production implementation
19//! ([`ProdMlxDomainOps`]) delegates to the real functions and the
20//! [`rdmaxcel_sys::rdmaxcel_bind_mr_list`] shim. Teardown is the `Drop` of the
21//! owning RAII wrappers ([`IbvMr`], [`Mlx5dvMkey`]), correct by construction.
22
23use std::collections::HashMap;
24use std::collections::HashSet;
25use std::sync::Arc;
26use std::sync::Mutex;
27use std::sync::OnceLock;
28
29use anyhow::Context;
30
31use super::device_selection::get_cuda_device_to_ibv_device;
32use super::domain::IbvDomain;
33use super::domain::IbvDomainImpl;
34use super::domain::register_dmabuf_range;
35use super::domain::register_host_or_dmabuf_mr;
36use super::memory_region::IbvMemoryRegionKeepalive;
37use super::memory_region::IbvMemoryRegionView;
38use super::mlx_queue_pair::MlxQueuePair;
39use super::primitives::GidScope;
40use super::primitives::GidType;
41use super::primitives::IbvConfig;
42use super::primitives::IbvContext;
43use super::primitives::IbvDeviceInfo;
44use super::primitives::IbvMr;
45use super::primitives::IbvPd;
46use super::primitives::IbvQp;
47use super::queue_pair::connect;
48use super::queue_pair::get_qp_info;
49use crate::backend::ibverbs::mlx_device::MlxDevice;
50use crate::local_memory::KeepaliveLocalMemory;
51use crate::local_memory::is_device_ptr;
52
53/// A single MR must be 2 MiB aligned and covers at most 4 GiB (one page
54/// under). Larger segments are split across multiple MRs bound to one key.
55const MR_ALIGNMENT: usize = 2 * 1024 * 1024;
56const MAX_MR_SIZE: usize = 4 * 1024 * 1024 * 1024 - MR_ALIGNMENT;
57
58// ===========================================================================
59// CUDA segment scanner
60// ===========================================================================
61
62/// A CUDA memory segment reported by a registered scanner.
63#[derive(Debug, Clone, Copy, PartialEq, Eq)]
64pub struct ScannedSegment {
65    /// Base virtual address of the segment.
66    pub address: usize,
67    /// Size of the segment in bytes.
68    pub size: usize,
69    /// CUDA device ordinal the segment is allocated on.
70    pub cuda_ordinal: i32,
71    /// Whether the segment is an expandable (growable) allocation.
72    pub is_expandable: bool,
73}
74
75/// Enumerates the currently-live CUDA segments. An `Arc` (not `Box`) so
76/// [`scan_cuda_segments`] can clone it out and drop the registry lock before
77/// invoking it — see that function.
78pub type CudaSegmentScanner = Arc<dyn Fn() -> Vec<ScannedSegment> + Send + Sync>;
79
80static CUDA_SEGMENT_SCANNER: Mutex<Option<CudaSegmentScanner>> = Mutex::new(None);
81
82/// Register the process-wide CUDA segment scanner, replacing any previously
83/// registered one.
84pub fn register_cuda_segment_scanner(scanner: CudaSegmentScanner) {
85    *CUDA_SEGMENT_SCANNER
86        .lock()
87        .expect("CUDA segment scanner lock poisoned") = Some(scanner);
88}
89
90/// Invoke the registered CUDA segment scanner, returning an empty list when
91/// none is registered.
92fn scan_cuda_segments() -> Vec<ScannedSegment> {
93    // Clone the `Arc` and release the registry lock before invoking: the
94    // pytorch scanner takes the Python GIL, and even though scan_cuda_segments
95    // is never called concurrently from multiple threads (in the current design),
96    // this protects against the possibility of deadlock.
97    let scanner = CUDA_SEGMENT_SCANNER
98        .lock()
99        .expect("CUDA segment scanner lock poisoned")
100        .clone();
101    scanner.as_deref().map(|scan| scan()).unwrap_or_default()
102}
103
104// ===========================================================================
105// MlxDomainOps: the device/FFI surface MlxDomain delegates to
106// ===========================================================================
107
108/// Device/FFI operations the mlx5dv binding logic depends on. Production
109/// uses [`ProdMlxDomainOps`]; tests substitute a mock so the scan/bind
110/// algorithm can be exercised without hardware. Creation returns the owning
111/// RAII wrappers ([`IbvMr`], [`Mlx5dvMkey`]), which free their resources in the
112/// right order on `Drop`; the mock fabricates null-handle wrappers whose `Drop`
113/// is a no-op.
114pub(super) trait MlxDomainOps: Send + Sync + 'static {
115    /// Name of the RDMA device this domain drives.
116    fn device_name(&self) -> String;
117
118    /// Whether the device supports mlx5dv direct verbs.
119    fn mlx5dv_enabled(&self) -> bool;
120
121    /// CUDA ordinals whose optimal NIC is this domain's device; only segments
122    /// on these ordinals are bound here.
123    fn assigned_cuda_devices(&self) -> Vec<i32>;
124
125    /// Enumerate the currently-live CUDA segments.
126    fn scan_segments(&self) -> Vec<ScannedSegment>;
127
128    /// Register `[addr, addr + size)` of device memory as a dmabuf MR, returned
129    /// as an [`IbvMr`] (owning `pd`) that deregisters it on drop.
130    ///
131    /// # Safety
132    ///
133    /// `pd`, if non-null, must wrap a live protection domain.
134    unsafe fn register_dmabuf_range(
135        &self,
136        pd: &Arc<IbvPd>,
137        addr: usize,
138        size: usize,
139        access: i32,
140    ) -> anyhow::Result<IbvMr>;
141
142    /// Creates a loopback-connected queue pair against `domain`'s PD, returned
143    /// as an [`IbvQp`] owning its completion queues and PD. The caller stores it
144    /// for the domain's lifetime.
145    ///
146    /// # Safety
147    ///
148    /// `domain`'s context and PD, if non-null, must be live. A null context or
149    /// PD yields `Err`.
150    unsafe fn create_loopback_qp(
151        &self,
152        domain: &IbvDomain<MlxDomain>,
153        config: &IbvConfig,
154    ) -> anyhow::Result<IbvQp>;
155
156    /// Bind `mrs` to a freshly created indirect key (sized to hold
157    /// `mkey_max_entries` MRs) using `qp`'s work-request builder, returning it
158    /// as a [`Mlx5dvMkey`] owning those MR references. On failure the `mrs` are
159    /// dropped.
160    ///
161    /// # Safety
162    ///
163    /// `pd` (if non-null) must be a live protection domain, `qp` a valid queue
164    /// pair, and each MR in `mrs` null or a live MR allocated against `pd` — all
165    /// valid for this call.
166    unsafe fn bind_mr_list(
167        &self,
168        pd: &IbvPd,
169        qp: &IbvQp,
170        access: i32,
171        mrs: Vec<Arc<IbvMr>>,
172        mkey_max_entries: usize,
173    ) -> anyhow::Result<Mlx5dvMkey>;
174}
175
176/// Production [`MlxDomainOps`] backed by the real ibverbs / mlx5dv FFI.
177///
178/// The device name (taken from the queried [`IbvDeviceInfo`]) and mlx5dv
179/// support are immutable properties of the device, recorded once at
180/// construction; the per-op FFI methods operate on the `pd` passed in by
181/// [`MlxDomain`], so no context handle needs to be retained here.
182pub(super) struct ProdMlxDomainOps {
183    device_name: String,
184    mlx5dv_enabled: bool,
185}
186
187impl ProdMlxDomainOps {
188    fn new(context: &IbvContext, device_info: &IbvDeviceInfo) -> Self {
189        let ctx = context.as_ptr();
190        // A null context (e.g. a test double) has no device to query, so treat
191        // it as mlx5dv-unsupported rather than dereferencing null.
192        let mlx5dv_enabled = if ctx.is_null() {
193            false
194        } else {
195            // SAFETY: `ctx` is a non-null, live `ibv_context`; its `device`
196            // field is the `ibv_device` we query for mlx5dv support.
197            unsafe { rdmaxcel_sys::mlx5dv_is_supported((*ctx).device) }
198        };
199        Self {
200            device_name: device_info.name().to_string(),
201            mlx5dv_enabled,
202        }
203    }
204}
205
206impl MlxDomainOps for ProdMlxDomainOps {
207    fn device_name(&self) -> String {
208        self.device_name.clone()
209    }
210
211    fn mlx5dv_enabled(&self) -> bool {
212        self.mlx5dv_enabled
213    }
214
215    fn assigned_cuda_devices(&self) -> Vec<i32> {
216        get_cuda_device_to_ibv_device::<MlxDevice>()
217            .iter()
218            .enumerate()
219            .filter_map(|(ordinal, nic)| {
220                nic.as_ref()
221                    .filter(|n| n.name() == &self.device_name)
222                    .map(|_| ordinal as i32)
223            })
224            .collect()
225    }
226
227    fn scan_segments(&self) -> Vec<ScannedSegment> {
228        scan_cuda_segments()
229    }
230
231    unsafe fn register_dmabuf_range(
232        &self,
233        pd: &Arc<IbvPd>,
234        addr: usize,
235        size: usize,
236        access: i32,
237    ) -> anyhow::Result<IbvMr> {
238        // SAFETY: forwards this method's contract (non-null `pd` is a live PD).
239        unsafe { register_dmabuf_range(pd, addr, size, access) }
240    }
241
242    unsafe fn create_loopback_qp(
243        &self,
244        domain: &IbvDomain<MlxDomain>,
245        config: &IbvConfig,
246    ) -> anyhow::Result<IbvQp> {
247        // The `IbvQp` owns its completion queues and PD, so an early return or
248        // panic in the connect below still tears everything down in order.
249        // SAFETY: an `IbvDomain` holds a null-or-live context and PD.
250        let qp = unsafe { MlxQueuePair::create_ibv_qp(domain, config) }
251            .context("could not create loopback QP for mkey binding")?;
252        let context = qp.context().as_ptr();
253        let access_flags = domain.access_flags();
254        let gid = domain.device_info().select_gid(
255            config.port_num,
256            Some(GidScope::Global),
257            Some(GidType::RoCEv2),
258        )?;
259
260        // Connect the QP to itself (loopback) so it reaches RTS, the state
261        // required to post work requests.
262        // SAFETY: `qp` wraps the live QP just created above and `context` is its
263        // live device context.
264        let info = unsafe { get_qp_info(qp.as_ptr(), context, config, gid) }
265            .context("could not query loopback QP info for mkey binding")?;
266        // SAFETY: as above.
267        unsafe { connect(qp.as_ptr(), config, access_flags, &info, gid.index()) }
268            .context("could not connect loopback QP for mkey binding")?;
269
270        Ok(qp)
271    }
272
273    unsafe fn bind_mr_list(
274        &self,
275        pd: &IbvPd,
276        qp: &IbvQp,
277        access: i32,
278        mrs: Vec<Arc<IbvMr>>,
279        mkey_max_entries: usize,
280    ) -> anyhow::Result<Mlx5dvMkey> {
281        if pd.as_ptr().is_null() || qp.as_ptr().is_null() {
282            anyhow::bail!("bind_mr_list called with a null protection domain or queue pair");
283        }
284        let ptrs: Vec<*mut rdmaxcel_sys::ibv_mr> = mrs
285            .iter()
286            .map(|m| {
287                let p = m.as_ptr();
288                if p.is_null() {
289                    Err(anyhow::anyhow!(
290                        "bind_mr_list called with a null memory region"
291                    ))
292                } else {
293                    Ok(p)
294                }
295            })
296            .collect::<anyhow::Result<Vec<_>>>()?;
297        // A null out-param makes `rdmaxcel_bind_mr_list` create the key in place.
298        let mut mkey: *mut rdmaxcel_sys::mlx5dv_mkey = std::ptr::null_mut();
299        // SAFETY: `pd`/`qp` are non-null (checked above) and valid, `ptrs` holds
300        // the MRs' pointers, and failure is reported via the return code.
301        let ret = unsafe {
302            rdmaxcel_sys::rdmaxcel_bind_mr_list(
303                pd.as_ptr(),
304                qp.as_ptr(),
305                access,
306                ptrs.as_ptr() as *mut *mut rdmaxcel_sys::ibv_mr,
307                ptrs.len(),
308                mkey_max_entries,
309                &mut mkey,
310            )
311        };
312        if ret != 0 {
313            anyhow::bail!("rdmaxcel_bind_mr_list failed: error code {}", ret);
314        }
315        // SAFETY: `mkey` is a live key freshly bound over `mrs` (`ret == 0`).
316        Ok(unsafe { Mlx5dvMkey::from_raw(mkey, mrs) })
317    }
318}
319
320/// Owns an `mlx5dv_mkey` together with the [`Arc<IbvMr>`]s it binds, destroying
321/// the key on drop (a no-op if null). Caches the `(lkey, rkey)` read at bind
322/// time so a view can address through the key without further FFI.
323///
324/// The MRs are `Arc` because successive keys over a growing segment bind
325/// overlapping sets; an MR is deregistered only once the last key referencing it
326/// drops.
327#[derive(Debug)]
328pub(super) struct Mlx5dvMkey {
329    mkey: *mut rdmaxcel_sys::mlx5dv_mkey,
330    lkey: u32,
331    rkey: u32,
332    mrs: Vec<Arc<IbvMr>>,
333}
334
335// SAFETY: the only raw member is the `mlx5dv_mkey` pointer (the MRs are already
336// `Send`/`Sync`), which the mlx5dv API treats as usable and destroyable from any
337// thread (`Send`); `Mlx5dvMkey` exposes no operation that mutates the key
338// through a shared `&` (`keys` reads immutable fields), so sharing a
339// `&Mlx5dvMkey` cannot race (`Sync`).
340unsafe impl Send for Mlx5dvMkey {}
341// SAFETY: as for `Send` above.
342unsafe impl Sync for Mlx5dvMkey {}
343
344impl Mlx5dvMkey {
345    /// Takes ownership of a raw `mlx5dv_mkey` bound over `mrs`, reading its
346    /// `(lkey, rkey)` and destroying it on drop.
347    ///
348    /// # Safety
349    ///
350    /// `mkey` must be a live key returned by `rdmaxcel_bind_mr_list` over `mrs`,
351    /// owned solely by the returned value.
352    pub(super) unsafe fn from_raw(
353        mkey: *mut rdmaxcel_sys::mlx5dv_mkey,
354        mrs: Vec<Arc<IbvMr>>,
355    ) -> Self {
356        // SAFETY: per this function's contract `mkey` is a live bound key, so its
357        // `lkey`/`rkey` fields are initialized.
358        let (lkey, rkey) = unsafe { ((*mkey).lkey, (*mkey).rkey) };
359        Self {
360            mkey,
361            lkey,
362            rkey,
363            mrs,
364        }
365    }
366
367    /// The `(lkey, rkey)` to address memory through this key.
368    fn keys(&self) -> (u32, u32) {
369        (self.lkey, self.rkey)
370    }
371
372    /// The MRs this key binds.
373    fn mrs(&self) -> &Vec<Arc<IbvMr>> {
374        &self.mrs
375    }
376
377    /// Fabricates a key with the given `(lkey, rkey)` over a null `mkey` (whose
378    /// `Drop` is a no-op), binding `mrs`. Lets the mock hand back a usable key
379    /// without touching the FFI.
380    #[cfg(test)]
381    pub(super) fn with_test_keys(lkey: u32, rkey: u32, mrs: Vec<Arc<IbvMr>>) -> Self {
382        Self {
383            mkey: std::ptr::null_mut(),
384            lkey,
385            rkey,
386            mrs,
387        }
388    }
389}
390
391impl Drop for Mlx5dvMkey {
392    fn drop(&mut self) {
393        if !self.mkey.is_null() {
394            // SAFETY: a non-null `self.mkey` came from `rdmaxcel_bind_mr_list`
395            // and, since `Mlx5dvMkey` is not `Clone`, is destroyed exactly once.
396            unsafe { rdmaxcel_sys::rdmaxcel_destroy_mkey(self.mkey) };
397        }
398    }
399}
400
401// ===========================================================================
402// RegisteredSegment: a CUDA segment bound to an indirect key
403// ===========================================================================
404
405/// The mutable state of a [`RegisteredSegment`], behind one `Mutex` so the
406/// current key, superseded keys, and size all change together.
407#[derive(Debug)]
408struct RegisteredSegmentState {
409    /// Keys superseded by growth, each still owning the MRs it bound, kept alive
410    /// so views built against an earlier key stay valid.
411    stale_mkeys: Vec<Mlx5dvMkey>,
412    /// Current indirect key over the segment's MRs. `None` until the first bind;
413    /// swapped on growth, the prior key moved to `stale_mkeys`.
414    mkey: Option<Mlx5dvMkey>,
415    /// Bytes currently covered by `mkey`.
416    size: usize,
417}
418
419/// A CUDA segment bound to the device via an indirect mlx5dv key, covering
420/// `[base_virtual_addr, base_virtual_addr + size)`. Shared as
421/// `Arc<RegisteredSegment>` by [`MlxDomain`] and every view over it, so it (and
422/// the keys, MRs, and PD it owns) lives until nothing references it.
423///
424/// All mutation is serialized by the owning [`MlxDomain`]'s `segments` lock;
425/// `state` is behind a `Mutex` only to allow mutation through the shared `Arc`.
426struct RegisteredSegment {
427    ops: Arc<dyn MlxDomainOps>,
428    base_virtual_addr: usize,
429    /// The most MRs that can bind to this segment's indirect key.
430    mkey_max_entries: usize,
431    state: Mutex<RegisteredSegmentState>,
432}
433
434impl std::fmt::Debug for RegisteredSegment {
435    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
436        f.debug_struct("RegisteredSegment")
437            .field(
438                "base_virtual_addr",
439                &format!("0x{:x}", self.base_virtual_addr),
440            )
441            .field("state", &self.state)
442            .finish_non_exhaustive()
443    }
444}
445
446impl RegisteredSegment {
447    /// An unbound segment for `base_virtual_addr`: no MRs, no key, zero size.
448    /// [`Self::grow`] binds its first generation. `mkey_max_entries` caps the
449    /// MRs bound to the segment's key.
450    fn empty(
451        ops: Arc<dyn MlxDomainOps>,
452        base_virtual_addr: usize,
453        mkey_max_entries: usize,
454    ) -> Self {
455        Self {
456            ops,
457            base_virtual_addr,
458            mkey_max_entries,
459            state: Mutex::new(RegisteredSegmentState {
460                stale_mkeys: Vec::new(),
461                mkey: None,
462                size: 0,
463            }),
464        }
465    }
466
467    /// Bytes currently covered by the segment's MRs and bound to its key.
468    fn size(&self) -> usize {
469        self.state.lock().expect("segment state lock poisoned").size
470    }
471
472    /// True when `[addr, addr + size)` lies entirely within this segment.
473    fn covers(&self, addr: usize, size: usize) -> bool {
474        let end = self.base_virtual_addr + self.size();
475        addr >= self.base_virtual_addr && addr <= end && size <= end - addr
476    }
477
478    /// Grow the segment to `scanned_seg.size` (must exceed its current size):
479    /// register the new tail — the whole range on the first call from
480    /// [`Self::empty`] — bind the existing + new MRs to a fresh key, and retire
481    /// the prior key to `stale_mkeys` (rather than rebinding the in-use key,
482    /// which could race in-flight ops); the initial null key is not retired. On
483    /// any failure the freshly-registered tail MRs are deregistered and the
484    /// segment is left unchanged.
485    ///
486    /// # Safety
487    ///
488    /// If `pd` is non-null it must be a live protection domain and `qp` a valid
489    /// queue pair, both valid for this call.
490    unsafe fn grow(
491        &self,
492        pd: &Arc<IbvPd>,
493        qp: &IbvQp,
494        access: i32,
495        scanned_seg: &ScannedSegment,
496    ) -> anyhow::Result<()> {
497        let mut state = self.state.lock().expect("segment state lock poisoned");
498        assert!(
499            scanned_seg.address == self.base_virtual_addr,
500            "grow called for base 0x{:x} on a segment based at 0x{:x}",
501            scanned_seg.address,
502            self.base_virtual_addr
503        );
504        // A segment only ever grows; shrinking would underflow
505        // `scanned_seg.size - state.size` below.
506        assert!(
507            scanned_seg.size >= state.size,
508            "segment at 0x{:x} shrank from {} to {} bytes; segments must never shrink",
509            self.base_virtual_addr,
510            state.size,
511            scanned_seg.size
512        );
513        // Growing to the current size has nothing to add.
514        if scanned_seg.size == state.size {
515            return Ok(());
516        }
517        // SAFETY: per this function's contract `pd` is null or a live PD and
518        // `qp` a valid QP; the tail MRs registered here belong to this segment.
519        let new_tail = unsafe {
520            register_range(
521                &self.ops,
522                pd,
523                access,
524                self.base_virtual_addr + state.size,
525                scanned_seg.size - state.size,
526            )
527        }?;
528
529        // Bind the existing MRs (reused from the current key) plus the new tail
530        // to a fresh key. On failure `all` is dropped by `bind_mr_list`: the new
531        // tail's MRs deregister while the existing ones survive via the current
532        // key, leaving the segment unchanged.
533        let mut all = state
534            .mkey
535            .as_ref()
536            .map(|k| k.mrs().clone())
537            .unwrap_or_default();
538        all.extend(new_tail);
539        // A single indirect key binds at most `mkey_max_entries` MRs; exceeding
540        // it would fault at transfer time. Fail here instead; the caller falls
541        // back to per-region dmabuf registration. The freshly-registered tail
542        // drops as `all` unwinds, leaving the segment unchanged.
543        if all.len() > self.mkey_max_entries {
544            anyhow::bail!(
545                "segment at 0x{:x} needs {} MRs, exceeding mkey max entries {}",
546                self.base_virtual_addr,
547                all.len(),
548                self.mkey_max_entries
549            );
550        }
551        // SAFETY: same contract; `all` are this segment's live MRs.
552        let new_mkey = unsafe {
553            self.ops
554                .bind_mr_list(pd, qp, access, all, self.mkey_max_entries)
555        }?;
556
557        // Retire the prior key (kept for in-flight ops built against it) and
558        // install the new one.
559        if let Some(prior) = state.mkey.replace(new_mkey) {
560            state.stale_mkeys.push(prior);
561        }
562        state.size = scanned_seg.size;
563        Ok(())
564    }
565
566    /// Build a view anchored at `addr`. The view's guard is the segment itself,
567    /// pinning it (and thus its current key, the key's MRs, and the PD they own)
568    /// alive for the view's lifetime, so the bound key stays valid while the view
569    /// is in use. Indirect keys present their MRs as a flat zero-based space, so
570    /// `rdma_addr` is the offset from the segment base.
571    fn view(seg: &Arc<Self>, addr: usize, size: usize) -> IbvMemoryRegionView {
572        let (lkey, rkey) = {
573            let state = seg.state.lock().expect("segment state lock poisoned");
574            // `view` is only called on a segment that covers the request, which
575            // means it has been bound, so the current key is `Some`.
576            state
577                .mkey
578                .as_ref()
579                .map(Mlx5dvMkey::keys)
580                .expect("view of a segment with no bound key")
581        };
582        // The segment is its own keepalive: cloning the `Arc<RegisteredSegment>`
583        // pins the current key (which owns the MRs, which own the PD) for the
584        // view's life.
585        let guard: Arc<dyn IbvMemoryRegionKeepalive> = seg.clone();
586        IbvMemoryRegionView::new(
587            addr,
588            addr - seg.base_virtual_addr,
589            size,
590            lkey,
591            rkey,
592            seg.ops.device_name(),
593            guard,
594        )
595    }
596}
597
598// A `RegisteredSegment` is its own MR keepalive: it owns the current key, which
599// owns the bound MRs, which own the PD — all freed in order on its `Drop`.
600impl IbvMemoryRegionKeepalive for RegisteredSegment {}
601
602/// Register `[start, start + len)` of device memory as dmabuf MRs in
603/// `<= MAX_MR_SIZE` chunks. All-or-nothing: on any failure the MRs registered
604/// so far drop (deregistering) as the returned `Vec` unwinds.
605///
606/// # Safety
607///
608/// If `pd` is non-null it must be a live protection domain whose context
609/// outlives this call.
610unsafe fn register_range(
611    ops: &Arc<dyn MlxDomainOps>,
612    pd: &Arc<IbvPd>,
613    access: i32,
614    start: usize,
615    len: usize,
616) -> anyhow::Result<Vec<Arc<IbvMr>>> {
617    let mut mrs: Vec<Arc<IbvMr>> = Vec::new();
618    let mut chunk_start = start;
619    let mut remaining = len;
620    while remaining > 0 {
621        let chunk = remaining.min(MAX_MR_SIZE);
622        let result = if chunk.is_multiple_of(MR_ALIGNMENT) {
623            // SAFETY: `pd` is null or a live PD per this function's contract.
624            unsafe { ops.register_dmabuf_range(pd, chunk_start, chunk, access) }
625        } else {
626            Err(anyhow::anyhow!(
627                "CUDA chunk size {} is not a multiple of {}",
628                chunk,
629                MR_ALIGNMENT
630            ))
631        };
632        // On error, return early; the MRs collected so far drop here,
633        // deregistering them.
634        mrs.push(Arc::new(result?));
635        chunk_start += chunk;
636        remaining -= chunk;
637    }
638    Ok(mrs)
639}
640
641// ===========================================================================
642// MlxDomain
643// ===========================================================================
644
645/// Mellanox [`IbvDomainImpl`].
646pub struct MlxDomain {
647    ops: Arc<dyn MlxDomainOps>,
648    /// Config for the loopback binding QP.
649    config: IbvConfig,
650    mlx5dv_enabled: bool,
651    /// CUDA ordinals whose optimal NIC is this device. Only segments on
652    /// these ordinals are bound here.
653    cuda_ordinals: Vec<i32>,
654    /// Caps the MRs bound to each segment's indirect key.
655    mkey_max_entries: usize,
656    /// Lazily-created loopback QP (an [`IbvQp`] owning its completion queues and
657    /// PD) used to post key-binding work requests, destroyed when this domain
658    /// drops.
659    loopback: OnceLock<IbvQp>,
660    /// Currently-bound segments, keyed by `(base address, CUDA ordinal)`. Each
661    /// grows in place (reusing its MRs, retiring superseded keys internally);
662    /// a key whose base vanishes from the scan is dropped (a live view keeps
663    /// its own `Arc` alive regardless).
664    segments: Mutex<HashMap<(usize, i32), Arc<RegisteredSegment>>>,
665}
666
667impl std::fmt::Debug for MlxDomain {
668    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
669        f.debug_struct("MlxDomain")
670            .field("mlx5dv_enabled", &self.mlx5dv_enabled)
671            .field("cuda_ordinals", &self.cuda_ordinals)
672            .finish_non_exhaustive()
673    }
674}
675
676impl MlxDomain {
677    /// Build a domain over the given ops, deriving mlx5dv support and the
678    /// served CUDA ordinals from them. `mkey_max_entries` caps the MRs bound to
679    /// every segment's indirect key.
680    fn new_with_ops(
681        ops: Arc<dyn MlxDomainOps>,
682        config: IbvConfig,
683        mkey_max_entries: usize,
684    ) -> Self {
685        let mlx5dv_enabled = ops.mlx5dv_enabled();
686        let cuda_ordinals = ops.assigned_cuda_devices();
687        Self {
688            ops,
689            config,
690            mlx5dv_enabled,
691            cuda_ordinals,
692            mkey_max_entries,
693            loopback: OnceLock::new(),
694            segments: Mutex::new(HashMap::new()),
695        }
696    }
697
698    /// Get-or-create the loopback QP.
699    fn loopback_qp_ptr(&self, domain: &IbvDomain<MlxDomain>) -> anyhow::Result<&IbvQp> {
700        // `OnceLock::get_or_try_init` would fit here but is still unstable
701        // (`once_cell_try`); calls are serialized under the `segments` lock,
702        // so this check-then-set is race-free.
703        if let Some(qp) = self.loopback.get() {
704            return Ok(qp);
705        }
706        // SAFETY: an `IbvDomain` guarantees its context and PD are null or live;
707        // `create_loopback_qp` rejects null.
708        let qp = unsafe { self.ops.create_loopback_qp(domain, &self.config) }?;
709        let _ = self.loopback.set(qp);
710        Ok(self.loopback.get().expect("loopback just set"))
711    }
712
713    /// Bind CUDA `[addr, addr + size)` via an indirect mlx5dv key. Scans for
714    /// live segments on the ordinals this NIC serves, binding a fresh segment
715    /// for any that appeared and growing in place any that expanded, dropping
716    /// any whose base address vanished from the scan, then returns a view
717    /// anchored at `addr`.
718    ///
719    /// # Safety
720    ///
721    /// `domain.as_ptr()` must be null or a live protection domain whose context
722    /// outlives this call.
723    unsafe fn register_cuda_mlx5dv_mr(
724        &self,
725        domain: &IbvDomain<MlxDomain>,
726        addr: usize,
727        size: usize,
728    ) -> anyhow::Result<IbvMemoryRegionView> {
729        let pd = domain.pd();
730        let access = self.access_flags();
731        let mut segments = self
732            .segments
733            .lock()
734            .expect("mlx domain segments lock poisoned");
735
736        // Fast path: a current binding already covers the request.
737        if let Some(seg) = segments.values().find(|s| s.covers(addr, size)) {
738            return Ok(RegisteredSegment::view(seg, addr, size));
739        }
740
741        // Pull live segments and keep only the ones on ordinals we serve.
742        let scanned: Vec<ScannedSegment> = self
743            .ops
744            .scan_segments()
745            .into_iter()
746            .filter(|s| self.cuda_ordinals.contains(&s.cuda_ordinal))
747            .collect();
748
749        let qp = self.loopback_qp_ptr(domain)?;
750
751        let mut snapshot: HashSet<(usize, i32)> = HashSet::new();
752        for scanned_seg in &scanned {
753            let key = (scanned_seg.address, scanned_seg.cuda_ordinal);
754            snapshot.insert(key);
755            match segments.get(&key) {
756                // Already bound at this extent: nothing to do.
757                Some(seg) if seg.size() == scanned_seg.size => {}
758                // Grew: extend the existing segment in place (reuses its MRs,
759                // retires its prior key internally).
760                // SAFETY: `pd`/`qp` satisfy this function's contract (live PD or
761                // null; valid loopback QP) and are forwarded unchanged.
762                Some(seg) => unsafe { seg.grow(pd, qp, access, scanned_seg) }?,
763                // New: create an empty segment and grow it to the full range.
764                None => {
765                    let fresh = Arc::new(RegisteredSegment::empty(
766                        self.ops.clone(),
767                        scanned_seg.address,
768                        self.mkey_max_entries,
769                    ));
770                    // SAFETY: as above.
771                    unsafe { fresh.grow(pd, qp, access, scanned_seg) }?;
772                    segments.insert(key, fresh);
773                }
774            }
775        }
776
777        // Drop segments whose base address vanished from the scan: the
778        // allocator freed that region. A still-live view keeps its own `Arc`
779        // alive regardless, so this never frees memory in use.
780        segments.retain(|key, _| snapshot.contains(key));
781
782        // Serve the caller's view from a current segment that covers it.
783        segments
784            .values()
785            .find(|s| s.covers(addr, size))
786            .map(|s| RegisteredSegment::view(s, addr, size))
787            .ok_or_else(|| {
788                anyhow::anyhow!(
789                    "CUDA address 0x{:x} + size {} is not covered by any scanned segment on the CUDA ordinals {:?} mapped to this NIC",
790                    addr,
791                    size,
792                    self.cuda_ordinals,
793                )
794            })
795    }
796}
797
798impl IbvDomainImpl for MlxDomain {
799    type QueuePair = MlxQueuePair;
800
801    unsafe fn new(context: &IbvContext, device_info: &IbvDeviceInfo, config: &IbvConfig) -> Self {
802        Self::new_with_ops(
803            Arc::new(ProdMlxDomainOps::new(context, device_info)),
804            config.clone(),
805            // An indirect mkey cannot bind more MRs than its device's `max_sge`.
806            device_info.max_sge().max(0) as usize,
807        )
808    }
809
810    fn access_flags(&self) -> i32 {
811        (rdmaxcel_sys::ibv_access_flags::IBV_ACCESS_LOCAL_WRITE
812            | rdmaxcel_sys::ibv_access_flags::IBV_ACCESS_REMOTE_WRITE
813            | rdmaxcel_sys::ibv_access_flags::IBV_ACCESS_REMOTE_READ
814            | rdmaxcel_sys::ibv_access_flags::IBV_ACCESS_REMOTE_ATOMIC)
815            .0 as i32
816    }
817
818    unsafe fn register_mr(
819        domain: &IbvDomain<MlxDomain>,
820        mem: &KeepaliveLocalMemory,
821    ) -> anyhow::Result<IbvMemoryRegionView> {
822        let this = domain.domain_impl();
823        if this.mlx5dv_enabled && is_device_ptr(mem.addr()) {
824            // SAFETY: `domain.as_ptr()` is null or a live PD, per this method's
825            // contract.
826            match unsafe { this.register_cuda_mlx5dv_mr(domain, mem.addr(), mem.size()) } {
827                Ok(view) => return Ok(view),
828                Err(e) => {
829                    tracing::warn!("mlx5dv CUDA registration failed, falling back to dmabuf: {e}")
830                }
831            }
832        }
833        // SAFETY: `domain.as_ptr()` is null or a live PD (per this method's
834        // contract; `register_host_or_dmabuf_mr` errors on null), and the caller
835        // keeps `mem`'s backing memory valid for the MR's lifetime.
836        unsafe { register_host_or_dmabuf_mr(domain, mem) }
837    }
838}
839
840#[cfg(test)]
841mod tests {
842    use std::sync::Mutex;
843    use std::sync::MutexGuard;
844
845    use super::super::primitives::IbvDeviceInfo;
846    use super::*;
847
848    const MIB2: usize = 2 * 1024 * 1024;
849    const SERVED_NIC: &str = "mlx5_0";
850
851    fn seg(address: usize, size: usize, cuda_ordinal: i32) -> ScannedSegment {
852        ScannedSegment {
853            address,
854            size,
855            cuda_ordinal,
856            is_expandable: false,
857        }
858    }
859
860    /// A recorded `register_dmabuf_range` call.
861    #[derive(Debug, Clone, PartialEq, Eq)]
862    struct DmabufCall {
863        addr: usize,
864        size: usize,
865        access: i32,
866    }
867
868    /// A recorded (successful) `bind_mr_list` call: the MRs it bound (compared
869    /// by `Arc::ptr_eq`, since the mock's MRs wrap null pointers).
870    #[derive(Debug, Clone)]
871    struct BindCall {
872        mrs: Vec<Arc<IbvMr>>,
873    }
874
875    /// Recorded state + scripted behavior for [`MockOps`]. Creation calls
876    /// (`dmabuf_calls`, `bind_calls`, `scan_calls`, `loopback_created`) are
877    /// recorded so tests can assert on the scan/bind algorithm. The returned
878    /// [`IbvMr`]/[`Mlx5dvMkey`]/[`IbvQp`] wrap null handles, so their `Drop` is a
879    /// no-op and FFI teardown is not observed here — that ordering is
880    /// structurally guaranteed by ownership and exercised by the hardware tests.
881    #[derive(Default)]
882    struct MockState {
883        device_name: String,
884        mlx5dv_enabled: bool,
885        served_ordinals: Vec<i32>,
886        scan: Vec<ScannedSegment>,
887        next_handle: usize,
888        /// Number of `create_loopback_qp` calls; tests check the QP is
889        /// created once and reused.
890        loopback_created: usize,
891        fail_dmabuf_after: Option<usize>,
892        fail_bind: bool,
893        scan_calls: usize,
894        /// Every `register_dmabuf_range` call, including a failing one.
895        dmabuf_calls: Vec<DmabufCall>,
896        /// Every successful `bind_mr_list` call.
897        bind_calls: Vec<BindCall>,
898    }
899
900    impl MockState {
901        fn mint(&mut self) -> usize {
902            self.next_handle += 0x1000;
903            self.next_handle
904        }
905    }
906
907    struct MockOps {
908        state: Mutex<MockState>,
909    }
910
911    impl MockOps {
912        fn new(device_name: &str, mlx5dv_enabled: bool, served_ordinals: &[i32]) -> Arc<Self> {
913            Arc::new(Self {
914                state: Mutex::new(MockState {
915                    device_name: device_name.to_string(),
916                    mlx5dv_enabled,
917                    served_ordinals: served_ordinals.to_vec(),
918                    next_handle: 0x1_0000,
919                    ..Default::default()
920                }),
921            })
922        }
923
924        fn lock(&self) -> MutexGuard<'_, MockState> {
925            self.state.lock().expect("mock state poisoned")
926        }
927    }
928
929    impl MlxDomainOps for MockOps {
930        fn device_name(&self) -> String {
931            self.lock().device_name.clone()
932        }
933
934        fn mlx5dv_enabled(&self) -> bool {
935            self.lock().mlx5dv_enabled
936        }
937
938        fn assigned_cuda_devices(&self) -> Vec<i32> {
939            self.lock().served_ordinals.clone()
940        }
941
942        fn scan_segments(&self) -> Vec<ScannedSegment> {
943            let mut s = self.lock();
944            s.scan_calls += 1;
945            s.scan.clone()
946        }
947
948        unsafe fn register_dmabuf_range(
949            &self,
950            _pd: &Arc<IbvPd>,
951            addr: usize,
952            size: usize,
953            access: i32,
954        ) -> anyhow::Result<IbvMr> {
955            let mut s = self.lock();
956            s.dmabuf_calls.push(DmabufCall { addr, size, access });
957            if let Some(n) = s.fail_dmabuf_after
958                && s.dmabuf_calls.len() > n
959            {
960                anyhow::bail!("mock dmabuf registration failure");
961            }
962            // A null MR: its `Drop` is a no-op, so the mock need not track it.
963            Ok(IbvMr::null())
964        }
965
966        unsafe fn create_loopback_qp(
967            &self,
968            _domain: &IbvDomain<MlxDomain>,
969            _config: &IbvConfig,
970        ) -> anyhow::Result<IbvQp> {
971            // A null QP placeholder (its `Drop` is a no-op); just count the call.
972            self.lock().loopback_created += 1;
973            Ok(IbvQp::null())
974        }
975
976        unsafe fn bind_mr_list(
977            &self,
978            _pd: &IbvPd,
979            _qp: &IbvQp,
980            _access: i32,
981            mrs: Vec<Arc<IbvMr>>,
982            _mkey_max_entries: usize,
983        ) -> anyhow::Result<Mlx5dvMkey> {
984            let mut s = self.lock();
985            if s.fail_bind {
986                anyhow::bail!("mock bind failure");
987            }
988            s.bind_calls.push(BindCall { mrs: mrs.clone() });
989            // Derive distinct keys from a freshly minted handle so tests can tell
990            // segments and generations apart; the key wraps a null `mkey`.
991            let v = s.mint() as u32;
992            Ok(Mlx5dvMkey::with_test_keys(v, v ^ 0xffff, mrs))
993        }
994    }
995
996    /// `mkey_max_entries` for tests: large enough that the small MR counts here
997    /// never trip the segment cap (that limit has its own test).
998    const TEST_MKEY_MAX_ENTRIES: usize = 32;
999
1000    /// A domain wrapping the mock-driven [`MlxDomain`] under test. Its `pd`
1001    /// (and, through it, its context) is null (no-op `Drop`). Drive the strategy
1002    /// via [`IbvDomain::domain_impl`].
1003    fn domain(mock: Arc<MockOps>) -> Arc<IbvDomain<MlxDomain>> {
1004        let mlx = MlxDomain::new_with_ops(mock, IbvConfig::default(), TEST_MKEY_MAX_ENTRIES);
1005        // SAFETY: `IbvPd::null()` holds a null PD (and, through it, a null
1006        // context) whose `Drop`s are no-ops.
1007        unsafe {
1008            Arc::new(IbvDomain::for_test(
1009                Arc::new(IbvPd::null()),
1010                IbvDeviceInfo::for_test_named("test"),
1011                mlx,
1012            ))
1013        }
1014    }
1015
1016    /// Drive `domain`'s own [`MlxDomain`] strategy to register CUDA memory.
1017    fn register_cuda(
1018        domain: &Arc<IbvDomain<MlxDomain>>,
1019        addr: usize,
1020        size: usize,
1021    ) -> anyhow::Result<IbvMemoryRegionView> {
1022        // SAFETY: `domain`'s pd is null and the `MockOps` never dereference it.
1023        unsafe {
1024            domain
1025                .domain_impl()
1026                .register_cuda_mlx5dv_mr(domain, addr, size)
1027        }
1028    }
1029
1030    /// A null `Arc<IbvPd>` for driving [`RegisteredSegment::grow`] directly; the
1031    /// `MockOps` never dereference it.
1032    fn null_pd() -> Arc<IbvPd> {
1033        Arc::new(IbvPd::null())
1034    }
1035
1036    fn null_qp() -> IbvQp {
1037        IbvQp::null()
1038    }
1039
1040    /// Upcast the mock to the `Arc<dyn MlxDomainOps>` `RegisteredSegment` takes.
1041    fn dyn_ops(ops: &Arc<MockOps>) -> Arc<dyn MlxDomainOps> {
1042        ops.clone()
1043    }
1044
1045    /// Bind a fresh segment `[base, base + size)`: an empty segment grown once.
1046    fn bind_fresh(ops: &Arc<MockOps>, base: usize, size: usize) -> Arc<RegisteredSegment> {
1047        let segment = Arc::new(RegisteredSegment::empty(
1048            dyn_ops(ops),
1049            base,
1050            TEST_MKEY_MAX_ENTRIES,
1051        ));
1052        // SAFETY: `MockOps` ignores the `pd`/`qp`; the nulls are never deref'd.
1053        unsafe { segment.grow(&null_pd(), &null_qp(), 0, &seg(base, size, 0)) }
1054            .expect("fresh bind should succeed");
1055        segment
1056    }
1057
1058    // ----- RegisteredSegment binding-core unit tests -----
1059
1060    #[test]
1061    fn test_scanner_registration_round_trips() {
1062        register_cuda_segment_scanner(Arc::new(|| vec![seg(0x1000, MIB2, 3)]));
1063        assert_eq!(
1064            scan_cuda_segments(),
1065            vec![seg(0x1000, MIB2, 3)],
1066            "the registered scanner's output is returned"
1067        );
1068    }
1069
1070    #[test]
1071    fn test_fresh_bind_registers_one_mr_and_key() {
1072        let ops = MockOps::new(SERVED_NIC, true, &[0]);
1073        let base = 0x10_0000_0000;
1074        let rs = bind_fresh(&ops, base, MIB2);
1075
1076        assert_eq!(rs.size(), MIB2);
1077        assert!(
1078            rs.state.lock().unwrap().stale_mkeys.is_empty(),
1079            "a fresh segment has no superseded keys"
1080        );
1081        assert_eq!(
1082            rs.state.lock().unwrap().mkey.as_ref().unwrap().mrs().len(),
1083            1,
1084            "the current key binds the one MR"
1085        );
1086        let s = ops.lock();
1087        assert_eq!(
1088            s.dmabuf_calls,
1089            vec![DmabufCall {
1090                addr: base,
1091                size: MIB2,
1092                access: 0,
1093            }],
1094            "the whole segment is registered as one MR at its base"
1095        );
1096        assert_eq!(s.bind_calls.len(), 1);
1097        assert_eq!(s.bind_calls[0].mrs.len(), 1, "one MR bound");
1098    }
1099
1100    #[test]
1101    fn test_large_segment_splits_into_chunks() {
1102        let ops = MockOps::new(SERVED_NIC, true, &[0]);
1103        let base = 0x10_0000_0000;
1104        // One MR maxes out at MAX_MR_SIZE, so MAX_MR_SIZE + 2 MiB needs two,
1105        // both bound to a single key.
1106        let rs = bind_fresh(&ops, base, MAX_MR_SIZE + MIB2);
1107
1108        assert_eq!(
1109            rs.state.lock().unwrap().mkey.as_ref().unwrap().mrs().len(),
1110            2
1111        );
1112        let s = ops.lock();
1113        assert_eq!(
1114            s.dmabuf_calls,
1115            vec![
1116                DmabufCall {
1117                    addr: base,
1118                    size: MAX_MR_SIZE,
1119                    access: 0,
1120                },
1121                DmabufCall {
1122                    addr: base + MAX_MR_SIZE,
1123                    size: MIB2,
1124                    access: 0,
1125                },
1126            ],
1127            "the range splits into a MAX_MR_SIZE chunk and a 2 MiB tail"
1128        );
1129        assert_eq!(s.bind_calls.len(), 1);
1130        assert_eq!(
1131            s.bind_calls[0].mrs.len(),
1132            2,
1133            "both chunk MRs bound to one key"
1134        );
1135    }
1136
1137    #[test]
1138    fn test_grow_reuses_mrs_and_retires_prior_key() {
1139        let ops = MockOps::new(SERVED_NIC, true, &[0]);
1140        let base = 0x10_0000_0000;
1141        let rs = bind_fresh(&ops, base, MIB2);
1142        let mkey_a = rs.state.lock().unwrap().mkey.as_ref().unwrap().keys();
1143        let mr_a = ops.lock().bind_calls[0].mrs[0].clone();
1144
1145        // SAFETY: `MockOps` ignores the `pd`/`qp`; the nulls are never deref'd.
1146        unsafe { rs.grow(&null_pd(), &null_qp(), 0, &seg(base, 2 * MIB2, 0)) }
1147            .expect("growth should succeed");
1148
1149        assert_eq!(rs.size(), 2 * MIB2);
1150        assert_eq!(
1151            rs.state.lock().unwrap().mkey.as_ref().unwrap().mrs().len(),
1152            2,
1153            "the original MR is reused and one tail MR appended"
1154        );
1155        assert_eq!(
1156            rs.state.lock().unwrap().stale_mkeys.len(),
1157            1,
1158            "the prior key is parked as stale"
1159        );
1160        assert_eq!(
1161            rs.state.lock().unwrap().stale_mkeys[0].keys(),
1162            mkey_a,
1163            "the parked key is the original, not a fresh one"
1164        );
1165        let s = ops.lock();
1166        // Only the new tail is registered; the original MR is reused.
1167        assert_eq!(
1168            s.dmabuf_calls,
1169            vec![
1170                DmabufCall {
1171                    addr: base,
1172                    size: MIB2,
1173                    access: 0,
1174                },
1175                DmabufCall {
1176                    addr: base + MIB2,
1177                    size: MIB2,
1178                    access: 0,
1179                },
1180            ],
1181            "growth registers only the new tail, at base + MIB2"
1182        );
1183        assert_eq!(s.bind_calls.len(), 2);
1184        assert_eq!(s.bind_calls[0].mrs.len(), 1);
1185        assert!(
1186            Arc::ptr_eq(&s.bind_calls[0].mrs[0], &mr_a),
1187            "the first bind covered just the original MR"
1188        );
1189        assert_eq!(s.bind_calls[1].mrs.len(), 2);
1190        assert!(
1191            Arc::ptr_eq(&s.bind_calls[1].mrs[0], &mr_a)
1192                && !Arc::ptr_eq(&s.bind_calls[1].mrs[1], &mr_a),
1193            "the second bind reuses the original MR and adds a new tail"
1194        );
1195    }
1196
1197    #[test]
1198    fn test_grow_failure_leaves_segment_unchanged() {
1199        let ops = MockOps::new(SERVED_NIC, true, &[0]);
1200        let base = 0x10_0000_0000;
1201        let rs = bind_fresh(&ops, base, MIB2);
1202        // The fresh bind used one dmabuf call; fail the second tail chunk.
1203        ops.lock().fail_dmabuf_after = Some(2);
1204
1205        // SAFETY: `MockOps` ignores the `pd`/`qp`; the nulls are never deref'd.
1206        let result = unsafe {
1207            rs.grow(
1208                &null_pd(),
1209                &null_qp(),
1210                0,
1211                &seg(base, MAX_MR_SIZE + 2 * MIB2, 0),
1212            )
1213        };
1214        assert!(
1215            result.is_err(),
1216            "growth fails when a tail MR fails to register"
1217        );
1218
1219        assert_eq!(rs.size(), MIB2, "size unchanged after a failed growth");
1220        assert_eq!(
1221            rs.state.lock().unwrap().mkey.as_ref().unwrap().mrs().len(),
1222            1,
1223            "no tail MRs committed"
1224        );
1225        assert!(
1226            rs.state.lock().unwrap().stale_mkeys.is_empty(),
1227            "no key retired"
1228        );
1229        assert_eq!(
1230            ops.lock().bind_calls.len(),
1231            1,
1232            "only the original bind happened; the growth bind was never reached"
1233        );
1234    }
1235
1236    #[test]
1237    fn test_grow_to_equal_size_is_noop() {
1238        let ops = MockOps::new(SERVED_NIC, true, &[0]);
1239        let base = 0x10_0000_0000;
1240        let rs = bind_fresh(&ops, base, 2 * MIB2);
1241        let binds_before = ops.lock().bind_calls.len();
1242
1243        // A scan reporting the same size must be a no-op rather than re-binding.
1244        // SAFETY: `MockOps` ignores the `pd`/`qp`; the nulls are never deref'd.
1245        unsafe { rs.grow(&null_pd(), &null_qp(), 0, &seg(base, 2 * MIB2, 0)) }
1246            .expect("equal-size grow is a no-op");
1247
1248        assert_eq!(rs.size(), 2 * MIB2, "size is unchanged");
1249        let s = ops.lock();
1250        assert_eq!(s.bind_calls.len(), binds_before, "no new bind performed");
1251        assert_eq!(s.dmabuf_calls.len(), 1, "no new MR registered");
1252    }
1253
1254    #[test]
1255    fn test_view_pins_segment_and_anchors_at_base() {
1256        let ops = MockOps::new(SERVED_NIC, true, &[0]);
1257        let base = 0x10_0000_0000;
1258        let rs = bind_fresh(&ops, base, MIB2);
1259
1260        assert!(rs.covers(base + 0x400, 4096));
1261        let view = RegisteredSegment::view(&rs, base + 0x400, 4096);
1262        assert_eq!(
1263            view.rdma_addr, 0x400,
1264            "rdma_addr is the offset from the segment base"
1265        );
1266        assert_eq!(view.size, 4096);
1267        assert_eq!(view.device_name, SERVED_NIC);
1268        assert_eq!(
1269            Arc::strong_count(&rs),
1270            2,
1271            "the view's guard pins the segment alive (our ref + the view's)"
1272        );
1273
1274        drop(view);
1275        assert_eq!(
1276            Arc::strong_count(&rs),
1277            1,
1278            "dropping the view releases its segment ref"
1279        );
1280    }
1281
1282    #[test]
1283    fn test_partial_dmabuf_failure_cleans_up() {
1284        let ops = MockOps::new(SERVED_NIC, true, &[0]);
1285        ops.lock().fail_dmabuf_after = Some(1);
1286        let base = 0x10_0000_0000;
1287        let segment = RegisteredSegment::empty(dyn_ops(&ops), base, TEST_MKEY_MAX_ENTRIES);
1288
1289        // Two chunks; the second dmabuf registration fails.
1290        // SAFETY: `MockOps` ignores the `pd`/`qp`; the nulls are never deref'd.
1291        let result =
1292            unsafe { segment.grow(&null_pd(), &null_qp(), 0, &seg(base, MAX_MR_SIZE + MIB2, 0)) };
1293        assert!(
1294            result.is_err(),
1295            "the first bind fails when a chunk registration fails"
1296        );
1297        assert_eq!(segment.size(), 0, "the segment is left empty");
1298        assert!(
1299            segment.state.lock().unwrap().mkey.is_none(),
1300            "no key installed"
1301        );
1302        let s = ops.lock();
1303        assert!(s.bind_calls.is_empty(), "no bind performed");
1304    }
1305
1306    #[test]
1307    fn test_bind_failure_cleans_up() {
1308        let ops = MockOps::new(SERVED_NIC, true, &[0]);
1309        ops.lock().fail_bind = true;
1310        let base = 0x10_0000_0000;
1311        let segment = RegisteredSegment::empty(dyn_ops(&ops), base, TEST_MKEY_MAX_ENTRIES);
1312
1313        // SAFETY: `MockOps` ignores the `pd`/`qp`; the nulls are never deref'd.
1314        let result = unsafe { segment.grow(&null_pd(), &null_qp(), 0, &seg(base, MIB2, 0)) };
1315        assert!(result.is_err(), "the bind fails when bind_mr_list fails");
1316        assert_eq!(segment.size(), 0, "the segment is left empty");
1317        assert!(
1318            segment.state.lock().unwrap().mkey.is_none(),
1319            "no key installed on bind failure"
1320        );
1321    }
1322
1323    #[test]
1324    fn test_grow_exceeding_mkey_max_entries_errors() {
1325        let ops = MockOps::new(SERVED_NIC, true, &[0]);
1326        let base = 0x10_0000_0000;
1327        // Cap at one MR; a two-chunk segment needs two, so grow must reject it
1328        // rather than bind an over-capacity (and silently truncated) key.
1329        let segment = RegisteredSegment::empty(dyn_ops(&ops), base, 1);
1330        // SAFETY: `MockOps` ignores the `pd`/`qp`; the nulls are never deref'd.
1331        let result =
1332            unsafe { segment.grow(&null_pd(), &null_qp(), 0, &seg(base, MAX_MR_SIZE + MIB2, 0)) };
1333        assert!(
1334            result.is_err(),
1335            "grow must reject a segment needing more MRs than mkey max entries"
1336        );
1337        assert_eq!(segment.size(), 0, "the segment is left unchanged");
1338        assert!(
1339            ops.lock().bind_calls.is_empty(),
1340            "no bind attempted past the cap"
1341        );
1342    }
1343
1344    // ----- MlxDomain integration tests -----
1345
1346    #[test]
1347    fn test_cuda_ordinals_and_mlx5dv_enabled_derived_from_ops() {
1348        let mock = MockOps::new(SERVED_NIC, true, &[0, 2]);
1349        let domain = domain(mock);
1350        assert_eq!(domain.domain_impl().cuda_ordinals, vec![0, 2]);
1351        assert!(domain.domain_impl().mlx5dv_enabled);
1352    }
1353
1354    #[test]
1355    fn test_fresh_bind() {
1356        let base = 0x10_0000_0000;
1357        let mock = MockOps::new(SERVED_NIC, true, &[0]);
1358        mock.lock().scan = vec![seg(base, MIB2, 0)];
1359        let domain = domain(mock.clone());
1360
1361        let view = register_cuda(&domain, base, 4096).expect("fresh bind should succeed");
1362
1363        // The per-segment binding details are covered by the binding-core unit
1364        // tests above; here we only check the domain wired scan → bind → view.
1365        assert_eq!(view.rdma_addr, 0, "view is anchored at the segment base");
1366        assert_eq!(view.size, 4096);
1367        assert_eq!(view.device_name, SERVED_NIC);
1368        assert_eq!(mock.lock().bind_calls.len(), 1, "one segment bound");
1369    }
1370
1371    #[test]
1372    fn test_fast_path_skips_scan() {
1373        let mock = MockOps::new(SERVED_NIC, true, &[0]);
1374        mock.lock().scan = vec![seg(0x10_0000_0000, MIB2, 0)];
1375        let domain = domain(mock.clone());
1376
1377        register_cuda(&domain, 0x10_0000_0000, 4096).unwrap();
1378        let scans_after_first = mock.lock().scan_calls;
1379
1380        // A second request covered by the existing binding must not rescan
1381        // or rebind.
1382        register_cuda(&domain, 0x10_0000_0400, 4096).unwrap();
1383        let s = mock.lock();
1384        assert_eq!(s.scan_calls, scans_after_first, "fast path skips the scan");
1385        assert_eq!(s.bind_calls.len(), 1, "fast path performs no new bind");
1386    }
1387
1388    #[test]
1389    fn test_segment_growth_via_register() {
1390        let base = 0x10_0000_0000;
1391        let mock = MockOps::new(SERVED_NIC, true, &[0]);
1392        mock.lock().scan = vec![seg(base, MIB2, 0)];
1393        let domain = domain(mock.clone());
1394
1395        register_cuda(&domain, base, 4096).unwrap();
1396
1397        // The scanner now reports the segment has grown; a request into the new
1398        // tail grows the mapped segment in place and serves a view from it. (MR
1399        // reuse / key retirement are covered by the binding-core unit tests.)
1400        mock.lock().scan = vec![seg(base, 2 * MIB2, 0)];
1401        let view =
1402            register_cuda(&domain, base + MIB2, 4096).expect("growth registration should succeed");
1403
1404        assert_eq!(view.rdma_addr, MIB2, "the view anchors into the grown tail");
1405        assert_eq!(
1406            mock.lock().bind_calls.len(),
1407            2,
1408            "growth created a new key (a second bind), retiring the prior one"
1409        );
1410    }
1411
1412    #[test]
1413    fn test_unserved_ordinal_is_not_covered() {
1414        let mock = MockOps::new(SERVED_NIC, true, &[0]);
1415        // Segment is on ordinal 5, which this NIC does not serve.
1416        mock.lock().scan = vec![seg(0x10_0000_0000, MIB2, 5)];
1417        let domain = domain(mock.clone());
1418
1419        let result = register_cuda(&domain, 0x10_0000_0000, 4096);
1420        assert!(
1421            result.is_err(),
1422            "memory on an unserved ordinal is not bound"
1423        );
1424        assert!(
1425            mock.lock().bind_calls.is_empty(),
1426            "no bind attempted for an unserved ordinal"
1427        );
1428    }
1429
1430    #[test]
1431    fn test_loopback_qp_created_once() {
1432        let base = 0x10_0000_0000;
1433        let other = 0x20_0000_0000;
1434        let mock = MockOps::new(SERVED_NIC, true, &[0]);
1435        mock.lock().scan = vec![seg(base, MIB2, 0)];
1436        let domain = domain(mock.clone());
1437
1438        // The first binding creates the loopback QP used to post key-binding
1439        // work requests.
1440        register_cuda(&domain, base, 4096).unwrap();
1441        assert_eq!(
1442            mock.lock().loopback_created,
1443            1,
1444            "the first binding creates the loopback QP"
1445        );
1446
1447        // Later scans — a growth, then a brand-new segment — each consult the
1448        // loopback QP, but must reuse the cached one rather than create another.
1449        mock.lock().scan = vec![seg(base, 2 * MIB2, 0)];
1450        register_cuda(&domain, base + MIB2, 4096).unwrap();
1451        assert_eq!(
1452            mock.lock().loopback_created,
1453            1,
1454            "growth reuses the cached loopback QP"
1455        );
1456
1457        mock.lock().scan = vec![seg(base, 2 * MIB2, 0), seg(other, MIB2, 0)];
1458        register_cuda(&domain, other, 4096).unwrap();
1459        assert_eq!(
1460            mock.lock().loopback_created,
1461            1,
1462            "binding a new segment reuses the cached loopback QP"
1463        );
1464    }
1465
1466    #[test]
1467    fn test_multi_device_subset_growth_new_segments_and_boundaries() {
1468        // Three CUDA devices are visible, but this NIC only serves ordinals
1469        // 0 and 2; ordinal 1's segment must never be bound.
1470        let a = 0x10_0000_0000usize; // ordinal 0
1471        let a2 = 0x18_0000_0000usize; // ordinal 0, appears later
1472        let b = 0x20_0000_0000usize; // ordinal 1 (unserved)
1473        let c = 0x30_0000_0000usize; // ordinal 2
1474        let mock = MockOps::new(SERVED_NIC, true, &[0, 2]);
1475        mock.lock().scan = vec![seg(a, MIB2, 0), seg(b, MIB2, 1), seg(c, 2 * MIB2, 2)];
1476        let domain = domain(mock.clone());
1477        // The registration path registers MRs with the domain's own access
1478        // flags; capture them to assert the dmabuf registration arguments.
1479        let access = domain.domain_impl().access_flags();
1480
1481        // Validate every field of a returned view: its address fields, its
1482        // size, the serving NIC's name, and the keys. The mock binds each key
1483        // with `(lkey, rkey) = (handle, handle ^ 0xffff)` for a freshly minted
1484        // handle, so a bound view always has a non-zero lkey and an rkey that is
1485        // its lkey xor 0xffff.
1486        let assert_view =
1487            |view: &IbvMemoryRegionView, virtual_addr: usize, rdma_addr: usize, size: usize| {
1488                assert_eq!(
1489                    view.virtual_addr, virtual_addr,
1490                    "virtual_addr is the requested address"
1491                );
1492                assert_eq!(
1493                    view.rdma_addr, rdma_addr,
1494                    "rdma_addr is the offset from the segment base"
1495                );
1496                assert_eq!(view.size, size, "size matches the request");
1497                assert_eq!(view.device_name, SERVED_NIC, "view names the serving NIC");
1498                assert_ne!(view.lkey, 0, "a bound view carries a real lkey");
1499                assert_eq!(
1500                    view.rkey,
1501                    view.lkey ^ 0xffff,
1502                    "rkey and lkey are derived from the same mkey"
1503                );
1504            };
1505
1506        // First registration binds every served segment in the scan (ordinals
1507        // 0 and 2), skipping the unserved ordinal-1 segment, then serves the
1508        // requested view from ordinal 0's segment.
1509        let view_a = register_cuda(&domain, a, 0x2000).expect("served ordinal binds");
1510        assert_view(&view_a, a, 0, 0x2000);
1511        let (mr_a, mr_c) = {
1512            let s = mock.lock();
1513            assert_eq!(s.bind_calls.len(), 2, "one bind per served segment");
1514            // Each served segment fits in a single MR, so each bind got a
1515            // one-element MR list.
1516            assert_eq!(s.bind_calls[0].mrs.len(), 1, "segment a binds a single MR");
1517            assert_eq!(s.bind_calls[1].mrs.len(), 1, "segment c binds a single MR");
1518            // Each served segment is registered as one MR covering its whole
1519            // extent, with the domain's access flags; the unserved ordinal-1
1520            // segment is absent.
1521            assert_eq!(
1522                s.dmabuf_calls,
1523                vec![
1524                    DmabufCall {
1525                        addr: a,
1526                        size: MIB2,
1527                        access,
1528                    },
1529                    DmabufCall {
1530                        addr: c,
1531                        size: 2 * MIB2,
1532                        access,
1533                    },
1534                ],
1535                "a and c are each registered once, covering their full extent"
1536            );
1537            assert_eq!(s.scan_calls, 1);
1538            let (mr_a, mr_c) = (
1539                s.bind_calls[0].mrs[0].clone(),
1540                s.bind_calls[1].mrs[0].clone(),
1541            );
1542            assert!(!Arc::ptr_eq(&mr_a, &mr_c), "a and c are distinct MRs");
1543            (mr_a, mr_c)
1544        };
1545
1546        // A request into the other served segment (ordinal 2) resolves to it,
1547        // not segment a, and is served from the fast path (no rescan).
1548        let view_c =
1549            register_cuda(&domain, c + 0x800, 0x4000).expect("segment c covers the request");
1550        assert_view(&view_c, c + 0x800, 0x800, 0x4000);
1551        assert_ne!(
1552            view_c.lkey, view_a.lkey,
1553            "distinct segments are backed by distinct keys"
1554        );
1555        assert_eq!(
1556            mock.lock().scan_calls,
1557            1,
1558            "fast path serves c without a scan"
1559        );
1560
1561        // Boundary handling: a request ending exactly at segment c's end is
1562        // covered (and shares c's key, since c has not been rebound); one that
1563        // crosses the end is not.
1564        let tail = register_cuda(&domain, c + 2 * MIB2 - 0x6000, 0x6000)
1565            .expect("a request ending exactly at the boundary is covered");
1566        assert_view(&tail, c + 2 * MIB2 - 0x6000, 2 * MIB2 - 0x6000, 0x6000);
1567        assert_eq!(
1568            (tail.lkey, tail.rkey),
1569            (view_c.lkey, view_c.rkey),
1570            "both views of segment c share its current key"
1571        );
1572        assert!(
1573            register_cuda(&domain, c + 2 * MIB2 - 2048, 4096).is_err(),
1574            "a request straddling the segment's end boundary is rejected"
1575        );
1576
1577        // The scanner now reports segment a grew, segment c is unchanged, and a
1578        // brand-new segment a2 appeared on ordinal 0. A single request into a's
1579        // grown tail processes all of that: grow a, skip c, bind a2.
1580        mock.lock().scan = vec![
1581            seg(a, 3 * MIB2, 0),
1582            seg(b, MIB2, 1),
1583            seg(c, 2 * MIB2, 2),
1584            seg(a2, MIB2, 0),
1585        ];
1586        let grown =
1587            register_cuda(&domain, a + 2 * MIB2, 0x3000).expect("a's grown tail is now covered");
1588        assert_view(&grown, a + 2 * MIB2, 2 * MIB2, 0x3000);
1589        assert_ne!(
1590            grown.lkey, view_a.lkey,
1591            "growing segment a rotated it onto a new key"
1592        );
1593        {
1594            let s = mock.lock();
1595            // Two binds added: a's growth rebind and a2's fresh bind (c was
1596            // already full-size, so it is not rebound).
1597            assert_eq!(s.bind_calls.len(), 4);
1598            // a's growth rebinds its reused original MR followed by the one new
1599            // tail MR.
1600            assert_eq!(
1601                s.bind_calls[2].mrs.len(),
1602                2,
1603                "a's growth binds the reused original MR plus the new tail"
1604            );
1605            assert!(
1606                Arc::ptr_eq(&s.bind_calls[2].mrs[0], &mr_a),
1607                "the original MR is reused, not re-registered"
1608            );
1609            let mr_a_tail = s.bind_calls[2].mrs[1].clone();
1610            // a2's fresh bind passes a single new MR.
1611            assert_eq!(
1612                s.bind_calls[3].mrs.len(),
1613                1,
1614                "the new segment a2 binds a single MR"
1615            );
1616            let mr_a2 = s.bind_calls[3].mrs[0].clone();
1617            // a's original + tail, c, and a2 are four distinct MRs.
1618            let all = [&mr_a, &mr_c, &mr_a_tail, &mr_a2];
1619            for (i, x) in all.iter().enumerate() {
1620                for y in &all[i + 1..] {
1621                    assert!(
1622                        !Arc::ptr_eq(x, y),
1623                        "a, a-tail, c, and a2 are four distinct MRs"
1624                    );
1625                }
1626            }
1627            // Growth registered only a's new tail (at a + MIB2, covering the 2
1628            // MiB it grew by) and a2's single MR; c was left untouched and the
1629            // first two registrations are unchanged. MR extents track segment
1630            // sizes, never the (varied) request sizes.
1631            assert_eq!(
1632                s.dmabuf_calls,
1633                vec![
1634                    DmabufCall {
1635                        addr: a,
1636                        size: MIB2,
1637                        access,
1638                    },
1639                    DmabufCall {
1640                        addr: c,
1641                        size: 2 * MIB2,
1642                        access,
1643                    },
1644                    DmabufCall {
1645                        addr: a + MIB2,
1646                        size: 2 * MIB2,
1647                        access,
1648                    },
1649                    DmabufCall {
1650                        addr: a2,
1651                        size: MIB2,
1652                        access,
1653                    },
1654                ],
1655                "growth registers only a's new tail and a2; c is not re-registered"
1656            );
1657        }
1658
1659        // The brand-new segment is independently usable, carries its own
1660        // distinct key, and has correct boundary handling of its own.
1661        let view_a2 = register_cuda(&domain, a2 + 0x400, 0x800)
1662            .expect("the new segment a2 covers the request");
1663        assert_view(&view_a2, a2 + 0x400, 0x400, 0x800);
1664        assert_ne!(
1665            view_a2.lkey, grown.lkey,
1666            "the new segment a2 has a key distinct from a's"
1667        );
1668        assert_ne!(
1669            view_a2.lkey, view_c.lkey,
1670            "the new segment a2 has a key distinct from c's"
1671        );
1672        assert!(
1673            register_cuda(&domain, a2 + MIB2 - 512, 1024).is_err(),
1674            "a request past a2's end boundary is rejected"
1675        );
1676
1677        // Even after all the rebinding, memory on the unserved ordinal is still
1678        // never bound or served.
1679        assert!(
1680            register_cuda(&domain, b, 4096).is_err(),
1681            "the unserved ordinal-1 segment is never served"
1682        );
1683        assert!(
1684            !mock.lock().dmabuf_calls.iter().any(|call| call.addr == b),
1685            "the unserved ordinal-1 segment was never registered"
1686        );
1687    }
1688}