1use 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
47const 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
53unsafe 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 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
124pub struct ScannedSegment {
125 pub address: usize,
127 pub size: usize,
129 pub cuda_ordinal: i32,
131 pub is_expandable: bool,
133}
134
135pub type CudaSegmentScanner = Arc<dyn Fn() -> Vec<ScannedSegment> + Send + Sync>;
139
140static CUDA_SEGMENT_SCANNER: Mutex<Option<CudaSegmentScanner>> = Mutex::new(None);
141
142pub fn register_cuda_segment_scanner(scanner: CudaSegmentScanner) {
145 *CUDA_SEGMENT_SCANNER
146 .lock()
147 .expect("CUDA segment scanner lock poisoned") = Some(scanner);
148}
149
150fn scan_cuda_segments() -> Vec<ScannedSegment> {
153 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
164pub(super) trait MlxDomainOps: Send + Sync + 'static {
175 fn device_name(&self) -> String;
177
178 fn mlx5dv_enabled(&self) -> bool;
180
181 fn assigned_cuda_devices(&self) -> anyhow::Result<Vec<i32>>;
184
185 fn scan_segments(&self) -> Vec<ScannedSegment>;
187
188 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 unsafe fn bind_mr_list(
211 &self,
212 pd: &IbvPd,
213 access: i32,
214 mrs: Vec<Arc<Mlx5DevxMr>>,
215 ) -> anyhow::Result<Mlx5DevxMkey>;
216}
217
218pub(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 let mlx5dv_supported = if ctx.is_null() {
236 false
237 } else {
238 unsafe { rdmaxcel_sys::mlx5dv_is_supported((*ctx).device) }
241 };
242 let mut mkey_max_entries = 0;
243 let mlx5dv_enabled = if mlx5dv_supported {
244 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 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 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 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 Ok(unsafe { Mlx5DevxMkey::from_raw(mkey, lkey, rkey, mrs) })
373 }
374}
375
376#[derive(Debug)]
378pub(super) struct Mlx5DevxMr {
379 mkey: *mut rdmaxcel_sys::rdmaxcel_devx_mkey_t,
380 _pd: Arc<IbvPd>,
381}
382
383unsafe impl Send for Mlx5DevxMr {}
386unsafe impl Sync for Mlx5DevxMr {}
388
389impl Mlx5DevxMr {
390 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 unsafe { rdmaxcel_sys::rdmaxcel_destroy_devx_mkey(self.mkey) };
419 }
420 }
421}
422
423#[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
439unsafe impl Send for Mlx5DevxMkey {}
445unsafe impl Sync for Mlx5DevxMkey {}
447
448impl Mlx5DevxMkey {
449 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 fn keys(&self) -> (u32, u32) {
473 (self.lkey, self.rkey)
474 }
475
476 fn mrs(&self) -> &Vec<Arc<Mlx5DevxMr>> {
478 &self.mrs
479 }
480
481 #[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 unsafe { rdmaxcel_sys::rdmaxcel_destroy_devx_mkey(self.mkey) };
502 }
503 }
504}
505
506#[derive(Debug)]
513struct RegisteredSegmentState {
514 stale_mkeys: Vec<Mlx5DevxMkey>,
517 mkey: Option<Mlx5DevxMkey>,
520 size: usize,
522 scanned_size: usize,
526}
527
528struct RegisteredSegment {
536 ops: Arc<dyn MlxDomainOps>,
537 base_virtual_addr: usize,
538 mkey_max_entries: usize,
540 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 #[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 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 #[cfg(test)]
594 fn size(&self) -> usize {
595 self.state.lock().expect("segment state lock poisoned").size
596 }
597
598 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 fn scanned_size(&self) -> usize {
611 self.state
612 .lock()
613 .expect("segment state lock poisoned")
614 .scanned_size
615 }
616
617 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 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 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 if scanned_seg.size == state.size {
675 return Ok(());
676 }
677 state.scanned_size = scanned_seg.size;
678
679 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 if tail == 0 {
704 return Ok(());
705 }
706
707 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 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 let new_mkey = unsafe { self.ops.bind_mr_list(pd, access, all) }?;
738
739 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 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 state
759 .mkey
760 .as_ref()
761 .map(Mlx5DevxMkey::keys)
762 .expect("view of a segment with no bound key")
763 };
764 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
780impl IbvMemoryRegionKeepalive for RegisteredSegment {}
783
784unsafe 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 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 mrs.push(Arc::new(result?));
817 chunk_start += chunk;
818 remaining -= chunk;
819 }
820 Ok(mrs)
821}
822
823pub struct MlxDomain {
829 ops: Arc<dyn MlxDomainOps>,
830 mlx5dv_enabled: bool,
831 mkey_max_entries: usize,
833 max_mr_size: usize,
835 require_devx_mkeys: bool,
837 host_odp_mr: Mutex<Option<Arc<IbvMr>>>,
838 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 #[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 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 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 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 unsafe { advise_odp_mr_write(domain.pd(), domain.device_info().name(), &view) };
940 Ok(view)
941 }
942
943 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 if let Some(result) = Self::lookup(&segments, addr, size) {
970 return result;
971 }
972
973 let cuda_ordinals = self.ops.assigned_cuda_devices()?;
979
980 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 Some(seg) if seg.scanned_size() == scanned_seg.size => {}
995 Some(seg) => unsafe { seg.grow(pd, access, scanned_seg) }?,
1000 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 unsafe { fresh.grow(pd, access, scanned_seg) }?;
1014 segments.insert(key, fresh);
1015 }
1016 }
1017 }
1018
1019 segments.retain(|key, _| snapshot.contains(key));
1023
1024 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 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 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 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 #[derive(Debug, Clone, PartialEq, Eq)]
1167 struct DmabufCall {
1168 addr: usize,
1169 size: usize,
1170 access: i32,
1171 }
1172
1173 #[derive(Debug, Clone)]
1176 struct BindCall {
1177 mrs: Vec<Arc<Mlx5DevxMr>>,
1178 }
1179
1180 #[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 dmabuf_calls: Vec<DmabufCall>,
1198 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 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 let v = s.mint() as u32;
1283 Ok(Mlx5DevxMkey::with_test_keys(v, v ^ 0xffff, mrs))
1284 }
1285 }
1286
1287 const TEST_MKEY_MAX_ENTRIES: usize = 32;
1290
1291 fn domain(mock: Arc<MockOps>) -> Arc<IbvDomain<MlxDomain>> {
1295 domain_with_cap(mock, TEST_MKEY_MAX_ENTRIES)
1296 }
1297
1298 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 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 fn register_cuda(
1314 domain: &Arc<IbvDomain<MlxDomain>>,
1315 addr: usize,
1316 size: usize,
1317 ) -> anyhow::Result<IbvMemoryRegionView> {
1318 unsafe {
1320 domain
1321 .domain_impl()
1322 .register_cuda_mlx5dv_mr(domain, addr, size)
1323 }
1324 }
1325
1326 fn null_pd() -> Arc<IbvPd> {
1329 Arc::new(IbvPd::null())
1330 }
1331
1332 fn dyn_ops(ops: &Arc<MockOps>) -> Arc<dyn MlxDomainOps> {
1334 ops.clone()
1335 }
1336
1337 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 unsafe { segment.grow(&null_pd(), 0, &seg(base, size, 0)) }
1346 .expect("fresh bind should succeed");
1347 segment
1348 }
1349
1350 #[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 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 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 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 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 ops.lock().fail_dmabuf_after = Some(2);
1514
1515 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 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 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 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 let segment = RegisteredSegment::empty(dyn_ops(&ops), base, 1);
1632 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 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 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 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 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 #[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 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 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 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 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 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 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 let a = 0x10_0000_0000usize; let a2 = 0x18_0000_0000usize; let b = 0x20_0000_0000usize; let c = 0x30_0000_0000usize; 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 let access = domain.domain_impl().access_flags();
1863
1864 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 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 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 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 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 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 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 assert_eq!(s.bind_calls.len(), 4);
1981 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 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 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 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 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 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}