Skip to main content

monarch_rdma/
local_memory.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//! Local memory abstractions for RDMA operations.
10//!
11//! [`KeepaliveLocalMemory`] wraps a raw pointer with a [`Keepalive`]
12//! guard and dispatches reads/writes to CPU or CUDA paths.
13
14use std::collections::HashMap;
15use std::collections::hash_map::Entry;
16use std::fmt::Debug;
17use std::sync::Arc;
18use std::sync::Condvar;
19use std::sync::Mutex;
20
21use dashmap::DashMap;
22
23use crate::backend::ibverbs::device::IbvDeviceImpl;
24use crate::backend::ibverbs::memory_region::IbvMemoryRegionView;
25use crate::device_selection::MemoryLocation;
26
27/// Returns `true` when `addr` is a CUDA device pointer.
28///
29/// Probes the CUDA driver via `cuPointerGetAttribute`; returns `false`
30/// when CUDA is unavailable or the pointer is not device memory.
31pub fn is_device_ptr(addr: usize) -> bool {
32    // SAFETY: FFI call that queries pointer metadata without accessing
33    // the pointed-to memory.
34    unsafe {
35        let mut mem_type: u32 = 0;
36        let err = rdmaxcel_sys::rdmaxcel_cuPointerGetAttribute(
37            &mut mem_type as *mut _ as *mut std::ffi::c_void,
38            rdmaxcel_sys::CU_POINTER_ATTRIBUTE_MEMORY_TYPE,
39            addr as rdmaxcel_sys::CUdeviceptr,
40        );
41        err == rdmaxcel_sys::CUDA_SUCCESS && mem_type == rdmaxcel_sys::CU_MEMORYTYPE_DEVICE
42    }
43}
44
45/// RAII guard that restores the previous CUDA context on drop and, if a
46/// primary context was retained, releases it.
47pub(crate) struct CudaCtxGuard {
48    prev: rdmaxcel_sys::CUcontext,
49    /// Set when the fallback path called `cuDevicePrimaryCtxRetain`.
50    retained_device: Option<rdmaxcel_sys::CUdevice>,
51}
52
53impl Drop for CudaCtxGuard {
54    fn drop(&mut self) {
55        unsafe {
56            rdmaxcel_sys::rdmaxcel_cuCtxSetCurrent(self.prev);
57            if let Some(device) = self.retained_device {
58                rdmaxcel_sys::rdmaxcel_cuDevicePrimaryCtxRelease(device);
59            }
60        }
61    }
62}
63
64/// Make the CUDA context that owns `addr` current on the calling
65/// thread, returning a guard that restores the previous context on
66/// drop.
67///
68/// First tries `CU_POINTER_ATTRIBUTE_CONTEXT` to get the exact context
69/// the allocation belongs to.  When that returns null (runtime-API or
70/// memory-pool allocations such as PyTorch's caching allocator), falls
71/// back to the device's primary context via
72/// `CU_POINTER_ATTRIBUTE_DEVICE_ORDINAL` + `cuDevicePrimaryCtxRetain`.
73///
74/// # Safety
75///
76/// `addr` must be a valid CUDA device pointer.
77pub(crate) unsafe fn set_ctx_for_ptr(addr: usize) -> Result<CudaCtxGuard, anyhow::Error> {
78    let mut prev: rdmaxcel_sys::CUcontext = std::ptr::null_mut();
79    unsafe {
80        rdmaxcel_sys::rdmaxcel_cuCtxGetCurrent(&mut prev);
81    }
82
83    let mut ctx: rdmaxcel_sys::CUcontext = std::ptr::null_mut();
84    let rc = unsafe {
85        rdmaxcel_sys::rdmaxcel_cuPointerGetAttribute(
86            &mut ctx as *mut _ as *mut std::ffi::c_void,
87            rdmaxcel_sys::CU_POINTER_ATTRIBUTE_CONTEXT,
88            addr as rdmaxcel_sys::CUdeviceptr,
89        )
90    };
91
92    // Null context: allocation came from the runtime API or a memory
93    // pool.  Fall back to the owning device's primary context.
94    let mut retained_device = None;
95    if rc != rdmaxcel_sys::CUDA_SUCCESS || ctx.is_null() {
96        let mut ordinal: i32 = -1;
97        let rc = unsafe {
98            rdmaxcel_sys::rdmaxcel_cuPointerGetAttribute(
99                &mut ordinal as *mut _ as *mut std::ffi::c_void,
100                rdmaxcel_sys::CU_POINTER_ATTRIBUTE_DEVICE_ORDINAL,
101                addr as rdmaxcel_sys::CUdeviceptr,
102            )
103        };
104        anyhow::ensure!(
105            rc == rdmaxcel_sys::CUDA_SUCCESS,
106            "cuPointerGetAttribute(DEVICE_ORDINAL) failed with error code {rc}"
107        );
108
109        let mut device: rdmaxcel_sys::CUdevice = 0;
110        let rc = unsafe { rdmaxcel_sys::rdmaxcel_cuDeviceGet(&mut device, ordinal) };
111        anyhow::ensure!(
112            rc == rdmaxcel_sys::CUDA_SUCCESS,
113            "cuDeviceGet({ordinal}) failed with error code {rc}"
114        );
115
116        let rc = unsafe { rdmaxcel_sys::rdmaxcel_cuDevicePrimaryCtxRetain(&mut ctx, device) };
117        anyhow::ensure!(
118            rc == rdmaxcel_sys::CUDA_SUCCESS,
119            "cuDevicePrimaryCtxRetain failed with error code {rc}"
120        );
121        retained_device = Some(device);
122    }
123
124    let rc = unsafe { rdmaxcel_sys::rdmaxcel_cuCtxSetCurrent(ctx) };
125    anyhow::ensure!(
126        rc == rdmaxcel_sys::CUDA_SUCCESS,
127        "cuCtxSetCurrent failed with error code {rc}"
128    );
129
130    Ok(CudaCtxGuard {
131        prev,
132        retained_device,
133    })
134}
135
136/// Verify that an access at `offset` with `len` bytes fits within `size`.
137fn check_bounds(offset: usize, len: usize, size: usize) -> Result<(), anyhow::Error> {
138    anyhow::ensure!(
139        offset.checked_add(len).is_some_and(|end| end <= size),
140        "access at offset {offset} with length {len} exceeds region size {size}"
141    );
142    Ok(())
143}
144
145/// Copy `dst.len()` bytes from host memory at `addr + offset` into `dst`.
146///
147/// # Safety
148///
149/// The caller must ensure that `addr` points to a valid host allocation of
150/// at least `offset + dst.len()` bytes.
151unsafe fn read_cpu(addr: usize, offset: usize, dst: &mut [u8]) {
152    unsafe {
153        std::ptr::copy_nonoverlapping((addr + offset) as *const u8, dst.as_mut_ptr(), dst.len());
154    }
155}
156
157/// Copy `src.len()` bytes from `src` into host memory at `addr + offset`.
158///
159/// # Safety
160///
161/// The caller must ensure that `addr` points to a valid host allocation of
162/// at least `offset + src.len()` bytes.
163unsafe fn write_cpu(addr: usize, offset: usize, src: &[u8]) {
164    unsafe {
165        std::ptr::copy_nonoverlapping(src.as_ptr(), (addr + offset) as *mut u8, src.len());
166    }
167}
168
169/// Copy `dst.len()` bytes from device memory at `addr + offset` into `dst`.
170///
171/// # Safety
172///
173/// The caller must ensure that `addr` is a valid CUDA device pointer to an
174/// allocation of at least `offset + dst.len()` bytes.
175unsafe fn read_gpu(addr: usize, offset: usize, dst: &mut [u8]) -> Result<(), anyhow::Error> {
176    let _guard = unsafe { set_ctx_for_ptr(addr)? };
177    let rc = unsafe {
178        rdmaxcel_sys::rdmaxcel_cuMemcpyDtoH_v2(
179            dst.as_mut_ptr() as *mut std::ffi::c_void,
180            (addr + offset) as rdmaxcel_sys::CUdeviceptr,
181            dst.len(),
182        )
183    };
184    anyhow::ensure!(
185        rc == rdmaxcel_sys::CUDA_SUCCESS,
186        "cuMemcpyDtoH failed with error code {rc}"
187    );
188    Ok(())
189}
190
191/// Copy `src.len()` bytes from `src` into device memory at `addr + offset`.
192///
193/// # Safety
194///
195/// The caller must ensure that `addr` is a valid CUDA device pointer to an
196/// allocation of at least `offset + src.len()` bytes.
197unsafe fn write_gpu(addr: usize, offset: usize, src: &[u8]) -> Result<(), anyhow::Error> {
198    let _guard = unsafe { set_ctx_for_ptr(addr)? };
199    let rc = unsafe {
200        rdmaxcel_sys::rdmaxcel_cuMemcpyHtoD_v2(
201            (addr + offset) as rdmaxcel_sys::CUdeviceptr,
202            src.as_ptr() as *const std::ffi::c_void,
203            src.len(),
204        )
205    };
206    anyhow::ensure!(
207        rc == rdmaxcel_sys::CUDA_SUCCESS,
208        "cuMemcpyHtoD failed with error code {rc}"
209    );
210    Ok(())
211}
212
213/// Three-mode access lock used by [`KeepaliveLocalMemory`] to coordinate
214/// concurrent reads, exclusive writes, and parallel "disjoint" writes
215/// (writers that the caller has promised target disjoint ranges).
216///
217/// - [`AccessLock::read`] returns when no exclusive writer and no
218///   disjoint writer is active. Multiple readers are permitted to hold
219///   the lock at the same time.
220/// - [`AccessLock::disjoint_write`] returns when no reader and no
221///   exclusive writer is active. Multiple disjoint writers are
222///   permitted to hold the lock at the same time.
223/// - [`AccessLock::exclusive`] returns only when no one else holds the
224///   lock.
225///
226/// Read mode and disjoint-write mode are mutually exclusive, which is
227/// what gives readers a torn-free view of memory in the presence of
228/// disjoint parallel writers.
229#[derive(Debug, Default)]
230struct AccessLock {
231    state: Mutex<AccessState>,
232    cond: Condvar,
233}
234
235#[derive(Debug, Default)]
236enum AccessState {
237    #[default]
238    Idle,
239    Reading(usize),
240    DisjointWriting(usize),
241    Exclusive,
242}
243
244impl AccessLock {
245    fn new() -> Self {
246        Self::default()
247    }
248
249    fn read(&self) -> AccessReadGuard<'_> {
250        let mut state = self.state.lock().expect("AccessLock poisoned");
251        loop {
252            match &mut *state {
253                AccessState::Idle => {
254                    *state = AccessState::Reading(1);
255                    return AccessReadGuard(self);
256                }
257                AccessState::Reading(n) => {
258                    *n += 1;
259                    return AccessReadGuard(self);
260                }
261                AccessState::DisjointWriting(_) | AccessState::Exclusive => {
262                    state = self.cond.wait(state).expect("AccessLock poisoned");
263                }
264            }
265        }
266    }
267
268    fn disjoint_write(&self) -> AccessDisjointWriteGuard<'_> {
269        let mut state = self.state.lock().expect("AccessLock poisoned");
270        loop {
271            match &mut *state {
272                AccessState::Idle => {
273                    *state = AccessState::DisjointWriting(1);
274                    return AccessDisjointWriteGuard(self);
275                }
276                AccessState::DisjointWriting(n) => {
277                    *n += 1;
278                    return AccessDisjointWriteGuard(self);
279                }
280                AccessState::Reading(_) | AccessState::Exclusive => {
281                    state = self.cond.wait(state).expect("AccessLock poisoned");
282                }
283            }
284        }
285    }
286
287    fn exclusive(&self) -> AccessExclusiveGuard<'_> {
288        let mut state = self.state.lock().expect("AccessLock poisoned");
289        loop {
290            if matches!(*state, AccessState::Idle) {
291                *state = AccessState::Exclusive;
292                return AccessExclusiveGuard(self);
293            }
294            state = self.cond.wait(state).expect("AccessLock poisoned");
295        }
296    }
297}
298
299struct AccessReadGuard<'a>(&'a AccessLock);
300impl Drop for AccessReadGuard<'_> {
301    fn drop(&mut self) {
302        let mut state = self.0.state.lock().expect("AccessLock poisoned");
303        match &mut *state {
304            AccessState::Reading(1) => {
305                *state = AccessState::Idle;
306                self.0.cond.notify_all();
307            }
308            AccessState::Reading(n) => *n -= 1,
309            other => unreachable!("AccessReadGuard dropped in non-Reading state: {other:?}"),
310        }
311    }
312}
313
314struct AccessDisjointWriteGuard<'a>(&'a AccessLock);
315impl Drop for AccessDisjointWriteGuard<'_> {
316    fn drop(&mut self) {
317        let mut state = self.0.state.lock().expect("AccessLock poisoned");
318        match &mut *state {
319            AccessState::DisjointWriting(1) => {
320                *state = AccessState::Idle;
321                self.0.cond.notify_all();
322            }
323            AccessState::DisjointWriting(n) => *n -= 1,
324            other => unreachable!(
325                "AccessDisjointWriteGuard dropped in non-DisjointWriting state: {other:?}"
326            ),
327        }
328    }
329}
330
331struct AccessExclusiveGuard<'a>(&'a AccessLock);
332impl Drop for AccessExclusiveGuard<'_> {
333    fn drop(&mut self) {
334        let mut state = self.0.state.lock().expect("AccessLock poisoned");
335        debug_assert!(matches!(*state, AccessState::Exclusive));
336        *state = AccessState::Idle;
337        self.0.cond.notify_all();
338    }
339}
340
341/// Trait for values that keep a backing memory allocation alive and
342/// know its address and size.
343///
344/// As long as a value implementing this trait exists, the memory region
345/// it describes is guaranteed to remain valid.
346pub trait Keepalive: Send + Sync {
347    /// Start address of the memory region this keepalive pins.
348    fn addr(&self) -> usize;
349
350    /// Size in bytes of the memory region this keepalive pins.
351    fn size(&self) -> usize;
352
353    /// Produce a [`WeakKeepalive`] pointing at the same underlying
354    /// resource. Defaults to `None` for impls with no weak form.
355    fn downgrade(&self) -> Option<Arc<dyn WeakKeepalive>> {
356        None
357    }
358}
359
360/// Counterpart to [`Keepalive`]: a non-pinning reference to the same
361/// underlying resource that can be re-promoted to a [`Keepalive`] as
362/// long as the resource is still alive.
363pub trait WeakKeepalive: Send + Sync {
364    /// Re-acquire a strong [`Keepalive`] for the underlying resource,
365    /// or `None` if the referent has gone away.
366    fn upgrade(&self) -> Option<Arc<dyn Keepalive>>;
367}
368
369impl Keepalive for Box<[u8]> {
370    fn addr(&self) -> usize {
371        self.as_ptr() as usize
372    }
373
374    fn size(&self) -> usize {
375        self.len()
376    }
377}
378
379/// Backing state of a [`KeepaliveLocalMemory`].
380///
381/// Holds the addressing/bandwidth metadata, the access-coordination
382/// lock, and the per-device [`IbvMemoryRegionView`]s this handle has
383/// registered against the region. Cloning shares the registrations and
384/// the access lock by `Arc`, so every clone observes the same registered
385/// MRs and the same reader/writer coordination. Two of these built
386/// separately over the same allocation share nothing.
387///
388/// All access goes through methods on [`KeepaliveLocalMemory`];
389/// nothing outside the module pokes at these fields directly.
390#[derive(Clone)]
391pub(crate) struct LocalMemoryInner {
392    addr: usize,
393    size: usize,
394    /// Where the region lives, resolved once at construction.
395    location: MemoryLocation,
396    /// Bandwidth (bytes/s) for direct host-thread pointer access, or `None`
397    /// if the memory is not host-accessible.
398    direct_access_host_bandwidth: Option<u64>,
399    /// Bandwidth (bytes/s) for direct device-thread pointer access, or
400    /// `None` if the memory is not device-accessible.
401    direct_access_device_bandwidth: Option<u64>,
402    /// The registrations this handle has made, keyed by the backend that made
403    /// them ([`IbvDeviceImpl::typename`]) and then by RDMA device name.
404    /// Populated lazily by `IbvManagerActor::resolve_local_mrs` as devices are
405    /// needed.
406    mrs: Arc<DashMap<&'static str, HashMap<String, MrEntry>>>,
407    /// Coordinates concurrent reads, exclusive writes, and parallel
408    /// disjoint writes against this region.
409    access: Arc<AccessLock>,
410}
411
412/// The outcome of registering a region on one device.
413///
414/// Failures are recorded, not just dropped, so a device that cannot
415/// register this region is not retried on every subsequent transfer.
416/// Registration is not cheap, and a device that fails once for a
417/// given region fails for a reason that will not have changed.
418#[derive(Debug, Clone)]
419enum MrEntry {
420    Registered(IbvMemoryRegionView),
421    Failed(String),
422}
423
424impl LocalMemoryInner {
425    fn try_new(addr: usize, size: usize) -> Result<Self, anyhow::Error> {
426        let location = MemoryLocation::from_addr(addr)?;
427        // TODO(slurye): Using placeholder values for now. Fill in with real values.
428        let (host_bw, device_bw) = match location {
429            MemoryLocation::Cpu(_) => (Some(1), None),
430            MemoryLocation::Gpu(_) => (None, Some(1)),
431        };
432        Ok(Self {
433            addr,
434            size,
435            location,
436            direct_access_host_bandwidth: host_bw,
437            direct_access_device_bandwidth: device_bw,
438            mrs: Arc::new(DashMap::new()),
439            access: Arc::new(AccessLock::new()),
440        })
441    }
442}
443
444/// Local memory handle that keeps its backing allocation alive via an
445/// [`Arc<dyn Keepalive>`].
446///
447/// Detects at construction time whether the address is a CUDA device
448/// pointer and dispatches `read_at`/`write_at` accordingly.
449///
450/// All three access methods are `unsafe`: the [`Keepalive`] only
451/// guarantees the allocation stays mapped, not that this handle has
452/// unique ownership. The internal [`AccessLock`] coordinates concurrent
453/// callers that share the same clone of this handle (readers run in
454/// parallel, exclusive writers run alone, disjoint writers run in
455/// parallel with one another but exclude readers and exclusive
456/// writers), but callers must additionally rule out concurrent access
457/// through other views of the same allocation.
458///
459/// The `direct_access_host_bandwidth` and `direct_access_device_bandwidth`
460/// fields indicate the speed of reading the memory via pointer dereference
461/// on a host or device thread, respectively. A value of `None` means the
462/// memory is not directly accessible from that context.
463#[derive(Clone)]
464pub struct KeepaliveLocalMemory {
465    // Field order carries the drop order, and it matters: `inner` holds this
466    // handle's MR registrations, and they must go before `_keepalive` releases
467    // the memory. An `ibv_mr` does not keep its pages mapped, so a registration
468    // outliving the allocation is a window in which a peer's in-flight DMA
469    // lands in memory that has been freed and possibly handed out again.
470    inner: LocalMemoryInner,
471    _keepalive: Arc<dyn Keepalive>,
472}
473
474impl Debug for KeepaliveLocalMemory {
475    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
476        f.debug_struct("KeepaliveLocalMemory")
477            .field("addr", &self.inner.addr)
478            .field("size", &self.inner.size)
479            .field("location", &self.inner.location)
480            .field(
481                "direct_access_host_bandwidth",
482                &self.inner.direct_access_host_bandwidth,
483            )
484            .field(
485                "direct_access_device_bandwidth",
486                &self.inner.direct_access_device_bandwidth,
487            )
488            .finish_non_exhaustive()
489    }
490}
491
492impl KeepaliveLocalMemory {
493    /// Try to create a new handle. Derives `addr` and `size` from the
494    /// `keepalive` via [`Keepalive::addr`] / [`Keepalive::size`], then
495    /// resolves the region's [`MemoryLocation`] and sets the bandwidth fields
496    /// accordingly.
497    ///
498    /// Errors when the location cannot be resolved.
499    pub fn try_new(keepalive: Arc<dyn Keepalive>) -> Result<Self, anyhow::Error> {
500        let addr = keepalive.addr();
501        let size = keepalive.size();
502        Ok(Self {
503            inner: LocalMemoryInner::try_new(addr, size)?,
504            _keepalive: keepalive,
505        })
506    }
507
508    /// Starting virtual address of the memory region.
509    pub fn addr(&self) -> usize {
510        self.inner.addr
511    }
512
513    /// Size of the memory region in bytes.
514    pub fn size(&self) -> usize {
515        self.inner.size
516    }
517
518    /// Where this region lives.
519    pub fn location(&self) -> MemoryLocation {
520        self.inner.location
521    }
522
523    /// This handle's registration of the region on backend `I`'s `device`, if
524    /// it has one.
525    ///
526    /// `Err` means a previous attempt to register on `device` failed and
527    /// carries that error; the caller should not retry. `Ok(None)` means this
528    /// handle has never registered there.
529    ///
530    /// The registrations are shared by `Arc` across clones of this handle, and
531    /// with any [`WeakLocalMemory`] downgraded from it, so one clone's
532    /// registration is visible to all of them. Handles built separately over
533    /// the same region share nothing.
534    pub fn registered_mr<I: IbvDeviceImpl>(
535        &self,
536        device: &str,
537    ) -> Result<Option<IbvMemoryRegionView>, anyhow::Error> {
538        match self
539            .inner
540            .mrs
541            .get(I::typename())
542            .and_then(|backend| backend.get(device).cloned())
543        {
544            Some(MrEntry::Registered(view)) => Ok(Some(view)),
545            Some(MrEntry::Failed(error)) => anyhow::bail!(
546                "registering [{:#x}, {:#x}) on {device} failed earlier: {error}",
547                self.inner.addr,
548                self.inner.addr + self.inner.size,
549            ),
550            None => Ok(None),
551        }
552    }
553
554    /// Every registration this handle has on a device of backend `I`.
555    pub fn registered_mrs<I: IbvDeviceImpl>(&self) -> Vec<IbvMemoryRegionView> {
556        self.inner
557            .mrs
558            .get(I::typename())
559            .map(|backend| {
560                backend
561                    .values()
562                    .filter_map(|entry| match entry {
563                        MrEntry::Registered(view) => Some(view.clone()),
564                        MrEntry::Failed(_) => None,
565                    })
566                    .collect()
567            })
568            .unwrap_or_default()
569    }
570
571    /// Record `view` as this handle's registration on backend `I`'s
572    /// `view.device_name` and return the registration now in force there.
573    ///
574    /// Idempotent: a registration already present wins, and `view` is dropped
575    /// (deregistering it, since nothing else holds it).
576    ///
577    /// Errors when the device already carries a recorded failure, which two
578    /// callers registering the same region on the same device at once can
579    /// reach: one fails and records it, the other succeeds and arrives here.
580    /// The recorded failure stands and `view` is dropped, so the region keeps
581    /// answering with the failure rather than flip-flopping on who got there
582    /// last.
583    pub fn install_mr<I: IbvDeviceImpl>(
584        &self,
585        view: IbvMemoryRegionView,
586    ) -> Result<IbvMemoryRegionView, anyhow::Error> {
587        let mut backend = self.inner.mrs.entry(I::typename()).or_default();
588        match backend.entry(view.device_name.clone()) {
589            Entry::Vacant(vacant) => {
590                vacant.insert(MrEntry::Registered(view.clone()));
591                Ok(view)
592            }
593            Entry::Occupied(occupied) => match occupied.get() {
594                MrEntry::Registered(installed) => Ok(installed.clone()),
595                MrEntry::Failed(error) => anyhow::bail!(
596                    "cannot install a registration of [{:#x}, {:#x}) on {}: an earlier attempt there failed: {error}",
597                    self.inner.addr,
598                    self.inner.addr + self.inner.size,
599                    view.device_name,
600                ),
601            },
602        }
603    }
604
605    /// Record that registering this region on backend `I`'s `device` failed, so
606    /// later transfers see the failure instead of retrying it. A registration
607    /// already in force on `device` wins: one caller's failure does not
608    /// invalidate another's working keys.
609    pub fn record_mr_failure<I: IbvDeviceImpl>(&self, device: &str, error: &anyhow::Error) {
610        self.inner
611            .mrs
612            .entry(I::typename())
613            .or_default()
614            .entry(device.to_string())
615            .or_insert_with(|| MrEntry::Failed(format!("{error:#}")));
616    }
617
618    /// Copy `dst.len()` bytes from this memory region starting at `offset`
619    /// into `dst`.
620    ///
621    /// Mutually exclusive with both `write_at` and `write_at_disjoint`
622    /// *across clones of this handle*: the [`AccessLock`] guarantees a
623    /// reader and any writer (exclusive or disjoint) that share the
624    /// same lock never observe each other's partial state. Multiple
625    /// concurrent `read_at` calls on shared clones are permitted and
626    /// run in parallel.
627    ///
628    /// # Safety
629    ///
630    /// The [`Keepalive`] guarantees the allocation stays mapped, but it
631    /// does *not* imply unique ownership: another component may hold its
632    /// own view of the same allocation and read or write it concurrently
633    /// outside this handle's [`AccessLock`]. The caller must ensure that
634    /// no such external access produces a torn read of
635    /// `offset..offset + dst.len()` for the duration of this call.
636    pub unsafe fn read_at(&self, offset: usize, dst: &mut [u8]) -> Result<(), anyhow::Error> {
637        let _guard = self.inner.access.read();
638        check_bounds(offset, dst.len(), self.inner.size)?;
639        // SAFETY: the `_keepalive` field keeps the allocation live, the
640        // read guard above excludes concurrent exclusive and disjoint
641        // writers that share this lock, `check_bounds` verified the access
642        // is in range, and the caller upholds the no-external-writer
643        // obligation documented on this method.
644        unsafe {
645            if self.inner.direct_access_host_bandwidth.is_some() {
646                read_cpu(self.inner.addr, offset, dst);
647                Ok(())
648            } else {
649                read_gpu(self.inner.addr, offset, dst)
650            }
651        }
652    }
653
654    /// Copy `src.len()` bytes from `src` into this memory region starting
655    /// at `offset`.
656    ///
657    /// Mutually exclusive with every other read and write against this
658    /// region *across clones of this handle*: the [`AccessLock`] blocks
659    /// concurrent readers and writers that share the same lock. Use
660    /// [`KeepaliveLocalMemory::write_at_disjoint`] when multiple writers
661    /// can be proven to target disjoint byte ranges.
662    ///
663    /// # Safety
664    ///
665    /// See [`KeepaliveLocalMemory::read_at`]. The [`Keepalive`] guarantee
666    /// covers liveness only; the caller must ensure no concurrent
667    /// external reader or writer observes an overlapping byte range.
668    pub unsafe fn write_at(&self, offset: usize, src: &[u8]) -> Result<(), anyhow::Error> {
669        let _guard = self.inner.access.exclusive();
670        check_bounds(offset, src.len(), self.inner.size)?;
671        // SAFETY: the `_keepalive` field keeps the allocation live, the
672        // exclusive guard above excludes every other reader and writer
673        // that shares this lock, `check_bounds` verified the access is
674        // in range, and the caller upholds the no-external-access
675        // obligation documented on this method.
676        unsafe {
677            if self.inner.direct_access_host_bandwidth.is_some() {
678                write_cpu(self.inner.addr, offset, src);
679                Ok(())
680            } else {
681                write_gpu(self.inner.addr, offset, src)
682            }
683        }
684    }
685
686    /// Like [`KeepaliveLocalMemory::write_at`], but allows other
687    /// concurrent `write_at_disjoint` calls (across clones of this
688    /// handle) to proceed in parallel. Still mutually exclusive with
689    /// `read_at` and `write_at` through the [`AccessLock`].
690    ///
691    /// # Safety
692    ///
693    /// In addition to the obligations of
694    /// [`KeepaliveLocalMemory::write_at`] (no external concurrent
695    /// reader or writer of the same byte range), the caller must
696    /// ensure that no other concurrent call to this method targets a
697    /// byte range that overlaps `offset..offset + src.len()`. Disjoint
698    /// byte ranges across concurrent disjoint callers are sound.
699    pub unsafe fn write_at_disjoint(&self, offset: usize, src: &[u8]) -> Result<(), anyhow::Error> {
700        let _guard = self.inner.access.disjoint_write();
701        check_bounds(offset, src.len(), self.inner.size)?;
702        // SAFETY: the `_keepalive` field keeps the allocation live, the
703        // disjoint-write guard above excludes concurrent readers and
704        // exclusive writers that share this lock, `check_bounds`
705        // verified the access is in range, and the caller upholds both
706        // safety obligations documented on this method (no external access,
707        // no overlap with other concurrent disjoint writers).
708        unsafe {
709            if self.inner.direct_access_host_bandwidth.is_some() {
710                write_cpu(self.inner.addr, offset, src);
711                Ok(())
712            } else {
713                write_gpu(self.inner.addr, offset, src)
714            }
715        }
716    }
717
718    /// Pair off a [`WeakLocalMemory`] that shares this handle's
719    /// [`LocalMemoryInner`] (and therefore the same registrations and
720    /// access lock). Returns `None` when the underlying [`Keepalive`]
721    /// does not provide a weak form.
722    pub fn downgrade(&self) -> Option<WeakLocalMemory> {
723        let weak_keepalive = self._keepalive.downgrade()?;
724        Some(WeakLocalMemory {
725            inner: self.inner.clone(),
726            weak_keepalive,
727        })
728    }
729}
730
731/// Non-pinning counterpart of [`KeepaliveLocalMemory`].
732///
733/// Holds the shared [`LocalMemoryInner`] (so a re-promoted strong
734/// handle sees the same registrations and access lock) plus a
735/// [`WeakKeepalive`] that can be upgraded to a fresh
736/// [`Arc<dyn Keepalive>`] as long as the referent is still alive.
737#[derive(Clone)]
738pub struct WeakLocalMemory {
739    inner: LocalMemoryInner,
740    weak_keepalive: Arc<dyn WeakKeepalive>,
741}
742
743impl WeakLocalMemory {
744    /// Starting virtual address of the memory region.
745    pub fn addr(&self) -> usize {
746        self.inner.addr
747    }
748
749    /// Size of the memory region in bytes.
750    pub fn size(&self) -> usize {
751        self.inner.size
752    }
753
754    /// Materialize a strong [`KeepaliveLocalMemory`] sharing this
755    /// handle's [`LocalMemoryInner`]. Returns `None` if the
756    /// referent has gone away **or** if its currently-computed
757    /// `(addr, size)` no longer matches the values stored on this
758    /// handle — the latter guarding against the live referent
759    /// describing a different memory region than the one this weak
760    /// handle was paired with at downgrade time.
761    pub fn upgrade(&self) -> Option<KeepaliveLocalMemory> {
762        let keepalive = self.weak_keepalive.upgrade()?;
763        let new_addr = keepalive.addr();
764        let new_size = keepalive.size();
765        if new_addr != self.inner.addr || new_size != self.inner.size {
766            tracing::warn!(
767                expected_addr = self.inner.addr,
768                actual_addr = new_addr,
769                expected_size = self.inner.size,
770                actual_size = new_size,
771                "WeakLocalMemory upgrade rejected: backing keepalive's (addr, size) changed since downgrade",
772            );
773            return None;
774        }
775        Some(KeepaliveLocalMemory {
776            inner: self.inner.clone(),
777            _keepalive: keepalive,
778        })
779    }
780}
781
782#[cfg(test)]
783mod tests {
784    use super::*;
785    use crate::backend::cuda_test_utils::CudaAllocator;
786    use crate::backend::ibverbs::mlx_device::MlxDevice;
787
788    // -- KeepaliveLocalMemory (host) --
789
790    fn host_keepalive_mem(data: Box<[u8]>) -> KeepaliveLocalMemory {
791        KeepaliveLocalMemory::try_new(Arc::new(data)).expect("host memory has a location")
792    }
793
794    #[test]
795    fn keepalive_host_location() {
796        let mem = host_keepalive_mem(Box::from([1, 2, 3]));
797        // No NUMA node is resolved.
798        assert_eq!(mem.location(), MemoryLocation::Cpu(None));
799    }
800
801    #[test]
802    fn keepalive_device_location_names_its_cuda_ordinal() {
803        let alloc = CudaAllocator::get().allocate(0, 4096, 4096);
804        assert_eq!(
805            alloc.keepalive_slice(0, 4096).location(),
806            MemoryLocation::Gpu(Some(0)),
807            "a device pointer should resolve to the ordinal that owns it",
808        );
809        alloc.try_free();
810    }
811
812    // -- per-device registrations --
813
814    #[test]
815    fn registrations_are_independent_per_device() {
816        let mem = host_keepalive_mem(vec![0; 8].into_boxed_slice());
817        let first = mem
818            .install_mr::<MlxDevice>(IbvMemoryRegionView::for_test("mlx5_0", 10))
819            .unwrap();
820        assert_eq!(first.device_name, "mlx5_0");
821        mem.install_mr::<MlxDevice>(IbvMemoryRegionView::for_test("mlx5_1", 20))
822            .unwrap();
823
824        // Each device answers with its own registration; keys issued by one
825        // device's protection domain mean nothing to another.
826        for (device, key) in [("mlx5_0", 10), ("mlx5_1", 20)] {
827            let view = mem.registered_mr::<MlxDevice>(device).unwrap().unwrap();
828            assert_eq!(view.device_name, device);
829            assert_eq!(
830                view.lkey, key,
831                "{device} answered with another device's key"
832            );
833        }
834        assert!(mem.registered_mr::<MlxDevice>("mlx5_2").unwrap().is_none());
835    }
836
837    #[test]
838    fn install_mr_keeps_the_registration_already_in_force() {
839        let mem = host_keepalive_mem(vec![0; 8].into_boxed_slice());
840        mem.install_mr::<MlxDevice>(IbvMemoryRegionView::for_test("mlx5_0", 10))
841            .unwrap();
842        // A second registration of the same region on the same device loses:
843        // holders of the first view keep addressing through it.
844        let second = mem
845            .install_mr::<MlxDevice>(IbvMemoryRegionView::for_test("mlx5_0", 20))
846            .unwrap();
847        assert_eq!(second.lkey, 10, "the loser's key must not be handed back");
848        assert_eq!(
849            mem.registered_mr::<MlxDevice>("mlx5_0")
850                .unwrap()
851                .unwrap()
852                .lkey,
853            10,
854            "nor recorded for later resolutions",
855        );
856    }
857
858    #[test]
859    fn install_mr_leaves_a_recorded_failure_standing() {
860        let mem = host_keepalive_mem(vec![0; 8].into_boxed_slice());
861        mem.record_mr_failure::<MlxDevice>("mlx5_0", &anyhow::anyhow!("out of memory keys"));
862        // Reached when two callers register the same region on the same device
863        // at once and only one of them fails.
864        let error = format!(
865            "{:#}",
866            mem.install_mr::<MlxDevice>(IbvMemoryRegionView::for_test("mlx5_0", 10))
867                .expect_err("installing over a recorded failure should fail")
868        );
869        assert!(
870            error.contains("out of memory keys"),
871            "the error should carry the recorded failure: {error}",
872        );
873        assert!(
874            mem.registered_mr::<MlxDevice>("mlx5_0").is_err(),
875            "the recorded failure should still be what the device answers with",
876        );
877    }
878
879    #[test]
880    fn a_recorded_failure_is_reported_rather_than_retried() {
881        let mem = host_keepalive_mem(vec![0; 8].into_boxed_slice());
882        mem.record_mr_failure::<MlxDevice>("mlx5_0", &anyhow::anyhow!("out of memory keys"));
883        let error = format!(
884            "{:#}",
885            mem.registered_mr::<MlxDevice>("mlx5_0")
886                .expect_err("a recorded failure should surface")
887        );
888        assert!(
889            error.contains("out of memory keys"),
890            "the error should carry the original failure: {error}",
891        );
892        // Other devices are unaffected by one device's failure.
893        assert!(mem.registered_mr::<MlxDevice>("mlx5_1").unwrap().is_none());
894    }
895
896    #[test]
897    fn keepalive_host_read_at() {
898        let mem = host_keepalive_mem(Box::from([1, 2, 3, 4, 5]));
899        let mut buf = [0u8; 3];
900        // SAFETY: `mem` is the sole handle to the allocation, no other
901        // thread or component holds a view of it.
902        unsafe { mem.read_at(1, &mut buf) }.unwrap();
903        assert_eq!(buf, [2, 3, 4]);
904    }
905
906    #[test]
907    fn keepalive_host_write_then_read() {
908        let mem = host_keepalive_mem(vec![0; 5].into_boxed_slice());
909        // SAFETY: `mem` is the sole handle to the allocation, no other
910        // thread or component holds a view of it.
911        unsafe { mem.write_at(1, &[7, 8, 9]) }.unwrap();
912        let mut buf = [0u8; 5];
913        // SAFETY: same as above.
914        unsafe { mem.read_at(0, &mut buf) }.unwrap();
915        assert_eq!(buf, [0, 7, 8, 9, 0]);
916    }
917
918    #[test]
919    fn keepalive_host_out_of_bounds() {
920        let mem = host_keepalive_mem(vec![0; 3].into_boxed_slice());
921        let mut buf = [0u8; 3];
922        // SAFETY: `mem` is the sole handle to the allocation; the
923        // bounds check fires before any pointer dereference.
924        assert!(unsafe { mem.read_at(1, &mut buf) }.is_err());
925        // SAFETY: same as above.
926        assert!(unsafe { mem.write_at(1, &[7, 8, 9]) }.is_err());
927    }
928}