use std::collections::BTreeMap;
use std::path::PathBuf;
use std::sync::OnceLock;
use super::axis_curve::LeafAxisCurve;
use super::perf_interp::LeafValue;
use super::{SourceResolver, kernel_source_ok};
use crate::common::error::AicError;
use crate::config::{PerfDbSources, PerfSource};
use crate::perf_database::parquet_loader::PerfReader;
pub struct MhcTable {
data_root: PathBuf,
mhc_sources: Vec<PerfSource>,
module: OnceLock<Result<MhcGrids, AicError>>,
}
struct MhcGrids {
by_keys: BTreeMap<MhcKey, LeafAxisCurve>,
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
struct MhcKey {
op_name: String,
hc_mult: u32,
hidden_size: u32,
}
impl MhcTable {
pub fn new(data_root: PathBuf) -> Self {
Self::with_sources(data_root, &SourceResolver::fixed(PerfDbSources::default()))
.expect("fixed-map resolution is infallible")
}
pub fn with_sources(data_root: PathBuf, resolver: &SourceResolver) -> Result<Self, AicError> {
let mhc_sources = resolver.sources_for("mhc_module_perf.parquet", &data_root)?;
Ok(Self {
data_root,
mhc_sources,
module: OnceLock::new(),
})
}
pub fn query_module(
&self,
op: &str,
num_tokens: u32,
hc_mult: u32,
hidden_size: u32,
sol: &dyn Fn(&str, f64) -> f64,
) -> Result<LeafValue, AicError> {
let grids = self.load()?;
if op == "both" {
let pre = self.query_single("pre", num_tokens, hc_mult, hidden_size, sol, grids)?;
let post = self.query_single("post", num_tokens, hc_mult, hidden_size, sol, grids)?;
return Ok(LeafValue {
latency: pre.latency + post.latency,
power: 0.0,
energy: pre.energy + post.energy,
});
}
self.query_single(op, num_tokens, hc_mult, hidden_size, sol, grids)
}
fn query_single(
&self,
op: &str,
num_tokens: u32,
hc_mult: u32,
hidden_size: u32,
sol: &dyn Fn(&str, f64) -> f64,
grids: &MhcGrids,
) -> Result<LeafValue, AicError> {
let key = MhcKey {
op_name: op.to_string(),
hc_mult,
hidden_size,
};
let by_tokens = grids.by_keys.get(&key).ok_or_else(|| {
AicError::PerfDatabase(format!(
"MHC module data missing for {key:?} at {}",
self.data_root.display()
))
})?;
by_tokens.query(num_tokens as f64, &|t| sol(op, t))
}
pub fn module_points(
&self,
op: &str,
hc_mult: u32,
hidden_size: u32,
) -> Result<Vec<(Vec<f64>, f64)>, AicError> {
let grids = self.load()?;
let key = MhcKey {
op_name: op.to_string(),
hc_mult,
hidden_size,
};
let by_tokens = grids.by_keys.get(&key).ok_or_else(|| {
AicError::PerfDatabase(format!(
"MHC module data missing for {key:?} at {}",
self.data_root.display()
))
})?;
if by_tokens.is_empty() {
return Err(AicError::PerfDatabase(format!(
"MHC module data empty for {key:?} at {}",
self.data_root.display()
)));
}
Ok(by_tokens
.iter()
.map(|(tokens, leaf)| (vec![f64::from(tokens)], leaf.latency))
.collect())
}
fn load(&self) -> Result<&MhcGrids, AicError> {
let cell = self
.module
.get_or_init(|| load_mhc_parquet(&self.mhc_sources));
cell.as_ref().map_err(clone_err)
}
}
fn load_mhc_parquet(sources: &[PerfSource]) -> Result<MhcGrids, AicError> {
let mut by_keys: BTreeMap<MhcKey, BTreeMap<u32, LeafValue>> = BTreeMap::new();
let mut any_source = false;
for source in sources {
let path = source.path();
if !path.exists() {
continue;
}
any_source = true;
let reader = PerfReader::open(path)?;
let op_name_col = reader.col("op_name")?;
let num_tokens_col = reader.col("num_tokens")?;
let hc_mult_col = reader.col("hc_mult")?;
let hidden_size_col = reader.col("hidden_size")?;
let latency_col = reader.col("latency")?;
let power_col = reader.col_optional("power");
let ks_col = reader.col_optional("kernel_source");
for row in reader.rows()? {
let row = row?;
if !kernel_source_ok(source.kernel_sources(), ks_col, &row)? {
continue;
}
let key = MhcKey {
op_name: row.str_owned(op_name_col)?,
hc_mult: row.u32(hc_mult_col)?,
hidden_size: row.u32(hidden_size_col)?,
};
let latency = row.f64(latency_col)?;
let power = row.f64_optional(power_col)?.unwrap_or(0.0);
by_keys
.entry(key)
.or_default()
.entry(row.u32(num_tokens_col)?)
.or_insert(LeafValue::with_power(latency, power));
}
}
if !any_source || by_keys.is_empty() {
return Err(AicError::PerfDatabase(format!(
"no MHC module rows loaded from {} source(s) (first: {})",
sources.len(),
sources
.first()
.map(|s| s.path().display().to_string())
.unwrap_or_default()
)));
}
Ok(MhcGrids {
by_keys: by_keys
.into_iter()
.map(|(key, curve)| (key, LeafAxisCurve::from_map("num_tokens", curve)))
.collect(),
})
}
fn clone_err(err: &AicError) -> AicError {
AicError::PerfDatabase(err.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::Path;
use std::sync::Arc;
fn linear_sol(_op: &str, t: f64) -> f64 {
t
}
#[test]
fn mhc_query_regime_routing() {
let table = MhcTable::new(PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(
"../../python/aisimulate/src/aiconfigurator_core/systems/data/b200_sxm/sglang/0.5.14",
));
for &(op, nt) in &[("pre", 3u32), ("post", 3), ("both", 3), ("pre", 8)] {
let got = table
.query_module(op, nt, 4, 7168, &linear_sol)
.expect("query must succeed")
.latency;
assert!(got.is_finite() && got > 0.0, "op={op}, nt={nt}: got {got}");
}
}
#[test]
fn mhc_absent_data_errors_clearly() {
let empty = tempfile::tempdir().expect("tmpdir");
let table = MhcTable::new(empty.path().to_path_buf());
let err = table
.query_module("pre", 1024, 2, 4096, &linear_sol)
.unwrap_err();
match err {
AicError::Io { .. } | AicError::PerfDatabase(_) => {}
other => panic!("unexpected error: {other:?}"),
}
}
fn write_mhc_parquet(path: &Path, rows: &[(&str, &str, i64, i64, i64, f64)]) {
use parquet::data_type::{ByteArray, ByteArrayType, DoubleType, Int64Type};
use parquet::file::properties::WriterProperties;
use parquet::file::writer::SerializedFileWriter;
use parquet::schema::parser::parse_message_type;
let schema = "message schema {
REQUIRED BINARY architecture (UTF8);
REQUIRED BINARY op_name (UTF8);
REQUIRED INT64 num_tokens;
REQUIRED INT64 hc_mult;
REQUIRED INT64 hidden_size;
REQUIRED DOUBLE latency;
}";
let schema = Arc::new(parse_message_type(schema).expect("schema must parse"));
let file = std::fs::File::create(path).expect("create parquet");
let mut writer =
SerializedFileWriter::new(file, schema, Arc::new(WriterProperties::builder().build()))
.expect("writer");
let mut rg = writer.next_row_group().expect("row group");
for str_field in [0usize, 1] {
let values: Vec<ByteArray> = rows
.iter()
.map(|r| ByteArray::from(if str_field == 0 { r.0 } else { r.1 }))
.collect();
let mut col = rg.next_column().expect("next col").expect("str col");
col.typed::<ByteArrayType>()
.write_batch(&values, None, None)
.expect("write str");
col.close().expect("close col");
}
let int_cols: [Vec<i64>; 3] = [
rows.iter().map(|r| r.2).collect(),
rows.iter().map(|r| r.3).collect(),
rows.iter().map(|r| r.4).collect(),
];
for values in &int_cols {
let mut col = rg.next_column().expect("next col").expect("int col");
col.typed::<Int64Type>()
.write_batch(values, None, None)
.expect("write ints");
col.close().expect("close col");
}
let latencies: Vec<f64> = rows.iter().map(|r| r.5).collect();
let mut col = rg.next_column().expect("next col").expect("latency col");
col.typed::<DoubleType>()
.write_batch(&latencies, None, None)
.expect("write latency");
col.close().expect("close col");
rg.close().expect("close row group");
writer.close().expect("close writer");
}
#[test]
fn mhc_rows_differing_only_in_architecture_merge_first_wins() {
let tmp = tempfile::tempdir().expect("tmpdir");
write_mhc_parquet(
&tmp.path().join("mhc_module_perf.parquet"),
&[
("ArchA", "pre", 8, 4, 7168, 1.0),
("ArchB", "pre", 8, 4, 7168, 2.0), ("ArchA", "pre", 16, 4, 7168, 4.0), ],
);
let table = MhcTable::new(tmp.path().to_path_buf());
let got = table
.query_module("pre", 8, 4, 7168, &linear_sol)
.expect("query must succeed")
.latency;
assert_eq!(got, 1.0);
let mid = table
.query_module("pre", 12, 4, 7168, &linear_sol)
.expect("query must succeed")
.latency;
assert_eq!(mid, 2.5);
}
#[test]
fn mhc_beyond_range_hold_uses_threaded_sol() {
let tmp = tempfile::tempdir().expect("tmpdir");
write_mhc_parquet(
&tmp.path().join("mhc_module_perf.parquet"),
&[
("DeepseekV4ForCausalLM", "pre", 65536, 4, 7168, 1.0),
("DeepseekV4ForCausalLM", "pre", 131072, 4, 7168, 3.0),
],
);
let table = MhcTable::new(tmp.path().to_path_buf());
let quadratic = |_op: &str, t: f64| t * t;
let got = table
.query_module("pre", 262144, 4, 7168, &quadratic)
.expect("query must succeed")
.latency;
assert!(
(got - 12.0).abs() < 1e-12,
"hold must scale by the threaded sol ratio (expected 12.0, got {got})"
);
}
#[test]
fn mhc_energy_matches_python_oracle() {
use crate::perf_database::energy_test_fixtures::{Col, write_parquet};
let tmp = tempfile::tempdir().expect("tmpdir");
write_parquet(
&tmp.path().join("mhc_module_perf.parquet"),
&[
Col::Str("architecture", vec!["DeepseekV4ForCausalLM"; 4]),
Col::Str("op_name", vec!["pre", "pre", "post", "post"]),
Col::I64("num_tokens", vec![8, 16, 8, 16]),
Col::I64("hc_mult", vec![4, 4, 4, 4]),
Col::I64("hidden_size", vec![7168, 7168, 7168, 7168]),
Col::F64("latency", vec![1.0, 3.0, 0.5, 1.5]),
Col::F64("power", vec![100.0, 200.0, 50.0, 100.0]),
],
);
let table = MhcTable::new(tmp.path().to_path_buf());
let pre = table.query_module("pre", 12, 4, 7168, &linear_sol).unwrap();
assert!((pre.latency - 2.0).abs() < 1e-9, "latency {}", pre.latency);
assert!(
(pre.energy - 300.0).abs() < 1e-9 * 300.0,
"energy {}",
pre.energy
);
let both = table
.query_module("both", 12, 4, 7168, &linear_sol)
.unwrap();
assert!(
(both.latency - 3.0).abs() < 1e-9,
"latency {}",
both.latency
);
assert!(
(both.energy - 375.0).abs() < 1e-9 * 375.0,
"energy {}",
both.energy
);
}
}