use super::TopologyModel;
use super::classify_edge::classify;
const MIN_CELL: usize = 5;
const MAX_CELL: usize = 6;
pub fn render_matrix(model: &TopologyModel, width: u16) -> String {
if model.gpus.is_empty() {
return " (no GPUs on this host)\n".to_string();
}
let gpu_count = model.gpus.len();
let available = width as usize;
let cell_width = pick_cell_width(gpu_count, available);
if cell_width < MIN_CELL {
return render_narrow_fallback(model);
}
let mut out = String::new();
let label_col_w = gpu_label_width(model);
out.push_str(&" ".repeat(label_col_w));
for gpu in &model.gpus {
let hdr = format!("GPU{}", gpu.index);
out.push_str(¢er(&hdr, cell_width));
}
out.push_str(" NUMA\n");
let gpu_count_u32 = gpu_count as u32;
for (r, row_gpu) in model.gpus.iter().enumerate() {
let row_label = format!("GPU{}", row_gpu.index);
out.push_str(&right_pad(&row_label, label_col_w));
for (c, col_gpu) in model.gpus.iter().enumerate() {
let edge = classify(
r as u32,
c as u32,
&pseudo_info(row_gpu),
&pseudo_info(col_gpu),
gpu_count_u32,
);
let label = edge.label();
out.push_str(¢er(&label, cell_width));
}
let numa = row_gpu
.numa_node
.map(|n| n.to_string())
.unwrap_or_else(|| "-".to_string());
out.push_str(&format!(" {numa}\n"));
}
out.push_str("\nLegend: X=self NVn=NvLink Gen-n NV=NvLink (gen unknown) ");
out.push_str("NSW=NvSwitch PXB=PCIe bridge NODE=PCIe same NUMA SYS=PCIe across NUMA\n");
out
}
fn pseudo_info(gpu: &super::TopologyGpu) -> crate::device::GpuInfo {
use std::collections::HashMap;
crate::device::GpuInfo {
uuid: gpu.uuid.clone(),
time: String::new(),
name: gpu.name.clone(),
device_type: "GPU".to_string(),
host_id: String::new(),
hostname: String::new(),
instance: String::new(),
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: gpu.numa_node,
gsp_firmware_mode: None,
gsp_firmware_version: None,
nvlink_remote_devices: gpu.links.clone(),
gpm_metrics: None,
detail: HashMap::new(),
}
}
fn gpu_label_width(model: &TopologyModel) -> usize {
let max = model
.gpus
.iter()
.map(|g| format!("GPU{}", g.index).len())
.max()
.unwrap_or(3);
max.max(4) + 1 }
fn pick_cell_width(gpu_count: usize, available: usize) -> usize {
if gpu_count == 0 {
return MAX_CELL;
}
let overhead = 5 + 8;
let usable = available.saturating_sub(overhead);
for cw in (MIN_CELL..=MAX_CELL).rev() {
if cw * gpu_count <= usable {
return cw;
}
}
0
}
fn center(s: &str, w: usize) -> String {
if s.len() >= w {
let trimmed: String = s.chars().take(w).collect();
return trimmed;
}
let total = w - s.len();
let left = total / 2;
let right = total - left;
format!("{}{s}{}", " ".repeat(left), " ".repeat(right))
}
fn right_pad(s: &str, w: usize) -> String {
if s.len() >= w {
let trimmed: String = s.chars().take(w).collect();
return trimmed;
}
format!("{s}{}", " ".repeat(w - s.len()))
}
fn render_narrow_fallback(model: &TopologyModel) -> String {
let mut out = String::new();
out.push_str(" (terminal too narrow for matrix view — summary only)\n");
for gpu in &model.gpus {
let links = gpu.links.len();
let numa = gpu
.numa_node
.map(|n| n.to_string())
.unwrap_or_else(|| "?".to_string());
out.push_str(&format!(
" GPU{idx:<3} NUMA {numa} {links} active NvLinks\n",
idx = gpu.index,
));
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::device::{GpuInfo, NvLinkRemoteDevice, NvLinkRemoteType};
use std::collections::HashMap;
fn mk_gpu(index: u32, numa: Option<i32>, links: u32) -> 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 80GB HBM3".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: (0..links)
.map(|i| NvLinkRemoteDevice {
link_index: i,
remote_type: NvLinkRemoteType::Gpu,
bandwidth_mb_s: Some(50_000),
})
.collect(),
gpm_metrics: None,
detail,
}
}
#[test]
fn renders_header_with_gpu_columns() {
let gpus: Vec<_> = (0..4).map(|i| mk_gpu(i, Some(0), 7)).collect();
let model = TopologyModel::from_host("h", &gpus);
let out = render_matrix(&model, 120);
assert!(out.contains("GPU0"), "{out}");
assert!(out.contains("GPU3"), "{out}");
assert!(out.contains("NUMA"), "{out}");
assert!(!out.contains("CPU Affinity"), "{out}");
}
#[test]
fn full_mesh_classifies_as_nv5_with_50gbs_bandwidth() {
let gpus: Vec<_> = (0..8).map(|i| mk_gpu(i, Some(i as i32 / 4), 7)).collect();
let model = TopologyModel::from_host("h", &gpus);
let out = render_matrix(&model, 140);
assert!(out.contains("NV5"), "{out}");
}
#[test]
fn self_cell_is_x_label() {
let gpus: Vec<_> = (0..2).map(|i| mk_gpu(i, Some(0), 1)).collect();
let model = TopologyModel::from_host("h", &gpus);
let out = render_matrix(&model, 120);
let lines: Vec<&str> = out.lines().collect();
assert!(lines.iter().any(|l| l.contains("X")), "{out}");
}
#[test]
fn falls_back_to_summary_under_80_col() {
let gpus: Vec<_> = (0..16).map(|i| mk_gpu(i, Some(0), 4)).collect();
let model = TopologyModel::from_host("h", &gpus);
let out = render_matrix(&model, 60);
assert!(out.contains("summary only"), "{out}");
}
#[test]
fn cross_numa_renders_sys_for_non_nvlink() {
let gpus = vec![mk_gpu(0, Some(0), 0), mk_gpu(1, Some(1), 0)];
let model = TopologyModel::from_host("h", &gpus);
let out = render_matrix(&model, 120);
assert!(out.contains("SYS"), "{out}");
}
#[test]
fn same_numa_renders_node_for_non_nvlink() {
let gpus = vec![mk_gpu(0, Some(0), 0), mk_gpu(1, Some(0), 0)];
let model = TopologyModel::from_host("h", &gpus);
let out = render_matrix(&model, 120);
assert!(out.contains("NODE"), "{out}");
}
#[test]
fn legend_contains_vocabulary() {
let gpus = vec![mk_gpu(0, Some(0), 0), mk_gpu(1, Some(0), 0)];
let model = TopologyModel::from_host("h", &gpus);
let out = render_matrix(&model, 120);
for term in ["X=self", "PXB", "NODE", "SYS"] {
assert!(out.contains(term), "legend missing {term}: {out}");
}
}
#[test]
fn empty_model_renders_placeholder() {
let model = TopologyModel::default();
let out = render_matrix(&model, 120);
assert!(out.contains("no GPUs"), "{out}");
}
#[test]
fn cell_width_picker_honours_available_space() {
let cw = pick_cell_width(8, 80);
assert!(cw >= MIN_CELL || cw == 0);
}
}