1use std::fmt;
14use std::fs;
15use std::path::Path;
16use std::path::PathBuf;
17use std::sync::LazyLock;
18
19use anyhow::Error;
20use anyhow::Result;
21use dashmap::DashSet;
22use rdmaxcel_sys::CUresult;
23
24use crate::local_memory::is_device_ptr;
25
26fn cuda_error_string(rc: CUresult) -> String {
27 if rc == rdmaxcel_sys::CUDA_ERROR_NOT_INITIALIZED {
31 return "CUDA_ERROR_NOT_INITIALIZED".to_owned();
32 }
33 let mut s: *const std::os::raw::c_char = std::ptr::null();
34 unsafe { rdmaxcel_sys::rdmaxcel_cuGetErrorString(rc, &mut s) };
37 if s.is_null() {
38 format!("unknown error code ({rc})")
39 } else {
40 unsafe { std::ffi::CStr::from_ptr(s) }
44 .to_string_lossy()
45 .into_owned()
46 }
47}
48
49pub fn cuda_device_count() -> Result<i32> {
57 let mut count: i32 = 0;
58 let rc = unsafe { rdmaxcel_sys::rdmaxcel_cuDeviceGetCount(&mut count) };
61 anyhow::ensure!(
62 rc == rdmaxcel_sys::CUDA_SUCCESS,
63 "cuDeviceGetCount failed: {}",
64 cuda_error_string(rc),
65 );
66 Ok(count)
67}
68
69fn cuda_device_attribute(attr: rdmaxcel_sys::CUdevice_attribute, device: i32) -> Result<i32> {
71 let mut value: i32 = 0;
72 let rc = unsafe { rdmaxcel_sys::rdmaxcel_cuDeviceGetAttribute(&mut value, attr, device) };
74 anyhow::ensure!(
75 rc == rdmaxcel_sys::CUDA_SUCCESS,
76 "cuDeviceGetAttribute({attr}) failed on CUDA device {device}: {}",
77 cuda_error_string(rc),
78 );
79 Ok(value)
80}
81
82pub fn cuda_pci_address(ordinal: u32) -> Result<PCIAddress> {
100 let count = cuda_device_count()?;
101 anyhow::ensure!(
102 i64::from(ordinal) < i64::from(count),
103 "CUDA device {ordinal} is not visible to this process ({count} visible)"
104 );
105
106 let mut device: rdmaxcel_sys::CUdevice = 0;
107 let rc = unsafe { rdmaxcel_sys::rdmaxcel_cuDeviceGet(&mut device, ordinal as i32) };
110 anyhow::ensure!(
111 rc == rdmaxcel_sys::CUDA_SUCCESS,
112 "cuDeviceGet failed for CUDA device {ordinal}: {}",
113 cuda_error_string(rc),
114 );
115
116 let domain = cuda_device_attribute(rdmaxcel_sys::CU_DEVICE_ATTRIBUTE_PCI_DOMAIN_ID, device)?;
117 let bus = cuda_device_attribute(rdmaxcel_sys::CU_DEVICE_ATTRIBUTE_PCI_BUS_ID, device)?;
118 let slot = cuda_device_attribute(rdmaxcel_sys::CU_DEVICE_ATTRIBUTE_PCI_DEVICE_ID, device)?;
119 Ok(PCIAddress {
120 domain: u16::try_from(domain)?,
121 bus: u8::try_from(bus)?,
122 device: u8::try_from(slot)?,
123 function: 0,
125 })
126}
127
128#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
131pub struct PCIAddress {
132 pub domain: u16,
133 pub bus: u8,
134 pub device: u8,
135 pub function: u8,
136}
137
138impl PCIAddress {
139 pub fn parse(s: &str) -> Option<Self> {
141 let (domain, rest) = s.split_once(':')?;
142 let (bus, rest) = rest.split_once(':')?;
143 let (device, function) = rest.split_once('.')?;
144 Some(Self {
145 domain: u16::from_str_radix(domain, 16).ok()?,
146 bus: u8::from_str_radix(bus, 16).ok()?,
147 device: u8::from_str_radix(device, 16).ok()?,
148 function: u8::from_str_radix(function, 16).ok()?,
149 })
150 }
151
152 pub fn sysfs_path(&self) -> PathBuf {
154 Path::new("/sys/bus/pci/devices").join(self.to_string())
155 }
156}
157
158impl fmt::Display for PCIAddress {
159 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
160 write!(
161 f,
162 "{:04x}:{:02x}:{:02x}.{:x}",
163 self.domain, self.bus, self.device, self.function
164 )
165 }
166}
167
168#[derive(
172 Debug,
173 Clone,
174 Copy,
175 PartialEq,
176 Eq,
177 Hash,
178 serde::Serialize,
179 serde::Deserialize
180)]
181pub enum MemoryLocation {
182 Cpu(Option<u32>),
184 Gpu(Option<u32>),
186}
187
188impl MemoryLocation {
189 pub fn from_addr(addr: usize) -> Result<Self> {
198 if !is_device_ptr(addr) {
199 return Ok(Self::Cpu(None));
200 }
201 let mut ordinal: i32 = -1;
202 let rc = unsafe {
205 rdmaxcel_sys::rdmaxcel_cuPointerGetAttribute(
206 &mut ordinal as *mut _ as *mut std::ffi::c_void,
207 rdmaxcel_sys::CU_POINTER_ATTRIBUTE_DEVICE_ORDINAL,
208 addr as rdmaxcel_sys::CUdeviceptr,
209 )
210 };
211 anyhow::ensure!(
212 rc == rdmaxcel_sys::CUDA_SUCCESS,
213 "cuPointerGetAttribute(DEVICE_ORDINAL) failed for device memory at {addr:#x}: {}",
214 cuda_error_string(rc),
215 );
216 Ok(Self::Gpu(Some(
217 u32::try_from(ordinal).expect("CUDA device ordinal should be non-negative"),
218 )))
219 }
220}
221
222#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
225pub enum PathType {
226 Pix,
228 Pxb,
230 Phb,
232 Sys,
234 Dis,
236}
237
238#[derive(Debug, Clone, Copy, PartialEq, Eq)]
241pub struct PciPath {
242 pub path_type: PathType,
243 pub bottleneck_mbytes_per_sec: u32,
244}
245
246impl PciPath {
247 pub fn is_better_than(&self, other: &PciPath) -> bool {
251 (self.path_type, other.bottleneck_mbytes_per_sec)
252 < (other.path_type, self.bottleneck_mbytes_per_sec)
253 }
254}
255
256pub fn pci_path(a: &PCIAddress, b: &PCIAddress) -> PciPath {
266 classify(
267 &ancestor_chain(a),
268 numa_node(a),
269 &ancestor_chain(b),
270 numa_node(b),
271 )
272}
273
274pub fn cpu_path(addr: &PCIAddress, numa: Option<u32>) -> PciPath {
280 let bottleneck_mbytes_per_sec = min_link_mbytes_per_sec(&ancestor_chain(addr));
281 let path_type = match numa {
282 Some(node) if numa_node(addr) != Some(node) => PathType::Sys,
283 _ => PathType::Phb,
284 };
285 PciPath {
286 path_type,
287 bottleneck_mbytes_per_sec,
288 }
289}
290
291struct PciHop {
294 sysfs: PathBuf,
295 link_mbytes_per_sec: u32,
296}
297
298fn classify(a: &[PciHop], numa_a: Option<u32>, b: &[PciHop], numa_b: Option<u32>) -> PciPath {
301 if let Some((ia, ib)) = common_ancestor(a, b) {
302 let bottleneck_mbytes_per_sec = a[..ia]
307 .iter()
308 .chain(&b[..ib])
309 .map(|h| h.link_mbytes_per_sec)
310 .min()
311 .unwrap_or(0);
312 let single_switch = ia <= 2 && ib <= 2;
316 let path_type = if single_switch {
317 PathType::Pix
318 } else {
319 PathType::Pxb
320 };
321 return PciPath {
322 path_type,
323 bottleneck_mbytes_per_sec,
324 };
325 }
326 let bottleneck_mbytes_per_sec = min_link_mbytes_per_sec(a).min(min_link_mbytes_per_sec(b));
330 let path_type = match (numa_a, numa_b) {
331 (Some(x), Some(y)) if x == y => PathType::Phb,
332 _ => PathType::Sys,
333 };
334 PciPath {
335 path_type,
336 bottleneck_mbytes_per_sec,
337 }
338}
339
340fn common_ancestor(a: &[PciHop], b: &[PciHop]) -> Option<(usize, usize)> {
343 a.iter().enumerate().find_map(|(ia, na)| {
344 b.iter()
345 .position(|nb| nb.sysfs == na.sysfs)
346 .map(|ib| (ia, ib))
347 })
348}
349
350fn min_link_mbytes_per_sec(hops: &[PciHop]) -> u32 {
355 hops.iter()
356 .map(|h| h.link_mbytes_per_sec)
357 .min()
358 .unwrap_or(0)
359}
360
361fn ancestor_chain(addr: &PCIAddress) -> Vec<PciHop> {
365 let mut chain = Vec::new();
366 let mut current = match fs::canonicalize(addr.sysfs_path()) {
367 Ok(p) => p,
368 Err(_) => return chain,
369 };
370 loop {
371 let link_mbytes_per_sec = link_bandwidth_mbytes_per_sec(¤t);
372 let parent = current.parent().map(Path::to_path_buf);
373 chain.push(PciHop {
374 sysfs: current,
375 link_mbytes_per_sec,
376 });
377 match parent {
378 Some(parent) if is_pci_bdf(&parent) => current = parent,
379 _ => break,
380 }
381 }
382 chain
383}
384
385fn is_pci_bdf(path: &Path) -> bool {
389 path.file_name()
390 .and_then(|n| n.to_str())
391 .is_some_and(|n| PCIAddress::parse(n).is_some())
392}
393
394fn numa_node(addr: &PCIAddress) -> Option<u32> {
397 let raw = fs::read_to_string(addr.sysfs_path().join("numa_node")).ok()?;
398 u32::try_from(raw.trim().parse::<i32>().ok()?).ok()
399}
400
401const DEFAULT_SPEED_MBITS_PER_LANE: u32 = 6000;
405
406const DEFAULT_LINK_WIDTH: u32 = 16;
409
410fn link_bandwidth_mbytes_per_sec(sysfs: &Path) -> u32 {
418 let speed = read_speed_mbits_per_lane(sysfs);
419 let width = read_link_width(sysfs);
420 speed.saturating_mul(width) / 8
424}
425
426fn warn_unusable_link_attr(attr: &str, dir: &Path, error: &str) {
431 static WARNED: LazyLock<DashSet<(String, String)>> = LazyLock::new(DashSet::new);
432 if WARNED.insert((attr.to_string(), dir.to_string_lossy().into_owned())) {
435 tracing::warn!(
436 "unusable PCIe {attr} at {}: {error}; assuming Gen3 x16, so RDMA device selection may rank NICs on an assumed bandwidth",
437 dir.display()
438 );
439 }
440}
441
442fn read_link_width(dir: &Path) -> u32 {
445 let parsed = fs::read_to_string(dir.join("max_link_width"))
446 .map_err(Error::from)
447 .and_then(|raw| {
448 raw.trim()
449 .parse::<u32>()
450 .map_err(Error::from)
451 .and_then(|w| {
452 if w == 0 {
453 Err(anyhow::anyhow!("max_link_width must be > 0"))
454 } else {
455 Ok(w)
456 }
457 })
458 });
459 match parsed {
460 Ok(width) => width,
461 Err(error) => {
462 warn_unusable_link_attr("max_link_width", dir, &format!("{error:#}"));
463 DEFAULT_LINK_WIDTH
464 }
465 }
466}
467
468fn read_speed_mbits_per_lane(dir: &Path) -> u32 {
472 let parsed = fs::read_to_string(dir.join("max_link_speed"))
473 .map_err(Error::from)
474 .and_then(|raw| pcie_speed_mbits_per_lane(&raw));
475 match parsed {
476 Ok(speed) => speed,
477 Err(error) => {
478 warn_unusable_link_attr("max_link_speed", dir, &format!("{error:#}"));
479 DEFAULT_SPEED_MBITS_PER_LANE
480 }
481 }
482}
483
484fn pcie_speed_mbits_per_lane(speed: &str) -> Result<u32> {
493 let rate = speed.split_whitespace().next().unwrap_or("");
496 match rate {
497 "2.5" => Ok(1500), "5" | "5.0" => Ok(3000), "8" | "8.0" => Ok(6000), "16" | "16.0" => Ok(12000), "32" | "32.0" => Ok(24000), "64" | "64.0" => Ok(48000), other => anyhow::bail!("unrecognized PCIe rate {other:?}"),
504 }
505}
506
507#[cfg(test)]
508mod tests {
509 use super::*;
510
511 #[test]
512 fn test_pci_address_parse_and_display() {
513 let addr = PCIAddress::parse("0000:07:00.0").unwrap();
514 assert_eq!(
515 addr,
516 PCIAddress {
517 domain: 0,
518 bus: 7,
519 device: 0,
520 function: 0,
521 }
522 );
523 assert_eq!(addr.to_string(), "0000:07:00.0");
524 assert_eq!(
526 PCIAddress::parse("00ff:1a:1f.7").unwrap().to_string(),
527 "00ff:1a:1f.7"
528 );
529 assert_eq!(PCIAddress::parse("not-an-address"), None);
530 assert_eq!(PCIAddress::parse("0000:07:00"), None);
531 }
532
533 #[test]
534 fn test_cuda_pci_address_rejects_an_absent_ordinal() {
535 let error = format!(
540 "{:#}",
541 cuda_pci_address(4096).expect_err("ordinal 4096 must not resolve")
542 );
543 assert!(
544 error.contains("CUDA_ERROR_NOT_INITIALIZED") || error.contains("not visible"),
545 "the error should name its cause, got: {error}"
546 );
547 }
548
549 #[test]
550 fn test_path_type_orders_best_to_worst() {
551 assert!(PathType::Pix < PathType::Pxb);
552 assert!(PathType::Pxb < PathType::Phb);
553 assert!(PathType::Phb < PathType::Sys);
554 assert!(PathType::Sys < PathType::Dis);
555 }
556
557 #[test]
558 fn test_pci_path_is_better_than() {
559 let pix_slow = PciPath {
560 path_type: PathType::Pix,
561 bottleneck_mbytes_per_sec: 1000,
562 };
563 let pix_fast = PciPath {
564 path_type: PathType::Pix,
565 bottleneck_mbytes_per_sec: 2000,
566 };
567 let phb_fast = PciPath {
568 path_type: PathType::Phb,
569 bottleneck_mbytes_per_sec: 9000,
570 };
571 assert!(pix_slow.is_better_than(&phb_fast));
573 assert!(!phb_fast.is_better_than(&pix_slow));
574 assert!(pix_fast.is_better_than(&pix_slow));
576 assert!(!pix_slow.is_better_than(&pix_fast));
577 assert!(!pix_fast.is_better_than(&pix_fast));
579 }
580
581 #[test]
582 fn test_pcie_speed_mbits_per_lane() {
583 let rate = |s: &str| pcie_speed_mbits_per_lane(s).expect("known generation");
584 assert_eq!(rate("2.5 GT/s PCIe"), 1500);
585 assert_eq!(rate("5 GT/s"), 3000);
586 assert_eq!(rate("8.0 GT/s"), 6000);
587 assert_eq!(rate("16 GT/s PCIe"), 12000);
588 assert_eq!(rate("32 GT/s"), 24000);
589 assert_eq!(rate("64 GT/s"), 48000);
590 for unknown in ["garbage", "", "128 GT/s"] {
594 assert!(
595 pcie_speed_mbits_per_lane(unknown).is_err(),
596 "{unknown:?} names no known PCIe generation"
597 );
598 }
599 }
600
601 #[test]
602 fn test_missing_link_attrs_fall_back_to_gen3_x16() {
603 let missing = Path::new("/nonexistent/pci/device");
607 assert_eq!(read_link_width(missing), DEFAULT_LINK_WIDTH);
608 assert_eq!(
609 read_speed_mbits_per_lane(missing),
610 DEFAULT_SPEED_MBITS_PER_LANE
611 );
612 assert_eq!(
613 link_bandwidth_mbytes_per_sec(missing),
614 12000,
615 "Gen3 (6000 Mbit/s per lane) x16 is 12000 MB/s"
616 );
617 }
618
619 fn hop(sysfs: &str, link_mbytes_per_sec: u32) -> PciHop {
620 PciHop {
621 sysfs: PathBuf::from(sysfs),
622 link_mbytes_per_sec,
623 }
624 }
625
626 #[test]
627 fn test_classify_pix_single_switch() {
628 let a = vec![
631 hop("/d/a", 12000),
632 hop("/d/sw_a", 12000),
633 hop("/d/sw", 8000),
634 ];
635 let b = vec![hop("/d/b", 12000), hop("/d/sw_b", 6000), hop("/d/sw", 8000)];
636 let path = classify(&a, Some(0), &b, Some(0));
637 assert_eq!(path.path_type, PathType::Pix);
638 assert_eq!(path.bottleneck_mbytes_per_sec, 6000);
639 }
640
641 #[test]
642 fn test_classify_excludes_common_ancestor_upstream() {
643 let a = vec![hop("/d/a", 12000), hop("/d/sw", 2000)];
647 let b = vec![hop("/d/b", 12000), hop("/d/sw", 2000)];
648 let path = classify(&a, Some(0), &b, Some(0));
649 assert_eq!(path.path_type, PathType::Pix);
650 assert_eq!(
651 path.bottleneck_mbytes_per_sec, 12000,
652 "the common ancestor's 2000 MB/s upstream link is above the path and must be excluded"
653 );
654 }
655
656 #[test]
657 fn test_classify_pxb_multiple_switches() {
658 let a = vec![
660 hop("/d/a", 12000),
661 hop("/d/sw_a1", 12000),
662 hop("/d/sw_a2", 12000),
663 hop("/d/sw_a3", 12000),
664 hop("/d/root", 16000),
665 ];
666 let b = vec![
667 hop("/d/b", 10000),
668 hop("/d/sw_b", 10000),
669 hop("/d/root", 16000),
670 ];
671 let path = classify(&a, Some(0), &b, Some(0));
672 assert_eq!(path.path_type, PathType::Pxb);
673 assert_eq!(path.bottleneck_mbytes_per_sec, 10000);
674 }
675
676 #[test]
677 fn test_classify_through_cpu() {
678 let a = vec![hop("/d/a", 4000), hop("/d/root_a", 16000)];
680 let b = vec![hop("/d/b", 8000), hop("/d/root_b", 16000)];
681 let phb = classify(&a, Some(0), &b, Some(0));
683 assert_eq!(phb.path_type, PathType::Phb);
684 assert_eq!(phb.bottleneck_mbytes_per_sec, 4000);
685 assert_eq!(classify(&a, Some(0), &b, Some(1)).path_type, PathType::Sys);
687 }
688
689 #[test]
690 fn test_classify_unknown_numa_is_reachable() {
691 let a = vec![hop("/d/a", 4000), hop("/d/root_a", 16000)];
695 let b = vec![hop("/d/b", 8000), hop("/d/root_b", 16000)];
696 assert_eq!(classify(&a, None, &b, Some(0)).path_type, PathType::Sys);
697 assert_eq!(classify(&a, Some(0), &b, None).path_type, PathType::Sys);
698 assert_eq!(classify(&a, None, &b, None).path_type, PathType::Sys);
699 }
700}