use fuel_ir::{Error, Result};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StateSlot {
pub trailing: Vec<usize>,
}
impl StateSlot {
pub fn new(trailing: impl Into<Vec<usize>>) -> Self {
Self {
trailing: trailing.into(),
}
}
pub fn elems_per_token(&self) -> usize {
self.trailing.iter().product::<usize>().max(1)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum LayerStateSpec {
KeyValue { n_kv_heads: usize, head_dim: usize },
Slots(Vec<StateSlot>),
}
impl LayerStateSpec {
pub fn slots(&self) -> Vec<StateSlot> {
match self {
Self::KeyValue {
n_kv_heads,
head_dim,
} => {
let t = vec![*n_kv_heads, *head_dim];
vec![StateSlot::new(t.clone()), StateSlot::new(t)]
}
Self::Slots(s) => s.clone(),
}
}
pub fn kv_dims(&self) -> Option<(usize, usize)> {
match self {
Self::KeyValue {
n_kv_heads,
head_dim,
} => Some((*n_kv_heads, *head_dim)),
Self::Slots(_) => None,
}
}
pub fn collapse_uniform(specs: &[LayerStateSpec]) -> Result<(usize, usize)> {
let first = specs.first().ok_or_else(|| {
Error::Msg(
"LayerStateSpec::collapse_uniform: no layers — a model with zero \
layers has no cache geometry to collapse"
.into(),
)
.bt()
})?;
let (n_kv_heads, head_dim) = first.kv_dims().ok_or_else(|| {
Error::Msg(
"LayerStateSpec::collapse_uniform: layer 0 is not per-head KV, so it \
has no (n_kv_heads, head_dim) to collapse to. This model needs a \
slot-aware allocator, not a collapsed pair — see GAP-166."
.into(),
)
.bt()
})?;
for (i, s) in specs.iter().enumerate().skip(1) {
match s.kv_dims() {
Some(d) if d == (n_kv_heads, head_dim) => {}
Some((h, d)) => {
return Err(Error::Msg(format!(
"LayerStateSpec::collapse_uniform: layer {i} is per-head KV \
({h}, {d}) but layer 0 is ({n_kv_heads}, {head_dim}). A \
single pair cannot describe both; collapsing would silently \
allocate layer 0's geometry for every layer.",
))
.bt());
}
None => {
return Err(Error::Msg(format!(
"LayerStateSpec::collapse_uniform: layer {i} is not per-head \
KV while layer 0 is. Collapsing would allocate KV for a layer \
that keeps a different state kind entirely — the wrong state, \
not merely the wrong size.",
))
.bt());
}
}
}
Ok((n_kv_heads, head_dim))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn kv(h: usize, d: usize) -> LayerStateSpec {
LayerStateSpec::KeyValue {
n_kv_heads: h,
head_dim: d,
}
}
fn mla(kv_lora_rank: usize, qk_rope_head_dim: usize) -> LayerStateSpec {
LayerStateSpec::Slots(vec![
StateSlot::new(vec![kv_lora_rank]),
StateSlot::new(vec![qk_rope_head_dim]),
])
}
#[test]
fn kv_layer_is_the_two_slot_case() {
let slots = kv(8, 64).slots();
assert_eq!(slots.len(), 2, "K and V");
assert_eq!(slots[0].trailing, vec![8, 64]);
assert_eq!(slots[1].trailing, vec![8, 64]);
assert_eq!(slots[0].elems_per_token(), 512);
}
#[test]
fn mla_layer_is_expressible_and_is_not_kv() {
let m = mla(512, 64);
let slots = m.slots();
assert_eq!(slots[0].trailing, vec![512], "compressed latent");
assert_eq!(slots[1].trailing, vec![64], "post-RoPE k_pe");
assert_eq!(
m.kv_dims(),
None,
"an MLA layer must decline (n_kv_heads, head_dim) rather than \
fabricate a plausible pair — fabricating it is the exact defect \
GAP-166 records",
);
}
#[test]
fn scalar_slot_contributes_one_element_not_zero() {
assert_eq!(StateSlot::new(vec![]).elems_per_token(), 1);
}
#[test]
fn uniform_kv_collapses_to_the_pair_todays_consumers_expect() {
let specs = vec![kv(8, 64); 32];
assert_eq!(LayerStateSpec::collapse_uniform(&specs).unwrap(), (8, 64));
}
#[test]
fn non_uniform_kv_dims_refuse_to_collapse() {
let mut specs = vec![kv(8, 64); 4];
specs[2] = kv(4, 64); let err = LayerStateSpec::collapse_uniform(&specs)
.unwrap_err()
.to_string();
assert!(
err.contains("layer 2"),
"must name the offending layer: {err}"
);
}
#[test]
fn mixed_state_kinds_refuse_to_collapse() {
let specs = vec![kv(8, 64), mla(512, 64), kv(8, 64)];
let err = LayerStateSpec::collapse_uniform(&specs)
.unwrap_err()
.to_string();
assert!(
err.contains("wrong state"),
"the error must distinguish wrong-state from wrong-size: {err}",
);
}
#[test]
fn all_mla_refuses_to_collapse_even_though_it_is_uniform() {
let specs = vec![mla(512, 64); 8];
assert!(
LayerStateSpec::collapse_uniform(&specs).is_err(),
"uniform-but-not-KV must still refuse; uniformity and KV-shapedness \
are independent properties and conflating them is the bug",
);
}
#[test]
fn zero_layers_refuses_rather_than_returning_a_default() {
assert!(LayerStateSpec::collapse_uniform(&[]).is_err());
}
}