avarok_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 fn get_u64_arr(&self, _key: &str) -> Option<Vec<u64>> {
47 None
48 }
49 fn get_f64_arr(&self, _key: &str) -> Option<Vec<f64>> {
53 None
54 }
55}
56
57pub struct GgufConfigInputs<'a> {
60 pub meta: &'a dyn GgufMeta,
61 pub token_embd_vocab: Option<usize>,
65 pub has_output_weight: bool,
70}
71
72fn arch_to_model_type(arch: &str) -> Result<(&'static str, bool)> {
80 Ok(match arch {
82 "llama" => ("mistral", false),
83 "qwen2" => ("mistral", false),
86 "qwen3" => ("qwen3_5", false),
88 "qwen3moe" => ("qwen3_5_moe", false),
89 "gemma" | "gemma2" | "gemma3" | "gemma4" => ("gemma4", false),
91 "deepseek41" => ("deepseek_v41", false),
96 other => bail!(
97 "GGUF general.architecture '{other}' has no Atlas model_type mapping. \
98 Supported GGUF arches: llama, qwen2, qwen3, qwen3moe, gemma/gemma2/gemma3/gemma4, deepseek41."
99 ),
100 })
101}
102
103pub fn config_from_gguf(inputs: &GgufConfigInputs) -> Result<ModelConfig> {
105 let meta = inputs.meta;
106
107 let arch = meta
108 .get_str("general.architecture")
109 .context("GGUF metadata missing required key 'general.architecture'")?
110 .to_string();
111 let (model_type, attn_gated) = arch_to_model_type(&arch)?;
112
113 let k = |suffix: &str| format!("{arch}.{suffix}");
115 let req_u64 = |suffix: &str| -> Result<u64> {
116 meta.get_u64(&k(suffix))
117 .with_context(|| format!("GGUF metadata missing required key '{arch}.{suffix}'"))
118 };
119
120 let hidden_size = req_u64("embedding_length")? as usize;
122 let num_hidden_layers = req_u64("block_count")? as usize;
123 let has_experts = meta.get_u64(&k("expert_count")).unwrap_or(0) > 0;
128 let intermediate_size = match meta.get_u64(&k("feed_forward_length")) {
129 Some(v) => v as usize,
130 None if has_experts => 0,
131 None => bail!("GGUF metadata missing required key '{arch}.feed_forward_length'"),
132 };
133 let num_attention_heads = req_u64("attention.head_count")? as usize;
134
135 let num_key_value_heads = meta
138 .get_u64(&k("attention.head_count_kv"))
139 .map(|v| v as usize)
140 .unwrap_or(num_attention_heads);
141 if num_attention_heads > 0
142 && (num_key_value_heads == 0 || !num_attention_heads.is_multiple_of(num_key_value_heads))
143 {
144 bail!(
145 "GGUF metadata key '{}.attention.head_count_kv' ({num_key_value_heads}) must be a non-zero divisor of attention.head_count ({num_attention_heads})",
146 arch
147 );
148 }
149
150 let head_dim = match meta.get_u64(&k("attention.key_length")) {
153 Some(0) => bail!(
154 "GGUF metadata key '{}.attention.key_length' must be greater than zero",
155 arch
156 ),
157 Some(v) => v as usize,
158 None => {
159 if num_attention_heads == 0 || !hidden_size.is_multiple_of(num_attention_heads) {
160 bail!(
161 "GGUF: cannot derive head_dim — '{arch}.attention.key_length' absent and \
162 hidden_size ({hidden_size}) not divisible by head_count ({num_attention_heads})"
163 );
164 }
165 hidden_size / num_attention_heads
166 }
167 };
168
169 let metadata_vocab = meta.get_u64(&k("vocab_size")).map(|v| v as usize);
171 if let (Some(metadata_vocab), Some(tensor_vocab)) = (metadata_vocab, inputs.token_embd_vocab)
172 && metadata_vocab != tensor_vocab
173 {
174 bail!(
175 "GGUF: '{arch}.vocab_size' ({metadata_vocab}) does not match token_embd.weight rows \
176 ({tensor_vocab})"
177 );
178 }
179 let vocab_size = metadata_vocab
180 .or(inputs.token_embd_vocab)
181 .or_else(|| meta.get_arr_len("tokenizer.ggml.tokens"))
182 .context(
183 "GGUF: could not determine vocab_size (no '{arch}.vocab_size', no token_embd rows, \
184 no 'tokenizer.ggml.tokens')",
185 )?;
186 if vocab_size == 0 {
187 bail!("GGUF: vocab_size must be non-zero");
188 }
189
190 let rms_norm_eps = meta
194 .get_f64(&k("attention.layer_norm_rms_epsilon"))
195 .unwrap_or(1e-5);
196 let rope_theta = meta.get_f64(&k("rope.freq_base")).unwrap_or(10_000.0);
198 let max_position_embeddings = req_u64("context_length")? as usize;
200
201 let bos_token_id = meta.get_u64("tokenizer.ggml.bos_token_id").unwrap_or(0);
203 let eos_token_id = meta.get_u64("tokenizer.ggml.eos_token_id").unwrap_or(0);
204
205 let tie_word_embeddings = !inputs.has_output_weight;
207
208 let num_experts = if arch == "qwen3moe" {
210 req_u64("expert_count")? as usize
211 } else {
212 meta.get_u64(&k("expert_count"))
213 .map(|v| v as usize)
214 .unwrap_or(0)
215 };
216 if arch == "qwen3moe" && num_experts == 0 {
217 bail!("GGUF metadata key '{arch}.expert_count' must be greater than zero");
218 }
219
220 let mut body: Map<String, Value> = json!({
221 "hidden_size": hidden_size,
222 "num_hidden_layers": num_hidden_layers,
223 "intermediate_size": intermediate_size,
224 "vocab_size": vocab_size,
225 "num_attention_heads": num_attention_heads,
226 "num_key_value_heads": num_key_value_heads,
227 "head_dim": head_dim,
228 "rms_norm_eps": rms_norm_eps,
229 "rope_theta": rope_theta,
230 "max_position_embeddings": max_position_embeddings,
231 "bos_token_id": bos_token_id,
232 "eos_token_id": eos_token_id,
233 "tie_word_embeddings": tie_word_embeddings,
234 "model_type": model_type,
235 })
236 .as_object()
237 .expect("json! object literal")
238 .clone();
239
240 if num_experts > 0 {
241 let experts_per_tok = req_u64("expert_used_count").with_context(|| {
242 format!("GGUF: MoE arch '{arch}' has expert_count>0 but no '{arch}.expert_used_count'")
243 })? as usize;
244 let moe_ffn = req_u64("expert_feed_forward_length").with_context(|| {
245 format!("GGUF: MoE arch '{arch}' missing '{arch}.expert_feed_forward_length'")
246 })? as usize;
247 if experts_per_tok == 0 || experts_per_tok > num_experts {
248 bail!(
249 "GGUF metadata key '{arch}.expert_used_count' must be in 1..={num_experts}, \
250 found {experts_per_tok}"
251 );
252 }
253 if moe_ffn == 0 {
254 bail!(
255 "GGUF metadata key '{arch}.expert_feed_forward_length' must be greater than zero"
256 );
257 }
258 body.insert("num_experts".into(), json!(num_experts));
259 body.insert("num_experts_per_tok".into(), json!(experts_per_tok));
260 body.insert("moe_intermediate_size".into(), json!(moe_ffn));
261 }
262
263 if let Some(sw) = meta.get_u64(&k("attention.sliding_window")) {
265 body.insert("sliding_window".into(), json!(sw));
266 }
267
268 let raw = Value::Object(body);
270 let json_str = serde_json::to_string(&raw).context("serialize synthesized GGUF config")?;
271 let mut config: ModelConfig =
272 serde_json::from_str(&json_str).context("deserialize synthesized GGUF config")?;
273
274 config.model_type = model_type.to_string();
275 config.attn_gated = attn_gated;
276 config.weight_prefix = "model".to_string();
282
283 if model_type == "gemma4" {
285 config.embed_scale = (hidden_size as f32).sqrt();
286 config.final_logit_softcapping = match meta.get_f64(&k("final_logit_softcapping")) {
289 Some(v) if v >= 0.0 && v <= f32::MAX as f64 => v as f32,
290 Some(v) => bail!(
291 "GGUF metadata key '{}.final_logit_softcapping' must be non-negative and representable as a finite f32 (got {v})",
292 arch
293 ),
294 None => 0.0,
295 };
296 }
297
298 if model_type == "deepseek_v41" {
305 let req = |suffix: &str| -> Result<u64> {
306 meta.get_u64(&k(suffix))
307 .with_context(|| format!("DeepSeek-V4.1 GGUF missing '{arch}.{suffix}'"))
308 };
309 let req_arr = |suffix: &str| -> Result<Vec<u64>> {
310 meta.get_u64_arr(&k(suffix))
311 .with_context(|| format!("DeepSeek-V4.1 GGUF missing array '{arch}.{suffix}'"))
312 };
313
314 config.q_lora_rank = req("attention.q_lora_rank")? as usize;
316 config.o_lora_rank = req("attention.output_lora_rank")? as usize;
317 config.o_groups = req("attention.output_group_count")? as usize;
318 config.kv_lora_rank = req("attention.value_length")? as usize;
319 config.rotary_dim = req("rope.dimension_count")? as usize;
321
322 config.index_n_heads = req("attention.indexer.head_count")? as usize;
324 config.index_head_dim = req("attention.indexer.key_length")? as usize;
325 config.index_topk = req("attention.indexer.top_k")? as usize;
326 config.compress_ratios = req_arr("attention.compress_ratios")?
327 .into_iter()
328 .map(|v| v as usize)
329 .collect();
330 if config.compress_ratios.len() < num_hidden_layers {
334 bail!(
335 "DeepSeek-V4.1: '{arch}.attention.compress_ratios' has {} entries, fewer than \
336 block_count ({num_hidden_layers})",
337 config.compress_ratios.len()
338 );
339 }
340 config.compress_rope_theta = meta
341 .get_f64(&k("attention.compress_rope_freq_base"))
342 .context("DeepSeek-V4.1 GGUF missing 'attention.compress_rope_freq_base'")?
343 as f32;
344
345 config.hc_mult = req("hyper_connection.count")? as usize;
347 config.hc_sinkhorn_iters = req("hyper_connection.sinkhorn_iterations")? as usize;
348 config.hc_eps = meta
349 .get_f64(&k("hyper_connection.epsilon"))
350 .context("DeepSeek-V4.1 GGUF missing 'hyper_connection.epsilon'")?
351 as f32;
352
353 config.n_routed_experts = num_experts;
358 config.norm_topk_prob = req("expert_weights_norm")? != 0;
359 config.routed_scaling_factor = meta
360 .get_f64(&k("expert_weights_scale"))
361 .context("DeepSeek-V4.1 GGUF missing 'expert_weights_scale'")?;
362 config.scoring_func = match req("expert_gating_func")? {
363 4 => "sqrtsoftplus".to_string(),
364 other => bail!(
365 "DeepSeek-V4.1: unsupported '{arch}.expert_gating_func' ({other}); \
366 only 4 (sqrt-softplus) is implemented"
367 ),
368 };
369 config.num_hash_layers = meta.get_u64(&k("hash_layer_count")).unwrap_or(0) as usize;
370
371 if let Some(clamps) = meta.get_f64_arr(&k("swiglu_clamp_exp")) {
374 let first = clamps.first().copied().unwrap_or(0.0);
375 if clamps.iter().any(|v| (v - first).abs() > f64::EPSILON) {
376 bail!(
377 "DeepSeek-V4.1: '{arch}.swiglu_clamp_exp' is not uniform across layers; \
378 ModelConfig carries a single `swiglu_limit`"
379 );
380 }
381 config.swiglu_limit = first as f32;
382 }
383
384 config.engram_layer_ids = req_arr("engram.layer_ids")?
388 .into_iter()
389 .map(|v| v as usize)
390 .collect();
391 if !config.engram_layer_ids.is_empty() {
392 config.engram_max_ngram_size = req("engram.max_ngram_size")? as usize;
393 config.engram_n_heads = req("engram.head_count")? as usize;
394 config.engram_head_dim = req("engram.key_length")? as usize;
395 config.engram_pad_token_id = req("engram.pad_id")? as u32;
396 config.engram_multipliers = req_arr("engram.multipliers")?;
397 config.engram_primes = req_arr("engram.primes")?;
398 config.engram_offsets = req_arr("engram.offsets")?;
399 if meta.get_arr_len(&k("engram.token_map")).unwrap_or(0) != vocab_size {
402 bail!(
403 "DeepSeek-V4.1: '{arch}.engram.token_map' length does not match vocab_size \
404 ({vocab_size})"
405 );
406 }
407 }
408 }
409
410 finalize_config(&mut config, &raw)?;
412 Ok(config)
413}
414
415#[cfg(test)]
426mod tests;