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}