use oxicuda_driver::error::{CudaError, CudaResult};
pub const LOCAL_NUMA_DISTANCE: u8 = 10;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NumaTopology {
node_count: usize,
distances: Vec<u8>,
}
impl NumaTopology {
pub fn new(node_count: usize, distances: Vec<u8>) -> CudaResult<Self> {
if node_count == 0 {
return Err(CudaError::InvalidValue);
}
let expected = node_count
.checked_mul(node_count)
.ok_or(CudaError::InvalidValue)?;
if distances.len() != expected {
return Err(CudaError::InvalidValue);
}
for node in 0..node_count {
if distances[node * node_count + node] != LOCAL_NUMA_DISTANCE {
return Err(CudaError::InvalidValue);
}
}
Ok(Self {
node_count,
distances,
})
}
pub fn uniform(node_count: usize, remote: u8) -> CudaResult<Self> {
if node_count == 0 {
return Err(CudaError::InvalidValue);
}
let remote = remote.max(LOCAL_NUMA_DISTANCE.saturating_add(1));
let mut distances = vec![remote; node_count * node_count];
for node in 0..node_count {
distances[node * node_count + node] = LOCAL_NUMA_DISTANCE;
}
Ok(Self {
node_count,
distances,
})
}
#[must_use]
pub fn two_node(remote: u8) -> Self {
Self::uniform(2, remote).unwrap_or(Self {
node_count: 2,
distances: vec![LOCAL_NUMA_DISTANCE, 20, 20, LOCAL_NUMA_DISTANCE],
})
}
#[must_use]
pub fn single_node() -> Self {
Self {
node_count: 1,
distances: vec![LOCAL_NUMA_DISTANCE],
}
}
#[inline]
#[must_use]
pub fn node_count(&self) -> usize {
self.node_count
}
#[must_use]
pub fn distance(&self, from: usize, to: usize) -> Option<u8> {
if from >= self.node_count || to >= self.node_count {
return None;
}
Some(self.distances[from * self.node_count + to])
}
#[must_use]
pub fn nearest_node(&self, from: usize) -> Option<usize> {
if from >= self.node_count {
return None;
}
let mut best = from;
let mut best_dist = self.distances[from * self.node_count + from];
for to in 0..self.node_count {
let d = self.distances[from * self.node_count + to];
if d < best_dist {
best_dist = d;
best = to;
}
}
Some(best)
}
}
pub fn closest_node_to_gpu(topology: &NumaTopology, gpu_home_node: usize) -> CudaResult<usize> {
if gpu_home_node >= topology.node_count() {
return Err(CudaError::InvalidValue);
}
topology
.nearest_node(gpu_home_node)
.ok_or(CudaError::InvalidValue)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct NumaBuffer {
node: usize,
byte_size: usize,
}
impl NumaBuffer {
pub fn on_node(topology: &NumaTopology, node: usize, byte_size: usize) -> CudaResult<Self> {
if byte_size == 0 {
return Err(CudaError::InvalidValue);
}
if node >= topology.node_count() {
return Err(CudaError::InvalidValue);
}
Ok(Self { node, byte_size })
}
pub fn closest_to_gpu(
topology: &NumaTopology,
gpu_home_node: usize,
byte_size: usize,
) -> CudaResult<Self> {
let node = closest_node_to_gpu(topology, gpu_home_node)?;
Self::on_node(topology, node, byte_size)
}
#[inline]
#[must_use]
pub fn node(&self) -> usize {
self.node
}
#[inline]
#[must_use]
pub fn byte_size(&self) -> usize {
self.byte_size
}
#[inline]
#[must_use]
pub fn is_local_to(&self, access_node: usize) -> bool {
self.node == access_node
}
#[must_use]
pub fn access_distance(&self, topology: &NumaTopology, access_node: usize) -> Option<u8> {
topology.distance(access_node, self.node)
}
}
impl std::fmt::Display for NumaBuffer {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"NumaBuffer(node={}, {} bytes)",
self.node, self.byte_size
)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NumaAllocTracker {
per_node_bytes: Vec<usize>,
}
impl NumaAllocTracker {
#[must_use]
pub fn new(topology: &NumaTopology) -> Self {
Self {
per_node_bytes: vec![0; topology.node_count()],
}
}
pub fn record(&mut self, buf: &NumaBuffer) -> CudaResult<()> {
let slot = self
.per_node_bytes
.get_mut(buf.node())
.ok_or(CudaError::InvalidValue)?;
*slot = slot.saturating_add(buf.byte_size());
Ok(())
}
pub fn release(&mut self, buf: &NumaBuffer) -> CudaResult<()> {
let slot = self
.per_node_bytes
.get_mut(buf.node())
.ok_or(CudaError::InvalidValue)?;
*slot = slot.saturating_sub(buf.byte_size());
Ok(())
}
#[must_use]
pub fn bytes_on_node(&self, node: usize) -> Option<usize> {
self.per_node_bytes.get(node).copied()
}
#[must_use]
pub fn total_bytes(&self) -> usize {
self.per_node_bytes
.iter()
.copied()
.fold(0usize, |acc, b| acc.saturating_add(b))
}
#[must_use]
pub fn least_loaded_node(&self) -> Option<usize> {
self.per_node_bytes
.iter()
.enumerate()
.min_by_key(|&(_, &bytes)| bytes)
.map(|(idx, _)| idx)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn topology_uniform_diagonal_is_local() {
let topo = NumaTopology::uniform(4, 20).expect("topo");
assert_eq!(topo.node_count(), 4);
for node in 0..4 {
assert_eq!(topo.distance(node, node), Some(LOCAL_NUMA_DISTANCE));
}
}
#[test]
fn topology_uniform_remote_distance() {
let topo = NumaTopology::uniform(3, 32).expect("topo");
assert_eq!(topo.distance(0, 1), Some(32));
assert_eq!(topo.distance(2, 0), Some(32));
}
#[test]
fn topology_uniform_clamps_remote_below_local() {
let topo = NumaTopology::uniform(2, 5).expect("topo");
assert_eq!(topo.distance(0, 1), Some(LOCAL_NUMA_DISTANCE + 1));
}
#[test]
fn topology_two_node() {
let topo = NumaTopology::two_node(20);
assert_eq!(topo.node_count(), 2);
assert_eq!(topo.distance(0, 0), Some(10));
assert_eq!(topo.distance(0, 1), Some(20));
assert_eq!(topo.distance(1, 0), Some(20));
}
#[test]
fn topology_single_node() {
let topo = NumaTopology::single_node();
assert_eq!(topo.node_count(), 1);
assert_eq!(topo.distance(0, 0), Some(10));
assert_eq!(topo.distance(0, 1), None);
}
#[test]
fn topology_new_rejects_zero_nodes() {
assert_eq!(NumaTopology::new(0, vec![]), Err(CudaError::InvalidValue));
}
#[test]
fn topology_new_rejects_wrong_matrix_size() {
assert_eq!(
NumaTopology::new(2, vec![10, 20, 20]),
Err(CudaError::InvalidValue)
);
}
#[test]
fn topology_new_rejects_bad_diagonal() {
assert_eq!(
NumaTopology::new(2, vec![10, 20, 20, 11]),
Err(CudaError::InvalidValue)
);
}
#[test]
fn topology_new_accepts_valid_matrix() {
let topo = NumaTopology::new(2, vec![10, 21, 21, 10]).expect("topo");
assert_eq!(topo.distance(0, 1), Some(21));
}
#[test]
fn topology_distance_out_of_range_is_none() {
let topo = NumaTopology::two_node(20);
assert_eq!(topo.distance(0, 5), None);
assert_eq!(topo.distance(5, 0), None);
}
#[test]
fn nearest_node_is_self_for_local() {
let topo = NumaTopology::uniform(4, 20).expect("topo");
for node in 0..4 {
assert_eq!(topo.nearest_node(node), Some(node));
}
}
#[test]
fn nearest_node_picks_closer_remote() {
let distances = vec![
10, 40, 15, 40, 10, 25, 15, 25, 10, ];
let topo = NumaTopology::new(3, distances).expect("topo");
assert_eq!(topo.nearest_node(0), Some(0));
}
#[test]
fn nearest_node_out_of_range_is_none() {
let topo = NumaTopology::two_node(20);
assert_eq!(topo.nearest_node(2), None);
}
#[test]
fn closest_node_to_gpu_returns_home_node() {
let topo = NumaTopology::two_node(20);
assert_eq!(closest_node_to_gpu(&topo, 0), Ok(0));
assert_eq!(closest_node_to_gpu(&topo, 1), Ok(1));
}
#[test]
fn closest_node_to_gpu_rejects_bad_node() {
let topo = NumaTopology::two_node(20);
assert_eq!(closest_node_to_gpu(&topo, 9), Err(CudaError::InvalidValue));
}
#[test]
fn numa_buffer_on_node_valid() {
let topo = NumaTopology::two_node(20);
let buf = NumaBuffer::on_node(&topo, 1, 8192).expect("alloc");
assert_eq!(buf.node(), 1);
assert_eq!(buf.byte_size(), 8192);
}
#[test]
fn numa_buffer_rejects_zero_bytes() {
let topo = NumaTopology::two_node(20);
assert_eq!(
NumaBuffer::on_node(&topo, 0, 0),
Err(CudaError::InvalidValue)
);
}
#[test]
fn numa_buffer_rejects_bad_node() {
let topo = NumaTopology::two_node(20);
assert_eq!(
NumaBuffer::on_node(&topo, 7, 4096),
Err(CudaError::InvalidValue)
);
}
#[test]
fn numa_buffer_closest_to_gpu() {
let topo = NumaTopology::two_node(20);
let buf = NumaBuffer::closest_to_gpu(&topo, 1, 4096).expect("alloc");
assert_eq!(buf.node(), 1);
}
#[test]
fn numa_buffer_locality_and_distance() {
let topo = NumaTopology::two_node(20);
let buf = NumaBuffer::on_node(&topo, 1, 4096).expect("alloc");
assert!(buf.is_local_to(1));
assert!(!buf.is_local_to(0));
assert_eq!(buf.access_distance(&topo, 1), Some(10));
assert_eq!(buf.access_distance(&topo, 0), Some(20));
assert_eq!(buf.access_distance(&topo, 9), None);
}
#[test]
fn numa_buffer_display() {
let topo = NumaTopology::two_node(20);
let buf = NumaBuffer::on_node(&topo, 1, 4096).expect("alloc");
let s = format!("{buf}");
assert!(s.contains("node=1"));
assert!(s.contains("4096"));
}
#[test]
fn tracker_records_and_releases() {
let topo = NumaTopology::two_node(20);
let mut tracker = NumaAllocTracker::new(&topo);
let a = NumaBuffer::on_node(&topo, 0, 1000).expect("a");
let b = NumaBuffer::on_node(&topo, 1, 2000).expect("b");
let c = NumaBuffer::on_node(&topo, 0, 500).expect("c");
tracker.record(&a).expect("rec a");
tracker.record(&b).expect("rec b");
tracker.record(&c).expect("rec c");
assert_eq!(tracker.bytes_on_node(0), Some(1500));
assert_eq!(tracker.bytes_on_node(1), Some(2000));
assert_eq!(tracker.total_bytes(), 3500);
tracker.release(&a).expect("rel a");
assert_eq!(tracker.bytes_on_node(0), Some(500));
assert_eq!(tracker.total_bytes(), 2500);
}
#[test]
fn tracker_bytes_on_bad_node_is_none() {
let topo = NumaTopology::two_node(20);
let tracker = NumaAllocTracker::new(&topo);
assert_eq!(tracker.bytes_on_node(9), None);
}
#[test]
fn tracker_least_loaded_node() {
let topo = NumaTopology::uniform(3, 20).expect("topo");
let mut tracker = NumaAllocTracker::new(&topo);
let a = NumaBuffer::on_node(&topo, 0, 5000).expect("a");
let b = NumaBuffer::on_node(&topo, 1, 1000).expect("b");
tracker.record(&a).expect("rec a");
tracker.record(&b).expect("rec b");
assert_eq!(tracker.least_loaded_node(), Some(2));
}
#[test]
fn tracker_least_loaded_ties_pick_lowest_index() {
let topo = NumaTopology::two_node(20);
let tracker = NumaAllocTracker::new(&topo);
assert_eq!(tracker.least_loaded_node(), Some(0));
}
#[test]
fn tracker_release_saturates_at_zero() {
let topo = NumaTopology::single_node();
let mut tracker = NumaAllocTracker::new(&topo);
let buf = NumaBuffer::on_node(&topo, 0, 100).expect("buf");
tracker.release(&buf).expect("rel");
assert_eq!(tracker.bytes_on_node(0), Some(0));
}
}