use crate::mock::metrics::GpuMetrics;
pub const TOPOLOGY_ENV_VAR: &str = "ALL_SMI_MOCK_TOPOLOGY";
pub fn is_topology_enabled() -> bool {
std::env::var(TOPOLOGY_ENV_VAR)
.map(|v| !v.is_empty() && v != "0")
.unwrap_or(false)
}
pub const LINKS_PER_GPU: u32 = 8;
pub const LINK_BANDWIDTH_MB_S: u32 = 50_000;
pub fn maybe_add_topology_template(
template: &mut String,
gpu_name: &str,
instance_name: &str,
gpus: &[GpuMetrics],
) {
if !is_topology_enabled() {
return;
}
add_topology_template(template, gpu_name, instance_name, gpus);
}
pub fn add_topology_template(
template: &mut String,
gpu_name: &str,
instance_name: &str,
gpus: &[GpuMetrics],
) {
if gpus.is_empty() {
return;
}
template.push_str("# HELP all_smi_gpu_numa_node_id NUMA node the GPU is attached to\n");
template.push_str("# TYPE all_smi_gpu_numa_node_id gauge\n");
let mid = gpus.len() / 2;
for (i, gpu) in gpus.iter().enumerate() {
let numa = if i < mid { 0 } else { 1 };
let labels = format!(
"gpu=\"{gpu_name}\", instance=\"{instance_name}\", gpu_uuid=\"{}\", gpu_index=\"{i}\"",
gpu.uuid
);
template.push_str(&format!("all_smi_gpu_numa_node_id{{{labels}}} {numa}\n"));
}
template.push_str(
"# HELP all_smi_nvlink_remote_device_type NvLink remote endpoint classification per \
active link. Value is always 1; classification is carried in the `remote_type` label \
(gpu / switch / ibmnpu / unknown). Optional `bandwidth_mb_s` label carries the \
per-link bandwidth hint used by the topology tab's NVn classifier.\n",
);
template.push_str("# TYPE all_smi_nvlink_remote_device_type gauge\n");
for (i, gpu) in gpus.iter().enumerate() {
for link in 0..LINKS_PER_GPU {
let remote_type = if link == LINKS_PER_GPU - 1 {
"switch"
} else {
"gpu"
};
let labels = format!(
"gpu=\"{gpu_name}\", instance=\"{instance_name}\", gpu_uuid=\"{}\", \
gpu_index=\"{i}\", link_index=\"{link}\", remote_type=\"{remote_type}\", \
bandwidth_mb_s=\"{LINK_BANDWIDTH_MB_S}\"",
gpu.uuid
);
template.push_str(&format!(
"all_smi_nvlink_remote_device_type{{{labels}}} 1\n"
));
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::mock::metrics::GpuMetrics;
fn mk_mock_gpus(count: usize) -> Vec<GpuMetrics> {
(0..count)
.map(|i| GpuMetrics {
uuid: format!("GPU-mock-{i}"),
utilization: 10.0,
memory_used_bytes: 1024 * 1024 * 1024,
memory_total_bytes: 80 * 1024 * 1024 * 1024,
temperature_celsius: 50,
power_consumption_watts: 300.0,
frequency_mhz: 1800,
ane_utilization_watts: 0.0,
thermal_pressure_level: None,
})
.collect()
}
#[test]
fn add_template_emits_numa_and_nvlink_rows() {
let mut out = String::new();
add_topology_template(&mut out, "NVIDIA H100", "mock-0", &mk_mock_gpus(8));
assert!(
out.contains("all_smi_gpu_numa_node_id"),
"missing NUMA metric: {out}",
);
let link_rows = out
.lines()
.filter(|l| l.starts_with("all_smi_nvlink_remote_device_type{"))
.count();
assert_eq!(link_rows, 64, "{out}");
assert!(out.contains("bandwidth_mb_s=\"50000\""), "{out}");
assert!(out.contains("remote_type=\"switch\""), "{out}");
}
#[test]
fn two_numa_split() {
let mut out = String::new();
add_topology_template(&mut out, "NVIDIA H100", "mock-0", &mk_mock_gpus(8));
let numa_lines: Vec<&str> = out
.lines()
.filter(|l| l.starts_with("all_smi_gpu_numa_node_id{"))
.collect();
let zeros = numa_lines.iter().filter(|l| l.ends_with(" 0")).count();
let ones = numa_lines.iter().filter(|l| l.ends_with(" 1")).count();
assert_eq!(zeros, 4, "expected 4 GPUs in NUMA 0: {out}");
assert_eq!(ones, 4, "expected 4 GPUs in NUMA 1: {out}");
}
#[test]
fn empty_gpu_list_is_no_op() {
let mut out = String::new();
add_topology_template(&mut out, "NVIDIA H100", "mock-0", &[]);
assert!(out.is_empty(), "{out}");
}
#[test]
fn every_row_carries_the_instance_label() {
let mut out = String::new();
add_topology_template(&mut out, "NVIDIA H100", "dgx-7", &mk_mock_gpus(2));
for line in out.lines().filter(|l| l.starts_with("all_smi_")) {
assert!(line.contains("instance=\"dgx-7\""), "{line}");
}
}
}