use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use std::sync::OnceLock;
use super::gemm::quant_tc_flops;
use super::interpolation::Grid3;
use super::perf_interp::{self, LeafValue, Node, OpInterpConfig};
use super::{kernel_source_ok, SourceResolver};
use crate::common::enums::{FmhaQuantMode, KvCacheQuantMode};
use crate::common::error::AicError;
use crate::common::system_spec::SystemSpec;
use crate::config::{PerfDbSources, PerfSource};
use crate::operators::base::SolComponents;
use crate::perf_database::parquet_loader::{PerfReader, PerfRow};
pub const DEFAULT_LANE: &str = "default";
pub struct AttentionTable {
data_root: PathBuf,
system_spec: SystemSpec,
context_sources: Vec<PerfSource>,
generation_sources: Vec<PerfSource>,
encoder_sources: Vec<PerfSource>,
context: OnceLock<Result<ContextGrids, AicError>>,
context_lane_density: OnceLock<Vec<(String, u32, u32)>>,
generation: OnceLock<Result<GenerationGrids, AicError>>,
generation_lane_density: OnceLock<Vec<(String, u32, u32)>>,
encoder: OnceLock<Result<EncoderGrids, AicError>>,
}
struct ContextGrids {
by_lane: BTreeMap<String, BTreeMap<ContextKey, Node>>,
lanes_in_load_order: Vec<String>,
}
struct GenerationGrids {
by_lane: BTreeMap<String, BTreeMap<GenerationKey, Node>>,
lanes_in_load_order: Vec<String>,
}
struct EncoderGrids {
by_keys: BTreeMap<EncoderKey, Node>,
}
fn lane_slice<'a, K: Ord, V>(
by_lane: &'a BTreeMap<String, BTreeMap<K, V>>,
lanes_in_load_order: &[String],
lane_order: &[String],
key: &K,
) -> Option<&'a V> {
for (rank, lane) in lane_order.iter().enumerate() {
if let Some(v) = by_lane.get(lane).and_then(|slices| slices.get(key)) {
if rank > 0 {
log::debug!(
"attention lane donor substitution: {:?} missing this key, served by {lane:?} (position {rank} of {lane_order:?})",
lane_order[0]
);
}
return Some(v);
}
}
for lane in lanes_in_load_order {
if lane_order.iter().any(|tried| tried == lane) {
continue;
}
if let Some(v) = by_lane.get(lane).and_then(|slices| slices.get(key)) {
log::debug!(
"attention lane donor substitution: no lane in {lane_order:?} had this key, fell back to table lane {lane:?}"
);
return Some(v);
}
}
None
}
fn fallback_lanes<'a, K, V>(
by_lane: &'a BTreeMap<String, BTreeMap<K, V>>,
lanes_in_load_order: &'a [String],
lane_order: &[String],
) -> Vec<&'a str> {
lanes_in_load_order
.iter()
.filter(|lane| by_lane.contains_key(*lane))
.filter(|lane| !lane_order.iter().any(|tried| tried == *lane))
.map(String::as_str)
.collect()
}
fn lane_density<K>(slices: &BTreeMap<K, Node>) -> (u32, u32) {
let slice_count = slices.len() as u32;
let mut row_count = 0;
let mut prefix = Vec::new();
for node in slices.values() {
perf_interp::visit_leaves(node, &mut prefix, &mut |_, _| row_count += 1);
}
(slice_count, row_count)
}
fn row_lane(ks_col: Option<usize>, row: &PerfRow) -> Result<String, AicError> {
Ok(match row.str_optional(ks_col)? {
Some(ks) if !ks.is_empty() => ks.to_string(),
_ => DEFAULT_LANE.to_string(),
})
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
struct ContextKey {
fmha_quant: String,
kv_quant: String,
n_kv_lookup: u32,
head_size: u32,
window_size: u32,
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
struct GenerationKey {
kv_quant: String,
n_kv_lookup: u32,
head_size: u32,
window_size: u32,
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
struct EncoderKey {
fmha_quant: String,
head_size: u32,
}
impl AttentionTable {
pub fn new(data_root: PathBuf, system_spec: SystemSpec) -> Self {
Self::with_sources(
data_root,
system_spec,
&SourceResolver::fixed(PerfDbSources::default()),
)
.expect("fixed-map resolution is infallible")
}
pub fn with_sources(
data_root: PathBuf,
system_spec: SystemSpec,
resolver: &SourceResolver,
) -> Result<Self, AicError> {
let context_sources = resolver.sources_for("context_attention_perf.parquet", &data_root)?;
let generation_sources =
resolver.sources_for("generation_attention_perf.parquet", &data_root)?;
let encoder_sources = resolver.sources_for("encoder_attention_perf.parquet", &data_root)?;
Ok(Self {
data_root,
system_spec,
context_sources,
generation_sources,
encoder_sources,
context: OnceLock::new(),
context_lane_density: OnceLock::new(),
generation: OnceLock::new(),
generation_lane_density: OnceLock::new(),
encoder: OnceLock::new(),
})
}
#[allow(clippy::too_many_arguments)]
pub fn query_context(
&self,
lane_order: &[String],
b: u32,
full_seq_tokens: u32,
n: u32,
n_kv: u32,
head_size: u32,
window_size: u32,
kv_quant: KvCacheQuantMode,
fmha_quant: FmhaQuantMode,
) -> Result<LeafValue, AicError> {
let attn_flops = quant_tc_flops(&self.system_spec, fmha_quant.mapping())?;
let grids = self.load_context()?;
let key = ContextKey {
fmha_quant: fmha_quant.name().to_string(),
kv_quant: kv_quant.name().to_string(),
n_kv_lookup: normalize_kv(n, n_kv),
head_size,
window_size,
};
let node = lane_slice(&grids.by_lane, &grids.lanes_in_load_order, lane_order, &key)
.ok_or_else(|| {
missing_key(
&self.data_root,
lane_order,
&grids.by_lane,
&grids.lanes_in_load_order,
&key,
)
})?;
let spec = &self.system_spec;
let n_kv_lookup = key.n_kv_lookup;
let sol = move |c: &[f64]| {
context_attention_sol_ms(
spec,
n_kv_lookup,
head_size,
window_size,
kv_quant,
c[0],
c[1],
c[2],
attn_flops,
)
};
let cfg = OpInterpConfig::grid_sqrt_axis(&["num_heads", "seq_len", "batch"], 1, &sol);
perf_interp::query_value(&cfg, node, &[n as f64, full_seq_tokens as f64, b as f64])
}
#[allow(clippy::too_many_arguments)]
pub fn query_generation(
&self,
lane_order: &[String],
b: u32,
kv_seq_tokens: u32,
n: u32,
n_kv: u32,
head_size: u32,
window_size: u32,
kv_quant: KvCacheQuantMode,
) -> Result<LeafValue, AicError> {
let attn_flops = generation_attn_flops(&self.system_spec, kv_quant)?;
let grids = self.load_generation()?;
let key = GenerationKey {
kv_quant: kv_quant.name().to_string(),
n_kv_lookup: normalize_kv(n, n_kv),
head_size,
window_size,
};
let node = lane_slice(&grids.by_lane, &grids.lanes_in_load_order, lane_order, &key)
.ok_or_else(|| {
missing_gen_key(
&self.data_root,
lane_order,
&grids.by_lane,
&grids.lanes_in_load_order,
&key,
)
})?;
let spec = &self.system_spec;
let n_kv_lookup = key.n_kv_lookup;
let sol = move |c: &[f64]| {
generation_attention_sol_ms(
spec,
n_kv_lookup,
head_size,
window_size,
kv_quant,
c[0],
c[1],
c[2],
attn_flops,
)
};
let cfg = OpInterpConfig::grid(&["num_heads", "batch", "seq_len"], &sol);
let s = kv_seq_tokens;
let s_min = ((s as f64 * 0.9) as u32).max(1);
let s_max = ((s as f64 * 1.1) as u32).max(s_min);
const SAMPLE_CNT: u32 = 5;
let mut latency_sum = 0.0_f64;
let mut energy_sum = 0.0_f64;
for i in 0..SAMPLE_CNT {
let s_i = s_min
+ ((u64::from(s_max - s_min) * u64::from(i)) / u64::from(SAMPLE_CNT - 1)) as u32;
let sample = perf_interp::query_value(&cfg, node, &[n as f64, b as f64, s_i as f64])?;
latency_sum += sample.latency;
energy_sum += sample.energy;
}
Ok(LeafValue {
latency: latency_sum / SAMPLE_CNT as f64,
power: 0.0, energy: energy_sum / SAMPLE_CNT as f64,
})
}
pub fn query_encoder(
&self,
b: u32,
s: u32,
n: u32,
head_size: u32,
fmha_quant: FmhaQuantMode,
) -> Result<LeafValue, AicError> {
let attn_flops = quant_tc_flops(&self.system_spec, fmha_quant.mapping())?;
let grids = self.load_encoder()?;
let key = EncoderKey {
fmha_quant: fmha_quant.name().to_string(),
head_size,
};
let node = grids
.by_keys
.get(&key)
.ok_or_else(|| missing_encoder_key(&self.data_root, &key))?;
let spec = &self.system_spec;
let sol = move |c: &[f64]| {
encoder_attention_sol_ms(spec, head_size, c[0], c[1], c[2], attn_flops)
};
let cfg = OpInterpConfig::grid_sqrt_axis(&["num_heads", "seq_len", "batch"], 1, &sol);
perf_interp::query_value(&cfg, node, &[n as f64, s as f64, b as f64])
}
pub fn context_points(
&self,
lane_order: &[String],
fmha_quant: FmhaQuantMode,
kv_quant: KvCacheQuantMode,
n_kv_lookup: u32,
head_size: u32,
window_size: u32,
) -> Result<Vec<(Vec<f64>, f64)>, AicError> {
let grids = self.load_context()?;
let key = ContextKey {
fmha_quant: fmha_quant.name().to_string(),
kv_quant: kv_quant.name().to_string(),
n_kv_lookup,
head_size,
window_size,
};
let node = lane_slice(&grids.by_lane, &grids.lanes_in_load_order, lane_order, &key)
.ok_or_else(|| {
missing_key(
&self.data_root,
lane_order,
&grids.by_lane,
&grids.lanes_in_load_order,
&key,
)
})?;
let points = perf_interp::node_points(node);
if points.is_empty() {
return Err(missing_key(
&self.data_root,
lane_order,
&grids.by_lane,
&grids.lanes_in_load_order,
&key,
));
}
Ok(points)
}
pub fn context_has_slice(
&self,
lane: &str,
fmha_quant: FmhaQuantMode,
kv_quant: KvCacheQuantMode,
n_kv_lookup: u32,
head_size: u32,
window_size: u32,
) -> Result<bool, AicError> {
let grids = self.load_context()?;
let key = ContextKey {
fmha_quant: fmha_quant.name().to_string(),
kv_quant: kv_quant.name().to_string(),
n_kv_lookup,
head_size,
window_size,
};
Ok(grids
.by_lane
.get(lane)
.and_then(|slices| slices.get(&key))
.is_some())
}
pub fn context_head_sizes(
&self,
lane: &str,
fmha_quant: FmhaQuantMode,
kv_quant: KvCacheQuantMode,
n_kv_lookup: u32,
) -> Result<Vec<u32>, AicError> {
let grids = self.load_context()?;
let fmha = fmha_quant.name();
let kv = kv_quant.name();
let mut sizes: Vec<u32> = Vec::new();
for key in grids.by_lane.get(lane).into_iter().flat_map(|s| s.keys()) {
if key.fmha_quant == fmha
&& key.kv_quant == kv
&& key.n_kv_lookup == n_kv_lookup
&& !sizes.contains(&key.head_size)
{
sizes.push(key.head_size);
}
}
if sizes.is_empty() {
return Err(AicError::PerfDatabase(format!(
"context attention data missing for lane={lane}, fmha={fmha}, kv={kv}, \
n_kv={n_kv_lookup} at {}",
self.data_root.display()
)));
}
Ok(sizes)
}
pub fn context_lanes(&self) -> Result<Vec<(String, u32, u32)>, AicError> {
let grids = self.load_context()?;
Ok(self
.context_lane_density
.get_or_init(|| {
grids
.by_lane
.iter()
.map(|(lane, slices)| {
let (slice_count, row_count) = lane_density(slices);
(lane.clone(), slice_count, row_count)
})
.collect()
})
.clone())
}
pub fn generation_points(
&self,
lane_order: &[String],
kv_quant: KvCacheQuantMode,
n_kv_lookup: u32,
head_size: u32,
window_size: u32,
) -> Result<Vec<(Vec<f64>, f64)>, AicError> {
let grids = self.load_generation()?;
let key = GenerationKey {
kv_quant: kv_quant.name().to_string(),
n_kv_lookup,
head_size,
window_size,
};
let node = lane_slice(&grids.by_lane, &grids.lanes_in_load_order, lane_order, &key)
.ok_or_else(|| {
missing_gen_key(
&self.data_root,
lane_order,
&grids.by_lane,
&grids.lanes_in_load_order,
&key,
)
})?;
let points = perf_interp::node_points(node);
if points.is_empty() {
return Err(missing_gen_key(
&self.data_root,
lane_order,
&grids.by_lane,
&grids.lanes_in_load_order,
&key,
));
}
Ok(points)
}
pub fn generation_has_slice(
&self,
lane: &str,
kv_quant: KvCacheQuantMode,
n_kv_lookup: u32,
head_size: u32,
window_size: u32,
) -> Result<bool, AicError> {
let grids = self.load_generation()?;
let key = GenerationKey {
kv_quant: kv_quant.name().to_string(),
n_kv_lookup,
head_size,
window_size,
};
Ok(grids
.by_lane
.get(lane)
.and_then(|slices| slices.get(&key))
.is_some())
}
pub fn generation_head_sizes(
&self,
lane: &str,
kv_quant: KvCacheQuantMode,
n_kv_lookup: u32,
) -> Result<Vec<u32>, AicError> {
let grids = self.load_generation()?;
let kv = kv_quant.name();
let mut sizes: Vec<u32> = Vec::new();
for key in grids.by_lane.get(lane).into_iter().flat_map(|s| s.keys()) {
if key.kv_quant == kv
&& key.n_kv_lookup == n_kv_lookup
&& !sizes.contains(&key.head_size)
{
sizes.push(key.head_size);
}
}
if sizes.is_empty() {
return Err(AicError::PerfDatabase(format!(
"generation attention data missing for lane={lane}, kv={kv}, \
n_kv={n_kv_lookup} at {}",
self.data_root.display()
)));
}
Ok(sizes)
}
pub fn generation_lanes(&self) -> Result<Vec<(String, u32, u32)>, AicError> {
let grids = self.load_generation()?;
Ok(self
.generation_lane_density
.get_or_init(|| {
grids
.by_lane
.iter()
.map(|(lane, slices)| {
let (slice_count, row_count) = lane_density(slices);
(lane.clone(), slice_count, row_count)
})
.collect()
})
.clone())
}
pub fn encoder_points(
&self,
fmha_quant: FmhaQuantMode,
head_size: u32,
) -> Result<Vec<(Vec<f64>, f64)>, AicError> {
let grids = self.load_encoder()?;
let key = EncoderKey {
fmha_quant: fmha_quant.name().to_string(),
head_size,
};
let node = grids
.by_keys
.get(&key)
.ok_or_else(|| missing_encoder_key(&self.data_root, &key))?;
let points = perf_interp::node_points(node);
if points.is_empty() {
return Err(missing_encoder_key(&self.data_root, &key));
}
Ok(points)
}
fn load_context(&self) -> Result<&ContextGrids, AicError> {
let cell = self.context.get_or_init(|| {
let (raw, lanes_in_load_order) = load_context_parquet(&self.context_sources)?;
Ok(ContextGrids {
by_lane: raw
.into_iter()
.map(|(lane, slices)| {
(
lane,
slices
.into_iter()
.map(|(k, g)| (k, grid3_to_node(&g)))
.collect(),
)
})
.collect(),
lanes_in_load_order,
})
});
cell.as_ref().map_err(clone_err)
}
fn load_generation(&self) -> Result<&GenerationGrids, AicError> {
let cell = self.generation.get_or_init(|| {
let (mut raw, lanes_in_load_order) = load_generation_parquet(&self.generation_sources)?;
clamp_generation_attention_grids_to_sol(&self.system_spec, &mut raw);
Ok(GenerationGrids {
by_lane: raw
.into_iter()
.map(|(lane, slices)| {
(
lane,
slices
.into_iter()
.map(|(k, g)| (k, grid3_to_node(&g)))
.collect(),
)
})
.collect(),
lanes_in_load_order,
})
});
cell.as_ref().map_err(clone_err)
}
fn load_encoder(&self) -> Result<&EncoderGrids, AicError> {
let cell = self.encoder.get_or_init(|| {
let raw = load_encoder_parquet(&self.encoder_sources)?;
Ok(EncoderGrids {
by_keys: raw
.into_iter()
.map(|(k, g)| (k, grid3_to_node(&g)))
.collect(),
})
});
cell.as_ref().map_err(clone_err)
}
}
fn normalize_kv(n: u32, n_kv: u32) -> u32 {
if n_kv == n {
0
} else {
n_kv
}
}
fn grid3_to_node(grid: &Grid3<LeafValue>) -> Node {
let mut node = Node::branch();
for (&x, by_y) in grid {
for (&y, by_z) in by_y {
for (&z, &leaf) in by_z {
node.insert_value(&[x, y, z], leaf);
}
}
}
node
}
fn load_context_parquet(
sources: &[PerfSource],
) -> Result<
(
BTreeMap<String, BTreeMap<ContextKey, Grid3<LeafValue>>>,
Vec<String>,
),
AicError,
> {
let mut by_lane: BTreeMap<String, BTreeMap<ContextKey, Grid3<LeafValue>>> = BTreeMap::new();
let mut lanes_in_load_order = Vec::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 batch_size_col = reader.col("batch_size")?;
let isl_col = reader.col("isl")?;
let num_heads_col = reader.col("num_heads")?;
let num_kv_col = reader.col("num_key_value_heads")?;
let head_dim_col = reader.col("head_dim")?;
let attn_dtype_col = reader.col("attn_dtype")?;
let kv_cache_dtype_col = reader.col("kv_cache_dtype")?;
let latency_col = reader.col("latency")?;
let power_col = reader.col_optional("power");
let window_size_col = reader.col_optional("window_size");
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 num_heads = row.u32(num_heads_col)?;
let num_kv = row.u32(num_kv_col)?;
let key = ContextKey {
fmha_quant: row.str_owned(attn_dtype_col)?,
kv_quant: row.str_owned(kv_cache_dtype_col)?,
n_kv_lookup: normalize_kv(num_heads, num_kv),
head_size: row.u32(head_dim_col)?,
window_size: row.u32_optional(window_size_col)?.unwrap_or(0),
};
let latency = row.f64(latency_col)?;
let power = row.f64_optional(power_col)?.unwrap_or(0.0);
let lane = row_lane(ks_col, &row)?;
if !lanes_in_load_order.contains(&lane) {
lanes_in_load_order.push(lane.clone());
}
by_lane
.entry(lane)
.or_default()
.entry(key)
.or_default()
.entry(num_heads)
.or_default()
.entry(row.u32(isl_col)?)
.or_default()
.entry(row.u32(batch_size_col)?)
.or_insert(LeafValue::with_power(latency, power));
}
}
if !any_source || by_lane.is_empty() {
return Err(AicError::PerfDatabase(format!(
"no context-attention rows loaded from {} source(s) (first: {})",
sources.len(),
sources
.first()
.map(|s| s.path().display().to_string())
.unwrap_or_default()
)));
}
Ok((by_lane, lanes_in_load_order))
}
fn load_generation_parquet(
sources: &[PerfSource],
) -> Result<
(
BTreeMap<String, BTreeMap<GenerationKey, Grid3<LeafValue>>>,
Vec<String>,
),
AicError,
> {
let mut by_lane: BTreeMap<String, BTreeMap<GenerationKey, Grid3<LeafValue>>> = BTreeMap::new();
let mut lanes_in_load_order = Vec::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 batch_size_col = reader.col("batch_size")?;
let isl_col = reader.col("isl")?;
let num_heads_col = reader.col("num_heads")?;
let num_kv_col = reader.col("num_key_value_heads")?;
let head_dim_col = reader.col("head_dim")?;
let kv_cache_dtype_col = reader.col("kv_cache_dtype")?;
let step_col = reader.col("step")?;
let latency_col = reader.col("latency")?;
let power_col = reader.col_optional("power");
let window_size_col = reader.col_optional("window_size");
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 num_heads = row.u32(num_heads_col)?;
let num_kv = row.u32(num_kv_col)?;
let key = GenerationKey {
kv_quant: row.str_owned(kv_cache_dtype_col)?,
n_kv_lookup: normalize_kv(num_heads, num_kv),
head_size: row.u32(head_dim_col)?,
window_size: row.u32_optional(window_size_col)?.unwrap_or(0),
};
let sequence_tokens = row.u32(isl_col)? + row.u32(step_col)?;
let latency = row.f64(latency_col)?;
let power = row.f64_optional(power_col)?.unwrap_or(0.0);
let lane = row_lane(ks_col, &row)?;
if !lanes_in_load_order.contains(&lane) {
lanes_in_load_order.push(lane.clone());
}
by_lane
.entry(lane)
.or_default()
.entry(key)
.or_default()
.entry(num_heads)
.or_default()
.entry(row.u32(batch_size_col)?)
.or_default()
.entry(sequence_tokens)
.or_insert(LeafValue::with_power(latency, power));
}
}
if !any_source || by_lane.is_empty() {
return Err(AicError::PerfDatabase(format!(
"no generation-attention rows loaded from {} source(s) (first: {})",
sources.len(),
sources
.first()
.map(|s| s.path().display().to_string())
.unwrap_or_default()
)));
}
Ok((by_lane, lanes_in_load_order))
}
#[allow(clippy::too_many_arguments)]
#[allow(clippy::too_many_arguments)]
pub(crate) fn context_attention_sol(
spec: &SystemSpec,
n_kv_lookup: u32,
head_size: u32,
window_size: u32,
kv_quant: KvCacheQuantMode,
n: f64,
s: f64,
b: f64,
attn_flops: f64,
) -> SolComponents {
let h = head_size as f64;
let w = window_size as f64;
let n_kv = if n_kv_lookup == 0 {
n
} else {
n_kv_lookup as f64
};
let ops = if window_size > 0 && s > w {
2.0 * b * s * w * n * h * 2.0
} else {
2.0 * b * (s * s) * n * h * 2.0 / 2.0
};
let mem_bytes =
2.0 * b * (n * s * h + n * s * h) + kv_quant.mapping().memory * b * (2.0 * n_kv * s * h);
let sol_math = ops / attn_flops * 1000.0;
let sol_mem = mem_bytes / spec.gpu.mem_bw * 1000.0;
SolComponents::new(sol_math, sol_mem)
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn context_attention_sol_ms(
spec: &SystemSpec,
n_kv_lookup: u32,
head_size: u32,
window_size: u32,
kv_quant: KvCacheQuantMode,
n: f64,
s: f64,
b: f64,
attn_flops: f64,
) -> f64 {
context_attention_sol(
spec,
n_kv_lookup,
head_size,
window_size,
kv_quant,
n,
s,
b,
attn_flops,
)
.time_ms()
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn context_attention_sol_with_prefix(
spec: &SystemSpec,
b: f64,
s: f64,
prefix: f64,
n: f64,
n_kv: f64,
head_size: u32,
window_size: u32,
kv_quant: KvCacheQuantMode,
attn_flops: f64,
) -> SolComponents {
let h = head_size as f64;
let w = window_size as f64;
let full_s = s + prefix;
let ops = if window_size > 0 && full_s > w {
2.0 * b * (full_s - prefix) * w * n * h * 2.0
} else {
2.0 * b * (full_s * full_s - prefix * prefix) * n * h * 2.0 / 2.0
};
let mem_bytes = 2.0 * b * (n * (full_s - prefix) * h + n * (full_s - prefix) * h)
+ kv_quant.mapping().memory * b * (2.0 * n_kv * full_s * h);
let sol_math = ops / attn_flops * 1000.0;
let sol_mem = mem_bytes / spec.gpu.mem_bw * 1000.0;
SolComponents::new(sol_math, sol_mem)
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn context_attention_sol_with_prefix_ms(
spec: &SystemSpec,
b: f64,
s: f64,
prefix: f64,
n: f64,
n_kv: f64,
head_size: u32,
window_size: u32,
kv_quant: KvCacheQuantMode,
attn_flops: f64,
) -> f64 {
context_attention_sol_with_prefix(
spec,
b,
s,
prefix,
n,
n_kv,
head_size,
window_size,
kv_quant,
attn_flops,
)
.time_ms()
}
pub(crate) fn encoder_attention_sol(
spec: &SystemSpec,
head_size: u32,
n: f64,
s: f64,
b: f64,
attn_flops: f64,
) -> SolComponents {
let h = head_size as f64;
let ops = 2.0 * b * s * s * n * h * 2.0; let mem_bytes = 2.0 * b * (3.0 * n * s * h + n * s * h); let sol_math = ops / attn_flops * 1000.0;
let sol_mem = mem_bytes / spec.gpu.mem_bw * 1000.0;
SolComponents::new(sol_math, sol_mem)
}
pub(crate) fn encoder_attention_sol_ms(
spec: &SystemSpec,
head_size: u32,
n: f64,
s: f64,
b: f64,
attn_flops: f64,
) -> f64 {
encoder_attention_sol(spec, head_size, n, s, b, attn_flops).time_ms()
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn generation_attention_sol(
spec: &SystemSpec,
n_kv_lookup: u32,
head_size: u32,
window_size: u32,
kv_quant: KvCacheQuantMode,
n: f64,
b: f64,
s: f64,
attn_flops: f64,
) -> SolComponents {
let n_kv = if n_kv_lookup == 0 {
n
} else {
n_kv_lookup as f64
};
let kv_len = if window_size > 0 {
(s - 1.0).min(window_size as f64)
} else {
s - 1.0
};
let h = head_size as f64;
let kv_mem = kv_quant.mapping().memory;
let ops = 2.0 * b * n * h * 2.0 * kv_len;
let mem_bytes = b * (n * h * 2.0 + 2.0 * n_kv * kv_len * h * kv_mem + n * h * 2.0);
let sol_math = ops / attn_flops * 1000.0;
let sol_mem = mem_bytes / spec.gpu.mem_bw * 1000.0;
SolComponents::new(sol_math, sol_mem)
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn generation_attention_sol_ms(
spec: &SystemSpec,
n_kv_lookup: u32,
head_size: u32,
window_size: u32,
kv_quant: KvCacheQuantMode,
n: f64,
b: f64,
s: f64,
attn_flops: f64,
) -> f64 {
generation_attention_sol(
spec,
n_kv_lookup,
head_size,
window_size,
kv_quant,
n,
b,
s,
attn_flops,
)
.time_ms()
}
pub(crate) fn generation_attn_mode(spec: &SystemSpec, kv_quant: KvCacheQuantMode) -> FmhaQuantMode {
let has_fp8_mma = spec.gpu.sm_version.is_some_and(|sm| sm >= 89);
if kv_quant == KvCacheQuantMode::Fp8 && has_fp8_mma {
FmhaQuantMode::Fp8
} else {
FmhaQuantMode::Bfloat16
}
}
pub(crate) fn generation_attn_flops(
spec: &SystemSpec,
kv_quant: KvCacheQuantMode,
) -> Result<f64, AicError> {
quant_tc_flops(spec, generation_attn_mode(spec, kv_quant).mapping())
}
fn clamp_generation_attention_grids_to_sol(
spec: &SystemSpec,
grids: &mut BTreeMap<String, BTreeMap<GenerationKey, Grid3<LeafValue>>>,
) {
for (key, grid) in grids.values_mut().flat_map(|slices| slices.iter_mut()) {
let Some(kv_quant) = kv_cache_quant_by_name(&key.kv_quant) else {
continue;
};
let Ok(attn_flops) = generation_attn_flops(spec, kv_quant) else {
continue;
};
for (&n, by_b) in grid.iter_mut() {
for (&b, by_s) in by_b.iter_mut() {
for (&s, leaf) in by_s.iter_mut() {
let sol = generation_attention_sol_ms(
spec,
key.n_kv_lookup,
key.head_size,
key.window_size,
kv_quant,
n as f64,
b as f64,
s as f64,
attn_flops,
);
if sol > leaf.latency {
leaf.latency = sol;
}
}
}
}
}
}
fn kv_cache_quant_by_name(name: &str) -> Option<KvCacheQuantMode> {
use KvCacheQuantMode::*;
Some(match name {
"bfloat16" => Bfloat16,
"int8" => Int8,
"fp8" => Fp8,
_ => return None,
})
}
fn load_encoder_parquet(
sources: &[PerfSource],
) -> Result<BTreeMap<EncoderKey, Grid3<LeafValue>>, AicError> {
let mut by_keys: BTreeMap<EncoderKey, Grid3<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 batch_size_col = reader.col("batch_size")?;
let isl_col = reader.col("isl")?;
let num_heads_col = reader.col("num_heads")?;
let head_dim_col = reader.col("head_dim")?;
let attn_dtype_col = reader.col("attn_dtype")?;
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 = EncoderKey {
fmha_quant: row.str_owned(attn_dtype_col)?,
head_size: row.u32(head_dim_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_heads_col)?)
.or_default()
.entry(row.u32(isl_col)?)
.or_default()
.entry(row.u32(batch_size_col)?)
.or_insert(LeafValue::with_power(latency, power));
}
}
if !any_source || by_keys.is_empty() {
return Err(AicError::PerfDatabase(format!(
"no encoder-attention rows loaded from {} source(s) (first: {})",
sources.len(),
sources
.first()
.map(|s| s.path().display().to_string())
.unwrap_or_default()
)));
}
Ok(by_keys)
}
fn missing_key(
data_root: &Path,
lane_order: &[String],
by_lane: &BTreeMap<String, BTreeMap<ContextKey, Node>>,
lanes_in_load_order: &[String],
key: &ContextKey,
) -> AicError {
AicError::PerfDatabase(format!(
"context attention data missing for {key:?}: tried lane_order {lane_order:?}, then fell back through table lanes {:?} at {}",
fallback_lanes(by_lane, lanes_in_load_order, lane_order),
data_root.display()
))
}
fn missing_gen_key(
data_root: &Path,
lane_order: &[String],
by_lane: &BTreeMap<String, BTreeMap<GenerationKey, Node>>,
lanes_in_load_order: &[String],
key: &GenerationKey,
) -> AicError {
AicError::PerfDatabase(format!(
"generation attention data missing for {key:?}: tried lane_order {lane_order:?}, then fell back through table lanes {:?} at {}",
fallback_lanes(by_lane, lanes_in_load_order, lane_order),
data_root.display()
))
}
fn missing_encoder_key(data_root: &Path, key: &EncoderKey) -> AicError {
AicError::PerfDatabase(format!(
"encoder attention data missing for {key:?} at {}",
data_root.display()
))
}
fn clone_err(err: &AicError) -> AicError {
AicError::PerfDatabase(err.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
const REPO_ROOT_HINT: &str = env!("CARGO_MANIFEST_DIR");
fn b200_vllm_data_root() -> PathBuf {
PathBuf::from(REPO_ROOT_HINT)
.join("../..")
.join("src/aiconfigurator_core/systems/data/b200_sxm/vllm/0.24.0")
}
fn b200_sxm_spec() -> SystemSpec {
let systems_yaml = PathBuf::from(REPO_ROOT_HINT)
.join("../..")
.join("src/aiconfigurator_core/systems/b200_sxm.yaml");
SystemSpec::load(&systems_yaml).expect("b200_sxm.yaml must parse")
}
fn lanes(names: &[&str]) -> Vec<String> {
names.iter().map(|n| n.to_string()).collect()
}
fn vllm_context_lanes() -> Vec<String> {
lanes(&[
"vllm_flashinfer_trtllmprefill",
"vllm_flashinfer_trtllmdecode",
"vllm_triton_attn",
"fa3",
"fla",
"flashinfer",
"triton",
"trtllm_mha",
"default",
"vllm_flashinfer",
])
}
#[test]
fn context_attention_mha_normalizes_n_kv_to_zero() {
let table = AttentionTable::new(b200_vllm_data_root(), b200_sxm_spec());
let via_mha = table
.query_context(
&vllm_context_lanes(),
4,
16384,
64,
64,
128,
0,
KvCacheQuantMode::Fp8,
FmhaQuantMode::Bfloat16,
)
.expect("MHA lookup must normalize and find the row")
.latency;
let via_zero = table
.query_context(
&vllm_context_lanes(),
4,
16384,
64,
0,
128,
0,
KvCacheQuantMode::Fp8,
FmhaQuantMode::Bfloat16,
)
.expect("stored-lane lookup must succeed")
.latency;
assert!(
(via_mha - via_zero).abs() < 1e-12,
"n_kv=64 must normalize onto the stored n_kv=0 lane: {via_mha} vs {via_zero}"
);
assert!(via_mha.is_finite() && via_mha > 0.0);
}
#[test]
fn context_attention_missing_quant_combo_errors() {
let table = AttentionTable::new(b200_vllm_data_root(), b200_sxm_spec());
match table.query_context(
&vllm_context_lanes(),
1,
1024,
64,
1,
128,
0,
KvCacheQuantMode::Fp8,
FmhaQuantMode::Fp8,
) {
Err(AicError::PerfDatabase(_)) => {}
other => panic!("expected PerfDatabase error, got {other:?}"),
}
}
#[test]
fn encoder_attention_missing_head_size_errors() {
let table = AttentionTable::new(b200_vllm_data_root(), b200_sxm_spec());
match table.query_encoder(1, 1024, 16, 128, FmhaQuantMode::Bfloat16) {
Err(AicError::PerfDatabase(_)) => {}
other => panic!("expected PerfDatabase error, got {other:?}"),
}
}
#[test]
fn context_attention_lazy_loads_once() {
let table = AttentionTable::new(b200_vllm_data_root(), b200_sxm_spec());
let first = table
.query_context(
&vllm_context_lanes(),
8,
16384,
64,
1,
128,
0,
KvCacheQuantMode::Fp8,
FmhaQuantMode::Bfloat16,
)
.unwrap();
let second = table
.query_context(
&vllm_context_lanes(),
8,
16384,
64,
1,
128,
0,
KvCacheQuantMode::Fp8,
FmhaQuantMode::Bfloat16,
)
.unwrap();
assert_eq!(first, second);
}
#[test]
fn context_attention_energy_matches_python_oracle() {
use crate::perf_database::energy_test_fixtures::{energy_test_spec, write_parquet, Col};
let tmp = tempfile::tempdir().expect("tmpdir");
write_parquet(
&tmp.path().join("context_attention_perf.parquet"),
&[
Col::Str("attn_dtype", vec!["bfloat16", "bfloat16"]),
Col::Str("kv_cache_dtype", vec!["bfloat16", "bfloat16"]),
Col::I64("batch_size", vec![2, 2]),
Col::I64("isl", vec![1024, 2048]),
Col::I64("num_heads", vec![16, 16]),
Col::I64("num_key_value_heads", vec![16, 16]),
Col::I64("head_dim", vec![128, 128]),
Col::I64("step", vec![0, 0]),
Col::F64("latency", vec![1.0, 3.0]),
Col::F64("power", vec![100.0, 200.0]),
],
);
let table = AttentionTable::new(tmp.path().to_path_buf(), energy_test_spec());
let v = table
.query_context(
&lanes(&[DEFAULT_LANE]),
2,
1536,
16,
16,
128,
0,
KvCacheQuantMode::Bfloat16,
FmhaQuantMode::Bfloat16,
)
.unwrap();
assert!(
((v.latency - 1.8660254037844386) / 1.8660254037844386).abs() < 1e-9,
"latency {}",
v.latency
);
assert!(
((v.energy - 279.9038105676658) / 279.9038105676658).abs() < 1e-9,
"energy {}",
v.energy
);
}
#[test]
fn generation_five_sample_averaging_matches_hand_derivation() {
use crate::perf_database::energy_test_fixtures::{
write_energy_systems_root, write_parquet, Col,
};
let tmp = tempfile::tempdir().expect("tmpdir");
let data = write_energy_systems_root(tmp.path());
write_parquet(
&data.join("generation_attention_perf.parquet"),
&[
Col::Str("kv_cache_dtype", vec!["bfloat16", "bfloat16"]),
Col::I64("batch_size", vec![1, 1]),
Col::I64("isl", vec![50, 150]),
Col::I64("num_heads", vec![8, 8]),
Col::I64("num_key_value_heads", vec![1, 1]),
Col::I64("head_dim", vec![128, 128]),
Col::I64("step", vec![0, 0]),
Col::F64("latency", vec![0.5, 1.5]),
],
);
let spec = SystemSpec::load(&tmp.path().join("testsys.yaml")).expect("testsys spec");
let table = AttentionTable::new(data, spec);
let latency = table
.query_generation(
&lanes(&[DEFAULT_LANE]),
1,
100,
8,
1,
128,
0,
KvCacheQuantMode::Bfloat16,
)
.expect("query must succeed")
.latency;
assert!(
(latency - 1.0).abs() < 1e-9,
"5-sample average over a linear grid must be exactly 1.0, got {latency}"
);
}
#[test]
fn unnamed_lane_fallback_uses_parquet_load_order_not_lexical_order() {
use crate::perf_database::energy_test_fixtures::{
write_energy_systems_root, write_parquet, Col,
};
let tmp = tempfile::tempdir().expect("tmpdir");
let data = write_energy_systems_root(tmp.path());
write_parquet(
&data.join("context_attention_perf.parquet"),
&[
Col::Str("kernel_source", vec!["z_lane", "a_lane"]),
Col::Str("attn_dtype", vec!["bfloat16", "bfloat16"]),
Col::Str("kv_cache_dtype", vec!["bfloat16", "bfloat16"]),
Col::I64("batch_size", vec![1, 1]),
Col::I64("isl", vec![1024, 1024]),
Col::I64("num_heads", vec![8, 8]),
Col::I64("num_key_value_heads", vec![8, 8]),
Col::I64("head_dim", vec![128, 128]),
Col::F64("latency", vec![1.0, 2.0]),
],
);
write_parquet(
&data.join("generation_attention_perf.parquet"),
&[
Col::Str("kernel_source", vec!["z_lane", "a_lane"]),
Col::Str("kv_cache_dtype", vec!["bfloat16", "bfloat16"]),
Col::I64("batch_size", vec![1, 1]),
Col::I64("isl", vec![1024, 1024]),
Col::I64("num_heads", vec![8, 8]),
Col::I64("num_key_value_heads", vec![8, 8]),
Col::I64("head_dim", vec![128, 128]),
Col::I64("step", vec![1, 1]),
Col::F64("latency", vec![1.0, 2.0]),
],
);
let spec = SystemSpec::load(&tmp.path().join("testsys.yaml")).expect("testsys spec");
let table = AttentionTable::new(data, spec);
let context = table.load_context().expect("context table");
assert_eq!(
context.by_lane.keys().collect::<Vec<_>>(),
vec!["a_lane", "z_lane"]
);
assert_eq!(context.lanes_in_load_order, lanes(&["z_lane", "a_lane"]));
let context_key = ContextKey {
fmha_quant: "bfloat16".to_string(),
kv_quant: "bfloat16".to_string(),
n_kv_lookup: 0,
head_size: 128,
window_size: 0,
};
let context_fallback = lane_slice(
&context.by_lane,
&context.lanes_in_load_order,
&lanes(&["unnamed"]),
&context_key,
)
.expect("context fallback");
assert!(std::ptr::eq(
context_fallback,
context.by_lane["z_lane"].get(&context_key).unwrap()
));
let generation = table.load_generation().expect("generation table");
assert_eq!(
generation.by_lane.keys().collect::<Vec<_>>(),
vec!["a_lane", "z_lane"]
);
assert_eq!(generation.lanes_in_load_order, lanes(&["z_lane", "a_lane"]));
let generation_key = GenerationKey {
kv_quant: "bfloat16".to_string(),
n_kv_lookup: 0,
head_size: 128,
window_size: 0,
};
let generation_fallback = lane_slice(
&generation.by_lane,
&generation.lanes_in_load_order,
&lanes(&["unnamed"]),
&generation_key,
)
.expect("generation fallback");
assert!(std::ptr::eq(
generation_fallback,
generation.by_lane["z_lane"].get(&generation_key).unwrap()
));
}
fn b200_sglang_0514_data_root() -> PathBuf {
PathBuf::from(REPO_ROOT_HINT)
.join("../..")
.join("src/aiconfigurator_core/systems/data/b200_sxm/sglang/0.5.14")
}
fn in_memory_two_lane_context_table() -> AttentionTable {
let key = |head_size: u32| ContextKey {
fmha_quant: "bfloat16".to_string(),
kv_quant: "bfloat16".to_string(),
n_kv_lookup: 0,
head_size,
window_size: 0,
};
let leaf = |latency: f64| {
let mut node = Node::branch();
node.insert(&[8, 1024, 1], latency);
node
};
let mut by_lane: BTreeMap<String, BTreeMap<ContextKey, Node>> = BTreeMap::new();
by_lane
.entry("head".to_string())
.or_default()
.extend([(key(128), leaf(1.0)), (key(256), leaf(2.0))]);
by_lane
.entry("donor".to_string())
.or_default()
.extend([(key(64), leaf(3.0)), (key(256), leaf(4.0))]);
let table = AttentionTable::new(PathBuf::from("test-data"), b200_sxm_spec());
assert!(table
.context
.set(Ok(ContextGrids {
by_lane,
lanes_in_load_order: lanes(&["head", "donor"]),
}))
.is_ok());
table
}
#[test]
fn context_lane_density_is_cached_and_returned_by_value() {
let table = in_memory_two_lane_context_table();
let expected = vec![("donor".to_string(), 2, 2), ("head".to_string(), 2, 2)];
assert!(table.context_lane_density.get().is_none());
let mut first = table.context_lanes().expect("context density");
assert_eq!(first, expected);
assert_eq!(table.context_lane_density.get(), Some(&expected));
first.clear();
assert_eq!(
table.context_lanes().expect("cached context density"),
expected
);
}
#[test]
fn generation_lane_density_is_cached_and_returned_by_value() {
let key = |head_size: u32| GenerationKey {
kv_quant: "bfloat16".to_string(),
n_kv_lookup: 0,
head_size,
window_size: 0,
};
let leaf = |latency: f64| {
let mut node = Node::branch();
node.insert(&[8, 1024, 1], latency);
node
};
let mut by_lane: BTreeMap<String, BTreeMap<GenerationKey, Node>> = BTreeMap::new();
by_lane
.entry("donor".to_string())
.or_default()
.extend([(key(64), leaf(1.0)), (key(128), leaf(2.0))]);
by_lane
.entry("head".to_string())
.or_default()
.insert(key(128), leaf(3.0));
let table = AttentionTable::new(PathBuf::from("test-data"), b200_sxm_spec());
assert!(table
.generation
.set(Ok(GenerationGrids {
by_lane,
lanes_in_load_order: lanes(&["donor", "head"]),
}))
.is_ok());
let expected = vec![("donor".to_string(), 2, 2), ("head".to_string(), 1, 1)];
assert!(table.generation_lane_density.get().is_none());
let mut first = table.generation_lanes().expect("generation density");
assert_eq!(first, expected);
assert_eq!(table.generation_lane_density.get(), Some(&expected));
first.clear();
assert_eq!(
table.generation_lanes().expect("cached generation density"),
expected
);
}
fn two_lane_query(
table: &AttentionTable,
lane_order: &[String],
head_size: u32,
) -> Result<f64, AicError> {
table
.query_context(
lane_order,
1,
1024,
8,
8,
head_size,
0,
KvCacheQuantMode::Bfloat16,
FmhaQuantMode::Bfloat16,
)
.map(|v| v.latency)
}
#[test]
fn context_lane_walk_serves_head_lane_then_donor_then_misses() {
let table = in_memory_two_lane_context_table();
let order = lanes(&["head", "donor"]);
assert_eq!(two_lane_query(&table, &order, 256).unwrap(), 2.0);
assert_eq!(two_lane_query(&table, &order, 128).unwrap(), 1.0);
assert_eq!(two_lane_query(&table, &order, 64).unwrap(), 3.0);
assert_eq!(
two_lane_query(&table, &lanes(&["donor", "head"]), 256).unwrap(),
4.0
);
let fallback = two_lane_query(&table, &lanes(&["fa3", "default"]), 256);
assert_eq!(
fallback.unwrap(),
2.0,
"fallback must scan the table's OTHER lanes, not just miss"
);
let miss = two_lane_query(&table, &order, 999);
assert!(
matches!(miss, Err(AicError::PerfDatabase(_))),
"got {miss:?}"
);
let fallback = two_lane_query(&table, &[], 128);
assert_eq!(
fallback.unwrap(),
1.0,
"empty order must still fall back to the table's own lanes"
);
}
#[test]
fn loader_keeps_every_kernel_source_lane() {
let table = AttentionTable::new(b200_sglang_0514_data_root(), b200_sxm_spec());
let ctx_lanes: Vec<&str> = table
.load_context()
.expect("context table must load")
.by_lane
.keys()
.map(String::as_str)
.collect();
assert_eq!(ctx_lanes, vec!["flashinfer", "triton", "trtllm_mha"]);
let gen_lanes: Vec<&str> = table
.load_generation()
.expect("generation table must load")
.by_lane
.keys()
.map(String::as_str)
.collect();
assert_eq!(gen_lanes, vec!["flashinfer", "triton", "trtllm_mha"]);
}
}