1use std::fmt;
14use std::fs;
15use std::path::Path;
16use std::path::PathBuf;
17
18use regex::Regex;
19
20pub fn get_cuda_pci_address(idx: u32) -> Option<PCIAddress> {
24 let gpu_proc_dir = "/proc/driver/nvidia/gpus";
25 if !Path::new(gpu_proc_dir).exists() {
26 return None;
27 }
28
29 let minor_regex =
30 Regex::new(r"Device Minor:\s*(\d+)").expect("should compile: regex literal is valid");
31 for entry in fs::read_dir(gpu_proc_dir).ok()? {
32 let entry = entry.ok()?;
33 let info_file = entry.path().join("information");
34
35 if let Ok(content) = fs::read_to_string(&info_file)
36 && let Some(captures) = minor_regex.captures(&content)
37 && let Ok(device_minor) = captures
38 .get(1)
39 .expect("should be present: capture group 1 matched")
40 .as_str()
41 .parse::<u32>()
42 && device_minor == idx
43 {
44 return PCIAddress::parse(&entry.file_name().to_string_lossy().to_lowercase());
45 }
46 }
47 None
48}
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
53pub struct PCIAddress {
54 pub domain: u16,
55 pub bus: u8,
56 pub device: u8,
57 pub function: u8,
58}
59
60impl PCIAddress {
61 pub fn parse(s: &str) -> Option<Self> {
63 let (domain, rest) = s.split_once(':')?;
64 let (bus, rest) = rest.split_once(':')?;
65 let (device, function) = rest.split_once('.')?;
66 Some(Self {
67 domain: u16::from_str_radix(domain, 16).ok()?,
68 bus: u8::from_str_radix(bus, 16).ok()?,
69 device: u8::from_str_radix(device, 16).ok()?,
70 function: u8::from_str_radix(function, 16).ok()?,
71 })
72 }
73
74 pub fn sysfs_path(&self) -> PathBuf {
76 Path::new("/sys/bus/pci/devices").join(self.to_string())
77 }
78}
79
80impl fmt::Display for PCIAddress {
81 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
82 write!(
83 f,
84 "{:04x}:{:02x}:{:02x}.{:x}",
85 self.domain, self.bus, self.device, self.function
86 )
87 }
88}
89
90#[derive(
94 Debug,
95 Clone,
96 Copy,
97 PartialEq,
98 Eq,
99 Hash,
100 serde::Serialize,
101 serde::Deserialize
102)]
103pub enum MemoryLocation {
104 Cpu(Option<u32>),
106 Gpu(Option<u32>),
108}
109
110#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
113pub enum PathType {
114 Pix,
116 Pxb,
118 Phb,
120 Sys,
122 Dis,
124}
125
126#[derive(Debug, Clone, Copy, PartialEq, Eq)]
129pub struct PciPath {
130 pub path_type: PathType,
131 pub bottleneck_mbytes_per_sec: u32,
132}
133
134impl PciPath {
135 pub fn is_better_than(&self, other: &PciPath) -> bool {
139 (self.path_type, other.bottleneck_mbytes_per_sec)
140 < (other.path_type, self.bottleneck_mbytes_per_sec)
141 }
142}
143
144pub fn pci_path(a: &PCIAddress, b: &PCIAddress) -> PciPath {
153 classify(
154 &ancestor_chain(a),
155 numa_node(a),
156 &ancestor_chain(b),
157 numa_node(b),
158 )
159}
160
161pub fn cpu_path(addr: &PCIAddress, numa: Option<u32>) -> PciPath {
167 let bottleneck_mbytes_per_sec = min_link_mbytes_per_sec(&ancestor_chain(addr));
168 let path_type = match numa {
169 Some(node) if numa_node(addr) != Some(node) => PathType::Sys,
170 _ => PathType::Phb,
171 };
172 PciPath {
173 path_type,
174 bottleneck_mbytes_per_sec,
175 }
176}
177
178struct PciHop {
181 sysfs: PathBuf,
182 link_mbytes_per_sec: u32,
183}
184
185fn classify(a: &[PciHop], numa_a: Option<u32>, b: &[PciHop], numa_b: Option<u32>) -> PciPath {
188 if let Some((ia, ib)) = common_ancestor(a, b) {
189 let bottleneck_mbytes_per_sec = a[..ia]
194 .iter()
195 .chain(&b[..ib])
196 .map(|h| h.link_mbytes_per_sec)
197 .min()
198 .unwrap_or(0);
199 let single_switch = ia <= 2 && ib <= 2;
203 let path_type = if single_switch {
204 PathType::Pix
205 } else {
206 PathType::Pxb
207 };
208 return PciPath {
209 path_type,
210 bottleneck_mbytes_per_sec,
211 };
212 }
213 let bottleneck_mbytes_per_sec = min_link_mbytes_per_sec(a).min(min_link_mbytes_per_sec(b));
215 let path_type = match (numa_a, numa_b) {
216 (Some(x), Some(y)) if x == y => PathType::Phb,
217 (Some(_), Some(_)) => PathType::Sys,
218 _ => PathType::Dis,
219 };
220 PciPath {
221 path_type,
222 bottleneck_mbytes_per_sec,
223 }
224}
225
226fn common_ancestor(a: &[PciHop], b: &[PciHop]) -> Option<(usize, usize)> {
229 a.iter().enumerate().find_map(|(ia, na)| {
230 b.iter()
231 .position(|nb| nb.sysfs == na.sysfs)
232 .map(|ib| (ia, ib))
233 })
234}
235
236fn min_link_mbytes_per_sec(hops: &[PciHop]) -> u32 {
241 hops.iter()
242 .map(|h| h.link_mbytes_per_sec)
243 .min()
244 .unwrap_or(0)
245}
246
247fn ancestor_chain(addr: &PCIAddress) -> Vec<PciHop> {
251 let mut chain = Vec::new();
252 let mut current = match fs::canonicalize(addr.sysfs_path()) {
253 Ok(p) => p,
254 Err(_) => return chain,
255 };
256 loop {
257 let link_mbytes_per_sec = link_bandwidth_mbytes_per_sec(¤t);
258 let parent = current.parent().map(Path::to_path_buf);
259 chain.push(PciHop {
260 sysfs: current,
261 link_mbytes_per_sec,
262 });
263 match parent {
264 Some(parent) if is_pci_bdf(&parent) => current = parent,
265 _ => break,
266 }
267 }
268 chain
269}
270
271fn is_pci_bdf(path: &Path) -> bool {
275 path.file_name()
276 .and_then(|n| n.to_str())
277 .is_some_and(|n| PCIAddress::parse(n).is_some())
278}
279
280fn numa_node(addr: &PCIAddress) -> Option<u32> {
283 let raw = fs::read_to_string(addr.sysfs_path().join("numa_node")).ok()?;
284 u32::try_from(raw.trim().parse::<i32>().ok()?).ok()
285}
286
287fn link_bandwidth_mbytes_per_sec(sysfs: &Path) -> u32 {
291 let speed = read_speed_mbits_per_lane(sysfs);
292 let width = read_link_width(sysfs);
293 speed.saturating_mul(width) / 8
297}
298
299fn read_link_width(dir: &Path) -> u32 {
301 fs::read_to_string(dir.join("max_link_width"))
302 .ok()
303 .and_then(|s| s.trim().parse().ok())
304 .unwrap_or(0)
305}
306
307fn read_speed_mbits_per_lane(dir: &Path) -> u32 {
309 fs::read_to_string(dir.join("max_link_speed"))
310 .ok()
311 .map(|s| pcie_speed_mbits_per_lane(&s))
312 .unwrap_or(0)
313}
314
315fn pcie_speed_mbits_per_lane(speed: &str) -> u32 {
320 match speed.split_whitespace().next().unwrap_or("") {
323 "2.5" => 1500, "5" | "5.0" => 3000, "8" | "8.0" => 6000, "16" | "16.0" => 12000, "32" | "32.0" => 24000, "64" | "64.0" => 48000, _ => 6000,
330 }
331}
332
333#[cfg(test)]
334mod tests {
335 use super::*;
336
337 #[test]
338 fn test_pci_address_parse_and_display() {
339 let addr = PCIAddress::parse("0000:07:00.0").unwrap();
340 assert_eq!(
341 addr,
342 PCIAddress {
343 domain: 0,
344 bus: 7,
345 device: 0,
346 function: 0,
347 }
348 );
349 assert_eq!(addr.to_string(), "0000:07:00.0");
350 assert_eq!(
352 PCIAddress::parse("00ff:1a:1f.7").unwrap().to_string(),
353 "00ff:1a:1f.7"
354 );
355 assert_eq!(PCIAddress::parse("not-an-address"), None);
356 assert_eq!(PCIAddress::parse("0000:07:00"), None);
357 }
358
359 #[test]
360 fn test_path_type_orders_best_to_worst() {
361 assert!(PathType::Pix < PathType::Pxb);
362 assert!(PathType::Pxb < PathType::Phb);
363 assert!(PathType::Phb < PathType::Sys);
364 assert!(PathType::Sys < PathType::Dis);
365 }
366
367 #[test]
368 fn test_pci_path_is_better_than() {
369 let pix_slow = PciPath {
370 path_type: PathType::Pix,
371 bottleneck_mbytes_per_sec: 1000,
372 };
373 let pix_fast = PciPath {
374 path_type: PathType::Pix,
375 bottleneck_mbytes_per_sec: 2000,
376 };
377 let phb_fast = PciPath {
378 path_type: PathType::Phb,
379 bottleneck_mbytes_per_sec: 9000,
380 };
381 assert!(pix_slow.is_better_than(&phb_fast));
383 assert!(!phb_fast.is_better_than(&pix_slow));
384 assert!(pix_fast.is_better_than(&pix_slow));
386 assert!(!pix_slow.is_better_than(&pix_fast));
387 assert!(!pix_fast.is_better_than(&pix_fast));
389 }
390
391 #[test]
392 fn test_pcie_speed_mbits_per_lane() {
393 assert_eq!(pcie_speed_mbits_per_lane("2.5 GT/s PCIe"), 1500);
394 assert_eq!(pcie_speed_mbits_per_lane("5 GT/s"), 3000);
395 assert_eq!(pcie_speed_mbits_per_lane("8.0 GT/s"), 6000);
396 assert_eq!(pcie_speed_mbits_per_lane("16 GT/s PCIe"), 12000);
397 assert_eq!(pcie_speed_mbits_per_lane("32 GT/s"), 24000);
398 assert_eq!(pcie_speed_mbits_per_lane("64 GT/s"), 48000);
399 assert_eq!(pcie_speed_mbits_per_lane("garbage"), 6000);
401 assert_eq!(pcie_speed_mbits_per_lane(""), 6000);
402 }
403
404 fn hop(sysfs: &str, link_mbytes_per_sec: u32) -> PciHop {
405 PciHop {
406 sysfs: PathBuf::from(sysfs),
407 link_mbytes_per_sec,
408 }
409 }
410
411 #[test]
412 fn test_classify_pix_single_switch() {
413 let a = vec![
416 hop("/d/a", 12000),
417 hop("/d/sw_a", 12000),
418 hop("/d/sw", 8000),
419 ];
420 let b = vec![hop("/d/b", 12000), hop("/d/sw_b", 6000), hop("/d/sw", 8000)];
421 let path = classify(&a, Some(0), &b, Some(0));
422 assert_eq!(path.path_type, PathType::Pix);
423 assert_eq!(path.bottleneck_mbytes_per_sec, 6000);
424 }
425
426 #[test]
427 fn test_classify_excludes_common_ancestor_upstream() {
428 let a = vec![hop("/d/a", 12000), hop("/d/sw", 2000)];
432 let b = vec![hop("/d/b", 12000), hop("/d/sw", 2000)];
433 let path = classify(&a, Some(0), &b, Some(0));
434 assert_eq!(path.path_type, PathType::Pix);
435 assert_eq!(
436 path.bottleneck_mbytes_per_sec, 12000,
437 "the common ancestor's 2000 MB/s upstream link is above the path and must be excluded"
438 );
439 }
440
441 #[test]
442 fn test_classify_pxb_multiple_switches() {
443 let a = vec![
445 hop("/d/a", 12000),
446 hop("/d/sw_a1", 12000),
447 hop("/d/sw_a2", 12000),
448 hop("/d/sw_a3", 12000),
449 hop("/d/root", 16000),
450 ];
451 let b = vec![
452 hop("/d/b", 10000),
453 hop("/d/sw_b", 10000),
454 hop("/d/root", 16000),
455 ];
456 let path = classify(&a, Some(0), &b, Some(0));
457 assert_eq!(path.path_type, PathType::Pxb);
458 assert_eq!(path.bottleneck_mbytes_per_sec, 10000);
459 }
460
461 #[test]
462 fn test_classify_through_cpu() {
463 let a = vec![hop("/d/a", 4000), hop("/d/root_a", 16000)];
465 let b = vec![hop("/d/b", 8000), hop("/d/root_b", 16000)];
466 let phb = classify(&a, Some(0), &b, Some(0));
468 assert_eq!(phb.path_type, PathType::Phb);
469 assert_eq!(phb.bottleneck_mbytes_per_sec, 4000);
470 assert_eq!(classify(&a, Some(0), &b, Some(1)).path_type, PathType::Sys);
472 assert_eq!(classify(&a, None, &b, Some(0)).path_type, PathType::Dis);
474 }
475}