use crate::arch::decoder::families::{AttnScale, FamilyProfile};
use crate::format::FormatError;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum LayerKind {
Sliding,
Full,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum HiddenActivation {
Silu,
GeluTanh,
GeluErf,
}
#[derive(Clone, Debug)]
pub struct LlamaConfig {
pub model_type: String,
pub hidden_size: usize,
pub num_attention_heads: usize,
pub num_key_value_heads: usize,
pub num_hidden_layers: usize,
pub intermediate_size: usize,
pub vocab_size: usize,
pub max_position_embeddings: usize,
pub rope_theta: f32,
pub rms_norm_eps: f32,
pub tie_word_embeddings: bool,
pub head_dim: usize,
pub has_qk_norm: bool,
pub has_attn_bias: bool,
pub eos_token_ids: Vec<u32>,
pub layer_types: Vec<LayerKind>,
pub sliding_window: Option<usize>,
pub hidden_activation: HiddenActivation,
pub final_logit_softcapping: Option<f32>,
pub attention_k_eq_v: bool,
pub global_head_dim: Option<usize>,
pub num_global_key_value_heads: Option<usize>,
pub rope_theta_full: Option<f32>,
pub partial_rotary_factor_full: Option<f32>,
pub query_pre_attn_scalar: Option<usize>,
pub family: FamilyProfile,
}
impl LlamaConfig {
pub fn layer_head_dim(&self, layer: usize) -> usize {
match self.layer_types.get(layer).copied() {
Some(LayerKind::Full) => self.global_head_dim.unwrap_or(self.head_dim),
_ => self.head_dim,
}
}
pub fn layer_kv_heads(&self, layer: usize) -> usize {
match self.layer_types.get(layer).copied() {
Some(LayerKind::Full) => self
.num_global_key_value_heads
.unwrap_or(self.num_key_value_heads),
_ => self.num_key_value_heads,
}
}
pub fn layer_window(&self, layer: usize) -> Option<usize> {
match self.layer_types.get(layer).copied() {
Some(LayerKind::Sliding) => self.sliding_window,
_ => None,
}
}
pub fn layer_rope_theta(&self, layer: usize) -> f32 {
match self.layer_types.get(layer).copied() {
Some(LayerKind::Full) => self.rope_theta_full.unwrap_or(self.rope_theta),
_ => self.rope_theta,
}
}
pub fn layer_rope_dim(&self, layer: usize) -> usize {
let head_dim = self.layer_head_dim(layer);
match self.layer_types.get(layer).copied() {
Some(LayerKind::Full) => match self.partial_rotary_factor_full {
Some(f) if f > 0.0 && f < 1.0 => {
let d = (head_dim as f32 * f) as usize;
d & !1
}
_ => head_dim,
},
_ => head_dim,
}
}
pub fn layer_kv_cache_seq(&self, layer: usize, global_max_seq: usize) -> usize {
match self.layer_window(layer) {
Some(w) if w < global_max_seq => w,
_ => global_max_seq,
}
}
pub fn layer_attn_scale(&self, layer: usize) -> f32 {
match self.family.attn_scale {
AttnScale::Unity => 1.0,
AttnScale::FixedDivisor(n) => 1.0 / (n as f32).sqrt(),
AttnScale::PerHeadDim => 1.0 / (self.layer_head_dim(layer) as f32).sqrt(),
}
}
}
impl LlamaConfig {
pub fn parse(config_toml: &str, tensors: &[crate::format::TensorMeta]) -> Result<Self, FormatError> {
let value: toml::Value = toml::from_str(config_toml)
.map_err(|e| FormatError::Invalid(format!("config.toml: {e}")))?;
let model_type = value
.get("model_type")
.and_then(|v| v.as_str())
.unwrap_or("unknown")
.to_string();
let arch = value.get("architecture").ok_or_else(|| {
FormatError::Invalid("config.toml: missing [architecture]".into())
})?;
let get_usize = |key: &str| -> Result<usize, FormatError> {
arch.get(key)
.and_then(|v| v.as_integer())
.map(|i| i as usize)
.ok_or_else(|| FormatError::Invalid(format!("missing/invalid {key}")))
};
let get_usize_default = |key: &str, default: usize| -> usize {
arch.get(key)
.and_then(|v| v.as_integer())
.map(|i| i as usize)
.unwrap_or(default)
};
let hidden_size = get_usize("hidden_size")?;
let num_attention_heads = get_usize("num_attention_heads")?;
let num_key_value_heads =
get_usize_default("num_key_value_heads", num_attention_heads);
let num_hidden_layers = get_usize("num_hidden_layers")?;
let intermediate_size = get_usize("intermediate_size")?;
let vocab_size = get_usize("vocab_size")?;
let max_position_embeddings = get_usize_default("max_position_embeddings", 2048);
let rope_theta = arch
.get("rope_theta")
.and_then(|v| v.as_float().or_else(|| v.as_integer().map(|i| i as f64)))
.unwrap_or(10000.0) as f32;
let eps_raw = arch
.get("rms_norm_eps")
.and_then(|v| v.as_float().or_else(|| v.as_integer().map(|i| i as f64)))
.unwrap_or(1e-6);
let rms_norm_eps = if eps_raw >= 1.0 {
(1.0 / eps_raw) as f32
} else {
eps_raw as f32
};
let tie_word_embeddings = arch
.get("tie_word_embeddings")
.and_then(|v| v.as_bool())
.or_else(|| value.get("tie_word_embeddings").and_then(|v| v.as_bool()))
.unwrap_or(true);
let head_dim = arch
.get("head_dim")
.and_then(|v| v.as_integer())
.map(|i| i as usize)
.or_else(|| {
let inferred = tensors
.iter()
.find(|t| t.name == "model.layers.0.self_attn.q_proj.weight")
.map(|t| t.shape[0] / num_attention_heads);
if inferred.is_some() {
log::warn!("head_dim not in config β inferred {} from q_proj shape; add head_dim to config for reliability", inferred.unwrap());
}
inferred
})
.unwrap_or_else(|| {
let fallback = hidden_size / num_attention_heads;
log::warn!("head_dim not in config and q_proj tensor missing β falling back to hidden_size/num_heads = {fallback}");
fallback
});
if head_dim == 0 || head_dim % 2 != 0 {
return Err(FormatError::Invalid(format!(
"head_dim must be positive and even, got {head_dim}"
)));
}
if num_attention_heads == 0 {
return Err(FormatError::Invalid("num_attention_heads must be > 0".into()));
}
if num_key_value_heads == 0 || num_attention_heads % num_key_value_heads != 0 {
return Err(FormatError::Invalid(format!(
"GQA requires num_heads ({num_attention_heads}) divisible by kv_heads ({num_key_value_heads})"
)));
}
if num_hidden_layers == 0 {
return Err(FormatError::Invalid("num_hidden_layers must be > 0".into()));
}
if vocab_size == 0 {
return Err(FormatError::Invalid("vocab_size must be > 0".into()));
}
if rope_theta <= 0.0 {
return Err(FormatError::Invalid(format!(
"rope_theta must be positive, got {rope_theta}"
)));
}
if !(rms_norm_eps > 0.0 && rms_norm_eps < 1.0) {
return Err(FormatError::Invalid(format!(
"rms_norm_eps outside sane range (0, 1): {rms_norm_eps}"
)));
}
let has_qk_norm = tensors
.iter()
.any(|t| t.name == "model.layers.0.self_attn.q_norm.weight");
let has_attn_bias = tensors
.iter()
.any(|t| t.name == "model.layers.0.self_attn.q_proj.bias");
let eos_token_ids = value
.get("tokenizer")
.and_then(|t| t.get("eos_token_ids"))
.and_then(|v| v.as_array())
.map(|a| {
a.iter()
.filter_map(|v| v.as_integer().map(|i| i as u32))
.collect()
})
.unwrap_or_default();
let layer_types: Vec<LayerKind> = arch
.get("layer_types")
.and_then(|v| v.as_array())
.map(|a| {
a.iter()
.filter_map(|v| v.as_str())
.map(|s| match s {
"full_attention" | "full" => LayerKind::Full,
_ => LayerKind::Sliding,
})
.collect()
})
.unwrap_or_else(|| vec![LayerKind::Sliding; num_hidden_layers]);
if layer_types.len() != num_hidden_layers {
return Err(FormatError::Invalid(format!(
"layer_types length {} != num_hidden_layers {}",
layer_types.len(),
num_hidden_layers
)));
}
let sliding_window = arch
.get("sliding_window")
.and_then(|v| v.as_integer())
.map(|i| i as usize);
let hidden_activation = arch
.get("hidden_activation")
.and_then(|v| v.as_str())
.map(|s| match s {
"gelu_pytorch_tanh" | "gelu_tanh" => HiddenActivation::GeluTanh,
"gelu" | "gelu_erf" => HiddenActivation::GeluErf,
_ => HiddenActivation::Silu,
})
.unwrap_or(HiddenActivation::Silu);
let final_logit_softcapping = arch
.get("final_logit_softcapping")
.and_then(|v| v.as_float().or_else(|| v.as_integer().map(|i| i as f64)))
.map(|f| f as f32)
.filter(|&f| f > 0.0);
let attention_k_eq_v = arch
.get("attention_k_eq_v")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let global_head_dim = arch
.get("global_head_dim")
.and_then(|v| v.as_integer())
.map(|i| i as usize);
let num_global_key_value_heads = arch
.get("num_global_key_value_heads")
.and_then(|v| v.as_integer())
.map(|i| i as usize);
let rope_theta_full = arch
.get("rope_theta_full")
.and_then(|v| v.as_float().or_else(|| v.as_integer().map(|i| i as f64)))
.map(|f| f as f32);
let partial_rotary_factor_full = arch
.get("partial_rotary_factor_full")
.and_then(|v| v.as_float().or_else(|| v.as_integer().map(|i| i as f64)))
.map(|f| f as f32);
let query_pre_attn_scalar = arch
.get("query_pre_attn_scalar")
.and_then(|v| v.as_integer())
.map(|i| i as usize);
let family = FamilyProfile::for_model_type(&model_type, query_pre_attn_scalar);
Ok(Self {
model_type,
hidden_size,
num_attention_heads,
num_key_value_heads,
num_hidden_layers,
intermediate_size,
vocab_size,
max_position_embeddings,
rope_theta,
rms_norm_eps,
tie_word_embeddings,
head_dim,
has_qk_norm,
has_attn_bias,
eos_token_ids,
layer_types,
sliding_window,
hidden_activation,
final_logit_softcapping,
attention_k_eq_v,
global_head_dim,
num_global_key_value_heads,
rope_theta_full,
partial_rotary_factor_full,
query_pre_attn_scalar,
family,
})
}
}