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 memory uses per-allocation registration by default and may use one
12//! implicit-ODP MR per domain when configured. On an mlx5dv-capable device,
13//! CUDA device memory is bound to an indirect mlx5dv memory key so a whole
14//! allocator segment is addressable through a single key. Other device memory
15//! uses dmabuf registration.
16//!
17//! The segment scanning + binding bookkeeping lives here in Rust. Resource
18//! creation goes through [`MlxDomainOps`] so the (intricate) scan/bind logic
19//! can be unit-tested against a mock; the production implementation
20//! ([`ProdMlxDomainOps`]) delegates to the real functions and the
21//! [`rdmaxcel_sys::rdmaxcel_create_devx_mr_list`] shim. Teardown is the `Drop`
22//! of the owning RAII wrappers ([`Mlx5DevxMr`], [`Mlx5DevxMkey`]), correct by
23//! construction.
24
25use std::collections::HashMap;
26use std::collections::HashSet;
27use std::sync::Arc;
28use std::sync::Mutex;
29
30use super::device_selection::get_cuda_device_to_ibv_devices;
31use super::domain::IbvDomain;
32use super::domain::IbvDomainImpl;
33use super::domain::register_host_mr;
34use super::domain::register_host_or_dmabuf_mr;
35use super::memory_region::IbvMemoryRegionKeepalive;
36use super::memory_region::IbvMemoryRegionView;
37use super::mlx_queue_pair::MlxQueuePair;
38use super::primitives::IbvConfig;
39use super::primitives::IbvContext;
40use super::primitives::IbvDeviceInfo;
41use super::primitives::IbvMr;
42use super::primitives::IbvPd;
43use crate::backend::ibverbs::mlx_device::MlxDevice;
44use crate::device_selection::MemoryLocation;
45use crate::local_memory::KeepaliveLocalMemory;
46
47/// A single MR must be 2 MiB aligned and covers at most 4 GiB (one page
48/// under). Larger segments are split across multiple MRs bound to one key.
49const MR_ALIGNMENT: usize = 2 * 1024 * 1024;
50const MAX_MR_SIZE: usize = 4 * 1024 * 1024 * 1024 - MR_ALIGNMENT;
51const CUDA_MKEY_PAGE_SHIFT: u8 = 16;
52
53/// Submit asynchronous write-prefetch advice for an ODP-backed view.
54///
55/// # Safety
56///
57/// `pd.as_ptr()` must be null or a live protection domain that owns `view`'s
58/// ODP key and outlives this call.
59unsafe fn advise_odp_mr_write(pd: &IbvPd, device_name: &str, view: &IbvMemoryRegionView) {
60    if view.size == 0 {
61        return;
62    }
63
64    let max_sge_len = u32::MAX as usize;
65    let Some(mut sges) = (0..view.size.div_ceil(max_sge_len))
66        .map(|chunk| {
67            let offset = chunk.checked_mul(max_sge_len)?;
68            Some(rdmaxcel_sys::ibv_sge {
69                addr: view.virtual_addr.checked_add(offset)? as u64,
70                length: (view.size - offset).min(max_sge_len) as u32,
71                lkey: view.lkey,
72            })
73        })
74        .collect::<Option<Vec<_>>>()
75    else {
76        tracing::warn!(
77            device = device_name,
78            addr = format_args!("{:#x}", view.virtual_addr),
79            size = view.size,
80            "could not submit asynchronous host ODP write advice: address range overflow"
81        );
82        return;
83    };
84    let Ok(num_sge) = u32::try_from(sges.len()) else {
85        tracing::warn!(
86            device = device_name,
87            size = view.size,
88            "could not submit asynchronous host ODP write advice: too many ranges"
89        );
90        return;
91    };
92    // SAFETY: this function's contract permits a null PD, which the C shim
93    // rejects before dereferencing, or a live PD that owns `view`'s ODP key;
94    // `sges` describes in-bounds sub-ranges and outlives the call.
95    let ret = unsafe {
96        rdmaxcel_sys::rdmaxcel_advise_mr_write_async(pd.as_ptr(), sges.as_mut_ptr(), num_sge)
97    };
98    if ret != 0 {
99        tracing::warn!(
100            device = device_name,
101            addr = format_args!("{:#x}", view.virtual_addr),
102            size = view.size,
103            lkey = view.lkey,
104            error_code = ret,
105            "could not submit asynchronous host ODP write advice"
106        );
107        return;
108    }
109    tracing::debug!(
110        device = device_name,
111        addr = format_args!("{:#x}", view.virtual_addr),
112        size = view.size,
113        lkey = view.lkey,
114        "submitted asynchronous host ODP write advice"
115    );
116}
117
118// ===========================================================================
119// CUDA segment scanner
120// ===========================================================================
121
122/// A CUDA memory segment reported by a registered scanner.
123#[derive(Debug, Clone, Copy, PartialEq, Eq)]
124pub struct ScannedSegment {
125    /// Base virtual address of the segment.
126    pub address: usize,
127    /// Size of the segment in bytes.
128    pub size: usize,
129    /// CUDA device ordinal the segment is allocated on.
130    pub cuda_ordinal: i32,
131    /// Whether the segment is an expandable (growable) allocation.
132    pub is_expandable: bool,
133}
134
135/// Enumerates the currently-live CUDA segments. An `Arc` (not `Box`) so
136/// [`scan_cuda_segments`] can clone it out and drop the registry lock before
137/// invoking it — see that function.
138pub type CudaSegmentScanner = Arc<dyn Fn() -> Vec<ScannedSegment> + Send + Sync>;
139
140static CUDA_SEGMENT_SCANNER: Mutex<Option<CudaSegmentScanner>> = Mutex::new(None);
141
142/// Register the process-wide CUDA segment scanner, replacing any previously
143/// registered one.
144pub fn register_cuda_segment_scanner(scanner: CudaSegmentScanner) {
145    *CUDA_SEGMENT_SCANNER
146        .lock()
147        .expect("CUDA segment scanner lock poisoned") = Some(scanner);
148}
149
150/// Invoke the registered CUDA segment scanner, returning an empty list when
151/// none is registered.
152fn scan_cuda_segments() -> Vec<ScannedSegment> {
153    // Clone the `Arc` and release the registry lock before invoking: the
154    // pytorch scanner takes the Python GIL, and even though scan_cuda_segments
155    // is never called concurrently from multiple threads (in the current design),
156    // this protects against the possibility of deadlock.
157    let scanner = CUDA_SEGMENT_SCANNER
158        .lock()
159        .expect("CUDA segment scanner lock poisoned")
160        .clone();
161    scanner.as_deref().map(|scan| scan()).unwrap_or_default()
162}
163
164// ===========================================================================
165// MlxDomainOps: the device/FFI surface MlxDomain delegates to
166// ===========================================================================
167
168/// Device/FFI operations the mlx5dv binding logic depends on. Production
169/// uses [`ProdMlxDomainOps`]; tests substitute a mock so the scan/bind
170/// algorithm can be exercised without hardware. Creation returns the owning
171/// RAII wrappers ([`Mlx5DevxMr`], [`Mlx5DevxMkey`]), which free their resources
172/// in the right order on `Drop`; the mock fabricates null-handle wrappers whose
173/// `Drop` is a no-op.
174pub(super) trait MlxDomainOps: Send + Sync + 'static {
175    /// Name of the RDMA device this domain drives.
176    fn device_name(&self) -> String;
177
178    /// Whether the device supports mlx5dv direct verbs.
179    fn mlx5dv_enabled(&self) -> bool;
180
181    /// CUDA ordinals whose optimal NIC is this domain's device; only segments
182    /// on these ordinals are bound here.
183    fn assigned_cuda_devices(&self) -> anyhow::Result<Vec<i32>>;
184
185    /// Enumerate the currently-live CUDA segments.
186    fn scan_segments(&self) -> Vec<ScannedSegment>;
187
188    /// Pin `[addr, addr + size)` as a dma-buf DevX UMEM and create a direct MKey
189    /// over it.
190    ///
191    /// # Safety
192    ///
193    /// `pd`, if non-null, must wrap a live protection domain.
194    unsafe fn register_dmabuf_range(
195        &self,
196        pd: &Arc<IbvPd>,
197        addr: usize,
198        size: usize,
199        access: i32,
200    ) -> anyhow::Result<Mlx5DevxMr>;
201
202    /// Bind `mrs` to a freshly created, populated DevX indirect key, returning
203    /// it as a [`Mlx5DevxMkey`] owning those direct MKey references. On failure
204    /// the direct MKeys are dropped.
205    ///
206    /// # Safety
207    ///
208    /// `pd` (if non-null) must be a live protection domain and each MR in `mrs`
209    /// must be a live MR allocated against `pd` — all valid for this call.
210    unsafe fn bind_mr_list(
211        &self,
212        pd: &IbvPd,
213        access: i32,
214        mrs: Vec<Arc<Mlx5DevxMr>>,
215    ) -> anyhow::Result<Mlx5DevxMkey>;
216}
217
218/// Production [`MlxDomainOps`] backed by the real ibverbs / mlx5dv FFI.
219///
220/// The device name (taken from the queried [`IbvDeviceInfo`]) and mlx5dv
221/// support are immutable properties of the device, recorded once at
222/// construction; the per-op FFI methods operate on the `pd` passed in by
223/// [`MlxDomain`], so no context handle needs to be retained here.
224pub(super) struct ProdMlxDomainOps {
225    device_name: String,
226    mlx5dv_enabled: bool,
227    mkey_max_entries: usize,
228}
229
230impl ProdMlxDomainOps {
231    fn new(context: &IbvContext, device_info: &IbvDeviceInfo) -> Self {
232        let ctx = context.as_ptr();
233        // A null context (e.g. a test double) has no device to query, so treat
234        // it as mlx5dv-unsupported rather than dereferencing null.
235        let mlx5dv_supported = if ctx.is_null() {
236            false
237        } else {
238            // SAFETY: `ctx` is a non-null, live `ibv_context`; its `device`
239            // field is the `ibv_device` we query for mlx5dv support.
240            unsafe { rdmaxcel_sys::mlx5dv_is_supported((*ctx).device) }
241        };
242        let mut mkey_max_entries = 0;
243        let mlx5dv_enabled = if mlx5dv_supported {
244            // SAFETY: `ctx` is the live mlx5 context checked above and the
245            // out-pointer refers to a local `usize`.
246            let ret = unsafe {
247                rdmaxcel_sys::rdmaxcel_query_devx_mkey_max_entries(ctx, &mut mkey_max_entries)
248            };
249            if ret != 0 {
250                tracing::warn!(
251                    "could not query mlx5 DevX KLM capacity (error code {ret}); disabling indirect CUDA mkeys"
252                );
253            } else if mkey_max_entries == 0 {
254                tracing::warn!(
255                    "mlx5 DevX reported zero KLM capacity; disabling indirect CUDA mkeys"
256                );
257            }
258            ret == 0 && mkey_max_entries > 0
259        } else {
260            false
261        };
262        Self {
263            device_name: device_info.name().to_string(),
264            mlx5dv_enabled,
265            mkey_max_entries,
266        }
267    }
268}
269
270impl MlxDomainOps for ProdMlxDomainOps {
271    fn device_name(&self) -> String {
272        self.device_name.clone()
273    }
274
275    fn mlx5dv_enabled(&self) -> bool {
276        self.mlx5dv_enabled
277    }
278
279    fn assigned_cuda_devices(&self) -> anyhow::Result<Vec<i32>> {
280        Ok(get_cuda_device_to_ibv_devices::<MlxDevice>()?
281            .into_iter()
282            .enumerate()
283            .filter_map(|(ordinal, nics)| {
284                nics.iter()
285                    .any(|nic| nic.name() == &self.device_name)
286                    .then_some(ordinal as i32)
287            })
288            .collect())
289    }
290
291    fn scan_segments(&self) -> Vec<ScannedSegment> {
292        scan_cuda_segments()
293    }
294
295    unsafe fn register_dmabuf_range(
296        &self,
297        pd: &Arc<IbvPd>,
298        addr: usize,
299        size: usize,
300        access: i32,
301    ) -> anyhow::Result<Mlx5DevxMr> {
302        if pd.as_ptr().is_null() {
303            anyhow::bail!("register_dmabuf_range called with a null protection domain");
304        }
305        let mut mkey: *mut rdmaxcel_sys::rdmaxcel_devx_mkey_t = std::ptr::null_mut();
306        // SAFETY: `pd` is non-null (checked above), `[addr, addr + size)` is a
307        // live CUDA range per this method's contract, and all out-pointers
308        // refer to local variables.
309        let ret = unsafe {
310            rdmaxcel_sys::rdmaxcel_create_devx_dmabuf_mr(
311                pd.as_ptr(),
312                access,
313                addr,
314                size,
315                CUDA_MKEY_PAGE_SHIFT,
316                &mut mkey,
317            )
318        };
319        if ret != 0 {
320            anyhow::bail!("rdmaxcel_create_devx_dmabuf_mr failed: error code {ret}");
321        }
322        tracing::debug!(
323            addr,
324            size,
325            page_shift = CUDA_MKEY_PAGE_SHIFT,
326            "created direct DevX CUDA mkey"
327        );
328        // SAFETY: `mkey` is a live direct key freshly created for this range.
329        Ok(unsafe { Mlx5DevxMr::from_raw(mkey, pd.clone()) })
330    }
331
332    unsafe fn bind_mr_list(
333        &self,
334        pd: &IbvPd,
335        access: i32,
336        mrs: Vec<Arc<Mlx5DevxMr>>,
337    ) -> anyhow::Result<Mlx5DevxMkey> {
338        if pd.as_ptr().is_null() {
339            anyhow::bail!("bind_mr_list called with a null protection domain");
340        }
341        let ptrs: Vec<*mut rdmaxcel_sys::rdmaxcel_devx_mkey_t> =
342            mrs.iter().map(|m| m.as_ptr()).collect();
343        if ptrs.iter().any(|p| p.is_null()) {
344            anyhow::bail!("bind_mr_list called with a null direct mkey");
345        }
346        let mut mkey: *mut rdmaxcel_sys::rdmaxcel_devx_mkey_t = std::ptr::null_mut();
347        let mut lkey = 0;
348        let mut rkey = 0;
349        // SAFETY: `pd` is non-null (checked above) and valid, `ptrs` holds the
350        // MRs' pointers, and failure is reported via the return code.
351        let ret = unsafe {
352            rdmaxcel_sys::rdmaxcel_create_devx_mr_list(
353                pd.as_ptr(),
354                access,
355                ptrs.as_ptr(),
356                ptrs.len(),
357                &mut mkey,
358                &mut lkey,
359                &mut rkey,
360            )
361        };
362        if ret != 0 {
363            anyhow::bail!("rdmaxcel_create_devx_mr_list failed: error code {ret}");
364        }
365        tracing::debug!(
366            klm_entries = ptrs.len(),
367            lkey,
368            rkey,
369            "created DevX indirect mkey"
370        );
371        // SAFETY: `mkey` is a live key freshly bound over `mrs` (`ret == 0`).
372        Ok(unsafe { Mlx5DevxMkey::from_raw(mkey, lkey, rkey, mrs) })
373    }
374}
375
376/// Owns a direct DevX MKey and its pinned dma-buf UMEM.
377#[derive(Debug)]
378pub(super) struct Mlx5DevxMr {
379    mkey: *mut rdmaxcel_sys::rdmaxcel_devx_mkey_t,
380    _pd: Arc<IbvPd>,
381}
382
383// SAFETY: the opaque DevX owner is immutable through this wrapper, and mlx5dv
384// permits its resources to be used and destroyed from any thread.
385unsafe impl Send for Mlx5DevxMr {}
386// SAFETY: as for `Send` above.
387unsafe impl Sync for Mlx5DevxMr {}
388
389impl Mlx5DevxMr {
390    /// Takes ownership of a direct DevX MKey and its UMEM.
391    ///
392    /// # Safety
393    ///
394    /// `mkey` must be a live owner returned by
395    /// `rdmaxcel_create_devx_dmabuf_mr`, owned solely by the returned value.
396    unsafe fn from_raw(mkey: *mut rdmaxcel_sys::rdmaxcel_devx_mkey_t, pd: Arc<IbvPd>) -> Self {
397        Self { mkey, _pd: pd }
398    }
399
400    fn as_ptr(&self) -> *mut rdmaxcel_sys::rdmaxcel_devx_mkey_t {
401        self.mkey
402    }
403
404    #[cfg(test)]
405    fn null(pd: Arc<IbvPd>) -> Self {
406        Self {
407            mkey: std::ptr::null_mut(),
408            _pd: pd,
409        }
410    }
411}
412
413impl Drop for Mlx5DevxMr {
414    fn drop(&mut self) {
415        if !self.mkey.is_null() {
416            // SAFETY: a non-null pointer came from the direct-MKey constructor
417            // and this non-Clone owner destroys it exactly once.
418            unsafe { rdmaxcel_sys::rdmaxcel_destroy_devx_mkey(self.mkey) };
419        }
420    }
421}
422
423/// Owns an indirect DevX MKey together with the direct MKeys it binds,
424/// destroying the aggregate key on drop (a no-op if null). Caches the
425/// `(lkey, rkey)` read at bind time so a view can address through the key
426/// without further FFI.
427///
428/// The direct MKeys are `Arc` because successive aggregate keys over a growing
429/// segment bind overlapping sets. A leaf is destroyed only after the last
430/// aggregate key referencing it drops.
431#[derive(Debug)]
432pub(super) struct Mlx5DevxMkey {
433    mkey: *mut rdmaxcel_sys::rdmaxcel_devx_mkey_t,
434    lkey: u32,
435    rkey: u32,
436    mrs: Vec<Arc<Mlx5DevxMr>>,
437}
438
439// SAFETY: the only raw member is the opaque DevX mkey pointer (the MRs are already
440// `Send`/`Sync`), which the mlx5dv API treats as usable and destroyable from any
441// thread (`Send`); `Mlx5DevxMkey` exposes no operation that mutates the key
442// through a shared `&` (`keys` reads immutable fields), so sharing a
443// `&Mlx5DevxMkey` cannot race (`Sync`).
444unsafe impl Send for Mlx5DevxMkey {}
445// SAFETY: as for `Send` above.
446unsafe impl Sync for Mlx5DevxMkey {}
447
448impl Mlx5DevxMkey {
449    /// Takes ownership of a raw DevX mkey bound over `mrs`, destroying it on
450    /// drop.
451    ///
452    /// # Safety
453    ///
454    /// `mkey` must be a live aggregate key returned by
455    /// `rdmaxcel_create_devx_mr_list` over `mrs`, owned solely by the returned
456    /// value. `lkey` and `rkey` must be the keys returned by that same call.
457    pub(super) unsafe fn from_raw(
458        mkey: *mut rdmaxcel_sys::rdmaxcel_devx_mkey_t,
459        lkey: u32,
460        rkey: u32,
461        mrs: Vec<Arc<Mlx5DevxMr>>,
462    ) -> Self {
463        Self {
464            mkey,
465            lkey,
466            rkey,
467            mrs,
468        }
469    }
470
471    /// The `(lkey, rkey)` to address memory through this key.
472    fn keys(&self) -> (u32, u32) {
473        (self.lkey, self.rkey)
474    }
475
476    /// The direct MKeys this aggregate binds.
477    fn mrs(&self) -> &Vec<Arc<Mlx5DevxMr>> {
478        &self.mrs
479    }
480
481    /// Fabricates a key with the given `(lkey, rkey)` over a null `mkey` (whose
482    /// `Drop` is a no-op), binding `mrs`. Lets the mock hand back a usable key
483    /// without touching the FFI.
484    #[cfg(test)]
485    pub(super) fn with_test_keys(lkey: u32, rkey: u32, mrs: Vec<Arc<Mlx5DevxMr>>) -> Self {
486        Self {
487            mkey: std::ptr::null_mut(),
488            lkey,
489            rkey,
490            mrs,
491        }
492    }
493}
494
495impl Drop for Mlx5DevxMkey {
496    fn drop(&mut self) {
497        if !self.mkey.is_null() {
498            // SAFETY: a non-null `self.mkey` came from
499            // `rdmaxcel_create_devx_mr_list` and, since `Mlx5DevxMkey` is not
500            // `Clone`, is destroyed exactly once.
501            unsafe { rdmaxcel_sys::rdmaxcel_destroy_devx_mkey(self.mkey) };
502        }
503    }
504}
505
506// ===========================================================================
507// RegisteredSegment: a CUDA segment bound to an indirect key
508// ===========================================================================
509
510/// The mutable state of a [`RegisteredSegment`], behind one `Mutex` so the
511/// current key, superseded keys, and size all change together.
512#[derive(Debug)]
513struct RegisteredSegmentState {
514    /// Keys superseded by growth, each still owning the MRs it bound, kept alive
515    /// so views built against an earlier key stay valid.
516    stale_mkeys: Vec<Mlx5DevxMkey>,
517    /// Current indirect key over the segment's MRs. `None` until the first bind;
518    /// swapped on growth, the prior key moved to `stale_mkeys`.
519    mkey: Option<Mlx5DevxMkey>,
520    /// Bytes currently covered by `mkey`.
521    size: usize,
522    /// Bytes the most recent scan reported for this segment. Equal to `size`
523    /// while the key spans the whole segment, and larger once growth is refused
524    /// because the segment needs more MRs than one key can bind.
525    scanned_size: usize,
526}
527
528/// A CUDA segment bound to the device via an indirect mlx5dv key, covering
529/// `[base_virtual_addr, base_virtual_addr + size)`. Shared as
530/// `Arc<RegisteredSegment>` by [`MlxDomain`] and every view over it, so it (and
531/// the keys, MRs, and PD it owns) lives until nothing references it.
532///
533/// All mutation is serialized by the owning [`MlxDomain`]'s `segments` lock;
534/// `state` is behind a `Mutex` only to allow mutation through the shared `Arc`.
535struct RegisteredSegment {
536    ops: Arc<dyn MlxDomainOps>,
537    base_virtual_addr: usize,
538    /// The most MRs that can bind to this segment's indirect key.
539    mkey_max_entries: usize,
540    /// Maximum bytes covered by one MR/KLM entry.
541    max_mr_size: usize,
542    state: Mutex<RegisteredSegmentState>,
543}
544
545impl std::fmt::Debug for RegisteredSegment {
546    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
547        f.debug_struct("RegisteredSegment")
548            .field(
549                "base_virtual_addr",
550                &format!("0x{:x}", self.base_virtual_addr),
551            )
552            .field("state", &self.state)
553            .finish_non_exhaustive()
554    }
555}
556
557impl RegisteredSegment {
558    /// An unbound segment for `base_virtual_addr`: no MRs, no key, zero size.
559    /// [`Self::grow`] binds its first generation. `mkey_max_entries` caps the
560    /// MRs bound to the segment's key.
561    #[cfg(test)]
562    fn empty(
563        ops: Arc<dyn MlxDomainOps>,
564        base_virtual_addr: usize,
565        mkey_max_entries: usize,
566    ) -> Self {
567        Self::empty_with_mr_size(ops, base_virtual_addr, mkey_max_entries, MAX_MR_SIZE)
568    }
569
570    /// As [`Self::empty`], with a configurable MR size for hardware tests that
571    /// must exercise large KLM lists without allocating hundreds of GiB.
572    fn empty_with_mr_size(
573        ops: Arc<dyn MlxDomainOps>,
574        base_virtual_addr: usize,
575        mkey_max_entries: usize,
576        max_mr_size: usize,
577    ) -> Self {
578        Self {
579            ops,
580            base_virtual_addr,
581            mkey_max_entries,
582            max_mr_size,
583            state: Mutex::new(RegisteredSegmentState {
584                stale_mkeys: Vec::new(),
585                mkey: None,
586                size: 0,
587                scanned_size: 0,
588            }),
589        }
590    }
591
592    /// Bytes currently covered by the segment's MRs and bound to its key.
593    #[cfg(test)]
594    fn size(&self) -> usize {
595        self.state.lock().expect("segment state lock poisoned").size
596    }
597
598    /// True when `[addr, addr + size)` lies entirely within the extent bound to
599    /// this segment's key.
600    fn covers(&self, addr: usize, size: usize) -> bool {
601        let state = self.state.lock().expect("segment state lock poisoned");
602        if state.mkey.is_none() {
603            return false;
604        }
605        let end = self.base_virtual_addr + state.size;
606        addr >= self.base_virtual_addr && addr <= end && size <= end - addr
607    }
608
609    /// Bytes the most recent scan reported for this segment.
610    fn scanned_size(&self) -> usize {
611        self.state
612            .lock()
613            .expect("segment state lock poisoned")
614            .scanned_size
615    }
616
617    /// True when both conditions are true:
618    /// 1. The scanned size of the segment is larger than the registered
619    ///    size, which occurs only when the mkey for this segment cannot
620    ///    grow anymore.
621    /// 2. [addr, addr + size) overlaps the unregistered part of the scanned
622    ///    segment.
623    fn in_unregistered_tail(&self, addr: usize, size: usize) -> bool {
624        let state = self.state.lock().expect("segment state lock poisoned");
625        let registered_end = self.base_virtual_addr + state.size;
626        let scanned_end = self.base_virtual_addr + state.scanned_size;
627        addr >= self.base_virtual_addr
628            && addr <= scanned_end
629            && size <= scanned_end - addr
630            && addr + size > registered_end
631    }
632
633    /// Grow the segment towards `scanned_seg.size` (must not be below its
634    /// current size): register the new tail — the whole range on the first call
635    /// from [`Self::empty`] — bind the existing + new MRs to a fresh key, and
636    /// retire the prior key to `stale_mkeys` (rather than rebinding the in-use
637    /// key, which could race in-flight ops); the initial null key is not
638    /// retired. On any failure, any freshly registered tail MRs will be cleaned
639    /// up. However, the segment's `scanned_size` remains at the updated value.
640    /// This is necessary to prevent future registrations from repeatedly triggering
641    /// an expensive rescan only to fail again inside this function.
642    ///
643    /// A key binds at most `mkey_max_entries` MRs. When the tail needs more than
644    /// the key has room for, as much of it as fits is registered and the rest is
645    /// left out, so the segment still covers its prefix; [`Self::covers`] then
646    /// reports what was bound and [`Self::in_unregistered_tail`] the remainder.
647    ///
648    /// # Safety
649    ///
650    /// If `pd` is non-null it must be a live protection domain.
651    unsafe fn grow(
652        &self,
653        pd: &Arc<IbvPd>,
654        access: i32,
655        scanned_seg: &ScannedSegment,
656    ) -> anyhow::Result<()> {
657        let mut state = self.state.lock().expect("segment state lock poisoned");
658        assert!(
659            scanned_seg.address == self.base_virtual_addr,
660            "grow called for base 0x{:x} on a segment based at 0x{:x}",
661            scanned_seg.address,
662            self.base_virtual_addr
663        );
664        // A segment only ever grows; shrinking would underflow
665        // `scanned_seg.size - state.size` below.
666        assert!(
667            scanned_seg.size >= state.size,
668            "segment at 0x{:x} shrank from {} to {} bytes; segments must never shrink",
669            self.base_virtual_addr,
670            state.size,
671            scanned_seg.size
672        );
673        // Growing to the current size has nothing to add.
674        if scanned_seg.size == state.size {
675            return Ok(());
676        }
677        state.scanned_size = scanned_seg.size;
678
679        // `register_range` splits the tail into `max_mr_size` chunks, so how many
680        // MRs it needs is known before any of it is registered. Take only as many
681        // chunks as the key has room for.
682        let bound = state.mkey.as_ref().map_or(0, |k| k.mrs().len());
683        let room = self
684            .mkey_max_entries
685            .saturating_sub(bound)
686            .saturating_mul(self.max_mr_size);
687        let grew = scanned_seg.size - state.size;
688        let tail = grew.min(room);
689
690        if tail < grew {
691            tracing::warn!(
692                "CUDA segment at 0x{:x} grew from {} bytes to {} bytes, \
693            but its mkey only has room for {tail} more bytes. This segment's tail \
694            will remain unregistered, and some tensors inside it may use the dmabuf \
695            fallback.",
696                self.base_virtual_addr,
697                state.size,
698                state.scanned_size
699            );
700        }
701
702        // The key is full: the segment keeps the prefix it already covers.
703        if tail == 0 {
704            return Ok(());
705        }
706
707        // SAFETY: per this function's contract `pd` is null or a live PD; the
708        // tail MRs registered here belong to this segment.
709        let new_tail = unsafe {
710            register_range(
711                &self.ops,
712                pd,
713                access,
714                self.base_virtual_addr + state.size,
715                tail,
716                self.max_mr_size,
717            )
718        }?;
719
720        // Bind the existing MRs (reused from the current key) plus the new tail
721        // to a fresh key. On failure `all` is dropped by `bind_mr_list`: the new
722        // tail's MRs deregister while the existing ones survive via the current
723        // key, leaving the segment unchanged.
724        let mut all = state
725            .mkey
726            .as_ref()
727            .map(|k| k.mrs().clone())
728            .unwrap_or_default();
729        all.extend(new_tail);
730        debug_assert!(
731            all.len() <= self.mkey_max_entries,
732            "mkey max entries is {}, but got {} MRs",
733            self.mkey_max_entries,
734            all.len()
735        );
736        // SAFETY: same contract; `all` are this segment's live MRs.
737        let new_mkey = unsafe { self.ops.bind_mr_list(pd, access, all) }?;
738
739        // Retire the prior key (kept for in-flight ops built against it) and
740        // install the new one.
741        if let Some(prior) = state.mkey.replace(new_mkey) {
742            state.stale_mkeys.push(prior);
743        }
744        state.size += tail;
745        Ok(())
746    }
747
748    /// Build a view anchored at `addr`. The view's guard is the segment itself,
749    /// pinning it (and thus its current key, the key's MRs, and the PD they own)
750    /// alive for the view's lifetime, so the bound key stays valid while the view
751    /// is in use. Indirect keys present their MRs as a flat zero-based space, so
752    /// `rdma_addr` is the offset from the segment base.
753    fn view(seg: &Arc<Self>, addr: usize, size: usize) -> IbvMemoryRegionView {
754        let (lkey, rkey) = {
755            let state = seg.state.lock().expect("segment state lock poisoned");
756            // `view` is only called on a segment that covers the request, which
757            // means it has been bound, so the current key is `Some`.
758            state
759                .mkey
760                .as_ref()
761                .map(Mlx5DevxMkey::keys)
762                .expect("view of a segment with no bound key")
763        };
764        // The segment is its own keepalive: cloning the `Arc<RegisteredSegment>`
765        // pins the current key (which owns the MRs, which own the PD) for the
766        // view's life.
767        let guard: Arc<dyn IbvMemoryRegionKeepalive> = seg.clone();
768        IbvMemoryRegionView::new(
769            addr,
770            addr - seg.base_virtual_addr,
771            size,
772            lkey,
773            rkey,
774            seg.ops.device_name(),
775            guard,
776        )
777    }
778}
779
780// A `RegisteredSegment` is its own MR keepalive: it owns the current key, which
781// owns the bound MRs, which own the PD — all freed in order on its `Drop`.
782impl IbvMemoryRegionKeepalive for RegisteredSegment {}
783
784/// Register `[start, start + len)` as pinned dma-buf DevX MKeys in
785/// `<= MAX_MR_SIZE` chunks. All-or-nothing: on failure, the direct MKeys
786/// created so far drop as the returned `Vec` unwinds.
787///
788/// # Safety
789///
790/// If `pd` is non-null it must be a live protection domain whose context
791/// outlives this call.
792unsafe fn register_range(
793    ops: &Arc<dyn MlxDomainOps>,
794    pd: &Arc<IbvPd>,
795    access: i32,
796    start: usize,
797    len: usize,
798    max_mr_size: usize,
799) -> anyhow::Result<Vec<Arc<Mlx5DevxMr>>> {
800    let mut mrs: Vec<Arc<Mlx5DevxMr>> = Vec::new();
801    let mut chunk_start = start;
802    let mut remaining = len;
803    while remaining > 0 {
804        let chunk = remaining.min(max_mr_size);
805        let result = if chunk.is_multiple_of(MR_ALIGNMENT) {
806            // SAFETY: `pd` is null or a live PD per this function's contract.
807            unsafe { ops.register_dmabuf_range(pd, chunk_start, chunk, access) }
808        } else {
809            Err(anyhow::anyhow!(
810                "CUDA chunk size {} is not a multiple of {}",
811                chunk,
812                MR_ALIGNMENT
813            ))
814        };
815        // On error, return early; the direct MKeys collected so far drop here.
816        mrs.push(Arc::new(result?));
817        chunk_start += chunk;
818        remaining -= chunk;
819    }
820    Ok(mrs)
821}
822
823// ===========================================================================
824// MlxDomain
825// ===========================================================================
826
827/// Mellanox [`IbvDomainImpl`].
828pub struct MlxDomain {
829    ops: Arc<dyn MlxDomainOps>,
830    mlx5dv_enabled: bool,
831    /// Caps the MRs bound to each segment's indirect key.
832    mkey_max_entries: usize,
833    /// Caps the bytes covered by each MR/KLM entry.
834    max_mr_size: usize,
835    /// Return DevX registration failures instead of using the dmabuf fallback.
836    require_devx_mkeys: bool,
837    host_odp_mr: Mutex<Option<Arc<IbvMr>>>,
838    /// Currently-bound segments, keyed by `(base address, CUDA ordinal)`. Each
839    /// grows in place (reusing its MRs, retiring superseded keys internally);
840    /// a key whose base vanishes from the scan is dropped (a live view keeps
841    /// its own `Arc` alive regardless).
842    segments: Mutex<HashMap<(usize, i32), Arc<RegisteredSegment>>>,
843}
844
845impl std::fmt::Debug for MlxDomain {
846    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
847        f.debug_struct("MlxDomain")
848            .field("mlx5dv_enabled", &self.mlx5dv_enabled)
849            .finish_non_exhaustive()
850    }
851}
852
853impl MlxDomain {
854    /// Build a domain over the given ops, deriving mlx5dv support from them.
855    /// `mkey_max_entries` caps the MRs bound to every segment's indirect key.
856    #[cfg(test)]
857    fn new_with_ops(ops: Arc<dyn MlxDomainOps>, mkey_max_entries: usize) -> Self {
858        Self::new_with_ops_and_limits(ops, mkey_max_entries, MAX_MR_SIZE, false)
859    }
860
861    fn new_with_ops_and_limits(
862        ops: Arc<dyn MlxDomainOps>,
863        mkey_max_entries: usize,
864        max_mr_size: usize,
865        require_devx_mkeys: bool,
866    ) -> Self {
867        let mlx5dv_enabled = ops.mlx5dv_enabled();
868        Self {
869            ops,
870            mlx5dv_enabled,
871            mkey_max_entries,
872            max_mr_size,
873            require_devx_mkeys,
874            host_odp_mr: Mutex::new(None),
875            segments: Mutex::new(HashMap::new()),
876        }
877    }
878
879    fn register_host_odp_mr(
880        &self,
881        domain: &IbvDomain<MlxDomain>,
882        mem: &KeepaliveLocalMemory,
883    ) -> anyhow::Result<IbvMemoryRegionView> {
884        let mr = {
885            let mut host_odp_mr = self.host_odp_mr.lock().expect("host ODP MR lock poisoned");
886            match host_odp_mr.as_ref() {
887                Some(mr) => Arc::clone(mr),
888                None => {
889                    // SAFETY: the domain owns a live PD in production, while
890                    // `register_host_mr` rejects a null test PD. The literal
891                    // address zero and `usize::MAX` size describe implicit
892                    // ODP, and the access flags include `IBV_ACCESS_ON_DEMAND`.
893                    let mr = Arc::new(unsafe {
894                        register_host_mr(
895                            domain.pd(),
896                            0,
897                            usize::MAX,
898                            self.access_flags()
899                                | rdmaxcel_sys::ibv_access_flags::IBV_ACCESS_ON_DEMAND.0 as i32,
900                        )
901                    }?);
902                    // SAFETY: registration returned a live MR owned by `mr`.
903                    let (lkey, rkey) = unsafe { ((*mr.as_ptr()).lkey, (*mr.as_ptr()).rkey) };
904                    tracing::info!(
905                        device = domain.device_info().name(),
906                        lkey,
907                        rkey,
908                        "enabled implicit host ODP memory region"
909                    );
910                    *host_odp_mr = Some(Arc::clone(&mr));
911                    mr
912                }
913            }
914        };
915        // SAFETY: `mr` is non-null and stays live through the view's guard.
916        let (lkey, rkey) = unsafe { ((*mr.as_ptr()).lkey, (*mr.as_ptr()).rkey) };
917        let view = IbvMemoryRegionView::new(
918            mem.addr(),
919            mem.addr(),
920            mem.size(),
921            lkey,
922            rkey,
923            domain.device_info().name().to_string(),
924            mr,
925        );
926        // OOO_RW placement presents each sprayed ~4 KiB packet as an
927        // independent operation, so an unadvised cold range can trigger one
928        // ODP fault and translation update per packet. On H100/CX7, median
929        // cold latency for 64 KiB/1 MiB/4 MiB/16 MiB was
930        // 35.7 ms/791 ms/3.21 s/12.87 s without advice, versus
931        // 3.88 ms/4.86 ms/5.09 ms/9.30 ms with this asynchronous hint. Submit
932        // it when the view is created so it races ahead of any export, RDMA
933        // WRITE, or RDMA READ that consumes the registration. With flags=0, a
934        // hot advice call costs about 2 us and demand ODP still handles any
935        // pages that the operation reaches first.
936        // SAFETY: the domain PD is null only in tests, which the C shim rejects
937        // before dereferencing; otherwise it is live, owns `view`'s ODP key,
938        // and outlives this call.
939        unsafe { advise_odp_mr_write(domain.pd(), domain.device_info().name(), &view) };
940        Ok(view)
941    }
942
943    /// Bind CUDA `[addr, addr + size)` via an indirect mlx5dv key. Scans for
944    /// live segments on the ordinals this NIC serves, binding a fresh segment
945    /// for any that appeared and growing in place any that expanded, dropping
946    /// any whose base address vanished from the scan, then returns a view
947    /// anchored at `addr`.
948    ///
949    /// # Safety
950    ///
951    /// `domain.as_ptr()` must be null or a live protection domain whose context
952    /// outlives this call.
953    unsafe fn register_cuda_mlx5dv_mr(
954        &self,
955        domain: &IbvDomain<MlxDomain>,
956        addr: usize,
957        size: usize,
958    ) -> anyhow::Result<IbvMemoryRegionView> {
959        let pd = domain.pd();
960        let access = self.access_flags();
961        let mut segments = self
962            .segments
963            .lock()
964            .expect("mlx domain segments lock poisoned");
965
966        // Fast path: an existing scanned segment already fully covers the new
967        // range (`Ok` when the scanned segment's registered portion covers the
968        // range, `Err` when the range overlaps the segment's unregistered tail).
969        if let Some(result) = Self::lookup(&segments, addr, size) {
970            return result;
971        }
972
973        // Which ordinals this NIC serves, resolved now rather than at domain
974        // construction: the caller holds a CUDA address, so CUDA is initialized
975        // and the ordinal → NIC map is resolvable. If this were instead called
976        // only once at construction, if CUDA were not already initialized,
977        // this domain would permanently have an empty list of ordinals.
978        let cuda_ordinals = self.ops.assigned_cuda_devices()?;
979
980        // Pull live segments and keep only the ones on ordinals we serve.
981        let scanned: Vec<ScannedSegment> = self
982            .ops
983            .scan_segments()
984            .into_iter()
985            .filter(|s| cuda_ordinals.contains(&s.cuda_ordinal))
986            .collect();
987
988        let mut snapshot: HashSet<(usize, i32)> = HashSet::new();
989        for scanned_seg in &scanned {
990            let key = (scanned_seg.address, scanned_seg.cuda_ordinal);
991            snapshot.insert(key);
992            match segments.get(&key) {
993                // Already grown against this extent: nothing to do.
994                Some(seg) if seg.scanned_size() == scanned_seg.size => {}
995                // Grew: extend the existing segment in place (reuses its MRs,
996                // retires its prior key internally).
997                // SAFETY: `pd` satisfies this function's contract (live or null)
998                // and is forwarded unchanged.
999                Some(seg) => unsafe { seg.grow(pd, access, scanned_seg) }?,
1000                // New: create an empty segment and grow it towards the full
1001                // range. It is kept even when the growth covers only part of
1002                // that range, so the scanned extent is recorded and later
1003                // requests past the covered prefix are answered without another
1004                // scan.
1005                None => {
1006                    let fresh = Arc::new(RegisteredSegment::empty_with_mr_size(
1007                        self.ops.clone(),
1008                        scanned_seg.address,
1009                        self.mkey_max_entries,
1010                        self.max_mr_size,
1011                    ));
1012                    // SAFETY: as above.
1013                    unsafe { fresh.grow(pd, access, scanned_seg) }?;
1014                    segments.insert(key, fresh);
1015                }
1016            }
1017        }
1018
1019        // Drop segments whose base address vanished from the scan: the
1020        // allocator freed that region. A still-live view keeps its own `Arc`
1021        // alive regardless, so this never frees memory in use.
1022        segments.retain(|key, _| snapshot.contains(key));
1023
1024        // Serve the caller's view from a current segment that covers it.
1025        Self::lookup(&segments, addr, size).unwrap_or_else(|| {
1026            Err(anyhow::anyhow!(
1027                "CUDA address 0x{:x} + size {} is not covered by any scanned segment on the CUDA ordinals {:?} mapped to this NIC",
1028                addr,
1029                size,
1030                cuda_ordinals,
1031            ))
1032        })
1033    }
1034
1035    /// Look up how `[addr, addr + size)` relates to the already scanned
1036    /// and registered segments. There are 3 possibilities:
1037    /// 1. `[addr, addr + size)` does not fall within any scanned segment.
1038    ///    Return `None`.
1039    /// 2. `[addr, addr + size)` is fully covered by the registered portion
1040    ///    of a scanned segment. Return `Some(Ok(IbvMemoryRegionView))`.
1041    /// 3. `[addr, addr + size)` is fully covered by a scanned segment, but
1042    ///    part of it extends into that scanned segment's unregistered tail.
1043    ///    Return `Some(Err(anyhow::Error))`.
1044    fn lookup(
1045        segments: &HashMap<(usize, i32), Arc<RegisteredSegment>>,
1046        addr: usize,
1047        size: usize,
1048    ) -> Option<anyhow::Result<IbvMemoryRegionView>> {
1049        if let Some(seg) = segments.values().find(|s| s.covers(addr, size)) {
1050            return Some(Ok(RegisteredSegment::view(seg, addr, size)));
1051        }
1052        let seg = segments
1053            .values()
1054            .find(|s| s.in_unregistered_tail(addr, size))?;
1055        Some(Err(anyhow::anyhow!(
1056            "CUDA address 0x{:x} + size {} is in the unregistered tail of the segment at 0x{:x}, whose key has no more room",
1057            addr,
1058            size,
1059            seg.base_virtual_addr,
1060        )))
1061    }
1062}
1063
1064impl IbvDomainImpl for MlxDomain {
1065    type QueuePair = MlxQueuePair;
1066
1067    unsafe fn new(context: &IbvContext, device_info: &IbvDeviceInfo, config: &IbvConfig) -> Self {
1068        let ops = Arc::new(ProdMlxDomainOps::new(context, device_info));
1069        let mkey_max_entries = if config.mkey_max_entries_override == 0 {
1070            ops.mkey_max_entries
1071        } else {
1072            config.mkey_max_entries_override.min(ops.mkey_max_entries)
1073        };
1074        let max_mr_size = match config.max_mr_size_override {
1075            0 => MAX_MR_SIZE,
1076            override_size
1077                if override_size <= MAX_MR_SIZE && override_size.is_multiple_of(MR_ALIGNMENT) =>
1078            {
1079                override_size
1080            }
1081            override_size => {
1082                tracing::warn!(
1083                    override_size,
1084                    alignment = MR_ALIGNMENT,
1085                    production_limit = MAX_MR_SIZE,
1086                    "ignoring invalid max_mr_size_override"
1087                );
1088                MAX_MR_SIZE
1089            }
1090        };
1091        Self::new_with_ops_and_limits(
1092            ops,
1093            mkey_max_entries,
1094            max_mr_size,
1095            config.require_devx_mkeys,
1096        )
1097    }
1098
1099    fn access_flags(&self) -> i32 {
1100        (rdmaxcel_sys::ibv_access_flags::IBV_ACCESS_LOCAL_WRITE
1101            | rdmaxcel_sys::ibv_access_flags::IBV_ACCESS_REMOTE_WRITE
1102            | rdmaxcel_sys::ibv_access_flags::IBV_ACCESS_REMOTE_READ
1103            | rdmaxcel_sys::ibv_access_flags::IBV_ACCESS_REMOTE_ATOMIC)
1104            .0 as i32
1105    }
1106
1107    unsafe fn register_mr(
1108        domain: &IbvDomain<MlxDomain>,
1109        mem: &KeepaliveLocalMemory,
1110    ) -> anyhow::Result<IbvMemoryRegionView> {
1111        let this = domain.domain_impl();
1112        if matches!(mem.location(), MemoryLocation::Cpu(_))
1113            && hyperactor_config::global::get(crate::config::RDMA_HOST_ODP)
1114        {
1115            return this.register_host_odp_mr(domain, mem);
1116        }
1117        if matches!(mem.location(), MemoryLocation::Gpu(_)) {
1118            if this.mlx5dv_enabled {
1119                // SAFETY: `domain.as_ptr()` is null or a live PD, per this
1120                // method's contract.
1121                match unsafe { this.register_cuda_mlx5dv_mr(domain, mem.addr(), mem.size()) } {
1122                    Ok(view) => return Ok(view),
1123                    Err(e) => {
1124                        if this.require_devx_mkeys {
1125                            return Err(anyhow::anyhow!(
1126                                "required mlx5dv CUDA registration failed: {e}"
1127                            ));
1128                        }
1129                        tracing::warn!(
1130                            "mlx5dv CUDA registration failed, falling back to dmabuf: {e}"
1131                        )
1132                    }
1133                }
1134            } else if this.require_devx_mkeys {
1135                anyhow::bail!("required mlx5dv CUDA registration is unavailable");
1136            }
1137        }
1138        // SAFETY: `domain.as_ptr()` is null or a live PD (per this method's
1139        // contract; `register_host_or_dmabuf_mr` errors on null), and the caller
1140        // keeps `mem`'s backing memory valid for the MR's lifetime.
1141        unsafe { register_host_or_dmabuf_mr(domain, mem) }
1142    }
1143}
1144
1145#[cfg(test)]
1146mod tests {
1147    use std::sync::Mutex;
1148    use std::sync::MutexGuard;
1149
1150    use super::super::primitives::IbvDeviceInfo;
1151    use super::*;
1152
1153    const MIB2: usize = 2 * 1024 * 1024;
1154    const SERVED_NIC: &str = "mlx5_0";
1155
1156    fn seg(address: usize, size: usize, cuda_ordinal: i32) -> ScannedSegment {
1157        ScannedSegment {
1158            address,
1159            size,
1160            cuda_ordinal,
1161            is_expandable: false,
1162        }
1163    }
1164
1165    /// A recorded `register_dmabuf_range` call.
1166    #[derive(Debug, Clone, PartialEq, Eq)]
1167    struct DmabufCall {
1168        addr: usize,
1169        size: usize,
1170        access: i32,
1171    }
1172
1173    /// A recorded (successful) `bind_mr_list` call: the direct MKeys it bound,
1174    /// compared by `Arc::ptr_eq`.
1175    #[derive(Debug, Clone)]
1176    struct BindCall {
1177        mrs: Vec<Arc<Mlx5DevxMr>>,
1178    }
1179
1180    /// Recorded state + scripted behavior for [`MockOps`]. Creation calls
1181    /// (`dmabuf_calls`, `bind_calls`, `scan_calls`) are recorded so tests can
1182    /// assert on the scan/bind algorithm. The returned
1183    /// [`Mlx5DevxMr`]/[`Mlx5DevxMkey`] wrap null handles, so their `Drop` is a
1184    /// no-op and FFI teardown is not observed here. Ownership structurally
1185    /// guarantees that ordering, which the hardware tests exercise.
1186    #[derive(Default)]
1187    struct MockState {
1188        device_name: String,
1189        mlx5dv_enabled: bool,
1190        served_ordinals: Vec<i32>,
1191        scan: Vec<ScannedSegment>,
1192        next_handle: usize,
1193        fail_dmabuf_after: Option<usize>,
1194        fail_bind: bool,
1195        scan_calls: usize,
1196        /// Every `register_dmabuf_range` call, including a failing one.
1197        dmabuf_calls: Vec<DmabufCall>,
1198        /// Every successful `bind_mr_list` call.
1199        bind_calls: Vec<BindCall>,
1200    }
1201
1202    impl MockState {
1203        fn mint(&mut self) -> usize {
1204            self.next_handle += 0x1000;
1205            self.next_handle
1206        }
1207    }
1208
1209    struct MockOps {
1210        state: Mutex<MockState>,
1211    }
1212
1213    impl MockOps {
1214        fn new(device_name: &str, mlx5dv_enabled: bool, served_ordinals: &[i32]) -> Arc<Self> {
1215            Arc::new(Self {
1216                state: Mutex::new(MockState {
1217                    device_name: device_name.to_string(),
1218                    mlx5dv_enabled,
1219                    served_ordinals: served_ordinals.to_vec(),
1220                    next_handle: 0x1_0000,
1221                    ..Default::default()
1222                }),
1223            })
1224        }
1225
1226        fn lock(&self) -> MutexGuard<'_, MockState> {
1227            self.state.lock().expect("mock state poisoned")
1228        }
1229    }
1230
1231    impl MlxDomainOps for MockOps {
1232        fn device_name(&self) -> String {
1233            self.lock().device_name.clone()
1234        }
1235
1236        fn mlx5dv_enabled(&self) -> bool {
1237            self.lock().mlx5dv_enabled
1238        }
1239
1240        fn assigned_cuda_devices(&self) -> anyhow::Result<Vec<i32>> {
1241            Ok(self.lock().served_ordinals.clone())
1242        }
1243
1244        fn scan_segments(&self) -> Vec<ScannedSegment> {
1245            let mut s = self.lock();
1246            s.scan_calls += 1;
1247            s.scan.clone()
1248        }
1249
1250        unsafe fn register_dmabuf_range(
1251            &self,
1252            pd: &Arc<IbvPd>,
1253            addr: usize,
1254            size: usize,
1255            access: i32,
1256        ) -> anyhow::Result<Mlx5DevxMr> {
1257            let mut s = self.lock();
1258            s.dmabuf_calls.push(DmabufCall { addr, size, access });
1259            if let Some(n) = s.fail_dmabuf_after
1260                && s.dmabuf_calls.len() > n
1261            {
1262                anyhow::bail!("mock dmabuf registration failure");
1263            }
1264            // A null direct-MKey owner: its `Drop` is a no-op, so the mock need
1265            // not track FFI teardown.
1266            Ok(Mlx5DevxMr::null(pd.clone()))
1267        }
1268
1269        unsafe fn bind_mr_list(
1270            &self,
1271            _pd: &IbvPd,
1272            _access: i32,
1273            mrs: Vec<Arc<Mlx5DevxMr>>,
1274        ) -> anyhow::Result<Mlx5DevxMkey> {
1275            let mut s = self.lock();
1276            if s.fail_bind {
1277                anyhow::bail!("mock bind failure");
1278            }
1279            s.bind_calls.push(BindCall { mrs: mrs.clone() });
1280            // Derive distinct keys from a freshly minted handle so tests can tell
1281            // segments and generations apart; the key wraps a null `mkey`.
1282            let v = s.mint() as u32;
1283            Ok(Mlx5DevxMkey::with_test_keys(v, v ^ 0xffff, mrs))
1284        }
1285    }
1286
1287    /// `mkey_max_entries` for tests: large enough that the small MR counts here
1288    /// never trip the segment cap (that limit has its own test).
1289    const TEST_MKEY_MAX_ENTRIES: usize = 32;
1290
1291    /// A domain wrapping the mock-driven [`MlxDomain`] under test. Its `pd`
1292    /// (and, through it, its context) is null (no-op `Drop`). Drive the strategy
1293    /// via [`IbvDomain::domain_impl`].
1294    fn domain(mock: Arc<MockOps>) -> Arc<IbvDomain<MlxDomain>> {
1295        domain_with_cap(mock, TEST_MKEY_MAX_ENTRIES)
1296    }
1297
1298    /// A [`domain`] whose segments cap their keys at `mkey_max_entries` MRs.
1299    fn domain_with_cap(mock: Arc<MockOps>, mkey_max_entries: usize) -> Arc<IbvDomain<MlxDomain>> {
1300        let mlx = MlxDomain::new_with_ops(mock, mkey_max_entries);
1301        // SAFETY: `IbvPd::null()` holds a null PD (and, through it, a null
1302        // context) whose `Drop`s are no-ops.
1303        unsafe {
1304            Arc::new(IbvDomain::for_test(
1305                Arc::new(IbvPd::null()),
1306                IbvDeviceInfo::for_test_named("test"),
1307                mlx,
1308            ))
1309        }
1310    }
1311
1312    /// Drive `domain`'s own [`MlxDomain`] strategy to register CUDA memory.
1313    fn register_cuda(
1314        domain: &Arc<IbvDomain<MlxDomain>>,
1315        addr: usize,
1316        size: usize,
1317    ) -> anyhow::Result<IbvMemoryRegionView> {
1318        // SAFETY: `domain`'s pd is null and the `MockOps` never dereference it.
1319        unsafe {
1320            domain
1321                .domain_impl()
1322                .register_cuda_mlx5dv_mr(domain, addr, size)
1323        }
1324    }
1325
1326    /// A null `Arc<IbvPd>` for driving [`RegisteredSegment::grow`] directly; the
1327    /// `MockOps` never dereference it.
1328    fn null_pd() -> Arc<IbvPd> {
1329        Arc::new(IbvPd::null())
1330    }
1331
1332    /// Upcast the mock to the `Arc<dyn MlxDomainOps>` `RegisteredSegment` takes.
1333    fn dyn_ops(ops: &Arc<MockOps>) -> Arc<dyn MlxDomainOps> {
1334        ops.clone()
1335    }
1336
1337    /// Bind a fresh segment `[base, base + size)`: an empty segment grown once.
1338    fn bind_fresh(ops: &Arc<MockOps>, base: usize, size: usize) -> Arc<RegisteredSegment> {
1339        let segment = Arc::new(RegisteredSegment::empty(
1340            dyn_ops(ops),
1341            base,
1342            TEST_MKEY_MAX_ENTRIES,
1343        ));
1344        // SAFETY: `MockOps` ignores the null `pd`; it is never dereferenced.
1345        unsafe { segment.grow(&null_pd(), 0, &seg(base, size, 0)) }
1346            .expect("fresh bind should succeed");
1347        segment
1348    }
1349
1350    // ----- RegisteredSegment binding-core unit tests -----
1351
1352    #[test]
1353    fn test_scanner_registration_round_trips() {
1354        register_cuda_segment_scanner(Arc::new(|| vec![seg(0x1000, MIB2, 3)]));
1355        assert_eq!(
1356            scan_cuda_segments(),
1357            vec![seg(0x1000, MIB2, 3)],
1358            "the registered scanner's output is returned"
1359        );
1360    }
1361
1362    #[test]
1363    fn test_fresh_bind_registers_one_mr_and_key() {
1364        let ops = MockOps::new(SERVED_NIC, true, &[0]);
1365        let base = 0x10_0000_0000;
1366        let rs = bind_fresh(&ops, base, MIB2);
1367
1368        assert_eq!(rs.size(), MIB2);
1369        assert!(
1370            rs.state.lock().unwrap().stale_mkeys.is_empty(),
1371            "a fresh segment has no superseded keys"
1372        );
1373        assert_eq!(
1374            rs.state.lock().unwrap().mkey.as_ref().unwrap().mrs().len(),
1375            1,
1376            "the current key binds the one MR"
1377        );
1378        let s = ops.lock();
1379        assert_eq!(
1380            s.dmabuf_calls,
1381            vec![DmabufCall {
1382                addr: base,
1383                size: MIB2,
1384                access: 0,
1385            }],
1386            "the whole segment is registered as one MR at its base"
1387        );
1388        assert_eq!(s.bind_calls.len(), 1);
1389        assert_eq!(s.bind_calls[0].mrs.len(), 1, "one MR bound");
1390    }
1391
1392    #[test]
1393    fn test_large_segment_splits_into_chunks() {
1394        let ops = MockOps::new(SERVED_NIC, true, &[0]);
1395        let base = 0x10_0000_0000;
1396        // One MR maxes out at MAX_MR_SIZE, so MAX_MR_SIZE + 2 MiB needs two,
1397        // both bound to a single key.
1398        let rs = bind_fresh(&ops, base, MAX_MR_SIZE + MIB2);
1399
1400        assert_eq!(
1401            rs.state.lock().unwrap().mkey.as_ref().unwrap().mrs().len(),
1402            2
1403        );
1404        let s = ops.lock();
1405        assert_eq!(
1406            s.dmabuf_calls,
1407            vec![
1408                DmabufCall {
1409                    addr: base,
1410                    size: MAX_MR_SIZE,
1411                    access: 0,
1412                },
1413                DmabufCall {
1414                    addr: base + MAX_MR_SIZE,
1415                    size: MIB2,
1416                    access: 0,
1417                },
1418            ],
1419            "the range splits into a MAX_MR_SIZE chunk and a 2 MiB tail"
1420        );
1421        assert_eq!(s.bind_calls.len(), 1);
1422        assert_eq!(
1423            s.bind_calls[0].mrs.len(),
1424            2,
1425            "both chunk MRs bound to one key"
1426        );
1427    }
1428
1429    #[test]
1430    fn test_one_key_can_bind_more_than_inline_umr_limit() {
1431        let ops = MockOps::new(SERVED_NIC, true, &[0]);
1432        let base = 0x10_0000_0000;
1433        const ENTRIES: usize = 33;
1434        let segment = RegisteredSegment::empty(dyn_ops(&ops), base, ENTRIES);
1435
1436        // SAFETY: `MockOps` ignores the null `pd`; it is never dereferenced.
1437        unsafe { segment.grow(&null_pd(), 0, &seg(base, ENTRIES * MAX_MR_SIZE, 0)) }
1438            .expect("a DevX key should bind more than the inline UMR WQE limit");
1439
1440        assert_eq!(segment.size(), ENTRIES * MAX_MR_SIZE);
1441        assert_eq!(
1442            ops.lock().bind_calls[0].mrs.len(),
1443            ENTRIES,
1444            "all 33 MRs are bound to one key"
1445        );
1446    }
1447
1448    #[test]
1449    fn test_grow_reuses_mrs_and_retires_prior_key() {
1450        let ops = MockOps::new(SERVED_NIC, true, &[0]);
1451        let base = 0x10_0000_0000;
1452        let rs = bind_fresh(&ops, base, MIB2);
1453        let mkey_a = rs.state.lock().unwrap().mkey.as_ref().unwrap().keys();
1454        let mr_a = ops.lock().bind_calls[0].mrs[0].clone();
1455
1456        // SAFETY: `MockOps` ignores the null `pd`; it is never dereferenced.
1457        unsafe { rs.grow(&null_pd(), 0, &seg(base, 2 * MIB2, 0)) }.expect("growth should succeed");
1458
1459        assert_eq!(rs.size(), 2 * MIB2);
1460        assert_eq!(
1461            rs.state.lock().unwrap().mkey.as_ref().unwrap().mrs().len(),
1462            2,
1463            "the original MR is reused and one tail MR appended"
1464        );
1465        assert_eq!(
1466            rs.state.lock().unwrap().stale_mkeys.len(),
1467            1,
1468            "the prior key is parked as stale"
1469        );
1470        assert_eq!(
1471            rs.state.lock().unwrap().stale_mkeys[0].keys(),
1472            mkey_a,
1473            "the parked key is the original, not a fresh one"
1474        );
1475        let s = ops.lock();
1476        // Only the new tail is registered; the original MR is reused.
1477        assert_eq!(
1478            s.dmabuf_calls,
1479            vec![
1480                DmabufCall {
1481                    addr: base,
1482                    size: MIB2,
1483                    access: 0,
1484                },
1485                DmabufCall {
1486                    addr: base + MIB2,
1487                    size: MIB2,
1488                    access: 0,
1489                },
1490            ],
1491            "growth registers only the new tail, at base + MIB2"
1492        );
1493        assert_eq!(s.bind_calls.len(), 2);
1494        assert_eq!(s.bind_calls[0].mrs.len(), 1);
1495        assert!(
1496            Arc::ptr_eq(&s.bind_calls[0].mrs[0], &mr_a),
1497            "the first bind covered just the original MR"
1498        );
1499        assert_eq!(s.bind_calls[1].mrs.len(), 2);
1500        assert!(
1501            Arc::ptr_eq(&s.bind_calls[1].mrs[0], &mr_a)
1502                && !Arc::ptr_eq(&s.bind_calls[1].mrs[1], &mr_a),
1503            "the second bind reuses the original MR and adds a new tail"
1504        );
1505    }
1506
1507    #[test]
1508    fn test_grow_failure_leaves_segment_unchanged() {
1509        let ops = MockOps::new(SERVED_NIC, true, &[0]);
1510        let base = 0x10_0000_0000;
1511        let rs = bind_fresh(&ops, base, MIB2);
1512        // The fresh bind used one dmabuf call; fail the second tail chunk.
1513        ops.lock().fail_dmabuf_after = Some(2);
1514
1515        // SAFETY: `MockOps` ignores the null `pd`; it is never dereferenced.
1516        let result = unsafe { rs.grow(&null_pd(), 0, &seg(base, MAX_MR_SIZE + 2 * MIB2, 0)) };
1517        assert!(
1518            result.is_err(),
1519            "growth fails when a tail MR fails to register"
1520        );
1521
1522        assert_eq!(rs.size(), MIB2, "size unchanged after a failed growth");
1523        assert_eq!(
1524            rs.state.lock().unwrap().mkey.as_ref().unwrap().mrs().len(),
1525            1,
1526            "no tail MRs committed"
1527        );
1528        assert!(
1529            rs.state.lock().unwrap().stale_mkeys.is_empty(),
1530            "no key retired"
1531        );
1532        assert_eq!(
1533            ops.lock().bind_calls.len(),
1534            1,
1535            "only the original bind happened; the growth bind was never reached"
1536        );
1537    }
1538
1539    #[test]
1540    fn test_grow_to_equal_size_is_noop() {
1541        let ops = MockOps::new(SERVED_NIC, true, &[0]);
1542        let base = 0x10_0000_0000;
1543        let rs = bind_fresh(&ops, base, 2 * MIB2);
1544        let binds_before = ops.lock().bind_calls.len();
1545
1546        // A scan reporting the same size must be a no-op rather than re-binding.
1547        // SAFETY: `MockOps` ignores the null `pd`; it is never dereferenced.
1548        unsafe { rs.grow(&null_pd(), 0, &seg(base, 2 * MIB2, 0)) }
1549            .expect("equal-size grow is a no-op");
1550
1551        assert_eq!(rs.size(), 2 * MIB2, "size is unchanged");
1552        let s = ops.lock();
1553        assert_eq!(s.bind_calls.len(), binds_before, "no new bind performed");
1554        assert_eq!(s.dmabuf_calls.len(), 1, "no new MR registered");
1555    }
1556
1557    #[test]
1558    fn test_view_pins_segment_and_anchors_at_base() {
1559        let ops = MockOps::new(SERVED_NIC, true, &[0]);
1560        let base = 0x10_0000_0000;
1561        let rs = bind_fresh(&ops, base, MIB2);
1562
1563        assert!(rs.covers(base + 0x400, 4096));
1564        let view = RegisteredSegment::view(&rs, base + 0x400, 4096);
1565        assert_eq!(
1566            view.rdma_addr, 0x400,
1567            "rdma_addr is the offset from the segment base"
1568        );
1569        assert_eq!(view.size, 4096);
1570        assert_eq!(view.device_name, SERVED_NIC);
1571        assert_eq!(
1572            Arc::strong_count(&rs),
1573            2,
1574            "the view's guard pins the segment alive (our ref + the view's)"
1575        );
1576
1577        drop(view);
1578        assert_eq!(
1579            Arc::strong_count(&rs),
1580            1,
1581            "dropping the view releases its segment ref"
1582        );
1583    }
1584
1585    #[test]
1586    fn test_partial_dmabuf_failure_cleans_up() {
1587        let ops = MockOps::new(SERVED_NIC, true, &[0]);
1588        ops.lock().fail_dmabuf_after = Some(1);
1589        let base = 0x10_0000_0000;
1590        let segment = RegisteredSegment::empty(dyn_ops(&ops), base, TEST_MKEY_MAX_ENTRIES);
1591
1592        // Two chunks; the second dmabuf registration fails.
1593        // SAFETY: `MockOps` ignores the null `pd`; it is never dereferenced.
1594        let result = unsafe { segment.grow(&null_pd(), 0, &seg(base, MAX_MR_SIZE + MIB2, 0)) };
1595        assert!(
1596            result.is_err(),
1597            "the first bind fails when a chunk registration fails"
1598        );
1599        assert_eq!(segment.size(), 0, "the segment is left empty");
1600        assert!(
1601            segment.state.lock().unwrap().mkey.is_none(),
1602            "no key installed"
1603        );
1604        let s = ops.lock();
1605        assert!(s.bind_calls.is_empty(), "no bind performed");
1606    }
1607
1608    #[test]
1609    fn test_bind_failure_cleans_up() {
1610        let ops = MockOps::new(SERVED_NIC, true, &[0]);
1611        ops.lock().fail_bind = true;
1612        let base = 0x10_0000_0000;
1613        let segment = RegisteredSegment::empty(dyn_ops(&ops), base, TEST_MKEY_MAX_ENTRIES);
1614
1615        // SAFETY: `MockOps` ignores the null `pd`; it is never dereferenced.
1616        let result = unsafe { segment.grow(&null_pd(), 0, &seg(base, MIB2, 0)) };
1617        assert!(result.is_err(), "the bind fails when bind_mr_list fails");
1618        assert_eq!(segment.size(), 0, "the segment is left empty");
1619        assert!(
1620            segment.state.lock().unwrap().mkey.is_none(),
1621            "no key installed on bind failure"
1622        );
1623    }
1624
1625    #[test]
1626    fn test_grow_past_mkey_max_entries_covers_the_prefix_that_fits() {
1627        let ops = MockOps::new(SERVED_NIC, true, &[0]);
1628        let base = 0x10_0000_0000;
1629        // Cap at one MR against a two-chunk segment: the first chunk binds and
1630        // the second is left out rather than pushing the key over its cap.
1631        let segment = RegisteredSegment::empty(dyn_ops(&ops), base, 1);
1632        // SAFETY: `MockOps` ignores the null `pd`; it is never dereferenced.
1633        let result = unsafe { segment.grow(&null_pd(), 0, &seg(base, MAX_MR_SIZE + MIB2, 0)) };
1634        assert!(result.is_ok(), "grow binds the chunks that fit the cap");
1635        assert_eq!(segment.size(), MAX_MR_SIZE, "one chunk is bound");
1636        assert_eq!(
1637            segment.scanned_size(),
1638            MAX_MR_SIZE + MIB2,
1639            "the whole scanned extent is recorded"
1640        );
1641        assert!(
1642            segment.in_unregistered_tail(base + MAX_MR_SIZE, MIB2),
1643            "the chunk left out is reported as unregistered tail"
1644        );
1645        let s = ops.lock();
1646        assert_eq!(s.dmabuf_calls.len(), 1, "only the fitting chunk registers");
1647        assert_eq!(s.bind_calls.len(), 1, "the key binds that one chunk");
1648    }
1649
1650    #[test]
1651    fn test_grow_with_a_full_key_leaves_the_segment_bound() {
1652        let ops = MockOps::new(SERVED_NIC, true, &[0]);
1653        let base = 0x10_0000_0000;
1654        let segment = RegisteredSegment::empty(dyn_ops(&ops), base, 1);
1655        // SAFETY: `MockOps` ignores the null `pd`; it is never dereferenced.
1656        unsafe { segment.grow(&null_pd(), 0, &seg(base, MAX_MR_SIZE, 0)) }
1657            .expect("the first chunk fills the one-MR key");
1658        let registered = ops.lock().dmabuf_calls.len();
1659
1660        // SAFETY: as above.
1661        let result = unsafe { segment.grow(&null_pd(), 0, &seg(base, MAX_MR_SIZE + MIB2, 0)) };
1662        assert!(result.is_ok(), "a full key is not an error");
1663        assert_eq!(segment.size(), MAX_MR_SIZE, "the bound prefix is unchanged");
1664        assert_eq!(
1665            segment.scanned_size(),
1666            MAX_MR_SIZE + MIB2,
1667            "the new scanned extent is recorded"
1668        );
1669        assert_eq!(
1670            ops.lock().dmabuf_calls.len(),
1671            registered,
1672            "nothing registers once the key is full"
1673        );
1674    }
1675
1676    #[test]
1677    fn test_segment_too_large_for_one_key_still_covers_its_prefix() {
1678        let ops = MockOps::new(SERVED_NIC, true, &[0]);
1679        let base = 0x10_0000_0000;
1680        // Two chunks against a one-MR cap: the first binds, the second is left
1681        // out.
1682        ops.lock().scan = vec![seg(base, MAX_MR_SIZE + MIB2, 0)];
1683        let domain = domain_with_cap(ops.clone(), 1);
1684
1685        assert!(
1686            register_cuda(&domain, base, MIB2).is_ok(),
1687            "a request inside the bound prefix is served by the key"
1688        );
1689        let scans = ops.lock().scan_calls;
1690
1691        let tail = base + MAX_MR_SIZE;
1692        assert!(
1693            register_cuda(&domain, tail, MIB2).is_err(),
1694            "a request in the chunk left out cannot use the key"
1695        );
1696        assert_eq!(
1697            ops.lock().scan_calls,
1698            scans,
1699            "the unregistered tail does not trigger another scan"
1700        );
1701    }
1702
1703    #[test]
1704    fn test_growth_past_the_cap_refuses_the_new_tail_without_rescanning() {
1705        let ops = MockOps::new(SERVED_NIC, true, &[0]);
1706        let base = 0x10_0000_0000;
1707        // One chunk binds under a one-MR cap; a second would not.
1708        ops.lock().scan = vec![seg(base, MAX_MR_SIZE, 0)];
1709        let domain = domain_with_cap(ops.clone(), 1);
1710        assert!(
1711            register_cuda(&domain, base, MIB2).is_ok(),
1712            "the first chunk fits the cap"
1713        );
1714
1715        ops.lock().scan = vec![seg(base, MAX_MR_SIZE + MIB2, 0)];
1716        let tail = base + MAX_MR_SIZE;
1717        assert!(
1718            register_cuda(&domain, tail, MIB2).is_err(),
1719            "growing into a second chunk would exceed the cap"
1720        );
1721        let (scans, dmabufs) = {
1722            let s = ops.lock();
1723            (s.scan_calls, s.dmabuf_calls.len())
1724        };
1725
1726        assert!(
1727            register_cuda(&domain, tail, MIB2).is_err(),
1728            "the tail stays unservable"
1729        );
1730        let s = ops.lock();
1731        assert_eq!(s.scan_calls, scans, "no rescan for a known tail");
1732        assert_eq!(
1733            s.dmabuf_calls.len(),
1734            dmabufs,
1735            "no registration attempted for a refused tail"
1736        );
1737    }
1738
1739    // ----- MlxDomain integration tests -----
1740
1741    #[test]
1742    fn test_mlx5dv_enabled_derived_from_ops() {
1743        let mock = MockOps::new(SERVED_NIC, true, &[0, 2]);
1744        let domain = domain(mock);
1745        assert!(domain.domain_impl().mlx5dv_enabled);
1746    }
1747
1748    #[test]
1749    fn test_served_ordinals_resolved_per_use_not_at_construction() {
1750        // A domain is normally opened by a host registration, before CUDA is
1751        // initialized and so before any ordinal resolves to a NIC. Capturing the
1752        // served set at construction would exclude every CUDA segment for the
1753        // domain's lifetime, so it must be re-read per registration.
1754        let base = 0x10_0000_0000;
1755        let mock = MockOps::new(SERVED_NIC, true, &[]);
1756        let domain = domain(mock.clone());
1757        mock.lock().scan = vec![seg(base, MIB2, 0)];
1758
1759        assert!(
1760            register_cuda(&domain, base, 4096).is_err(),
1761            "ordinal 0 is not served yet, so its segment must not bind"
1762        );
1763
1764        // CUDA comes up; ordinal 0 now resolves to this NIC.
1765        mock.lock().served_ordinals = vec![0];
1766
1767        let view = register_cuda(&domain, base, 4096)
1768            .expect("the segment must bind once its ordinal becomes served");
1769        assert_eq!(view.size, 4096);
1770        assert_eq!(mock.lock().bind_calls.len(), 1, "one segment bound");
1771    }
1772
1773    #[test]
1774    fn test_fresh_bind() {
1775        let base = 0x10_0000_0000;
1776        let mock = MockOps::new(SERVED_NIC, true, &[0]);
1777        mock.lock().scan = vec![seg(base, MIB2, 0)];
1778        let domain = domain(mock.clone());
1779
1780        let view = register_cuda(&domain, base, 4096).expect("fresh bind should succeed");
1781
1782        // The per-segment binding details are covered by the binding-core unit
1783        // tests above; here we only check the domain wired scan → bind → view.
1784        assert_eq!(view.rdma_addr, 0, "view is anchored at the segment base");
1785        assert_eq!(view.size, 4096);
1786        assert_eq!(view.device_name, SERVED_NIC);
1787        assert_eq!(mock.lock().bind_calls.len(), 1, "one segment bound");
1788    }
1789
1790    #[test]
1791    fn test_fast_path_skips_scan() {
1792        let mock = MockOps::new(SERVED_NIC, true, &[0]);
1793        mock.lock().scan = vec![seg(0x10_0000_0000, MIB2, 0)];
1794        let domain = domain(mock.clone());
1795
1796        register_cuda(&domain, 0x10_0000_0000, 4096).unwrap();
1797        let scans_after_first = mock.lock().scan_calls;
1798
1799        // A second request covered by the existing binding must not rescan
1800        // or rebind.
1801        register_cuda(&domain, 0x10_0000_0400, 4096).unwrap();
1802        let s = mock.lock();
1803        assert_eq!(s.scan_calls, scans_after_first, "fast path skips the scan");
1804        assert_eq!(s.bind_calls.len(), 1, "fast path performs no new bind");
1805    }
1806
1807    #[test]
1808    fn test_segment_growth_via_register() {
1809        let base = 0x10_0000_0000;
1810        let mock = MockOps::new(SERVED_NIC, true, &[0]);
1811        mock.lock().scan = vec![seg(base, MIB2, 0)];
1812        let domain = domain(mock.clone());
1813
1814        register_cuda(&domain, base, 4096).unwrap();
1815
1816        // The scanner now reports the segment has grown; a request into the new
1817        // tail grows the mapped segment in place and serves a view from it. (MR
1818        // reuse / key retirement are covered by the binding-core unit tests.)
1819        mock.lock().scan = vec![seg(base, 2 * MIB2, 0)];
1820        let view =
1821            register_cuda(&domain, base + MIB2, 4096).expect("growth registration should succeed");
1822
1823        assert_eq!(view.rdma_addr, MIB2, "the view anchors into the grown tail");
1824        assert_eq!(
1825            mock.lock().bind_calls.len(),
1826            2,
1827            "growth created a new key (a second bind), retiring the prior one"
1828        );
1829    }
1830
1831    #[test]
1832    fn test_unserved_ordinal_is_not_covered() {
1833        let mock = MockOps::new(SERVED_NIC, true, &[0]);
1834        // Segment is on ordinal 5, which this NIC does not serve.
1835        mock.lock().scan = vec![seg(0x10_0000_0000, MIB2, 5)];
1836        let domain = domain(mock.clone());
1837
1838        let result = register_cuda(&domain, 0x10_0000_0000, 4096);
1839        assert!(
1840            result.is_err(),
1841            "memory on an unserved ordinal is not bound"
1842        );
1843        assert!(
1844            mock.lock().bind_calls.is_empty(),
1845            "no bind attempted for an unserved ordinal"
1846        );
1847    }
1848
1849    #[test]
1850    fn test_multi_device_subset_growth_new_segments_and_boundaries() {
1851        // Three CUDA devices are visible, but this NIC only serves ordinals
1852        // 0 and 2; ordinal 1's segment must never be bound.
1853        let a = 0x10_0000_0000usize; // ordinal 0
1854        let a2 = 0x18_0000_0000usize; // ordinal 0, appears later
1855        let b = 0x20_0000_0000usize; // ordinal 1 (unserved)
1856        let c = 0x30_0000_0000usize; // ordinal 2
1857        let mock = MockOps::new(SERVED_NIC, true, &[0, 2]);
1858        mock.lock().scan = vec![seg(a, MIB2, 0), seg(b, MIB2, 1), seg(c, 2 * MIB2, 2)];
1859        let domain = domain(mock.clone());
1860        // The registration path registers MRs with the domain's own access
1861        // flags; capture them to assert the dmabuf registration arguments.
1862        let access = domain.domain_impl().access_flags();
1863
1864        // Validate every field of a returned view: its address fields, its
1865        // size, the serving NIC's name, and the keys. The mock binds each key
1866        // with `(lkey, rkey) = (handle, handle ^ 0xffff)` for a freshly minted
1867        // handle, so a bound view always has a non-zero lkey and an rkey that is
1868        // its lkey xor 0xffff.
1869        let assert_view =
1870            |view: &IbvMemoryRegionView, virtual_addr: usize, rdma_addr: usize, size: usize| {
1871                assert_eq!(
1872                    view.virtual_addr, virtual_addr,
1873                    "virtual_addr is the requested address"
1874                );
1875                assert_eq!(
1876                    view.rdma_addr, rdma_addr,
1877                    "rdma_addr is the offset from the segment base"
1878                );
1879                assert_eq!(view.size, size, "size matches the request");
1880                assert_eq!(view.device_name, SERVED_NIC, "view names the serving NIC");
1881                assert_ne!(view.lkey, 0, "a bound view carries a real lkey");
1882                assert_eq!(
1883                    view.rkey,
1884                    view.lkey ^ 0xffff,
1885                    "rkey and lkey are derived from the same mkey"
1886                );
1887            };
1888
1889        // First registration binds every served segment in the scan (ordinals
1890        // 0 and 2), skipping the unserved ordinal-1 segment, then serves the
1891        // requested view from ordinal 0's segment.
1892        let view_a = register_cuda(&domain, a, 0x2000).expect("served ordinal binds");
1893        assert_view(&view_a, a, 0, 0x2000);
1894        let (mr_a, mr_c) = {
1895            let s = mock.lock();
1896            assert_eq!(s.bind_calls.len(), 2, "one bind per served segment");
1897            // Each served segment fits in a single MR, so each bind got a
1898            // one-element MR list.
1899            assert_eq!(s.bind_calls[0].mrs.len(), 1, "segment a binds a single MR");
1900            assert_eq!(s.bind_calls[1].mrs.len(), 1, "segment c binds a single MR");
1901            // Each served segment is registered as one MR covering its whole
1902            // extent, with the domain's access flags; the unserved ordinal-1
1903            // segment is absent.
1904            assert_eq!(
1905                s.dmabuf_calls,
1906                vec![
1907                    DmabufCall {
1908                        addr: a,
1909                        size: MIB2,
1910                        access,
1911                    },
1912                    DmabufCall {
1913                        addr: c,
1914                        size: 2 * MIB2,
1915                        access,
1916                    },
1917                ],
1918                "a and c are each registered once, covering their full extent"
1919            );
1920            assert_eq!(s.scan_calls, 1);
1921            let (mr_a, mr_c) = (
1922                s.bind_calls[0].mrs[0].clone(),
1923                s.bind_calls[1].mrs[0].clone(),
1924            );
1925            assert!(!Arc::ptr_eq(&mr_a, &mr_c), "a and c are distinct MRs");
1926            (mr_a, mr_c)
1927        };
1928
1929        // A request into the other served segment (ordinal 2) resolves to it,
1930        // not segment a, and is served from the fast path (no rescan).
1931        let view_c =
1932            register_cuda(&domain, c + 0x800, 0x4000).expect("segment c covers the request");
1933        assert_view(&view_c, c + 0x800, 0x800, 0x4000);
1934        assert_ne!(
1935            view_c.lkey, view_a.lkey,
1936            "distinct segments are backed by distinct keys"
1937        );
1938        assert_eq!(
1939            mock.lock().scan_calls,
1940            1,
1941            "fast path serves c without a scan"
1942        );
1943
1944        // Boundary handling: a request ending exactly at segment c's end is
1945        // covered (and shares c's key, since c has not been rebound); one that
1946        // crosses the end is not.
1947        let tail = register_cuda(&domain, c + 2 * MIB2 - 0x6000, 0x6000)
1948            .expect("a request ending exactly at the boundary is covered");
1949        assert_view(&tail, c + 2 * MIB2 - 0x6000, 2 * MIB2 - 0x6000, 0x6000);
1950        assert_eq!(
1951            (tail.lkey, tail.rkey),
1952            (view_c.lkey, view_c.rkey),
1953            "both views of segment c share its current key"
1954        );
1955        assert!(
1956            register_cuda(&domain, c + 2 * MIB2 - 2048, 4096).is_err(),
1957            "a request straddling the segment's end boundary is rejected"
1958        );
1959
1960        // The scanner now reports segment a grew, segment c is unchanged, and a
1961        // brand-new segment a2 appeared on ordinal 0. A single request into a's
1962        // grown tail processes all of that: grow a, skip c, bind a2.
1963        mock.lock().scan = vec![
1964            seg(a, 3 * MIB2, 0),
1965            seg(b, MIB2, 1),
1966            seg(c, 2 * MIB2, 2),
1967            seg(a2, MIB2, 0),
1968        ];
1969        let grown =
1970            register_cuda(&domain, a + 2 * MIB2, 0x3000).expect("a's grown tail is now covered");
1971        assert_view(&grown, a + 2 * MIB2, 2 * MIB2, 0x3000);
1972        assert_ne!(
1973            grown.lkey, view_a.lkey,
1974            "growing segment a rotated it onto a new key"
1975        );
1976        {
1977            let s = mock.lock();
1978            // Two binds added: a's growth rebind and a2's fresh bind (c was
1979            // already full-size, so it is not rebound).
1980            assert_eq!(s.bind_calls.len(), 4);
1981            // a's growth rebinds its reused original MR followed by the one new
1982            // tail MR.
1983            assert_eq!(
1984                s.bind_calls[2].mrs.len(),
1985                2,
1986                "a's growth binds the reused original MR plus the new tail"
1987            );
1988            assert!(
1989                Arc::ptr_eq(&s.bind_calls[2].mrs[0], &mr_a),
1990                "the original MR is reused, not re-registered"
1991            );
1992            let mr_a_tail = s.bind_calls[2].mrs[1].clone();
1993            // a2's fresh bind passes a single new MR.
1994            assert_eq!(
1995                s.bind_calls[3].mrs.len(),
1996                1,
1997                "the new segment a2 binds a single MR"
1998            );
1999            let mr_a2 = s.bind_calls[3].mrs[0].clone();
2000            // a's original + tail, c, and a2 are four distinct MRs.
2001            let all = [&mr_a, &mr_c, &mr_a_tail, &mr_a2];
2002            for (i, x) in all.iter().enumerate() {
2003                for y in &all[i + 1..] {
2004                    assert!(
2005                        !Arc::ptr_eq(x, y),
2006                        "a, a-tail, c, and a2 are four distinct MRs"
2007                    );
2008                }
2009            }
2010            // Growth registered only a's new tail (at a + MIB2, covering the 2
2011            // MiB it grew by) and a2's single MR; c was left untouched and the
2012            // first two registrations are unchanged. MR extents track segment
2013            // sizes, never the (varied) request sizes.
2014            assert_eq!(
2015                s.dmabuf_calls,
2016                vec![
2017                    DmabufCall {
2018                        addr: a,
2019                        size: MIB2,
2020                        access,
2021                    },
2022                    DmabufCall {
2023                        addr: c,
2024                        size: 2 * MIB2,
2025                        access,
2026                    },
2027                    DmabufCall {
2028                        addr: a + MIB2,
2029                        size: 2 * MIB2,
2030                        access,
2031                    },
2032                    DmabufCall {
2033                        addr: a2,
2034                        size: MIB2,
2035                        access,
2036                    },
2037                ],
2038                "growth registers only a's new tail and a2; c is not re-registered"
2039            );
2040        }
2041
2042        // The brand-new segment is independently usable, carries its own
2043        // distinct key, and has correct boundary handling of its own.
2044        let view_a2 = register_cuda(&domain, a2 + 0x400, 0x800)
2045            .expect("the new segment a2 covers the request");
2046        assert_view(&view_a2, a2 + 0x400, 0x400, 0x800);
2047        assert_ne!(
2048            view_a2.lkey, grown.lkey,
2049            "the new segment a2 has a key distinct from a's"
2050        );
2051        assert_ne!(
2052            view_a2.lkey, view_c.lkey,
2053            "the new segment a2 has a key distinct from c's"
2054        );
2055        assert!(
2056            register_cuda(&domain, a2 + MIB2 - 512, 1024).is_err(),
2057            "a request past a2's end boundary is rejected"
2058        );
2059
2060        // Even after all the rebinding, memory on the unserved ordinal is still
2061        // never bound or served.
2062        assert!(
2063            register_cuda(&domain, b, 4096).is_err(),
2064            "the unserved ordinal-1 segment is never served"
2065        );
2066        assert!(
2067            !mock.lock().dmabuf_calls.iter().any(|call| call.addr == b),
2068            "the unserved ordinal-1 segment was never registered"
2069        );
2070    }
2071}