use super::TopologyModel;
pub fn numa_box_width_cells(gpus_in_numa: u32) -> u16 {
let columns = numa_column_count(gpus_in_numa);
(columns as u16 * 13) + 6
}
pub fn numa_column_count(gpus_in_numa: u32) -> u32 {
match gpus_in_numa {
0 | 1 => 1,
2 => 2,
3 => 3,
4 => 2, 5 | 6 => 3,
7 | 8 => 4, _ => 4, }
}
pub fn numa_row_count(gpus_in_numa: u32) -> u32 {
match gpus_in_numa {
0 => 0,
1 => 1,
2 | 3 => 1,
4 => 2,
5 | 6 => 2,
7 | 8 => 2,
n => n.div_ceil(4),
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BoxStacking {
Horizontal,
Vertical,
}
#[derive(Debug, Clone)]
pub struct GraphLayout {
pub stacking: BoxStacking,
pub numa_groups: Vec<NumaGroup>,
#[allow(dead_code)]
pub used_width: u16,
}
#[derive(Debug, Clone)]
pub struct NumaGroup {
pub numa_node: Option<i32>,
pub gpu_slots: Vec<usize>,
pub columns: u32,
}
impl GraphLayout {
pub fn plan(model: &TopologyModel, available_width: u16) -> Self {
let numa_nodes = model.numa_nodes();
let mut numa_groups: Vec<NumaGroup> = numa_nodes
.iter()
.map(|numa| {
let gpu_slots: Vec<usize> = model
.gpus
.iter()
.enumerate()
.filter(|(_, g)| g.numa_node == *numa)
.map(|(i, _)| i)
.collect();
let columns = numa_column_count(gpu_slots.len() as u32);
NumaGroup {
numa_node: *numa,
gpu_slots,
columns,
}
})
.collect();
let horizontal_width: u16 = numa_groups
.iter()
.map(|grp| numa_box_width_cells(grp.gpu_slots.len() as u32))
.sum::<u16>()
+ numa_groups.len().saturating_sub(1) as u16 * 3;
let (stacking, used_width) =
if numa_groups.len() <= 1 || horizontal_width <= available_width {
(
BoxStacking::Horizontal,
horizontal_width.min(available_width),
)
} else {
let max_width = numa_groups
.iter()
.map(|grp| numa_box_width_cells(grp.gpu_slots.len() as u32))
.max()
.unwrap_or(0)
.min(available_width);
(BoxStacking::Vertical, max_width)
};
numa_groups.sort_by(|a, b| match (a.numa_node, b.numa_node) {
(Some(x), Some(y)) => x.cmp(&y),
(Some(_), None) => std::cmp::Ordering::Less,
(None, Some(_)) => std::cmp::Ordering::Greater,
(None, None) => std::cmp::Ordering::Equal,
});
Self {
stacking,
numa_groups,
used_width,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::device::{GpuInfo, NvLinkRemoteDevice, NvLinkRemoteType};
use std::collections::HashMap;
fn mk_gpu(index: u32, numa: Option<i32>) -> GpuInfo {
let mut detail = HashMap::new();
detail.insert("index".to_string(), index.to_string());
GpuInfo {
uuid: format!("GPU-{index}"),
time: String::new(),
name: "NVIDIA H100".to_string(),
device_type: "GPU".to_string(),
host_id: "h".to_string(),
hostname: "h".to_string(),
instance: "h".to_string(),
utilization: 0.0,
ane_utilization: 0.0,
dla_utilization: None,
tensorcore_utilization: None,
temperature: 0,
used_memory: 0,
total_memory: 0,
frequency: 0,
power_consumption: 0.0,
gpu_core_count: None,
temperature_threshold_slowdown: None,
temperature_threshold_shutdown: None,
temperature_threshold_max_operating: None,
temperature_threshold_acoustic: None,
performance_state: None,
fan_speed_rpm: None,
numa_node_id: numa,
gsp_firmware_mode: None,
gsp_firmware_version: None,
nvlink_remote_devices: vec![NvLinkRemoteDevice {
link_index: 0,
remote_type: NvLinkRemoteType::Gpu,
bandwidth_mb_s: None,
}],
gpm_metrics: None,
detail,
}
}
#[test]
fn two_gpus_one_numa_picks_horizontal() {
let model = TopologyModel::from_host("h", &[mk_gpu(0, Some(0)), mk_gpu(1, Some(0))]);
let layout = GraphLayout::plan(&model, 120);
assert_eq!(layout.stacking, BoxStacking::Horizontal);
assert_eq!(layout.numa_groups.len(), 1);
assert_eq!(layout.numa_groups[0].gpu_slots.len(), 2);
}
#[test]
fn four_gpus_single_numa_uses_2x2_grid() {
let gpus: Vec<_> = (0..4).map(|i| mk_gpu(i, Some(0))).collect();
let model = TopologyModel::from_host("h", &gpus);
let layout = GraphLayout::plan(&model, 200);
assert_eq!(layout.numa_groups.len(), 1);
assert_eq!(layout.numa_groups[0].columns, 2);
assert_eq!(numa_row_count(4), 2);
}
#[test]
fn eight_gpus_two_numa_fits_horizontally_on_wide_terminal() {
let mut gpus = Vec::new();
for i in 0..8 {
gpus.push(mk_gpu(i, Some((i as i32) / 4)));
}
let model = TopologyModel::from_host("h", &gpus);
let layout = GraphLayout::plan(&model, 200);
assert_eq!(layout.stacking, BoxStacking::Horizontal);
assert_eq!(layout.numa_groups.len(), 2);
assert_eq!(layout.numa_groups[0].gpu_slots.len(), 4);
assert_eq!(layout.numa_groups[1].gpu_slots.len(), 4);
}
#[test]
fn eight_gpus_two_numa_falls_back_to_vertical_on_narrow_terminal() {
let mut gpus = Vec::new();
for i in 0..8 {
gpus.push(mk_gpu(i, Some((i as i32) / 4)));
}
let model = TopologyModel::from_host("h", &gpus);
let layout = GraphLayout::plan(&model, 50);
assert_eq!(layout.stacking, BoxStacking::Vertical);
assert_eq!(layout.numa_groups.len(), 2);
}
#[test]
fn unknown_numa_sorts_last() {
let gpus = vec![mk_gpu(0, None), mk_gpu(1, Some(1)), mk_gpu(2, Some(0))];
let model = TopologyModel::from_host("h", &gpus);
let layout = GraphLayout::plan(&model, 200);
let numa_order: Vec<_> = layout.numa_groups.iter().map(|g| g.numa_node).collect();
assert_eq!(numa_order, vec![Some(0), Some(1), None]);
}
#[test]
fn used_width_never_exceeds_available_width() {
let gpus: Vec<_> = (0..8).map(|i| mk_gpu(i, Some((i as i32) / 4))).collect();
let model = TopologyModel::from_host("h", &gpus);
let layout = GraphLayout::plan(&model, 50);
assert!(layout.used_width <= 50, "{}", layout.used_width);
}
#[test]
fn single_gpu_has_one_column_one_row() {
assert_eq!(numa_column_count(1), 1);
assert_eq!(numa_row_count(1), 1);
}
#[test]
fn column_counts_match_2x_n_convention() {
assert_eq!(numa_column_count(4), 2);
assert_eq!(numa_column_count(8), 4);
assert_eq!(numa_row_count(8), 2);
}
}