use std::collections::BTreeMap;
use std::fs;
use std::path::{Path, PathBuf};
use serde::Deserialize;
use crate::common::enums::{ComputeDtype, QuantMapping};
use crate::common::error::AicError;
#[derive(Clone, Debug, Deserialize)]
pub struct SystemSpec {
pub data_dir: PathBuf,
pub gpu: GpuSpec,
pub node: NodeSpec,
#[serde(default)]
pub misc: MiscSpec,
}
#[derive(Clone, Debug, Deserialize)]
pub struct GpuSpec {
pub mem_bw: f64,
#[serde(default = "default_mem_bw_scaling")]
pub mem_bw_empirical_scaling_factor: f64,
#[serde(default)]
pub mem_empirical_constant_latency: f64,
#[serde(default)]
pub mem_capacity: Option<u64>,
#[serde(default)]
pub bfloat16_tc_flops: Option<f64>,
#[serde(default)]
pub int8_tc_flops: Option<f64>,
#[serde(default)]
pub fp8_tc_flops: Option<f64>,
#[serde(default)]
pub fp4_tc_flops: Option<f64>,
#[serde(default)]
pub power: Option<f64>,
#[serde(default)]
pub sm_version: Option<u32>,
}
pub(crate) fn quant_tc_flops(spec: &SystemSpec, mapping: QuantMapping) -> Result<f64, AicError> {
let Some(dtype) = mapping.compute_dtype else {
return Err(AicError::MissingSystemFlops(format!(
"quant mode '{}' is memory-only and has no compute FLOPS",
mapping.name
)));
};
let value = match dtype {
ComputeDtype::Bfloat16 => spec.gpu.bfloat16_tc_flops,
ComputeDtype::Int8 => spec.gpu.int8_tc_flops,
ComputeDtype::Fp8 => spec.gpu.fp8_tc_flops,
ComputeDtype::Fp4 => spec.gpu.fp4_tc_flops,
};
value.filter(|v| v.is_finite() && *v > 0.0).ok_or_else(|| {
AicError::MissingSystemFlops(format!(
"quant mode '{}' needs '{}', which this system's YAML does not define (or defines \
as a non-positive placeholder): either the platform does not support {:?} compute \
and the quant mode cannot be modeled on it, or the system YAML is missing the \
entry.",
mapping.name,
dtype.flops_key(),
dtype
))
})
}
#[derive(Clone, Debug, Deserialize)]
pub struct NodeSpec {
pub num_gpus_per_node: u32,
pub inter_node_bw: f64,
pub intra_node_bw: f64,
#[serde(default)]
pub pcie_bw: Option<f64>,
#[serde(default)]
pub p2p_latency: f64,
#[serde(default)]
pub num_gpus_per_rack: Option<u32>,
#[serde(default)]
pub inter_rack_bw: Option<f64>,
}
#[derive(Clone, Debug, Default, Deserialize)]
pub struct MiscSpec {
#[serde(default)]
pub nccl_mem: BTreeMap<u32, u64>,
#[serde(default)]
pub other_mem: Option<u64>,
#[serde(default)]
pub nccl_version: Option<String>,
#[serde(default)]
pub oneccl_version: Option<String>,
}
fn default_mem_bw_scaling() -> f64 {
1.0
}
impl SystemSpec {
pub fn load(path: &Path) -> Result<Self, AicError> {
let text = fs::read_to_string(path).map_err(|source| AicError::Io {
path: path.to_path_buf(),
source,
})?;
serde_yaml::from_str(&text).map_err(|source| AicError::Yaml {
path: path.to_path_buf(),
source,
})
}
pub fn get_p2p_bandwidth(&self, num_gpus: u32) -> f64 {
let node = &self.node;
if num_gpus <= node.num_gpus_per_node {
return node.intra_node_bw;
}
let per_rack = node.num_gpus_per_rack.unwrap_or(u32::MAX);
if num_gpus <= per_rack {
return node.inter_node_bw;
}
node.inter_rack_bw.unwrap_or(node.inter_node_bw)
}
}
#[cfg(test)]
mod tests {
use super::*;
const REPO_ROOT_HINT: &str = env!("CARGO_MANIFEST_DIR");
fn systems_root() -> PathBuf {
PathBuf::from(REPO_ROOT_HINT)
.join("../..")
.join("python/aisimulate/src/aiconfigurator_core/systems")
}
#[test]
fn parse_b200_sxm() {
let spec = SystemSpec::load(&systems_root().join("b200_sxm.yaml"))
.expect("b200_sxm.yaml parse must succeed");
assert_eq!(spec.data_dir, PathBuf::from("data/b200_sxm"));
assert_eq!(spec.gpu.mem_bw, 7_700_000_000_000.0);
assert_eq!(spec.gpu.mem_bw_empirical_scaling_factor, 0.8);
assert_eq!(spec.gpu.bfloat16_tc_flops, Some(2_250_000_000_000_000.0));
assert_eq!(spec.gpu.sm_version, Some(100));
assert_eq!(spec.node.num_gpus_per_node, 8);
assert!(spec.node.num_gpus_per_rack.is_none()); assert_eq!(spec.misc.nccl_mem.get(&8), Some(&411_041_792));
assert_eq!(spec.misc.nccl_version.as_deref(), Some("2.27.3"));
}
#[test]
fn parse_gb200_rack_tier() {
let spec = SystemSpec::load(&systems_root().join("gb200.yaml"))
.expect("gb200.yaml parse must succeed");
assert!(spec.node.num_gpus_per_rack.is_some());
assert!(spec.node.inter_rack_bw.is_some());
}
#[test]
fn p2p_bandwidth_three_tier_selection() {
let spec = SystemSpec {
data_dir: PathBuf::from("data/synthetic"),
gpu: GpuSpec {
mem_bw: 1.0,
mem_bw_empirical_scaling_factor: 1.0,
mem_empirical_constant_latency: 0.0,
mem_capacity: None,
bfloat16_tc_flops: None,
int8_tc_flops: None,
fp8_tc_flops: None,
fp4_tc_flops: None,
power: None,
sm_version: None,
},
node: NodeSpec {
num_gpus_per_node: 8,
intra_node_bw: 900.0,
inter_node_bw: 100.0,
pcie_bw: None,
p2p_latency: 0.0,
num_gpus_per_rack: Some(72),
inter_rack_bw: Some(10.0),
},
misc: MiscSpec::default(),
};
assert_eq!(spec.get_p2p_bandwidth(1), 900.0);
assert_eq!(spec.get_p2p_bandwidth(8), 900.0);
assert_eq!(spec.get_p2p_bandwidth(9), 100.0);
assert_eq!(spec.get_p2p_bandwidth(72), 100.0);
assert_eq!(spec.get_p2p_bandwidth(73), 10.0);
}
#[test]
fn p2p_bandwidth_falls_back_to_inter_node_when_inter_rack_unset() {
let spec = SystemSpec {
data_dir: PathBuf::from("data/synthetic"),
gpu: GpuSpec {
mem_bw: 1.0,
mem_bw_empirical_scaling_factor: 1.0,
mem_empirical_constant_latency: 0.0,
mem_capacity: None,
bfloat16_tc_flops: None,
int8_tc_flops: None,
fp8_tc_flops: None,
fp4_tc_flops: None,
power: None,
sm_version: None,
},
node: NodeSpec {
num_gpus_per_node: 8,
intra_node_bw: 900.0,
inter_node_bw: 100.0,
pcie_bw: None,
p2p_latency: 0.0,
num_gpus_per_rack: Some(72),
inter_rack_bw: None, },
misc: MiscSpec::default(),
};
assert_eq!(spec.get_p2p_bandwidth(100), 100.0);
}
}