atlas_core/config/
gguf.rs1use anyhow::{Context, Result, bail};
21use serde_json::{Map, Value, json};
22
23use super::{ModelConfig, finalize_config};
24
25pub trait GgufMeta {
30 fn get_u64(&self, key: &str) -> Option<u64>;
32 fn get_f64(&self, key: &str) -> Option<f64>;
34 fn get_str(&self, key: &str) -> Option<&str>;
36 fn get_arr_len(&self, key: &str) -> Option<usize>;
38}
39
40pub struct GgufConfigInputs<'a> {
43 pub meta: &'a dyn GgufMeta,
44 pub token_embd_vocab: Option<usize>,
48 pub has_output_weight: bool,
53}
54
55fn arch_to_model_type(arch: &str) -> Result<(&'static str, bool)> {
63 Ok(match arch {
65 "llama" => ("mistral", false),
66 "qwen2" => ("mistral", false),
69 "qwen3" => ("qwen3_5", false),
71 "qwen3moe" => ("qwen3_5_moe", false),
72 "gemma" | "gemma2" | "gemma3" | "gemma4" => ("gemma4", false),
74 other => bail!(
75 "GGUF general.architecture '{other}' has no Atlas model_type mapping. \
76 Supported GGUF arches: llama, qwen2, qwen3, qwen3moe, gemma/gemma2/gemma3/gemma4."
77 ),
78 })
79}
80
81pub fn config_from_gguf(inputs: &GgufConfigInputs) -> Result<ModelConfig> {
83 let meta = inputs.meta;
84
85 let arch = meta
86 .get_str("general.architecture")
87 .context("GGUF metadata missing required key 'general.architecture'")?
88 .to_string();
89 let (model_type, attn_gated) = arch_to_model_type(&arch)?;
90
91 let k = |suffix: &str| format!("{arch}.{suffix}");
93 let req_u64 = |suffix: &str| -> Result<u64> {
94 meta.get_u64(&k(suffix))
95 .with_context(|| format!("GGUF metadata missing required key '{arch}.{suffix}'"))
96 };
97
98 let hidden_size = req_u64("embedding_length")? as usize;
100 let num_hidden_layers = req_u64("block_count")? as usize;
101 let intermediate_size = req_u64("feed_forward_length")? as usize;
102 let num_attention_heads = req_u64("attention.head_count")? as usize;
103
104 let num_key_value_heads = meta
107 .get_u64(&k("attention.head_count_kv"))
108 .map(|v| v as usize)
109 .unwrap_or(num_attention_heads);
110 if num_attention_heads > 0
111 && (num_key_value_heads == 0 || !num_attention_heads.is_multiple_of(num_key_value_heads))
112 {
113 bail!(
114 "GGUF metadata key '{}.attention.head_count_kv' ({num_key_value_heads}) must be a non-zero divisor of attention.head_count ({num_attention_heads})",
115 arch
116 );
117 }
118
119 let head_dim = match meta.get_u64(&k("attention.key_length")) {
122 Some(0) => bail!(
123 "GGUF metadata key '{}.attention.key_length' must be greater than zero",
124 arch
125 ),
126 Some(v) => v as usize,
127 None => {
128 if num_attention_heads == 0 || !hidden_size.is_multiple_of(num_attention_heads) {
129 bail!(
130 "GGUF: cannot derive head_dim — '{arch}.attention.key_length' absent and \
131 hidden_size ({hidden_size}) not divisible by head_count ({num_attention_heads})"
132 );
133 }
134 hidden_size / num_attention_heads
135 }
136 };
137
138 let metadata_vocab = meta.get_u64(&k("vocab_size")).map(|v| v as usize);
140 if let (Some(metadata_vocab), Some(tensor_vocab)) = (metadata_vocab, inputs.token_embd_vocab)
141 && metadata_vocab != tensor_vocab
142 {
143 bail!(
144 "GGUF: '{arch}.vocab_size' ({metadata_vocab}) does not match token_embd.weight rows \
145 ({tensor_vocab})"
146 );
147 }
148 let vocab_size = metadata_vocab
149 .or(inputs.token_embd_vocab)
150 .or_else(|| meta.get_arr_len("tokenizer.ggml.tokens"))
151 .context(
152 "GGUF: could not determine vocab_size (no '{arch}.vocab_size', no token_embd rows, \
153 no 'tokenizer.ggml.tokens')",
154 )?;
155 if vocab_size == 0 {
156 bail!("GGUF: vocab_size must be non-zero");
157 }
158
159 let rms_norm_eps = meta
163 .get_f64(&k("attention.layer_norm_rms_epsilon"))
164 .unwrap_or(1e-5);
165 let rope_theta = meta.get_f64(&k("rope.freq_base")).unwrap_or(10_000.0);
167 let max_position_embeddings = req_u64("context_length")? as usize;
169
170 let bos_token_id = meta.get_u64("tokenizer.ggml.bos_token_id").unwrap_or(0);
172 let eos_token_id = meta.get_u64("tokenizer.ggml.eos_token_id").unwrap_or(0);
173
174 let tie_word_embeddings = !inputs.has_output_weight;
176
177 let num_experts = if arch == "qwen3moe" {
179 req_u64("expert_count")? as usize
180 } else {
181 meta.get_u64(&k("expert_count"))
182 .map(|v| v as usize)
183 .unwrap_or(0)
184 };
185 if arch == "qwen3moe" && num_experts == 0 {
186 bail!("GGUF metadata key '{arch}.expert_count' must be greater than zero");
187 }
188
189 let mut body: Map<String, Value> = json!({
190 "hidden_size": hidden_size,
191 "num_hidden_layers": num_hidden_layers,
192 "intermediate_size": intermediate_size,
193 "vocab_size": vocab_size,
194 "num_attention_heads": num_attention_heads,
195 "num_key_value_heads": num_key_value_heads,
196 "head_dim": head_dim,
197 "rms_norm_eps": rms_norm_eps,
198 "rope_theta": rope_theta,
199 "max_position_embeddings": max_position_embeddings,
200 "bos_token_id": bos_token_id,
201 "eos_token_id": eos_token_id,
202 "tie_word_embeddings": tie_word_embeddings,
203 "model_type": model_type,
204 })
205 .as_object()
206 .expect("json! object literal")
207 .clone();
208
209 if num_experts > 0 {
210 let experts_per_tok = req_u64("expert_used_count").with_context(|| {
211 format!("GGUF: MoE arch '{arch}' has expert_count>0 but no '{arch}.expert_used_count'")
212 })? as usize;
213 let moe_ffn = req_u64("expert_feed_forward_length").with_context(|| {
214 format!("GGUF: MoE arch '{arch}' missing '{arch}.expert_feed_forward_length'")
215 })? as usize;
216 if experts_per_tok == 0 || experts_per_tok > num_experts {
217 bail!(
218 "GGUF metadata key '{arch}.expert_used_count' must be in 1..={num_experts}, \
219 found {experts_per_tok}"
220 );
221 }
222 if moe_ffn == 0 {
223 bail!(
224 "GGUF metadata key '{arch}.expert_feed_forward_length' must be greater than zero"
225 );
226 }
227 body.insert("num_experts".into(), json!(num_experts));
228 body.insert("num_experts_per_tok".into(), json!(experts_per_tok));
229 body.insert("moe_intermediate_size".into(), json!(moe_ffn));
230 }
231
232 if let Some(sw) = meta.get_u64(&k("attention.sliding_window")) {
234 body.insert("sliding_window".into(), json!(sw));
235 }
236
237 let raw = Value::Object(body);
239 let json_str = serde_json::to_string(&raw).context("serialize synthesized GGUF config")?;
240 let mut config: ModelConfig =
241 serde_json::from_str(&json_str).context("deserialize synthesized GGUF config")?;
242
243 config.model_type = model_type.to_string();
244 config.attn_gated = attn_gated;
245 config.weight_prefix = "model".to_string();
251
252 if model_type == "gemma4" {
254 config.embed_scale = (hidden_size as f32).sqrt();
255 config.final_logit_softcapping = match meta.get_f64(&k("final_logit_softcapping")) {
258 Some(v) if v >= 0.0 && v <= f32::MAX as f64 => v as f32,
259 Some(v) => bail!(
260 "GGUF metadata key '{}.final_logit_softcapping' must be non-negative and representable as a finite f32 (got {v})",
261 arch
262 ),
263 None => 0.0,
264 };
265 }
266
267 finalize_config(&mut config, &raw)?;
269 Ok(config)
270}
271
272#[cfg(test)]
283mod tests;