1pub(crate) mod nvml;
29pub mod topology;
30pub mod worker_pool;
31
32use cudarc::driver::{result::device as cuda_device, sys as cuda_sys};
33use nix::libc;
34use serde::{Deserialize, Serialize};
35use std::collections::HashMap;
36use std::sync::{Mutex, OnceLock};
37use std::{fs, mem, process::Command};
38
39static NUMA_NODE_CACHE: OnceLock<Mutex<HashMap<String, Option<NumaNode>>>> = OnceLock::new();
43
44pub fn is_numa_enabled() -> bool {
49 !crate::env_is_truthy("DYN_MEMORY_DISABLE_NUMA")
50}
51
52pub fn is_numa_disabled() -> bool {
54 !is_numa_enabled()
55}
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
62pub struct NumaNode(pub u32);
63
64impl NumaNode {
65 pub const UNKNOWN: NumaNode = NumaNode(u32::MAX);
67
68 pub fn is_unknown(&self) -> bool {
70 self.0 == u32::MAX
71 }
72}
73
74impl std::fmt::Display for NumaNode {
75 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
76 if self.is_unknown() {
77 write!(f, "UNKNOWN")
78 } else {
79 write!(f, "NumaNode({})", self.0)
80 }
81 }
82}
83
84pub fn get_current_cpu_numa_node() -> NumaNode {
89 unsafe {
90 let mut cpu: libc::c_uint = 0;
91 let mut node: libc::c_uint = 0;
92
93 let result = libc::syscall(
95 libc::SYS_getcpu,
96 &mut cpu,
97 &mut node,
98 std::ptr::null_mut::<libc::c_void>(),
99 );
100 if result == 0 {
101 NumaNode(node)
102 } else {
103 NumaNode::UNKNOWN
104 }
105 }
106}
107
108fn read_numa_node_from_sysfs(pci_address: &str) -> Option<NumaNode> {
113 let path = format!("/sys/bus/pci/devices/{}/numa_node", pci_address);
114 let content = fs::read_to_string(&path).ok()?;
115 let node: i32 = content.trim().parse().ok()?;
116 if node < 0 {
117 None
119 } else {
120 Some(NumaNode(node as u32))
121 }
122}
123
124fn get_numa_node_from_nvidia_smi(pci_address: &str) -> Option<NumaNode> {
129 let output = Command::new("nvidia-smi")
130 .args(["topo", "--get-numa-id-of-nearby-cpu", "-i", pci_address])
131 .output()
132 .ok()?;
133
134 if !output.status.success() {
135 return None;
136 }
137
138 let stdout = std::str::from_utf8(&output.stdout).ok()?;
139 let line = stdout.lines().next()?;
140 let numa_str = line.split(':').nth(1)?;
141 let node: u32 = numa_str.trim().parse().ok()?;
142 Some(NumaNode(node))
143}
144
145pub fn get_device_numa_node(device_id: u32) -> Option<NumaNode> {
161 let pci_address = match get_pci_bus_address_from_cuda(device_id) {
163 Some(addr) => addr,
164 None => {
165 tracing::warn!(
166 "Failed to get PCI address from CUDA for device {}, skipping NUMA optimization",
167 device_id
168 );
169 return None;
170 }
171 };
172
173 let cache = NUMA_NODE_CACHE.get_or_init(|| Mutex::new(HashMap::new()));
175 {
176 let guard = cache.lock().unwrap();
177 if let Some(cached) = guard.get(&pci_address) {
178 return *cached;
179 }
180 }
181
182 let result = read_numa_node_from_sysfs(&pci_address)
184 .or_else(|| get_numa_node_from_nvidia_smi(&pci_address));
185
186 match result {
187 Some(node) => {
188 tracing::trace!(
189 "GPU {} (PCI {}) on NUMA node {}",
190 device_id,
191 pci_address,
192 node.0
193 );
194 }
195 None => {
196 tracing::warn!(
197 "Could not determine NUMA node for GPU {} (PCI {}), skipping NUMA optimization",
198 device_id,
199 pci_address
200 );
201 }
202 }
203
204 cache.lock().unwrap().insert(pci_address, result);
206 result
207}
208
209pub fn pin_thread_to_numa_node(node: NumaNode) -> Result<(), String> {
224 let topology =
225 topology::get_numa_topology().map_err(|e| format!("Can not get NUMA topology: {}", e))?;
226
227 let cpus = topology
228 .cpus_for_node(node.0)
229 .ok_or_else(|| format!("No CPUs found for NUMA node {}", node.0))?;
230
231 if cpus.is_empty() {
232 return Err(format!("No CPUs found for NUMA node {}", node.0));
233 }
234
235 unsafe {
236 let mut cpu_set: libc::cpu_set_t = mem::zeroed();
237
238 for cpu in cpus {
239 libc::CPU_SET(*cpu, &mut cpu_set);
240 }
241
242 let result = libc::sched_setaffinity(
243 0, mem::size_of::<libc::cpu_set_t>(),
245 &cpu_set,
246 );
247
248 if result != 0 {
249 let err = std::io::Error::last_os_error();
250 return Err(format!("Failed to set CPU affinity: {}", err));
251 }
252 }
253
254 Ok(())
255}
256
257fn get_pci_bus_address_from_cuda(device_id: u32) -> Option<String> {
262 unsafe {
266 let mut dev = std::mem::MaybeUninit::uninit();
267 if cuda_sys::cuDeviceGet(dev.as_mut_ptr(), device_id as i32)
268 .result()
269 .is_err()
270 {
271 return None;
272 }
273 let dev = dev.assume_init();
274
275 let domain = cuda_device::get_attribute(
276 dev,
277 cuda_sys::CUdevice_attribute::CU_DEVICE_ATTRIBUTE_PCI_DOMAIN_ID,
278 )
279 .ok()?;
280 let bus = cuda_device::get_attribute(
281 dev,
282 cuda_sys::CUdevice_attribute::CU_DEVICE_ATTRIBUTE_PCI_BUS_ID,
283 )
284 .ok()?;
285 let device = cuda_device::get_attribute(
286 dev,
287 cuda_sys::CUdevice_attribute::CU_DEVICE_ATTRIBUTE_PCI_DEVICE_ID,
288 )
289 .ok()?;
290
291 Some(format!("{:04x}:{:02x}:{:02x}.0", domain, bus, device))
292 }
293}
294
295#[derive(Debug, Clone)]
297struct GpuTopoInfo {
298 pci_address: String,
299 numa_node: Option<u32>,
300}
301
302fn enumerate_cuda_gpus() -> Vec<GpuTopoInfo> {
304 let count = match cuda_device::get_count() {
305 Ok(c) => c,
306 Err(_) => return Vec::new(),
307 };
308
309 (0..count as u32)
310 .filter_map(|i| {
311 let pci = get_pci_bus_address_from_cuda(i)?;
312 let numa = read_numa_node_from_sysfs(&pci).map(|n| n.0);
313 Some(GpuTopoInfo {
314 pci_address: pci,
315 numa_node: numa,
316 })
317 })
318 .collect()
319}
320
321fn enumerate_all_gpus() -> Vec<GpuTopoInfo> {
324 if let Some(nvml) = nvml::try_nvml() {
326 let nvml_gpus = nvml.enumerate_gpus();
327 if !nvml_gpus.is_empty() {
328 tracing::debug!(
329 "NVML enumerated {} GPUs (ignoring CUDA_VISIBLE_DEVICES)",
330 nvml_gpus.len()
331 );
332 return nvml_gpus
333 .into_iter()
334 .map(|g| {
335 let numa = read_numa_node_from_sysfs(&g.pci_address).map(|n| n.0);
336 GpuTopoInfo {
337 pci_address: g.pci_address,
338 numa_node: numa,
339 }
340 })
341 .collect();
342 }
343 }
344
345 tracing::debug!("Falling back to CUDA driver GPU enumeration");
347 enumerate_cuda_gpus()
348}
349
350static DEVICE_CPU_SETS: OnceLock<HashMap<u32, Option<Vec<usize>>>> = OnceLock::new();
352
353pub fn get_device_cpu_set(device_id: u32) -> Option<Vec<usize>> {
375 DEVICE_CPU_SETS
376 .get_or_init(compute_all_device_cpu_sets)
377 .get(&device_id)
378 .cloned()
379 .flatten()
380}
381
382fn compute_all_device_cpu_sets() -> HashMap<u32, Option<Vec<usize>>> {
383 let topology = match topology::get_numa_topology() {
384 Ok(t) => t,
385 Err(e) => {
386 tracing::warn!("Cannot subdivide CPU sets: {e}");
387 return HashMap::new();
388 }
389 };
390
391 let cuda_count = cuda_device::get_count().unwrap_or(0);
393 if cuda_count == 0 {
394 return HashMap::new();
395 }
396
397 let mut cuda_devices: Vec<(u32, String, Option<u32>)> = Vec::new();
399 for i in 0..cuda_count as u32 {
400 if let Some(pci) = get_pci_bus_address_from_cuda(i) {
401 let numa = read_numa_node_from_sysfs(&pci).map(|n| n.0);
402 cuda_devices.push((i, pci, numa));
403 }
404 }
405
406 let all_gpus = enumerate_all_gpus();
408
409 let mut node_groups: HashMap<u32, Vec<String>> = HashMap::new();
411 for gpu in &all_gpus {
412 if let Some(node) = gpu.numa_node {
413 node_groups
414 .entry(node)
415 .or_default()
416 .push(gpu.pci_address.clone());
417 }
418 }
419
420 for group in node_groups.values_mut() {
422 group.sort();
423 }
424
425 let mut results = HashMap::new();
427 for (device_id, pci_addr, numa_node) in &cuda_devices {
428 let cpu_set = numa_node.and_then(|node| {
429 let group = node_groups.get(&node)?;
430 let position = group.iter().position(|addr| addr == pci_addr)?;
431 let all_cpus = topology.cpus_for_node(node)?;
432
433 if all_cpus.is_empty() || group.is_empty() {
434 return None;
435 }
436
437 let n = group.len();
439 let chunk_size = all_cpus.len() / n;
440 if chunk_size == 0 {
441 return Some(all_cpus.to_vec());
443 }
444
445 let start = position * chunk_size;
446 let end = if position == n - 1 {
447 all_cpus.len() } else {
449 start + chunk_size
450 };
451
452 Some(all_cpus[start..end].to_vec())
453 });
454
455 results.insert(*device_id, cpu_set);
456 }
457
458 results
459}
460
461#[cfg(test)]
462mod tests {
463 use super::*;
464
465 #[test]
466 fn test_numa_node_equality() {
467 let node0a = NumaNode(0);
468 let node0b = NumaNode(0);
469 let node1 = NumaNode(1);
470
471 assert_eq!(node0a, node0b);
472 assert_ne!(node0a, node1);
473 }
474
475 #[test]
476 fn test_numa_node_unknown() {
477 let unknown = NumaNode::UNKNOWN;
478 assert!(unknown.is_unknown());
479 assert_eq!(unknown.0, u32::MAX);
480
481 let valid = NumaNode(0);
482 assert!(!valid.is_unknown());
483 }
484
485 #[test]
486 fn test_numa_node_display() {
487 assert_eq!(format!("{}", NumaNode(0)), "NumaNode(0)");
488 assert_eq!(format!("{}", NumaNode(7)), "NumaNode(7)");
489 assert_eq!(format!("{}", NumaNode::UNKNOWN), "UNKNOWN");
490 }
491
492 #[test]
493 fn test_numa_node_serialization() {
494 let node = NumaNode(1);
495 let json = serde_json::to_string(&node).unwrap();
496 let deserialized: NumaNode = serde_json::from_str(&json).unwrap();
497 assert_eq!(node, deserialized);
498 }
499
500 #[test]
501 fn test_get_current_cpu_numa_node() {
502 let node = get_current_cpu_numa_node();
503 if !node.is_unknown() {
504 assert!(node.0 < 8, "NUMA node {} seems unreasonably high", node.0);
505 }
506 }
507
508 #[test]
509 fn test_numa_node_hash() {
510 use std::collections::HashMap;
511
512 let mut map = HashMap::new();
513 map.insert(NumaNode(0), "node0");
514 map.insert(NumaNode(1), "node1");
515
516 assert_eq!(map.get(&NumaNode(0)), Some(&"node0"));
517 assert_eq!(map.get(&NumaNode(1)), Some(&"node1"));
518 assert_eq!(map.get(&NumaNode(2)), None);
519 }
520
521 #[test]
522 fn test_numa_node_copy_clone() {
523 let node1 = NumaNode(5);
524 let node2 = node1;
525 let node3 = node1;
526
527 assert_eq!(node1, node2);
528 assert_eq!(node1, node3);
529 assert_eq!(node2, node3);
530 }
531
532 #[test]
533 fn test_read_numa_node_from_sysfs_nonexistent() {
534 assert!(read_numa_node_from_sysfs("ffff:ff:ff.0").is_none());
535 }
536}
537
538#[cfg(all(test, feature = "testing-cuda"))]
539mod cuda_tests {
540 use super::*;
541
542 #[test]
543 fn test_get_pci_bus_address_from_cuda() {
544 let addr = get_pci_bus_address_from_cuda(0).expect("should get PCI address for GPU 0");
545 let parts: Vec<&str> = addr.split(':').collect();
547 assert_eq!(
548 parts.len(),
549 3,
550 "PCI address should have 3 colon-separated parts: {}",
551 addr
552 );
553 assert_eq!(parts[0].len(), 4, "domain should be 4 hex chars: {}", addr);
554 assert!(parts[2].ends_with(".0"), "should end with .0: {}", addr);
555 println!("GPU 0 PCI address: {}", addr);
556 }
557
558 #[test]
559 fn test_read_numa_node_from_sysfs_real_gpu() {
560 let addr = get_pci_bus_address_from_cuda(0).expect("should get PCI address for GPU 0");
561 if let Some(node) = read_numa_node_from_sysfs(&addr) {
562 assert!(node.0 < 16, "NUMA node {} seems unreasonably high", node.0);
563 println!("GPU 0 (PCI {}) sysfs NUMA node: {}", addr, node.0);
564 } else {
565 println!(
566 "GPU 0 (PCI {}) has no sysfs NUMA info (single-socket?)",
567 addr
568 );
569 }
570 }
571
572 #[test]
573 fn test_get_device_numa_node_returns_some_or_none() {
574 let result = get_device_numa_node(0);
575 match result {
576 Some(node) => {
577 assert!(node.0 < 16, "NUMA node {} seems unreasonably high", node.0);
578 assert!(
579 !node.is_unknown(),
580 "should never return UNKNOWN inside Some"
581 );
582 println!("GPU 0 detected on NUMA node: {}", node.0);
583 }
584 None => {
585 println!("GPU 0 has no determinable NUMA node (single-socket or no sysfs info)");
586 }
587 }
588 }
589}