1use oxicuda_driver::error::{CudaError, CudaResult};
39
40pub const LOCAL_NUMA_DISTANCE: u8 = 10;
42
43#[derive(Debug, Clone, PartialEq, Eq)]
54pub struct NumaTopology {
55 node_count: usize,
56 distances: Vec<u8>,
58}
59
60impl NumaTopology {
61 pub fn new(node_count: usize, distances: Vec<u8>) -> CudaResult<Self> {
69 if node_count == 0 {
70 return Err(CudaError::InvalidValue);
71 }
72 let expected = node_count
73 .checked_mul(node_count)
74 .ok_or(CudaError::InvalidValue)?;
75 if distances.len() != expected {
76 return Err(CudaError::InvalidValue);
77 }
78 for node in 0..node_count {
79 if distances[node * node_count + node] != LOCAL_NUMA_DISTANCE {
80 return Err(CudaError::InvalidValue);
81 }
82 }
83 Ok(Self {
84 node_count,
85 distances,
86 })
87 }
88
89 pub fn uniform(node_count: usize, remote: u8) -> CudaResult<Self> {
99 if node_count == 0 {
100 return Err(CudaError::InvalidValue);
101 }
102 let remote = remote.max(LOCAL_NUMA_DISTANCE.saturating_add(1));
103 let mut distances = vec![remote; node_count * node_count];
104 for node in 0..node_count {
105 distances[node * node_count + node] = LOCAL_NUMA_DISTANCE;
106 }
107 Ok(Self {
108 node_count,
109 distances,
110 })
111 }
112
113 #[must_use]
117 pub fn two_node(remote: u8) -> Self {
118 Self::uniform(2, remote).unwrap_or(Self {
120 node_count: 2,
121 distances: vec![LOCAL_NUMA_DISTANCE, 20, 20, LOCAL_NUMA_DISTANCE],
122 })
123 }
124
125 #[must_use]
127 pub fn single_node() -> Self {
128 Self {
129 node_count: 1,
130 distances: vec![LOCAL_NUMA_DISTANCE],
131 }
132 }
133
134 #[inline]
136 #[must_use]
137 pub fn node_count(&self) -> usize {
138 self.node_count
139 }
140
141 #[must_use]
144 pub fn distance(&self, from: usize, to: usize) -> Option<u8> {
145 if from >= self.node_count || to >= self.node_count {
146 return None;
147 }
148 Some(self.distances[from * self.node_count + to])
149 }
150
151 #[must_use]
156 pub fn nearest_node(&self, from: usize) -> Option<usize> {
157 if from >= self.node_count {
158 return None;
159 }
160 let mut best = from;
161 let mut best_dist = self.distances[from * self.node_count + from];
162 for to in 0..self.node_count {
163 let d = self.distances[from * self.node_count + to];
164 if d < best_dist {
165 best_dist = d;
166 best = to;
167 }
168 }
169 Some(best)
170 }
171}
172
173pub fn closest_node_to_gpu(topology: &NumaTopology, gpu_home_node: usize) -> CudaResult<usize> {
191 if gpu_home_node >= topology.node_count() {
192 return Err(CudaError::InvalidValue);
193 }
194 topology
196 .nearest_node(gpu_home_node)
197 .ok_or(CudaError::InvalidValue)
198}
199
200#[derive(Debug, Clone, Copy, PartialEq, Eq)]
211pub struct NumaBuffer {
212 node: usize,
213 byte_size: usize,
214}
215
216impl NumaBuffer {
217 pub fn on_node(topology: &NumaTopology, node: usize, byte_size: usize) -> CudaResult<Self> {
224 if byte_size == 0 {
225 return Err(CudaError::InvalidValue);
226 }
227 if node >= topology.node_count() {
228 return Err(CudaError::InvalidValue);
229 }
230 Ok(Self { node, byte_size })
231 }
232
233 pub fn closest_to_gpu(
241 topology: &NumaTopology,
242 gpu_home_node: usize,
243 byte_size: usize,
244 ) -> CudaResult<Self> {
245 let node = closest_node_to_gpu(topology, gpu_home_node)?;
246 Self::on_node(topology, node, byte_size)
247 }
248
249 #[inline]
251 #[must_use]
252 pub fn node(&self) -> usize {
253 self.node
254 }
255
256 #[inline]
258 #[must_use]
259 pub fn byte_size(&self) -> usize {
260 self.byte_size
261 }
262
263 #[inline]
266 #[must_use]
267 pub fn is_local_to(&self, access_node: usize) -> bool {
268 self.node == access_node
269 }
270
271 #[must_use]
276 pub fn access_distance(&self, topology: &NumaTopology, access_node: usize) -> Option<u8> {
277 topology.distance(access_node, self.node)
278 }
279}
280
281impl std::fmt::Display for NumaBuffer {
282 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
283 write!(
284 f,
285 "NumaBuffer(node={}, {} bytes)",
286 self.node, self.byte_size
287 )
288 }
289}
290
291#[derive(Debug, Clone, PartialEq, Eq)]
300pub struct NumaAllocTracker {
301 per_node_bytes: Vec<usize>,
302}
303
304impl NumaAllocTracker {
305 #[must_use]
307 pub fn new(topology: &NumaTopology) -> Self {
308 Self {
309 per_node_bytes: vec![0; topology.node_count()],
310 }
311 }
312
313 pub fn record(&mut self, buf: &NumaBuffer) -> CudaResult<()> {
320 let slot = self
321 .per_node_bytes
322 .get_mut(buf.node())
323 .ok_or(CudaError::InvalidValue)?;
324 *slot = slot.saturating_add(buf.byte_size());
325 Ok(())
326 }
327
328 pub fn release(&mut self, buf: &NumaBuffer) -> CudaResult<()> {
334 let slot = self
335 .per_node_bytes
336 .get_mut(buf.node())
337 .ok_or(CudaError::InvalidValue)?;
338 *slot = slot.saturating_sub(buf.byte_size());
339 Ok(())
340 }
341
342 #[must_use]
345 pub fn bytes_on_node(&self, node: usize) -> Option<usize> {
346 self.per_node_bytes.get(node).copied()
347 }
348
349 #[must_use]
351 pub fn total_bytes(&self) -> usize {
352 self.per_node_bytes
353 .iter()
354 .copied()
355 .fold(0usize, |acc, b| acc.saturating_add(b))
356 }
357
358 #[must_use]
363 pub fn least_loaded_node(&self) -> Option<usize> {
364 self.per_node_bytes
365 .iter()
366 .enumerate()
367 .min_by_key(|&(_, &bytes)| bytes)
368 .map(|(idx, _)| idx)
369 }
370}
371
372#[cfg(test)]
377mod tests {
378 use super::*;
379
380 #[test]
381 fn topology_uniform_diagonal_is_local() {
382 let topo = NumaTopology::uniform(4, 20).expect("topo");
383 assert_eq!(topo.node_count(), 4);
384 for node in 0..4 {
385 assert_eq!(topo.distance(node, node), Some(LOCAL_NUMA_DISTANCE));
386 }
387 }
388
389 #[test]
390 fn topology_uniform_remote_distance() {
391 let topo = NumaTopology::uniform(3, 32).expect("topo");
392 assert_eq!(topo.distance(0, 1), Some(32));
393 assert_eq!(topo.distance(2, 0), Some(32));
394 }
395
396 #[test]
397 fn topology_uniform_clamps_remote_below_local() {
398 let topo = NumaTopology::uniform(2, 5).expect("topo");
400 assert_eq!(topo.distance(0, 1), Some(LOCAL_NUMA_DISTANCE + 1));
401 }
402
403 #[test]
404 fn topology_two_node() {
405 let topo = NumaTopology::two_node(20);
406 assert_eq!(topo.node_count(), 2);
407 assert_eq!(topo.distance(0, 0), Some(10));
408 assert_eq!(topo.distance(0, 1), Some(20));
409 assert_eq!(topo.distance(1, 0), Some(20));
410 }
411
412 #[test]
413 fn topology_single_node() {
414 let topo = NumaTopology::single_node();
415 assert_eq!(topo.node_count(), 1);
416 assert_eq!(topo.distance(0, 0), Some(10));
417 assert_eq!(topo.distance(0, 1), None);
418 }
419
420 #[test]
421 fn topology_new_rejects_zero_nodes() {
422 assert_eq!(NumaTopology::new(0, vec![]), Err(CudaError::InvalidValue));
423 }
424
425 #[test]
426 fn topology_new_rejects_wrong_matrix_size() {
427 assert_eq!(
429 NumaTopology::new(2, vec![10, 20, 20]),
430 Err(CudaError::InvalidValue)
431 );
432 }
433
434 #[test]
435 fn topology_new_rejects_bad_diagonal() {
436 assert_eq!(
438 NumaTopology::new(2, vec![10, 20, 20, 11]),
439 Err(CudaError::InvalidValue)
440 );
441 }
442
443 #[test]
444 fn topology_new_accepts_valid_matrix() {
445 let topo = NumaTopology::new(2, vec![10, 21, 21, 10]).expect("topo");
446 assert_eq!(topo.distance(0, 1), Some(21));
447 }
448
449 #[test]
450 fn topology_distance_out_of_range_is_none() {
451 let topo = NumaTopology::two_node(20);
452 assert_eq!(topo.distance(0, 5), None);
453 assert_eq!(topo.distance(5, 0), None);
454 }
455
456 #[test]
457 fn nearest_node_is_self_for_local() {
458 let topo = NumaTopology::uniform(4, 20).expect("topo");
459 for node in 0..4 {
460 assert_eq!(topo.nearest_node(node), Some(node));
461 }
462 }
463
464 #[test]
465 fn nearest_node_picks_closer_remote() {
466 let distances = vec![
469 10, 40, 15, 40, 10, 25, 15, 25, 10, ];
473 let topo = NumaTopology::new(3, distances).expect("topo");
474 assert_eq!(topo.nearest_node(0), Some(0));
476 }
477
478 #[test]
479 fn nearest_node_out_of_range_is_none() {
480 let topo = NumaTopology::two_node(20);
481 assert_eq!(topo.nearest_node(2), None);
482 }
483
484 #[test]
485 fn closest_node_to_gpu_returns_home_node() {
486 let topo = NumaTopology::two_node(20);
487 assert_eq!(closest_node_to_gpu(&topo, 0), Ok(0));
488 assert_eq!(closest_node_to_gpu(&topo, 1), Ok(1));
489 }
490
491 #[test]
492 fn closest_node_to_gpu_rejects_bad_node() {
493 let topo = NumaTopology::two_node(20);
494 assert_eq!(closest_node_to_gpu(&topo, 9), Err(CudaError::InvalidValue));
495 }
496
497 #[test]
498 fn numa_buffer_on_node_valid() {
499 let topo = NumaTopology::two_node(20);
500 let buf = NumaBuffer::on_node(&topo, 1, 8192).expect("alloc");
501 assert_eq!(buf.node(), 1);
502 assert_eq!(buf.byte_size(), 8192);
503 }
504
505 #[test]
506 fn numa_buffer_rejects_zero_bytes() {
507 let topo = NumaTopology::two_node(20);
508 assert_eq!(
509 NumaBuffer::on_node(&topo, 0, 0),
510 Err(CudaError::InvalidValue)
511 );
512 }
513
514 #[test]
515 fn numa_buffer_rejects_bad_node() {
516 let topo = NumaTopology::two_node(20);
517 assert_eq!(
518 NumaBuffer::on_node(&topo, 7, 4096),
519 Err(CudaError::InvalidValue)
520 );
521 }
522
523 #[test]
524 fn numa_buffer_closest_to_gpu() {
525 let topo = NumaTopology::two_node(20);
526 let buf = NumaBuffer::closest_to_gpu(&topo, 1, 4096).expect("alloc");
527 assert_eq!(buf.node(), 1);
528 }
529
530 #[test]
531 fn numa_buffer_locality_and_distance() {
532 let topo = NumaTopology::two_node(20);
533 let buf = NumaBuffer::on_node(&topo, 1, 4096).expect("alloc");
534 assert!(buf.is_local_to(1));
535 assert!(!buf.is_local_to(0));
536 assert_eq!(buf.access_distance(&topo, 1), Some(10));
538 assert_eq!(buf.access_distance(&topo, 0), Some(20));
539 assert_eq!(buf.access_distance(&topo, 9), None);
540 }
541
542 #[test]
543 fn numa_buffer_display() {
544 let topo = NumaTopology::two_node(20);
545 let buf = NumaBuffer::on_node(&topo, 1, 4096).expect("alloc");
546 let s = format!("{buf}");
547 assert!(s.contains("node=1"));
548 assert!(s.contains("4096"));
549 }
550
551 #[test]
552 fn tracker_records_and_releases() {
553 let topo = NumaTopology::two_node(20);
554 let mut tracker = NumaAllocTracker::new(&topo);
555 let a = NumaBuffer::on_node(&topo, 0, 1000).expect("a");
556 let b = NumaBuffer::on_node(&topo, 1, 2000).expect("b");
557 let c = NumaBuffer::on_node(&topo, 0, 500).expect("c");
558
559 tracker.record(&a).expect("rec a");
560 tracker.record(&b).expect("rec b");
561 tracker.record(&c).expect("rec c");
562
563 assert_eq!(tracker.bytes_on_node(0), Some(1500));
564 assert_eq!(tracker.bytes_on_node(1), Some(2000));
565 assert_eq!(tracker.total_bytes(), 3500);
566
567 tracker.release(&a).expect("rel a");
568 assert_eq!(tracker.bytes_on_node(0), Some(500));
569 assert_eq!(tracker.total_bytes(), 2500);
570 }
571
572 #[test]
573 fn tracker_bytes_on_bad_node_is_none() {
574 let topo = NumaTopology::two_node(20);
575 let tracker = NumaAllocTracker::new(&topo);
576 assert_eq!(tracker.bytes_on_node(9), None);
577 }
578
579 #[test]
580 fn tracker_least_loaded_node() {
581 let topo = NumaTopology::uniform(3, 20).expect("topo");
582 let mut tracker = NumaAllocTracker::new(&topo);
583 let a = NumaBuffer::on_node(&topo, 0, 5000).expect("a");
584 let b = NumaBuffer::on_node(&topo, 1, 1000).expect("b");
585 tracker.record(&a).expect("rec a");
586 tracker.record(&b).expect("rec b");
587 assert_eq!(tracker.least_loaded_node(), Some(2));
589 }
590
591 #[test]
592 fn tracker_least_loaded_ties_pick_lowest_index() {
593 let topo = NumaTopology::two_node(20);
594 let tracker = NumaAllocTracker::new(&topo);
595 assert_eq!(tracker.least_loaded_node(), Some(0));
597 }
598
599 #[test]
600 fn tracker_release_saturates_at_zero() {
601 let topo = NumaTopology::single_node();
602 let mut tracker = NumaAllocTracker::new(&topo);
603 let buf = NumaBuffer::on_node(&topo, 0, 100).expect("buf");
604 tracker.release(&buf).expect("rel");
606 assert_eq!(tracker.bytes_on_node(0), Some(0));
607 }
608}