1use anyhow::{Context, Result, bail};
15use serde_json::{Map, Value, json};
16
17use super::{ModelConfig, finalize_config};
18
19pub trait GgufMeta {
23 fn get_u64(&self, key: &str) -> Option<u64>;
24 fn get_f64(&self, key: &str) -> Option<f64>;
25 fn get_str(&self, key: &str) -> Option<&str>;
26 fn get_arr_len(&self, key: &str) -> Option<usize>;
27 fn get_u64_arr(&self, _key: &str) -> Option<Vec<u64>> {
30 None
31 }
32 fn get_f64_arr(&self, _key: &str) -> Option<Vec<f64>> {
35 None
36 }
37}
38
39pub struct GgufConfigInputs<'a> {
42 pub meta: &'a dyn GgufMeta,
43 pub token_embd_vocab: Option<usize>,
47 pub has_output_weight: bool,
50}
51
52fn arch_to_model_type(arch: &str) -> Result<(&'static str, bool)> {
55 Ok(match arch {
56 "llama" => ("mistral", false),
57 "qwen2" => ("mistral", false),
58 "qwen3" => ("qwen3_5", false),
59 "qwen3moe" => ("qwen3_5_moe", false),
60 "gemma" | "gemma2" | "gemma3" | "gemma4" => ("gemma4", false),
61 "deepseek41" => ("deepseek_v41", false),
62 other => bail!(
63 "GGUF general.architecture '{other}' has no Metrale Engine model_type mapping. \
64 Supported GGUF arches: llama, qwen2, qwen3, qwen3moe, gemma/gemma2/gemma3/gemma4, deepseek41."
65 ),
66 })
67}
68
69pub fn config_from_gguf(inputs: &GgufConfigInputs) -> Result<ModelConfig> {
72 let meta = inputs.meta;
73
74 let arch = meta
75 .get_str("general.architecture")
76 .context("GGUF metadata missing required key 'general.architecture'")?
77 .to_string();
78 let (model_type, attn_gated) = arch_to_model_type(&arch)?;
79
80 let k = |suffix: &str| format!("{arch}.{suffix}");
81 let req_u64 = |suffix: &str| -> Result<u64> {
82 meta.get_u64(&k(suffix))
83 .with_context(|| format!("GGUF metadata missing required key '{arch}.{suffix}'"))
84 };
85
86 let hidden_size = req_u64("embedding_length")? as usize;
87 let num_hidden_layers = req_u64("block_count")? as usize;
88 let has_experts = meta.get_u64(&k("expert_count")).unwrap_or(0) > 0;
91 let intermediate_size = match meta.get_u64(&k("feed_forward_length")) {
92 Some(v) => v as usize,
93 None if has_experts => 0,
94 None => bail!("GGUF metadata missing required key '{arch}.feed_forward_length'"),
95 };
96 let num_attention_heads = req_u64("attention.head_count")? as usize;
97
98 let num_key_value_heads = meta
100 .get_u64(&k("attention.head_count_kv"))
101 .map(|v| v as usize)
102 .unwrap_or(num_attention_heads);
103 if num_attention_heads > 0
104 && (num_key_value_heads == 0 || !num_attention_heads.is_multiple_of(num_key_value_heads))
105 {
106 bail!(
107 "GGUF metadata key '{}.attention.head_count_kv' ({num_key_value_heads}) must be a non-zero divisor of attention.head_count ({num_attention_heads})",
108 arch
109 );
110 }
111
112 let head_dim = match meta.get_u64(&k("attention.key_length")) {
113 Some(0) => bail!(
114 "GGUF metadata key '{}.attention.key_length' must be greater than zero",
115 arch
116 ),
117 Some(v) => v as usize,
118 None => {
119 if num_attention_heads == 0 || !hidden_size.is_multiple_of(num_attention_heads) {
120 bail!(
121 "GGUF: cannot derive head_dim — '{arch}.attention.key_length' absent and \
122 hidden_size ({hidden_size}) not divisible by head_count ({num_attention_heads})"
123 );
124 }
125 hidden_size / num_attention_heads
126 }
127 };
128
129 let metadata_vocab = meta.get_u64(&k("vocab_size")).map(|v| v as usize);
132 if let (Some(metadata_vocab), Some(tensor_vocab)) = (metadata_vocab, inputs.token_embd_vocab)
133 && metadata_vocab != tensor_vocab
134 {
135 bail!(
136 "GGUF: '{arch}.vocab_size' ({metadata_vocab}) does not match token_embd.weight rows \
137 ({tensor_vocab})"
138 );
139 }
140 let vocab_size = metadata_vocab
141 .or(inputs.token_embd_vocab)
142 .or_else(|| meta.get_arr_len("tokenizer.ggml.tokens"))
143 .context(
144 "GGUF: could not determine vocab_size (no '{arch}.vocab_size', no token_embd rows, \
145 no 'tokenizer.ggml.tokens')",
146 )?;
147 if vocab_size == 0 {
148 bail!("GGUF: vocab_size must be non-zero");
149 }
150
151 let rms_norm_eps = meta
154 .get_f64(&k("attention.layer_norm_rms_epsilon"))
155 .unwrap_or(1e-5);
156 let rope_theta = meta.get_f64(&k("rope.freq_base")).unwrap_or(10_000.0);
157 let max_position_embeddings = req_u64("context_length")? as usize;
158
159 let bos_token_id = meta.get_u64("tokenizer.ggml.bos_token_id").unwrap_or(0);
160 let eos_token_id = meta.get_u64("tokenizer.ggml.eos_token_id").unwrap_or(0);
161
162 let tie_word_embeddings = !inputs.has_output_weight;
163
164 let num_experts = if arch == "qwen3moe" {
165 req_u64("expert_count")? as usize
166 } else {
167 meta.get_u64(&k("expert_count"))
168 .map(|v| v as usize)
169 .unwrap_or(0)
170 };
171 if arch == "qwen3moe" && num_experts == 0 {
172 bail!("GGUF metadata key '{arch}.expert_count' must be greater than zero");
173 }
174
175 let mut body: Map<String, Value> = json!({
176 "hidden_size": hidden_size,
177 "num_hidden_layers": num_hidden_layers,
178 "intermediate_size": intermediate_size,
179 "vocab_size": vocab_size,
180 "num_attention_heads": num_attention_heads,
181 "num_key_value_heads": num_key_value_heads,
182 "head_dim": head_dim,
183 "rms_norm_eps": rms_norm_eps,
184 "rope_theta": rope_theta,
185 "max_position_embeddings": max_position_embeddings,
186 "bos_token_id": bos_token_id,
187 "eos_token_id": eos_token_id,
188 "tie_word_embeddings": tie_word_embeddings,
189 "model_type": model_type,
190 })
191 .as_object()
192 .expect("json! object literal")
193 .clone();
194
195 if num_experts > 0 {
196 let experts_per_tok = req_u64("expert_used_count").with_context(|| {
197 format!("GGUF: MoE arch '{arch}' has expert_count>0 but no '{arch}.expert_used_count'")
198 })? as usize;
199 let moe_ffn = req_u64("expert_feed_forward_length").with_context(|| {
200 format!("GGUF: MoE arch '{arch}' missing '{arch}.expert_feed_forward_length'")
201 })? as usize;
202 if experts_per_tok == 0 || experts_per_tok > num_experts {
203 bail!(
204 "GGUF metadata key '{arch}.expert_used_count' must be in 1..={num_experts}, \
205 found {experts_per_tok}"
206 );
207 }
208 if moe_ffn == 0 {
209 bail!(
210 "GGUF metadata key '{arch}.expert_feed_forward_length' must be greater than zero"
211 );
212 }
213 body.insert("num_experts".into(), json!(num_experts));
214 body.insert("num_experts_per_tok".into(), json!(experts_per_tok));
215 body.insert("moe_intermediate_size".into(), json!(moe_ffn));
216 }
217
218 if let Some(sw) = meta.get_u64(&k("attention.sliding_window")) {
219 body.insert("sliding_window".into(), json!(sw));
220 }
221
222 let raw = Value::Object(body);
223 let json_str = serde_json::to_string(&raw).context("serialize synthesized GGUF config")?;
224 let mut config: ModelConfig =
225 serde_json::from_str(&json_str).context("deserialize synthesized GGUF config")?;
226
227 config.model_type = model_type.to_string();
228 config.attn_gated = attn_gated;
229 config.weight_prefix = "model".to_string();
233
234 if model_type == "gemma4" {
235 config.embed_scale = (hidden_size as f32).sqrt();
236 config.final_logit_softcapping = match meta.get_f64(&k("final_logit_softcapping")) {
237 Some(v) if v >= 0.0 && v <= f32::MAX as f64 => v as f32,
238 Some(v) => bail!(
239 "GGUF metadata key '{}.final_logit_softcapping' must be non-negative and representable as a finite f32 (got {v})",
240 arch
241 ),
242 None => 0.0,
243 };
244 }
245
246 if model_type == "deepseek_v41" {
250 let req = |suffix: &str| -> Result<u64> {
251 meta.get_u64(&k(suffix))
252 .with_context(|| format!("DeepSeek-V4.1 GGUF missing '{arch}.{suffix}'"))
253 };
254 let req_arr = |suffix: &str| -> Result<Vec<u64>> {
255 meta.get_u64_arr(&k(suffix))
256 .with_context(|| format!("DeepSeek-V4.1 GGUF missing array '{arch}.{suffix}'"))
257 };
258
259 config.q_lora_rank = req("attention.q_lora_rank")? as usize;
260 config.o_lora_rank = req("attention.output_lora_rank")? as usize;
261 config.o_groups = req("attention.output_group_count")? as usize;
262 config.kv_lora_rank = req("attention.value_length")? as usize;
263 config.rotary_dim = req("rope.dimension_count")? as usize;
264
265 config.index_n_heads = req("attention.indexer.head_count")? as usize;
266 config.index_head_dim = req("attention.indexer.key_length")? as usize;
267 config.index_topk = req("attention.indexer.top_k")? as usize;
268 config.compress_ratios = req_arr("attention.compress_ratios")?
269 .into_iter()
270 .map(|v| v as usize)
271 .collect();
272 if config.compress_ratios.len() < num_hidden_layers {
275 bail!(
276 "DeepSeek-V4.1: '{arch}.attention.compress_ratios' has {} entries, fewer than \
277 block_count ({num_hidden_layers})",
278 config.compress_ratios.len()
279 );
280 }
281 config.compress_rope_theta = meta
282 .get_f64(&k("attention.compress_rope_freq_base"))
283 .context("DeepSeek-V4.1 GGUF missing 'attention.compress_rope_freq_base'")?
284 as f32;
285
286 config.hc_mult = req("hyper_connection.count")? as usize;
287 config.hc_sinkhorn_iters = req("hyper_connection.sinkhorn_iterations")? as usize;
288 config.hc_eps = meta
289 .get_f64(&k("hyper_connection.epsilon"))
290 .context("DeepSeek-V4.1 GGUF missing 'hyper_connection.epsilon'")?
291 as f32;
292
293 config.n_routed_experts = num_experts;
294 config.norm_topk_prob = req("expert_weights_norm")? != 0;
295 config.routed_scaling_factor = meta
296 .get_f64(&k("expert_weights_scale"))
297 .context("DeepSeek-V4.1 GGUF missing 'expert_weights_scale'")?;
298 config.scoring_func = match req("expert_gating_func")? {
299 4 => "sqrtsoftplus".to_string(),
300 other => bail!(
301 "DeepSeek-V4.1: unsupported '{arch}.expert_gating_func' ({other}); \
302 only 4 (sqrt-softplus) is implemented"
303 ),
304 };
305 config.num_hash_layers = meta.get_u64(&k("hash_layer_count")).unwrap_or(0) as usize;
306
307 if let Some(clamps) = meta.get_f64_arr(&k("swiglu_clamp_exp")) {
310 let first = clamps.first().copied().unwrap_or(0.0);
311 if clamps.iter().any(|v| (v - first).abs() > f64::EPSILON) {
312 bail!(
313 "DeepSeek-V4.1: '{arch}.swiglu_clamp_exp' is not uniform across layers; \
314 ModelConfig carries a single `swiglu_limit`"
315 );
316 }
317 config.swiglu_limit = first as f32;
318 }
319
320 config.engram_layer_ids = req_arr("engram.layer_ids")?
321 .into_iter()
322 .map(|v| v as usize)
323 .collect();
324 if !config.engram_layer_ids.is_empty() {
325 config.engram_max_ngram_size = req("engram.max_ngram_size")? as usize;
326 config.engram_n_heads = req("engram.head_count")? as usize;
327 config.engram_head_dim = req("engram.key_length")? as usize;
328 config.engram_pad_token_id = req("engram.pad_id")? as u32;
329 config.engram_multipliers = req_arr("engram.multipliers")?;
330 config.engram_primes = req_arr("engram.primes")?;
331 config.engram_offsets = req_arr("engram.offsets")?;
332 if meta.get_arr_len(&k("engram.token_map")).unwrap_or(0) != vocab_size {
333 bail!(
334 "DeepSeek-V4.1: '{arch}.engram.token_map' length does not match vocab_size \
335 ({vocab_size})"
336 );
337 }
338 }
339 }
340
341 finalize_config(&mut config, &raw)?;
342 super::resolve_attn_position_encoding(&mut config)?;
343 Ok(config)
344}
345
346#[cfg(test)]
347mod tests;