Skip to main content

ferrox_models/
loader.rs

1//! Loads a real `Decoder` from an on-disk GGUF file, using the
2//! llama.cpp-style tensor naming convention
3//! (`token_embd.weight`, `blk.N.attn_q.weight`, `blk.N.ffn_gate.weight`
4//! or, for MoE, `blk.N.ffn_gate_exps.weight`, `output_norm.weight`,
5//! `output.weight`). Until this module existed, ferrox could only run
6//! correctly-shaped *random* weights.
7//!
8//! Quantized tensors (Q8_0 / Q4_0) are loaded as `WeightMatrix::Quantized`
9//! backed by `WeightBytes::Mapped` -- a zero-copy view into the same
10//! mmap `GgufFile` already holds, with no intermediate heap copy of the
11//! tensor's bytes at all. So a checkpoint's resident memory is the
12//! mmap page cache, not the mmap plus a second in-process copy of every
13//! weight. `WeightMatrix::apply` dispatches to ferrox-quant's fused
14//! dequant+dot kernels directly against those mapped bytes at inference
15//! time. F32 tensors (norms, embeddings, and any weight not natively
16//! quantized) still copy into an owned `Tensor`, since they're small
17//! relative to the quantized weight matrices and need per-element
18//! access patterns a raw byte view doesn't support as cleanly.
19//!
20//! Verified end to end (see `crates/ferrox-models/tests/gguf_roundtrip.rs`)
21//! against a genuinely Q8_0-quantized, generated on-disk GGUF fixture
22//! for the dense (single-expert) case, and against real OLMoE / Qwen2-MoE
23//! checkpoints for the multi-expert 3D-packed-tensor path.
24
25use ferrox_core::expert_store::{ExpertKey, ExpertSource, ExpertStore};
26use ferrox_core::tensor::Tensor;
27use ferrox_core::weight_matrix::quant_kind_for;
28use ferrox_core::weight_matrix::{QuantKind, WeightBytes, WeightMatrix};
29use ferrox_gguf::{GgmlType, GgufError, GgufValue, ShardedGguf, TensorInfo, TensorSource};
30use ferrox_moe::{ExpertWeights, GatingFunction, MoeLayerConfig};
31use std::sync::Arc;
32use thiserror::Error;
33
34use crate::config::ModelConfig;
35#[cfg(feature = "metal")]
36use crate::decoder::MoePackedQ4Planes;
37use crate::decoder::{AttnWeights, Decoder, ExpertBacking, LayerWeights, MoeWeights};
38
39#[derive(Debug, Error)]
40pub enum LoadError {
41    #[error(transparent)]
42    Gguf(#[from] GgufError),
43    #[error(transparent)]
44    Shard(#[from] ferrox_gguf::ShardError),
45    #[error("tensor '{0}' has unsupported dtype {1:?}")]
46    UnsupportedDtype(String, GgmlType),
47    #[error(
48        "MoE tensor '{0}' is not 3D or its expert count {1} does not match config n_experts {2}"
49    )]
50    ExpertCountMismatch(String, usize, usize),
51    #[error("GGUF file is missing required hparam metadata key '{0}'")]
52    MissingHparam(String),
53    /// `general.architecture` is not in the capability registry -- refuse
54    /// to guess RoPE/gating rather than emit fluent-but-wrong logits.
55    #[error(
56        "unsupported GGUF architecture '{0}': not in ferrox's capability registry \
57         (unknown required features fail closed; see ferrox_models::capability)"
58    )]
59    UnsupportedArchitecture(String),
60    /// Architecture exists but must not use the generic GQA decoder.
61    #[error("architecture '{0}' cannot use the generic Decoder: {1}")]
62    DedicatedArchitectureRequired(String, &'static str),
63    /// Metadata advertises a feature the generic decoder does not implement.
64    #[error("architecture '{0}' requires unimplemented feature: {1}")]
65    UnsupportedFeature(String, String),
66    #[error(
67        "architecture '{0}' has never been verified against llama.cpp. It would run on \
68         ferrox's shared generic-GQA path, which ASSUMES plain GQA with {1:?} RoPE and no \
69         ALiBi, no learned position embeddings and no per-layer rope skipping. That \
70         assumption has already been wrong for gpt2, mpt, refact, bloom and jais, each of \
71         which loaded clean and answered as a different model. {2} Set \
72         FERROX_ALLOW_UNAUDITED_ARCH=1 to run it anyway and compare the output against \
73         llama.cpp yourself"
74    )]
75    UnauditedArchitecture(String, crate::config::RopeLayout, String),
76    /// The checkpoint carries per-block tensors this build never reads,
77    /// i.e. weights that contribute to the real graph and would simply
78    /// be missing from ours. See [`assert_every_tensor_consumed`].
79    #[error(
80        "checkpoint carries {0} tensor(s) this build never reads, so its graph is not the one \
81         ferrox would run: {1}. This is a missing feature, not a corrupt file. Override with \
82         FERROX_ALLOW_UNKNOWN_TENSORS=1 to load anyway and accept wrong output."
83    )]
84    UnconsumedTensors(usize, String),
85    /// `FERROX_STRICT_KERNELS=1` and the model has weights with no
86    /// kernel on the selected accelerator, i.e. it would run, correctly,
87    /// on a silently slower path. Refusing is the point: a benchmark or
88    /// CI run must not be able to publish a number taken off the
89    /// backend it claims. See [`ferrox_core::kernel_registry`].
90    #[error("{0}")]
91    StrictKernels(String),
92}
93
94/// Architecture-family name strings (GGUF's `general.architecture` value)
95/// known, from reading ik_llama.cpp's `llama-hparams.cpp`
96/// (`LLM_ARCH_DEEPSEEK2`, `LLM_ARCH_GLM4_MOE` cases), to default to
97/// sigmoid MoE gating with post-selection renormalization rather than
98/// softmax. Every member's citation is inline here; `docs/MODELS.md`
99/// carries none and the pointer that used to send readers there was
100/// dangling.
101/// `afmoe`, `laguna` and `step35` added 2026-09-01 by the
102/// unaudited-refusal triage's gating sweep. Each reads
103/// `LLM_KV_EXPERT_GATING_FUNC` as OPTIONAL and then, when the key is
104/// absent, sets `LLAMA_EXPERT_GATING_FUNC_TYPE_SIGMOID`
105/// (`afmoe.cpp:29-30`, `laguna.cpp:55-56`, `step35.cpp:19-20`). Ferrox
106/// fell back to softmax for all three.
107///
108/// This is the `deepseek` shape a third, fourth and fifth time: a
109/// default that is right for most architectures and silently wrong for
110/// one, where the GGUF carries no key to correct it. Nothing is live
111/// today -- all three are `NewCode` for other reasons and refuse before
112/// reaching here -- but the list is what a later admission would trust.
113const SIGMOID_GATING_ARCHITECTURES: &[&str] =
114    &["afmoe", "deepseek2", "glm4moe", "laguna", "step35"];
115
116/// Names that appear in a behaviour table above but are `DedicatedOnly`
117/// or `Deferred`, together with the module that actually applies the
118/// behaviour for them.
119///
120/// Two true things were in conflict here, and deleting either would
121/// have lost one. `SIGMOID_GATING_ARCHITECTURES` records a fact about
122/// llama.cpp (these architectures default to sigmoid when the GGUF
123/// carries no `expert_gating_func`), and a test pins it as such. The
124/// cross-table test records a different fact: an entry for an
125/// architecture that never reaches THIS loader cannot fire, and a gate
126/// that cannot fire is worse than no gate because it reads as coverage.
127///
128/// Both hold. `deepseek2` and `glm4moe` are genuinely sigmoid-gated and
129/// genuinely never arrive here. So the resolution is not to drop a name
130/// from either place, it is to say out loud who owns it instead, and to
131/// make an unexplained dead entry still fail.
132///
133/// Adding a name here is a claim that the named module applies the
134/// behaviour. It is checked no further than that, so it is the one line
135/// in this file to be suspicious of.
136/// Test-only: it asserts a relationship rather than driving one, and a
137/// production reader would have to be told that.
138#[cfg(test)]
139const DEDICATED_OWNS_ITS_BEHAVIOUR: &[(&str, &str)] = &[
140    // `mla_gguf_loader` reads `expert_gating_func` and falls back to
141    // Sigmoid itself, so deepseek2's gating is decided there.
142    ("deepseek2", "mla_gguf_loader"),
143    // glm4moe is refused today (it needs gpt-oss's norm slot, see its
144    // refusal text). The entry stays because the fact about llama.cpp
145    // stays true, and it becomes live the moment the refusal lifts.
146    ("glm4moe", "refused today, see capability::unaudited_triage"),
147];
148
149/// Architecture-family names whose real reference implementation skips
150/// renormalizing top-k softmax routing weights after selection (GGUF
151/// carries no metadata key for this -- it's hardcoded per-architecture in
152/// both the real HF `transformers` model code and llama.cpp's
153/// `build_moe_ffn` call sites, not read from the file). Confirmed for
154/// `olmoe` against `OlmoeTopKRouter.forward` in
155/// `transformers/models/olmoe/modeling_olmoe.py` (`config.norm_topk_prob`
156/// is `false` in the real published config.json) and llama.cpp's
157/// `src/models/olmoe.cpp` (`build_moe_ffn(..., false, ...,
158/// LLAMA_EXPERT_GATING_FUNC_TYPE_SOFTMAX, ...)`). See
159/// `MoeLayerConfig::norm_topk_prob`'s doc comment for why this matters:
160/// getting it wrong silently produces wrong generation output even
161/// though the file loads and shape-validates fine.
162// Architectures whose reference graphs pass `norm_w=false` to
163// `build_moe_ffn` (llama.cpp) / `norm_topk_prob=false` in HF config.
164// Qwen2-MoE: `.scratch/llama.cpp/src/models/qwen2moe.cpp` -- Softmax +
165// `false` for the norm_topk slot. Renormalizing top-k weights made
166// Qwen1.5-MoE greedy decode emit garbage despite shared-expert load.
167// `deepseek` (V1) added 2026-09-01 by the unaudited-refusal triage.
168// `src/models/deepseek.cpp:145-155` passes `norm_w=false`, and
169// `conversion/deepseek.py`'s `DeepseekModel` never writes
170// `{arch}.expert_weights_norm` -- only `DeepseekV2Model` does -- so no
171// real `deepseek` GGUF carries the key to override the default with.
172// Ferrox therefore renormalised where llama.cpp does not. Same class of
173// bug as the OLMoE one above, and latent only because `deepseek` is
174// unaudited and refuses first.
175const NO_TOPK_RENORMALIZE_ARCHITECTURES: &[&str] = &["deepseek", "olmoe", "qwen2moe"];
176
177/// Architectures that store their **pre-FFN** norm under the tensor name
178/// `blk.N.post_attention_norm.weight` and carry no `blk.N.ffn_norm`.
179///
180/// Gemma writes the same tensor name for a genuinely different norm: it
181/// is applied to the attention output *inside* the attention residual,
182/// and Gemma also carries `ffn_norm`. Reading one file's tensor with the
183/// other's meaning silently moves a whole RMSNorm to the wrong side of a
184/// residual add, so the meaning is decided by architecture, not by which
185/// tensors happen to be present.
186///
187/// - `gpt-oss`: `openai-moe.cpp` norms `ffn_inp` with `attn_post_norm`.
188/// - `seed_oss`: `src/models/seed-oss.cpp:36-37` creates `attn_norm` and
189///   `attn_post_norm` and **no** `ffn_norm`, and `:113-115` norms
190///   `ffn_inp` -- the post-attention residual -- with `attn_post_norm`.
191///
192/// This is deliberately NOT `arch == "gpt-oss"`, which is what it used
193/// to be. That one flag also gated gpt-oss's five extra per-layer
194/// tensors (sinks, biases, the SwiGLU clamp), and widening it would have
195/// handed `seed_oss` attention sinks it does not have. Two facts, two
196/// predicates. `the_norm_slot_list_and_the_audit_list_agree` pins that a
197/// name added here is a name somebody actually read a graph for.
198const PRE_FFN_NORM_IS_POST_ATTENTION_NORM: &[&str] = &["gpt-oss", "seed_oss"];
199
200/// Does this architecture keep its pre-FFN norm in the
201/// `post_attention_norm` slot? See
202/// [`PRE_FFN_NORM_IS_POST_ATTENTION_NORM`].
203fn pre_ffn_norm_is_post_attention_norm(arch: &str) -> bool {
204    PRE_FFN_NORM_IS_POST_ATTENTION_NORM.contains(&arch)
205}
206
207/// Architectures whose checkpoints carry `{arch}.leading_dense_block_count`
208/// while their reference graph never branches on it: **every** layer is
209/// MoE regardless of what the key says.
210///
211/// `bailingmoe` is the case this list exists for.
212/// `src/models/bailingmoe.cpp:5` reads
213/// `LLM_KV_LEADING_DENSE_BLOCK_COUNT` into `n_layer_dense_lead` and then
214/// `load_arch_tensors` creates `ffn_gate_inp`, the expert tensors and
215/// the shared-expert tensors unconditionally for every layer (:39-54 --
216/// there is no `if (i < n_layer_dense_lead)` anywhere in the file) and
217/// the graph has no dense branch either (:119-152). Meanwhile
218/// `conversion/bailingmoe.py:27` writes `first_k_dense_replace` into the
219/// key verbatim, so real Ling checkpoints DO carry a nonzero value.
220///
221/// Ferrox's `ModelConfig::layer_is_dense` does branch on it, so without
222/// this list ferrox looks for `blk.0.ffn_gate.weight` on a layer that
223/// only ships experts and dies on a missing tensor. That is a load
224/// failure rather than wrong logits, which is why it stayed latent.
225///
226/// Do not read this as "the key is meaningless": for `deepseek`,
227/// `dots1`, `glm4moe` and every other leading-dense architecture the key
228/// is load-bearing and must be honoured. Membership here is a statement
229/// about ONE architecture's graph, checked in that graph.
230const LEADING_DENSE_KEY_IS_INERT: &[&str] = &["bailingmoe"];
231
232/// Architectures whose reference graph applies `attn_q_norm` /
233/// `attn_k_norm` AFTER `ggml_rope_ext`, not before it.
234///
235/// There is no GGUF key for this. llama.cpp writes the order into each
236/// hand-written graph, so the only place it can come from is the
237/// architecture string, and getting it wrong changes every layer's
238/// attention scores without changing a single tensor shape.
239///
240/// - `maincoder`: `src/models/maincoder.cpp:78-90` ropes Q and K, then
241///   norms them at `:92` and `:95`.
242/// - `hunyuan-moe`: `src/models/hunyuan-moe.cpp:93,104` rope, `:110,115`
243///   norm.
244///
245/// The audited majority is the other way round -- `qwen3moe.cpp:99,108`
246/// and `bailingmoe2.cpp:123-135` both norm first -- which is why the
247/// decoder's default is "before" and this list is the exception.
248/// `hunyuan-dense` shares the ordering but is NOT here: it has a second
249/// blocker (`{arch}.rope.scaling.alpha`) and stays refusing.
250const QK_NORM_AFTER_ROPE_ARCHITECTURES: &[&str] = &["hunyuan-moe", "maincoder"];
251
252fn metadata_u64_any(file: &impl TensorSource, keys: &[String]) -> Option<u64> {
253    keys.iter().find_map(|k| file.metadata_u64(k))
254}
255
256fn metadata_f32_any(file: &impl TensorSource, keys: &[String]) -> Option<f32> {
257    keys.iter()
258        .find_map(|k| file.metadata(k).and_then(GgufValue::as_f32))
259}
260
261impl ModelConfig {
262    /// Derives a `ModelConfig` from a real GGUF file's own hyperparameter
263    /// metadata, following llama.cpp's `general.architecture`-prefixed key
264    /// convention (`{arch}.block_count`, `{arch}.embedding_length`,
265    /// `{arch}.attention.head_count`, `{arch}.expert_count`, ...) rather
266    /// than requiring a hand-written preset to already match the file's
267    /// shape exactly. This is what lets `ferrox-server` (and `ferrox
268    /// run-real`) load an arbitrary checkpoint, not just the three
269    /// hand-tuned presets in `config.rs`.
270    ///
271    /// Fields with no corresponding metadata key fall back to widely-used
272    /// llama.cpp defaults (documented inline) and are listed in the
273    /// returned config's `best_effort_fields`, following the same
274    /// confirmed-vs-estimated discipline as the hand-written presets.
275    pub fn from_gguf(file: &impl TensorSource) -> Result<Self, LoadError> {
276        let arch = file
277            .metadata_str("general.architecture")
278            .ok_or_else(|| LoadError::MissingHparam("general.architecture".to_string()))?
279            .to_string();
280        let arch_profile = crate::capability::resolve_profile(&arch)
281            .ok_or_else(|| LoadError::UnsupportedArchitecture(arch.clone()))?;
282        let rope_layout = match arch_profile.path {
283            crate::capability::ArchPath::GenericGqa { rope }
284            | crate::capability::ArchPath::TestFixture { rope } => rope,
285            crate::capability::ArchPath::DedicatedOnly { reason } => {
286                return Err(LoadError::DedicatedArchitectureRequired(
287                    arch.clone(),
288                    reason,
289                ));
290            }
291            crate::capability::ArchPath::Deferred { reason } => {
292                return Err(LoadError::UnsupportedFeature(
293                    arch.clone(),
294                    format!("architecture deferred from Ferrox text-generation scope: {reason}"),
295                ));
296            }
297        };
298        let qk_norm_style = arch_profile.qk_norm;
299        for (meta_key, feature) in crate::capability::unsupported_feature_keys(&arch) {
300            if let Some(v) = metadata_f32_any(file, std::slice::from_ref(&meta_key)) {
301                if v > 0.0 {
302                    return Err(LoadError::UnsupportedFeature(
303                        arch.clone(),
304                        format!("{feature} (metadata {meta_key}={v})"),
305                    ));
306                }
307            }
308            if let Some(v) = metadata_u64_any(file, std::slice::from_ref(&meta_key)) {
309                if v > 0 {
310                    return Err(LoadError::UnsupportedFeature(
311                        arch.clone(),
312                        feature.to_string(),
313                    ));
314                }
315            }
316        }
317        // Metadata-declared multipliers the generic decoder does not
318        // apply. Unlike the tensor-consumption gate, nothing about these
319        // is visible in the weights, so a Granite checkpoint would load
320        // and answer at the wrong scale. See
321        // `capability::unsupported_scaling_keys`.
322        for (meta_key, feature, no_op) in crate::capability::unsupported_scaling_keys(&arch) {
323            if let Some(v) = metadata_f32_any(file, std::slice::from_ref(&meta_key)) {
324                if (v - no_op).abs() > 1e-6 {
325                    return Err(LoadError::UnsupportedFeature(
326                        arch.clone(),
327                        format!("{feature} (metadata {meta_key}={v})"),
328                    ));
329                }
330            }
331        }
332        let key = |suffix: &str| format!("{arch}.{suffix}");
333
334        let name: &'static str = Box::leak(
335            file.metadata_str("general.name")
336                .unwrap_or(&arch)
337                .to_string()
338                .into_boxed_str(),
339        );
340
341        let n_layers =
342            file.metadata_u64(&key("block_count"))
343                .ok_or_else(|| LoadError::MissingHparam(key("block_count")))? as usize;
344        // Baichuan is one architecture string covering two positional
345        // schemes: 7B rotates, 13B uses ALiBi and no RoPE at all
346        // (`src/models/baichuan.cpp:11-14`, `:57-58`, where `inp_pos` is
347        // `nullptr` for 13B, so `ggml_rope_ext` is never reached).
348        // llama.cpp decides that on the layer count and says so in a
349        // comment: "TODO: become GGUF KV parameter". There is therefore
350        // no key for `capability::unsupported_feature_keys` to test and
351        // no tensor for `assert_every_tensor_consumed` to miss. A
352        // Baichuan-13B checkpoint loads clean and is rotated anyway.
353        // Refuse it here, where the layer count is known.
354        if arch == "baichuan" && n_layers == 40 {
355            return Err(LoadError::UnsupportedFeature(
356                arch.clone(),
357                "Baichuan-13B (block_count=40) uses ALiBi and no RoPE, decided by layer \
358                 count with no GGUF key to declare it; the generic decoder would rotate \
359                 every Q/K head instead. Baichuan-7B (block_count=32) is unaffected"
360                    .to_string(),
361            ));
362        }
363        let hidden_dim = file
364            .metadata_u64(&key("embedding_length"))
365            .ok_or_else(|| LoadError::MissingHparam(key("embedding_length")))?
366            as usize;
367        let n_heads = file
368            .metadata_u64(&key("attention.head_count"))
369            .ok_or_else(|| LoadError::MissingHparam(key("attention.head_count")))?
370            as usize;
371
372        let mut best_effort_fields: Vec<&'static str> = Vec::new();
373
374        let n_kv_heads = file
375            .metadata_u64(&key("attention.head_count_kv"))
376            .map(|v| v as usize)
377            .unwrap_or_else(|| {
378                best_effort_fields.push("n_kv_heads (no attention.head_count_kv key; assumed equal to n_heads, i.e. plain MHA)");
379                n_heads
380            });
381        let head_dim = file
382            .metadata_u64(&key("attention.key_length"))
383            .map(|v| v as usize)
384            .unwrap_or_else(|| {
385                best_effort_fields.push(
386                    "head_dim (no attention.key_length key; derived as hidden_dim / n_heads)",
387                );
388                hidden_dim / n_heads
389            });
390        let v_head_dim = file
391            .metadata_u64(&key("attention.value_length"))
392            .map(|v| v as usize)
393            .unwrap_or(head_dim);
394        if v_head_dim != head_dim {
395            return Err(LoadError::UnsupportedFeature(
396                arch.clone(),
397                format!(
398                    "split K/V head dims (key_length={head_dim}, value_length={v_head_dim}); \
399                     generic decoder requires equal head dims"
400                ),
401            ));
402        }
403        let vocab_size = file
404            .metadata("tokenizer.ggml.tokens")
405            .and_then(|v| match v {
406                GgufValue::Array(items) => Some(items.len()),
407                _ => None,
408            })
409            .or_else(|| file.metadata_u64(&key("vocab_size")).map(|v| v as usize))
410            .unwrap_or_else(|| {
411                best_effort_fields.push("vocab_size (no tokenizer.ggml.tokens array or {arch}.vocab_size key; fell back to output.weight's own row count)");
412                // `output.weight`'s real raw shape is `[hidden_dim,
413                // vocab_size]` (ggml's fastest-first `ne[]` order --
414                // see `load_weight_matrix`'s doc comment), so vocab_size
415                // is the *last* element, not the first.
416                file.find_tensor("output.weight")
417                    .and_then(|t| t.shape.last().copied())
418                    .unwrap_or(0) as usize
419            });
420        let rope_theta = metadata_f32_any(file, &[key("rope.freq_base")]).unwrap_or_else(|| {
421            best_effort_fields.push("rope_theta (no rope.freq_base key; defaulted to 10000.0)");
422            10000.0
423        });
424        let rms_norm_eps = metadata_f32_any(
425            file,
426            &[
427                key("attention.layer_norm_rms_epsilon"),
428                key("attention.layer_norm_epsilon"),
429            ],
430        )
431        .unwrap_or_else(|| {
432            best_effort_fields
433                .push("rms_norm_eps (no layer_norm_rms_epsilon key; defaulted to 1e-5)");
434            1e-5
435        });
436
437        let n_experts = metadata_u64_any(file, &[key("expert_count")]).unwrap_or(0) as usize;
438        let is_moe = n_experts > 1;
439
440        let n_experts_active = if is_moe {
441            metadata_u64_any(file, &[key("expert_used_count")]).unwrap_or_else(|| {
442                best_effort_fields
443                    .push("moe.n_experts_active (no expert_used_count key; defaulted to 2)");
444                2
445            }) as usize
446        } else {
447            1
448        };
449        // Prefer the GGUF hparam when present. Qwen2MoE (and some other
450        // HF→GGUF exports) omit `expert_shared_count` but still ship
451        // `blk.N.ffn_{gate,up,down}_shexp.weight` -- without a tensor-
452        // presence fallback those weights are silently dropped and the
453        // model runs with a large chunk of active FFN missing.
454        let n_shared_experts = match metadata_u64_any(file, &[key("expert_shared_count")]) {
455            Some(n) => n as usize,
456            None if is_moe && file.find_tensor("blk.0.ffn_gate_shexp.weight").is_some() => {
457                best_effort_fields.push(
458                    "moe.n_shared_experts (no expert_shared_count; inferred 1 from blk.0.ffn_gate_shexp.weight)",
459                );
460                1
461            }
462            None => 0,
463        };
464        // MoE GGUFs often only set `feed_forward_length` (OLMoE=1024,
465        // Qwen2-MoE=5632 for the shared expert). `expert_feed_forward_length`
466        // is optional. llama.cpp `qwen2moe.cpp` uses
467        // `n_ff_exp = n_ff_exp ? n_ff_exp : n_ff / n_expert_used` (1408 for
468        // Qwen1.5-MoE); the shared expert keeps the full `n_ff` (5632).
469        let feed_forward_length = metadata_u64_any(file, &[key("feed_forward_length")]);
470        let expert_ffn_dim = metadata_u64_any(file, &[key("expert_feed_forward_length")])
471            .or_else(|| {
472                feed_forward_length.map(|ff| {
473                    if is_moe && n_experts_active > 0 {
474                        ff / n_experts_active as u64
475                    } else {
476                        ff
477                    }
478                })
479            })
480            .unwrap_or_else(|| {
481                best_effort_fields.push(
482                    "moe.expert_ffn_dim (no expert_feed_forward_length/feed_forward_length; defaulted to 4x hidden_dim)",
483                );
484                (hidden_dim * 4) as u64
485            }) as usize;
486        let n_dense_leading_layers = if LEADING_DENSE_KEY_IS_INERT.contains(&arch.as_str()) {
487            0
488        } else {
489            metadata_u64_any(file, &[key("leading_dense_block_count")]).unwrap_or(0) as usize
490        };
491
492        // ik_llama.cpp's real gating-function hparam
493        // (LLM_KV_EXPERT_GATING_FUNC: 1=softmax, 2=sigmoid) if the file
494        // carries it; otherwise fall back to the same architecture-name
495        // convention the hand-written presets in config.rs use (see
496        // docs/MODELS.md for the citations behind that list).
497        let gating = match metadata_u64_any(file, &[key("expert_gating_func")]) {
498            Some(2) => GatingFunction::Sigmoid,
499            Some(1) => GatingFunction::Softmax,
500            _ => {
501                if SIGMOID_GATING_ARCHITECTURES.contains(&arch.as_str()) {
502                    GatingFunction::Sigmoid
503                } else {
504                    if is_moe {
505                        best_effort_fields.push(
506                            "moe.gating (no expert_gating_func key and architecture not in the known-sigmoid list; defaulted to softmax)",
507                        );
508                    }
509                    GatingFunction::Softmax
510                }
511            }
512        };
513
514        // `{arch}.expert_weights_norm` (llama.cpp
515        // `LLM_KV_EXPERT_WEIGHTS_NORM`) is the real metadata key for
516        // whether the selected experts' weights are renormalised. Most
517        // checkpoints do not carry it, which is why the fallback below
518        // exists at all -- but when one does, the file's own answer wins
519        // over an architecture-name guess.
520        let norm_topk_prob = match file.metadata_bool(&key("expert_weights_norm")) {
521            Some(v) => v,
522            None => {
523                // See `NO_TOPK_RENORMALIZE_ARCHITECTURES`'s doc comment:
524                // an architecture-name lookup, the same convention
525                // `gating`'s fallback above uses.
526                if is_moe && matches!(gating, GatingFunction::Softmax) {
527                    best_effort_fields.push(
528                        "moe.norm_topk_prob (no expert_weights_norm key; defaulted by architecture-name lookup against NO_TOPK_RENORMALIZE_ARCHITECTURES)",
529                    );
530                }
531                !NO_TOPK_RENORMALIZE_ARCHITECTURES.contains(&arch.as_str())
532            }
533        };
534
535        // `{arch}.expert_weights_scale` (`LLM_KV_EXPERT_WEIGHTS_SCALE`).
536        // llama.cpp's `build_moe_ffn` skips the multiply for both 0.0 and
537        // 1.0, so both mean "no scaling" and both land on 1.0 here.
538        let expert_weights_scale = metadata_f32_any(file, &[key("expert_weights_scale")])
539            .filter(|s| *s != 0.0)
540            .unwrap_or(1.0);
541
542        // Real GGUF key (`{arch}.attention.sliding_window`, confirmed
543        // against `gguf-py/gguf/constants.py`'s real
544        // `LLM_KV_ATTENTION_SLIDING_WINDOW`). Some checkpoints
545        // (confirmed for real published Qwen1.5-MoE/Qwen2-MoE GGUFs)
546        // carry a nonzero window value even when the model's own
547        // config disables sliding-window attention entirely
548        // (`use_sliding_window: false`) -- llama.cpp's own convention
549        // is that a window of 0 means "unused," so only a real nonzero
550        // value here is treated as active.
551        let sliding_window = metadata_u64_any(file, &[key("attention.sliding_window")])
552            .map(|v| v as usize)
553            .filter(|&w| w > 0)
554            // `phi3` declares a window that llama.cpp deliberately does
555            // NOT honour -- see `capability::swa_disabled_by_arch`. This
556            // has to drop the window rather than pick a period, because
557            // upstream is declining to use the file's value, not
558            // choosing a different one.
559            .filter(|_| !crate::capability::swa_disabled_by_arch(&arch));
560
561        // Gemma alternating SWA period (`attention.sliding_window_pattern`).
562        // llama.cpp: gemma2 defaults period=2, gemma3 defaults period=6 when
563        // the pattern key is absent. A missing key must NOT mean "all SWA".
564        //
565        // The metadata key overrides the PERIOD only. The phase is a
566        // property of the architecture in llama.cpp -- `dense_first` is
567        // an argument to `set_swa_pattern`, not a GGUF key -- so it
568        // comes from the registry either way.
569        let swa_layout = crate::capability::default_swa_layout(&arch);
570        let swa_dense_first = swa_layout.is_some_and(|p| p.dense_first);
571        let swa_pattern = metadata_u64_any(file, &[key("attention.sliding_window_pattern")])
572            .map(|v| v as usize)
573            .or_else(|| {
574                sliding_window?;
575                // llama.cpp hardcodes the period per architecture and
576                // only lets the metadata key override it, so a missing
577                // key is *not* "every layer windowed" -- see
578                // `capability::default_swa_layout`.
579                swa_layout.map(|p| p.period).or(
580                    // Any Gemma variant not named in the table keeps the
581                    // gemma3+ period rather than going uniform.
582                    match arch_profile.family {
583                        crate::capability::DecoderFamily::GemmaFamily => Some(6),
584                        _ => None,
585                    },
586                )
587            });
588
589        let attn_logit_softcap = metadata_f32_any(
590            file,
591            &[
592                key("attention.logit_softcapping"),
593                key("attn_logit_softcapping"),
594            ],
595        )
596        .filter(|&v| v > 0.0);
597        let final_logit_softcap =
598            metadata_f32_any(file, &[key("final_logit_softcapping")]).filter(|&v| v > 0.0);
599
600        // Gemma: embeddings are scaled by sqrt(hidden_dim) at input.
601        let embedding_scale = if matches!(
602            arch_profile.family,
603            crate::capability::DecoderFamily::GemmaFamily
604        ) {
605            Some((hidden_dim as f32).sqrt())
606        } else {
607            None
608        };
609
610        // llama.cpp's `f_attention_scale`, and ONLY where it differs from
611        // the `1/sqrt(head_dim)` ferrox's attention kernels already
612        // apply -- `Some` here means "pre-scale Q", so restating the
613        // kernels' own scale would double-scale every score.
614        //
615        // For Gemma-2 and Gemma-3 that difference is real at 27B and
616        // nowhere else (`capability::attention_scale_override` carries
617        // the llama.cpp lines). This used to be a hardcoded `None` under
618        // a comment that NAMED the 27B exception without implementing
619        // it, so Gemma-2-27B scored 1.061x and Gemma-3-27B 1.146x too
620        // large on every layer: a sharper softmax than the trained one,
621        // fluent and wrong, with no error.
622        let attention_scale = crate::capability::attention_scale_override(
623            &arch, n_layers, hidden_dim, n_heads, head_dim,
624        );
625
626        // SWA-layer RoPE base. `llama_hparams` defaults it to 10000 and
627        // the Gemma-3 lineage relies on that default; the architectures
628        // in `swa_rope_base_follows_model` instead seed it from the
629        // model's own base before the key can override.
630        let rope_theta_swa = if sliding_window.is_some() {
631            let fallback = if crate::capability::swa_rope_base_follows_model(&arch) {
632                rope_theta
633            } else {
634                10_000.0
635            };
636            Some(
637                metadata_f32_any(
638                    file,
639                    &[key("rope.freq_base_swa"), key("rope_freq_base_swa")],
640                )
641                .unwrap_or(fallback),
642            )
643        } else {
644            None
645        };
646
647        let ffn_activation = match arch_profile.family {
648            // Per-ARCHITECTURE first, because llama.cpp's choice is per
649            // architecture and the family partition does not match it:
650            // `grok` is StandardGqa and passes `LLM_FFN_GELU`.
651            _ if crate::capability::uses_geglu(&arch) => crate::config::FfnActivation::Gelu,
652            crate::capability::DecoderFamily::GemmaFamily => crate::config::FfnActivation::Gelu,
653            crate::capability::DecoderFamily::PhiFamily => {
654                crate::config::FfnActivation::SwigluFused
655            }
656            _ => crate::config::FfnActivation::Swiglu,
657        };
658
659        // Llama 3/3.1/3.2's real per-band RoPE frequency correction: one
660        // model-level tensor (`TENSOR_NOT_REQUIRED`, `TENSOR_DUPLICATED`
661        // for every layer but the first in the real llama.cpp source --
662        // i.e. every layer shares this same array), not per-layer. See
663        // `ferrox_core::attention::apply_rope_with_freq_factors`'s doc
664        // comment for why this matters.
665        let rope_freqs = load_f32_vec_optional(file, "rope_freqs.weight")?;
666
667        // Phi-3/Phi-4 LongRoPE: two per-band factor tensors instead of
668        // Llama's single `rope_freqs.weight`, selected by context size
669        // (llama.cpp `llama_model::get_rope_factors`: `rope_freqs` wins if
670        // present, else `rope_long` when the run's context exceeds
671        // `rope.scaling.original_context_length`, else `rope_short`).
672        //
673        // The selection here uses the checkpoint's own advertised context
674        // length, which is what llama.cpp defaults `n_ctx` to. A run that
675        // caps the context below `original_context_length` should use the
676        // short set; ferrox's config is built before the context size is
677        // known, so that case is not yet handled -- recorded as a
678        // best-effort field rather than silently assumed correct.
679        let rope_orig_ctx = metadata_u64_any(file, &[key("rope.scaling.original_context_length")])
680            .map(|v| v as usize);
681        // `rope_freqs.weight` outranks the LongRoPE pair (llama.cpp
682        // `get_rope_factors` checks it first), so a checkpoint carrying
683        // it never populates these and the runtime re-pick below cannot
684        // overwrite a Llama-3 correction with a Phi one.
685        let (rope_freqs_long, rope_freqs_short) = if rope_freqs.is_some() {
686            (None, None)
687        } else {
688            (
689                load_f32_vec_optional(file, "rope_factors_long.weight")?,
690                load_f32_vec_optional(file, "rope_factors_short.weight")?,
691            )
692        };
693        // Provisional pick from the checkpoint's own advertised context;
694        // `ModelConfig::apply_runtime_context` re-picks once the run's
695        // `--ctx-size` is known, which is the number llama.cpp decides on.
696        let rope_freqs = match (rope_freqs, rope_orig_ctx) {
697            (Some(f), _) => Some(f),
698            (None, Some(orig)) => {
699                let model_ctx = metadata_u64_any(file, &[key("context_length")])
700                    .unwrap_or(orig as u64) as usize;
701                if model_ctx > orig {
702                    rope_freqs_long.clone().or_else(|| rope_freqs_short.clone())
703                } else {
704                    rope_freqs_short.clone().or_else(|| rope_freqs_long.clone())
705                }
706            }
707            (None, None) => None,
708        };
709
710        // Partial rotary: only when the file says the rotary width is
711        // narrower than a head. Equal values mean "whole head", which is
712        // the same thing as `None` and stays `None` so nothing downstream
713        // has to special-case it.
714        let rope_dim = metadata_u64_any(file, &[key("rope.dimension_count")])
715            .map(|d| d as usize)
716            .filter(|d| *d > 0 && *d < head_dim);
717
718        // See `ModelConfig::rope_attn_factor`.
719        let rope_attn_factor = metadata_f32_any(file, &[key("rope.scaling.attn_factor")])
720            .filter(|f| f.is_finite() && *f > 0.0)
721            .unwrap_or(1.0);
722
723        // YaRN long-context scaling. `rope.scaling.attn_factor` above is
724        // only YaRN's *magnitude* term (ggml `rope_yarn`'s `mscale`); the
725        // frequency half -- which bands get interpolated toward the
726        // trained context and which stay extrapolated -- lives in
727        // `rope.scaling.type` + `rope.scaling.factor`, and ferrox read
728        // neither before this. A YaRN checkpoint was therefore roped as
729        // if it declared no scaling at all: right near position 0 and
730        // progressively wrong further in, i.e. the failure that reads as
731        // long-prompt quality decay rather than as a bug.
732        //
733        // The rewrite is folded into `rope_freqs`, the same per-band
734        // divisor array Llama-3's `rope_freqs.weight` supplies (ggml
735        // divides each band's theta by it), so it rides the existing CPU
736        // and Metal RoPE paths unchanged. When a file carries both, the
737        // two corrections compose by multiplication, as they do in
738        // llama.cpp (`ggml_rope_cache_init` divides by `freq_factors`
739        // *and then* runs `rope_yarn`).
740        // Linear scaling, which was silently DROPPED before this.
741        //
742        // `rope.scaling.type = "linear"` with factor s means rotating
743        // position `p/s` instead of `p`. Since the angle is `p * freq`,
744        // that is exactly `p * (freq / s)`, and `rope_freqs` already
745        // divides each band's frequency. So a uniform vector of `s`
746        // expresses it exactly and rides the existing CPU and Metal RoPE
747        // paths unchanged, the same way YaRN does below.
748        //
749        // Before this, the type was compared against "yarn" and anything
750        // else returned None, so a checkpoint declaring linear scaling
751        // with factor 4 loaded and roped at UNSCALED positions where
752        // llama.cpp divides them by 4. It answered as a different model
753        // with no error. Affects the long-context community rescales
754        // (`*-16k`, `*-32k` Llama-2 derivatives).
755
756        // The file's own per-band factors, BEFORE any position-scaling
757        // fold. That is what a sliding layer uses on an architecture
758        // whose SWA layers do not inherit the trained scale -- llama.cpp
759        // keeps the two apart as `freq_factors` (a tensor, the same for
760        // every layer) and `freq_scale` (per layer,
761        // `llama-model.cpp:2033`), while ferrox folds them into one
762        // vector. See `config::RopeFreqs`.
763        let rope_freqs_unscaled = rope_freqs.clone();
764
765        let rope_freqs = match linear_scaling_from_gguf(file, &arch) {
766            None => rope_freqs,
767            Some(factor) => {
768                let rotary_dim = rope_dim.unwrap_or(head_dim);
769                if rotary_dim == 0 || !rotary_dim.is_multiple_of(2) {
770                    best_effort_fields.push(
771                        "rope_freqs (linear scaling declared but the rotary width is odd; \
772                         scaling not applied)",
773                    );
774                    rope_freqs
775                } else {
776                    let linear = vec![factor; rotary_dim / 2];
777                    match rope_freqs {
778                        None => Some(linear),
779                        // Compose by multiplication, as a file carrying
780                        // its own `rope_freqs.weight` tensor and a
781                        // declared linear factor means both.
782                        Some(own) if own.len() == linear.len() => {
783                            Some(own.iter().zip(linear.iter()).map(|(a, b)| a * b).collect())
784                        }
785                        Some(own) => {
786                            best_effort_fields.push(
787                                "rope_freqs (linear scaling declared but the file's own \
788                                 rope_freqs tensor has a different width; scaling not applied)",
789                            );
790                            Some(own)
791                        }
792                    }
793                }
794            }
795        };
796        let rope_freqs = match yarn_scaling_from_gguf(file, &arch, rope_orig_ctx) {
797            None => rope_freqs,
798            Some(scaling) => {
799                let rotary_dim = rope_dim.unwrap_or(head_dim);
800                if rotary_dim == 0 || !rotary_dim.is_multiple_of(2) {
801                    best_effort_fields.push(
802                        "rope_freqs (YaRN declared but the rotary width is odd; scaling not applied)",
803                    );
804                    rope_freqs
805                } else {
806                    let yarn =
807                        ferrox_core::attention::yarn_freq_factors(scaling, rotary_dim, rope_theta);
808                    match rope_freqs {
809                        None => Some(yarn),
810                        Some(own) if own.len() == yarn.len() => {
811                            Some(own.iter().zip(yarn.iter()).map(|(a, b)| a * b).collect())
812                        }
813                        Some(own) => {
814                            best_effort_fields.push(
815                                "rope_freqs (YaRN declared alongside a per-band factor tensor of a \
816                                 different width; the file's own tensor is used unscaled)",
817                            );
818                            Some(own)
819                        }
820                    }
821                }
822            }
823        };
824
825        // The SWA half of the split. llama.cpp defaults
826        // `rope_freq_scale_train_swa` to `1.0f`
827        // (`src/llama-hparams.h:129`) and only the architectures in
828        // `swa_rope_scale_follows_model` assign it from
829        // `rope_freq_scale_train`; `get_rope_freq_scale`
830        // (`llama-model.cpp:2033-2035`) then picks between them per
831        // layer. `gemma3.cpp` is not on that list and its converter
832        // writes the FULL-ATTENTION factor
833        // (`conversion/base.py:1222-1230`), so a Gemma-3 4B/12B/27B was
834        // rotating five layers in six at `p/8` where llama.cpp rotates
835        // at `p`.
836        //
837        // "No scaling" is spelled as an all-ones divisor vector, which
838        // is what dividing by nothing is, so the sliding layers need no
839        // second code path anywhere downstream.
840        let rope_freqs = rope_freqs.map(|full| {
841            let swa = (sliding_window.is_some()
842                && !crate::capability::swa_rope_scale_follows_model(&arch))
843            .then(|| rope_freqs_unscaled.unwrap_or_else(|| vec![1.0; full.len()]))
844            .filter(|swa| *swa != full);
845            crate::config::RopeFreqs { full, swa }
846        });
847
848        // RoPE layout comes from the capability registry above (fail-
849        // closed). Getting this wrong for `llama` (needs Norm) was the
850        // real root cause of the Llama-3.1-8B early-stop/wrong-logits bug.
851
852        if best_effort_fields.is_empty() {
853            best_effort_fields.push(
854                "none -- every field above was read directly from this file's own GGUF metadata",
855            );
856        }
857
858        // LAST, deliberately. The generic path is a GUESS, so it has to
859        // be opted into rather than fallen onto: it assumes plain GQA
860        // with no ALiBi, no learned position embeddings and no
861        // per-layer rope skipping, and that assumption was already
862        // wrong for gpt2, mpt, refact, bloom and jais.
863        //
864        // But it runs AFTER every architecture-specific refusal, so a
865        // checkpoint with a NAMED problem still reports that problem.
866        // Checking first would have replaced "this uses ALiBi" with
867        // "this is unaudited", which is true and much less useful.
868        if matches!(
869            arch_profile.path,
870            crate::capability::ArchPath::GenericGqa { .. }
871        ) && !crate::capability::is_audited_generic(&arch)
872            && !matches!(
873                std::env::var("FERROX_ALLOW_UNAUDITED_ARCH").ok().as_deref(),
874                Some("1") | Some("true") | Some("on")
875            )
876        {
877            return Err(LoadError::UnauditedArchitecture(
878                arch.clone(),
879                rope_layout,
880                crate::capability::unaudited_refusal_detail(&arch),
881            ));
882        }
883
884        Ok(ModelConfig {
885            name,
886            n_layers,
887            hidden_dim,
888            n_heads,
889            n_kv_heads,
890            head_dim,
891            vocab_size,
892            rope_theta,
893            rms_norm_eps,
894            // No GGUF file encodes a hybrid KDA/Gated-MLA attention
895            // topology today; every real checkpoint loaded this way
896            // runs the standard Gqa path.
897            attention: crate::config::AttentionKind::Gqa,
898            sliding_window,
899            swa_pattern,
900            swa_dense_first,
901            moe: MoeLayerConfig {
902                n_experts: n_experts.max(1),
903                n_experts_active,
904                n_shared_experts,
905                hidden_dim,
906                expert_ffn_dim,
907                gating,
908                norm_topk_prob,
909                expert_group_count: metadata_u64_any(file, &[key("expert_group_count")])
910                    .map(|v| v as usize)
911                    .filter(|&c| c > 1),
912                expert_group_used_count: metadata_u64_any(file, &[key("expert_group_used_count")])
913                    .map(|v| v as usize)
914                    .filter(|&c| c > 0),
915                expert_weights_scale,
916            },
917            n_dense_leading_layers,
918            rope_freqs,
919            rope_layout,
920            qk_norm_style,
921            attn_logit_softcap,
922            final_logit_softcap,
923            embedding_scale,
924            attention_scale,
925            rope_attn_factor,
926            rope_dim,
927            rope_freqs_long,
928            rope_freqs_short,
929            rope_orig_ctx,
930            rope_theta_swa,
931            ffn_activation,
932            best_effort_fields: Box::leak(best_effort_fields.into_boxed_slice()),
933        })
934    }
935}
936
937impl crate::sampling::RecommendedSampling {
938    /// The sampling a GGUF recommends for itself, from the
939    /// `general.sampling.*` metadata keys llama.cpp's converter writes
940    /// when the source checkpoint carried a `generation_config.json`.
941    ///
942    /// This is the GGUF half of FreeToken's `load_generation_sampling`
943    /// (`python/freetoken/utils/hf.py:92`), which checks the GGUF
944    /// metadata *first* and only falls back to a `generation_config.json`
945    /// sidecar for non-GGUF checkpoints -- a GGUF is a single file and
946    /// has no sidecar to read.
947    ///
948    /// Key names are llama.cpp's own (`general.sampling.temp`, not
949    /// `temperature`). Each key is independent: a file that names only
950    /// `top_k` recommends only `top_k`, and the two fields it did not
951    /// mention stay `None` so the server's own defaults keep speaking
952    /// for them.
953    ///
954    /// `temp` / `top_p` are read as float *or* integer, because a
955    /// converter that wrote `temp = 1` stores a GGUF integer and
956    /// dropping that value would silently serve the checkpoint greedy --
957    /// the exact repetition-loop failure the recommendation exists to
958    /// prevent.
959    pub fn from_gguf(file: &impl TensorSource) -> Self {
960        let number = |k: &str| -> Option<f32> {
961            file.metadata(k)
962                .and_then(|v| v.as_f32().or_else(|| v.as_u64().map(|u| u as f32)))
963        };
964        crate::sampling::RecommendedSampling {
965            temperature: number("general.sampling.temp"),
966            top_p: number("general.sampling.top_p"),
967            top_k: file
968                .metadata("general.sampling.top_k")
969                .and_then(|v| v.as_u64())
970                .map(|v| v as usize),
971        }
972    }
973}
974
975/// The `linear` RoPE scaling factor, if this file declares one.
976///
977/// Deliberately separate from [`yarn_scaling_from_gguf`]: YaRN needs an
978/// original context length and per-band betas, and linear needs neither.
979/// Any factor at or below one is not a correction, and is treated as
980/// absent rather than applied as a no-op.
981fn linear_scaling_from_gguf(file: &impl TensorSource, arch: &str) -> Option<f32> {
982    let key = |suffix: &str| format!("{arch}.{suffix}");
983    let scaling_type = file.metadata_str(&key("rope.scaling.type"))?;
984    if !scaling_type.eq_ignore_ascii_case("linear") {
985        return None;
986    }
987    metadata_f32_any(file, &[key("rope.scaling.factor")]).filter(|f| f.is_finite() && *f > 1.0)
988}
989
990/// The YaRN RoPE scaling a GGUF declares, or `None` when this file
991/// declares none that changes the rotation.
992///
993/// llama.cpp's key names (`llama-arch.cpp`
994/// `LLM_KV_ROPE_SCALING_TYPE` / `_FACTOR`): `<arch>.rope.scaling.type`
995/// is a string (`"none"`, `"linear"`, `"yarn"`, `"longrope"`) and
996/// `<arch>.rope.scaling.factor` the ratio of served to trained context.
997/// `beta_fast` / `beta_slow` are read from both the plain and the
998/// `yarn_`-prefixed spelling and otherwise fall back to the reference's
999/// own defaults (32.0 / 1.0), which is what a real checkpoint relies on
1000/// -- almost none of them write those two keys.
1001///
1002/// `None` is returned for every case where applying YaRN would be a
1003/// guess or a no-op rather than a correction, so that no checkpoint's
1004/// rotation moves without the file having asked for it:
1005///
1006/// * a scaling type other than `yarn` (`linear` divides positions,
1007///   `longrope` rides the `rope_factors_long`/`_short` tensors this
1008///   loader already reads -- neither is this rewrite, and treating them
1009///   as YaRN would rope them wrong in a *new* way instead of leaving
1010///   them as they are),
1011/// * a missing, non-finite or `<= 1.0` factor (the reference's own
1012///   `get_mscale` treats `scale <= 1` as unscaled, and a factor of 1.0
1013///   makes every band's divisor exactly 1.0 anyway),
1014/// * a missing `rope.scaling.original_context_length` -- the trained
1015///   context is what the correction range is measured against, and
1016///   inventing one (say, from `context_length`, which on a YaRN file is
1017///   the *extended* length) would put the ramp in the wrong place and
1018///   quietly rope the checkpoint at frequencies nobody trained.
1019fn yarn_scaling_from_gguf(
1020    file: &impl TensorSource,
1021    arch: &str,
1022    orig_ctx: Option<usize>,
1023) -> Option<ferrox_core::attention::YarnScaling> {
1024    let key = |suffix: &str| format!("{arch}.{suffix}");
1025    let scaling_type = file.metadata_str(&key("rope.scaling.type"))?;
1026    if !scaling_type.eq_ignore_ascii_case("yarn") {
1027        return None;
1028    }
1029    let factor = metadata_f32_any(file, &[key("rope.scaling.factor")])
1030        .filter(|f| f.is_finite() && *f > 1.0)?;
1031    let orig_max_pos = orig_ctx?;
1032    let beta = |suffix: &str, default: f32| -> f32 {
1033        metadata_f32_any(
1034            file,
1035            &[
1036                key(&format!("rope.scaling.{suffix}")),
1037                key(&format!("rope.scaling.yarn_{suffix}")),
1038            ],
1039        )
1040        .filter(|v| v.is_finite() && *v > 0.0)
1041        .unwrap_or(default)
1042    };
1043    Some(ferrox_core::attention::YarnScaling {
1044        factor,
1045        beta_fast: beta("beta_fast", 32.0),
1046        beta_slow: beta("beta_slow", 1.0),
1047        orig_max_pos,
1048        // No GGUF key carries the reference's `truncate` flag, and its
1049        // default is `true`; a file that wanted the fractional range
1050        // would have no way to say so here.
1051        truncate: true,
1052    })
1053}
1054
1055pub(crate) fn find_info<'a>(
1056    file: &'a impl TensorSource,
1057    name: &str,
1058) -> Result<&'a TensorInfo, LoadError> {
1059    file.find_tensor(name)
1060        .ok_or_else(|| LoadError::Gguf(GgufError::TensorNotFound(name.to_string())))
1061}
1062
1063/// Like `load_f32_vec`, but for tensors that only exist on some
1064/// checkpoints (e.g. `attn_q_norm`/`attn_k_norm` -- OLMoE-style
1065/// per-projection QK-RMSNorm applied to the full q_proj/k_proj output
1066/// before RoPE, confirmed against `OlmoeAttention.forward` in
1067/// `transformers/models/olmoe/modeling_olmoe.py`: `q_norm(q_proj(x))`,
1068/// `k_norm(k_proj(x))`, both plain RMSNorm over the whole projected
1069/// width, not per-head). Absent for every other preset/fixture this
1070/// loader already handles -- `None` there is correct, not a missing
1071/// feature.
1072/// Loads the five gpt-oss-only tensors for one layer.
1073///
1074/// Every one of them is **required**: a gpt-oss checkpoint that is
1075/// missing any of these is not a gpt-oss checkpoint ferrox can run, and
1076/// quietly substituting zeros would reintroduce exactly the
1077/// silently-wrong-graph failure this path exists to remove. The lengths
1078/// are asserted against the config for the same reason -- a bias of the
1079/// wrong width would otherwise be applied to a `zip`-truncated prefix
1080/// and produce a plausible, wrong answer.
1081///
1082/// Shapes follow `src/models/openai-moe.cpp::load_arch_tensors`:
1083/// `attn_sinks {n_head}`, `attn_output.bias {n_embd}`,
1084/// `ffn_gate_inp.bias {n_expert}`, `ffn_{gate,up}_exps.bias
1085/// {n_ff_exp, n_expert}`, `ffn_down_exps.bias {n_embd, n_expert}`.
1086/// GGUF stores the fastest dimension first, so the 2-D bias tensors
1087/// arrive expert-major and split by simple chunking.
1088fn load_gpt_oss_layer(
1089    file: &impl TensorSource,
1090    l: usize,
1091    config: &ModelConfig,
1092) -> Result<crate::decoder::GptOssLayer, LoadError> {
1093    let n_experts = config.moe.n_experts;
1094    let ff = config.moe.expert_ffn_dim;
1095
1096    let want = |name: &str, got: usize, expect: usize| -> Result<(), LoadError> {
1097        if got == expect {
1098            Ok(())
1099        } else {
1100            Err(LoadError::UnsupportedFeature(
1101                config.name.to_string(),
1102                format!("{name} has {got} elements, expected {expect}"),
1103            ))
1104        }
1105    };
1106
1107    let attn_sinks = load_f32_vec(file, &format!("blk.{l}.attn_sinks.weight"))?;
1108    want(
1109        &format!("blk.{l}.attn_sinks.weight"),
1110        attn_sinks.len(),
1111        config.n_heads,
1112    )?;
1113    let o_bias = load_f32_vec(file, &format!("blk.{l}.attn_output.bias"))?;
1114    want(
1115        &format!("blk.{l}.attn_output.bias"),
1116        o_bias.len(),
1117        config.hidden_dim,
1118    )?;
1119    let router_bias = load_f32_vec(file, &format!("blk.{l}.ffn_gate_inp.bias"))?;
1120    want(
1121        &format!("blk.{l}.ffn_gate_inp.bias"),
1122        router_bias.len(),
1123        n_experts,
1124    )?;
1125
1126    let gate_b = load_f32_vec(file, &format!("blk.{l}.ffn_gate_exps.bias"))?;
1127    want(
1128        &format!("blk.{l}.ffn_gate_exps.bias"),
1129        gate_b.len(),
1130        n_experts * ff,
1131    )?;
1132    let up_b = load_f32_vec(file, &format!("blk.{l}.ffn_up_exps.bias"))?;
1133    want(
1134        &format!("blk.{l}.ffn_up_exps.bias"),
1135        up_b.len(),
1136        n_experts * ff,
1137    )?;
1138    let down_b = load_f32_vec(file, &format!("blk.{l}.ffn_down_exps.bias"))?;
1139    want(
1140        &format!("blk.{l}.ffn_down_exps.bias"),
1141        down_b.len(),
1142        n_experts * config.hidden_dim,
1143    )?;
1144
1145    let expert_bias = (0..n_experts)
1146        .map(|e| ferrox_moe::ExpertBias {
1147            gate: gate_b[e * ff..(e + 1) * ff].to_vec(),
1148            up: up_b[e * ff..(e + 1) * ff].to_vec(),
1149            down: down_b[e * config.hidden_dim..(e + 1) * config.hidden_dim].to_vec(),
1150        })
1151        .collect();
1152
1153    Ok(crate::decoder::GptOssLayer {
1154        attn_sinks,
1155        o_bias,
1156        router_bias,
1157        expert_bias,
1158    })
1159}
1160
1161pub(crate) fn load_f32_vec_optional(
1162    file: &impl TensorSource,
1163    name: &str,
1164) -> Result<Option<Vec<f32>>, LoadError> {
1165    if file.find_tensor(name).is_none() {
1166        return Ok(None);
1167    }
1168    Ok(Some(load_f32_vec(file, name)?))
1169}
1170
1171/// Slice `n` rows starting at `start` out of a quantized matrix without
1172/// dequantizing: every `Quantized` kind stores one interleaved block
1173/// buffer per row (fixed `row_bytes`), so a row range is a contiguous
1174/// byte range. Mapped sources stay zero-copy (sub-range of the same
1175/// mmap); other backings get an owned copy. Returns `None` for non-
1176/// quantized matrices (F32 / MXFP4) -- callers fall back to dequant.
1177fn slice_quantized_rows(m: &WeightMatrix, start: usize, n: usize) -> Option<WeightMatrix> {
1178    let WeightMatrix::Quantized {
1179        data,
1180        rows,
1181        cols,
1182        kind,
1183    } = m
1184    else {
1185        return None;
1186    };
1187    let total = data.len();
1188    if *rows == 0 || total % *rows != 0 || start + n > *rows {
1189        return None;
1190    }
1191    let row_bytes = total / *rows;
1192    let (b0, b1) = (start * row_bytes, (start + n) * row_bytes);
1193    let bytes = match data {
1194        WeightBytes::Mapped { mmap, range } => WeightBytes::Mapped {
1195            mmap: mmap.clone(),
1196            range: range.start + b0..range.start + b1,
1197        },
1198        other => WeightBytes::Owned(other.as_slice()[b0..b1].to_vec()),
1199    };
1200    Some(WeightMatrix::Quantized {
1201        data: bytes,
1202        rows: n,
1203        cols: *cols,
1204        kind: *kind,
1205    })
1206}
1207
1208/// Loads Q/K/V projections: prefers split `attn_{q,k,v}.weight`, falls
1209/// back to fused `attn_qkv.weight` (Phi-3 / some Qwen GGUFs) by
1210/// slicing quantized rows (zero-copy for mmapped GGUFs; dequant only
1211/// for non-quantized storage). Mirrors llama.cpp `create_tensor_qkv`.
1212fn load_qkv_projections(
1213    file: &impl TensorSource,
1214    layer: usize,
1215    config: &ModelConfig,
1216) -> Result<(WeightMatrix, WeightMatrix, WeightMatrix), LoadError> {
1217    let q_name = format!("blk.{layer}.attn_q.weight");
1218    let k_name = format!("blk.{layer}.attn_k.weight");
1219    let v_name = format!("blk.{layer}.attn_v.weight");
1220    let fused_name = format!("blk.{layer}.attn_qkv.weight");
1221
1222    if file.find_tensor(&q_name).is_some() {
1223        return Ok((
1224            load_weight_matrix(file, &q_name)?,
1225            load_weight_matrix(file, &k_name)?,
1226            load_weight_matrix(file, &v_name)?,
1227        ));
1228    }
1229    if file.find_tensor(&fused_name).is_none() {
1230        return Err(LoadError::Gguf(GgufError::TensorNotFound(q_name)));
1231    }
1232
1233    let fused = load_weight_matrix(file, &fused_name)?;
1234    let q_rows = config.n_heads * config.head_dim;
1235    let kv_rows = config.n_kv_heads * config.head_dim;
1236    let expected = q_rows + 2 * kv_rows;
1237    if fused.rows() != expected {
1238        // Phi-3 sometimes stores Q as full n_embd (== q_rows when MHA).
1239        return Err(LoadError::UnsupportedFeature(
1240            config.name.to_string(),
1241            format!(
1242                "{fused_name} has {} rows; expected q+k+v = {} \
1243                 (n_heads*head_dim + 2*n_kv_heads*head_dim)",
1244                fused.rows(),
1245                expected
1246            ),
1247        ));
1248    }
1249    let cols = fused.cols();
1250    // Quantized fused tensor: split by row ranges without dequantizing,
1251    // keeping Q/K/V on the quantized (Metal-capable) matvec path.
1252    if let (Some(q), Some(k), Some(v)) = (
1253        slice_quantized_rows(&fused, 0, q_rows),
1254        slice_quantized_rows(&fused, q_rows, kv_rows),
1255        slice_quantized_rows(&fused, q_rows + kv_rows, kv_rows),
1256    ) {
1257        return Ok((q, k, v));
1258    }
1259    // Non-quantized storage: dequant once and split.
1260    let mut full = Vec::with_capacity(fused.rows() * cols);
1261    for r in 0..fused.rows() {
1262        full.extend_from_slice(&fused.dequant_row(r));
1263    }
1264    let q = WeightMatrix::F32(Tensor::new(
1265        full[..q_rows * cols].to_vec(),
1266        vec![q_rows, cols],
1267    ));
1268    let k = WeightMatrix::F32(Tensor::new(
1269        full[q_rows * cols..(q_rows + kv_rows) * cols].to_vec(),
1270        vec![kv_rows, cols],
1271    ));
1272    let v = WeightMatrix::F32(Tensor::new(
1273        full[(q_rows + kv_rows) * cols..].to_vec(),
1274        vec![kv_rows, cols],
1275    ));
1276    Ok((q, k, v))
1277}
1278
1279/// Dense-layer FFN tensors: standard gate/up/down, or Phi-3 fused
1280/// `ffn_up` with `2 * expert_ffn_dim` rows and no separate gate.
1281fn load_dense_expert(
1282    file: &impl TensorSource,
1283    layer: usize,
1284    config: &ModelConfig,
1285) -> Result<ExpertWeights, LoadError> {
1286    let gate_name = format!("blk.{layer}.ffn_gate.weight");
1287    let up_name = format!("blk.{layer}.ffn_up.weight");
1288    let down_name = format!("blk.{layer}.ffn_down.weight");
1289    if file.find_tensor(&gate_name).is_some() {
1290        return Ok(ExpertWeights {
1291            gate: load_weight_matrix(file, &gate_name)?,
1292            up: load_weight_matrix(file, &up_name)?,
1293            down: load_weight_matrix(file, &down_name)?,
1294        });
1295    }
1296    // Phi-3 fused SwiGLU: up is [hidden, 2*ff], first half gate, second up.
1297    let fused = load_weight_matrix(file, &up_name)?;
1298    let ff = config.moe.expert_ffn_dim;
1299    if fused.rows() != 2 * ff {
1300        return Err(LoadError::UnsupportedFeature(
1301            config.name.to_string(),
1302            format!(
1303                "{up_name} has {} rows without a companion ffn_gate; \
1304                 expected fused SwiGLU with 2*ffn_dim = {} rows",
1305                fused.rows(),
1306                2 * ff
1307            ),
1308        ));
1309    }
1310    let cols = fused.cols();
1311    // Quantized fused gate+up: split by rows, no dequant (Metal-capable).
1312    if let (Some(gate), Some(up)) = (
1313        slice_quantized_rows(&fused, 0, ff),
1314        slice_quantized_rows(&fused, ff, ff),
1315    ) {
1316        return Ok(ExpertWeights {
1317            gate,
1318            up,
1319            down: load_weight_matrix(file, &down_name)?,
1320        });
1321    }
1322    let mut full = Vec::with_capacity(fused.rows() * cols);
1323    for r in 0..fused.rows() {
1324        full.extend_from_slice(&fused.dequant_row(r));
1325    }
1326    let gate = WeightMatrix::F32(Tensor::new(full[..ff * cols].to_vec(), vec![ff, cols]));
1327    let up = WeightMatrix::F32(Tensor::new(full[ff * cols..].to_vec(), vec![ff, cols]));
1328    Ok(ExpertWeights {
1329        gate,
1330        up,
1331        down: load_weight_matrix(file, &down_name)?,
1332    })
1333}
1334
1335/// Widen a raw plain-float tensor (`F32` / `F16` / `BF16`) to `f32`.
1336///
1337/// The three unquantized element types are handled identically at every
1338/// call site (eager widening to an owned buffer -- none of them has a
1339/// block structure a fused dot kernel could exploit), and each of the
1340/// seven GGUF loaders used to inline the same two-way match. F16 had no
1341/// arm in any of them, which made every `*-f16.gguf` a hard
1342/// `UnsupportedDtype` even though the type was parsed and sized.
1343pub(crate) fn widen_plain_float(
1344    dtype: GgmlType,
1345    raw: &[u8],
1346    name: &str,
1347) -> Result<Vec<f32>, LoadError> {
1348    match dtype {
1349        GgmlType::F32 => {
1350            let mut out = Vec::with_capacity(raw.len() / 4);
1351            for chunk in raw.as_chunks::<4>().0 {
1352                out.push(f32::from_le_bytes(*chunk));
1353            }
1354            Ok(out)
1355        }
1356        GgmlType::F16 => ferrox_quant::dequant_f16(raw)
1357            .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::F16)),
1358        GgmlType::BF16 => ferrox_quant::dequant_bf16(raw)
1359            .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::BF16)),
1360        // MXFP4 is accepted as a weight matrix and as an MoE expert
1361        // tensor, and `WeightMatrix::dequant` already calls this
1362        // dequantizer, so refusing it here made a 1-D MXFP4 norm or
1363        // bias a hard load error on a checkpoint whose 2-D tensors of
1364        // the same type load fine. That contradicted this function's
1365        // own contract, which is to widen whatever the loaders accept.
1366        GgmlType::MXFP4 => ferrox_quant::dequant_mxfp4_gguf(raw)
1367            .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::MXFP4)),
1368        other => Err(LoadError::UnsupportedDtype(name.to_string(), other)),
1369    }
1370}
1371
1372pub(crate) fn load_f32_vec(file: &impl TensorSource, name: &str) -> Result<Vec<f32>, LoadError> {
1373    let info = find_info(file, name)?;
1374    let raw = file.tensor_bytes(name)?;
1375    match info.dtype {
1376        // MXFP4 rides with the plain floats because `widen_plain_float`
1377        // is where its arm already lives -- routing it here rather than
1378        // giving this table its own `dequant_mxfp4_gguf` call keeps ONE
1379        // MXFP4 arm in this file instead of two that can drift.
1380        //
1381        // It has to be in *both* tables' reach, and it was in neither's:
1382        // `load_weight_matrix` accepts MXFP4 as a 2-D weight and
1383        // `load_moe_expert_matrices` accepts it as an expert tensor, so
1384        // a checkpoint whose norms happen to be MXFP4 failed here with
1385        // `UnsupportedDtype` while its far larger tensors of the exact
1386        // same dtype loaded fine.
1387        GgmlType::F32 | GgmlType::F16 | GgmlType::BF16 | GgmlType::MXFP4 => {
1388            widen_plain_float(info.dtype, raw, name)
1389        }
1390        GgmlType::Q8_0 => ferrox_quant::dequant_q8_0(raw)
1391            .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::Q8_0)),
1392        GgmlType::Q4_0 => ferrox_quant::dequant_q4_0(raw)
1393            .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::Q4_0)),
1394        GgmlType::Q4K => ferrox_quant::dequant_q4_k(raw)
1395            .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::Q4K)),
1396        GgmlType::Q5K => ferrox_quant::dequant_q5_k(raw)
1397            .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::Q5K)),
1398        GgmlType::Q6K => ferrox_quant::dequant_q6_k(raw)
1399            .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::Q6K)),
1400        GgmlType::Q2K => ferrox_quant::dequant_q2_k(raw)
1401            .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::Q2K)),
1402        GgmlType::Q3K => ferrox_quant::dequant_q3_k(raw)
1403            .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::Q3K)),
1404        GgmlType::Q4_1 => ferrox_quant::dequant_q4_1(raw)
1405            .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::Q4_1)),
1406        GgmlType::Q5_0 => ferrox_quant::dequant_q5_0(raw)
1407            .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::Q5_0)),
1408        GgmlType::Q5_1 => ferrox_quant::dequant_q5_1(raw)
1409            .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::Q5_1)),
1410        GgmlType::Q8_1 => ferrox_quant::dequant_q8_1(raw)
1411            .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::Q8_1)),
1412        GgmlType::IQ4NL => ferrox_quant::dequant_iq4_nl(raw)
1413            .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::IQ4NL)),
1414        GgmlType::IQ4XS => ferrox_quant::dequant_iq4_xs(raw)
1415            .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::IQ4XS)),
1416        // The codebook-grid tiers. Rare on the 1-D tensors this
1417        // function widens (norms and biases are almost always F32),
1418        // but a dtype ferrox can decode should never be rejected here
1419        // just because the *other* dispatch table below knows it --
1420        // that split is how a supported format turns into a load
1421        // failure on the one checkpoint that uses it.
1422        GgmlType::IQ1S => ferrox_quant::dequant_iq1_s(raw)
1423            .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::IQ1S)),
1424        GgmlType::IQ1M => ferrox_quant::dequant_iq1_m(raw)
1425            .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::IQ1M)),
1426        GgmlType::IQ2XXS => ferrox_quant::dequant_iq2_xxs(raw)
1427            .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::IQ2XXS)),
1428        GgmlType::IQ2XS => ferrox_quant::dequant_iq2_xs(raw)
1429            .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::IQ2XS)),
1430        GgmlType::IQ2S => ferrox_quant::dequant_iq2_s(raw)
1431            .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::IQ2S)),
1432        GgmlType::IQ3XXS => ferrox_quant::dequant_iq3_xxs(raw)
1433            .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::IQ3XXS)),
1434        GgmlType::IQ3S => ferrox_quant::dequant_iq3_s(raw)
1435            .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::IQ3S)),
1436        other => Err(LoadError::UnsupportedDtype(name.to_string(), other)),
1437    }
1438}
1439
1440/// Loads a 2D weight matrix, keeping Q8_0/Q4_0 tensors quantized (raw
1441/// bytes copied out, never dequantized) and only expanding truly F32
1442/// tensors. This is the memory- and bandwidth-saving path: for a
1443/// multi-billion-parameter checkpoint the difference between this and
1444/// "dequant everything on load" is the difference between fitting in
1445/// RAM and not.
1446pub(crate) fn load_weight_matrix(
1447    file: &impl TensorSource,
1448    name: &str,
1449) -> Result<WeightMatrix, LoadError> {
1450    let info = find_info(file, name)?;
1451    // GGUF's on-disk `ne[]` shape array is fastest-varying-dimension-first
1452    // (ggml convention), i.e. `[in_features, out_features]` for a 2D
1453    // weight matrix -- the *reverse* of the row-major `[rows, cols]` =
1454    // `[out_features, in_features]` order `WeightMatrix`/`matmul_f32`
1455    // need. Reversed here once so every consumer below gets the correct
1456    // orientation. Before this reversal existed, every 2D tensor in an
1457    // externally-produced GGUF file was silently loaded transposed -- a
1458    // real bug found by running a real downloaded checkpoint
1459    // (TinyLlama-1.1B-Chat, e.g. `attn_k.weight`'s real raw shape is
1460    // `[2048, 256]` = `[hidden_dim, kv_dim]` = `[in, out]`) -- found
1461    // as a real transposition bug affecting every externally-produced
1462    // GGUF file, caught by serving a real downloaded checkpoint.
1463    let shape: Vec<usize> = info.shape.iter().rev().map(|&d| d as usize).collect();
1464    // A ggml tensor's `ne[]` is always four long and trailing 1s are
1465    // implicit, so a GGUF writer is free to store a `[in, 1]` matrix
1466    // with `n_dims = 1`. llama.cpp reads it back as a matrix anyway --
1467    // `check_tensor_dims` compares each requested dimension against
1468    // `cur->ne[i]` and requires 1 for the dimensions the file does not
1469    // carry -- so a single-output projection is a 2-D weight there and
1470    // must be one here. Refusing it instead made a real checkpoint
1471    // unloadable: `cross-encoder/ms-marco-MiniLM-L6-v2` writes
1472    // `cls.output.weight` as `[384]`, i.e. the 1x384 relevance head
1473    // that `/v1/rerank` exists to run, and the whole route died at load
1474    // with "expected 2D".
1475    let (rows, cols) = match shape.as_slice() {
1476        [r, c] => (*r, *c),
1477        [c] => (1, *c),
1478        other => {
1479            return Err(LoadError::UnsupportedDtype(
1480                format!("{name} (expected 2D, got shape {other:?})"),
1481                info.dtype,
1482            ))
1483        }
1484    };
1485    // `shape` is what the file said; `[rows, cols]` is what the matrix
1486    // is. They differ exactly in the 1-D case above, and the `Tensor`
1487    // must carry the matrix shape or `apply` reads it as a vector.
1488    let shape = vec![rows, cols];
1489
1490    match info.dtype {
1491        // BF16 has no block/scale structure to keep quantized-in-place
1492        // the way Q4_0/Q8_0/K-quants do -- there's no fused dot kernel
1493        // that would make sense for a plain narrowed float, so it's
1494        // eagerly widened to an owned f32 Tensor exactly like F32
1495        // tensors already are.
1496        GgmlType::F32 | GgmlType::F16 | GgmlType::BF16 => {
1497            let data = load_f32_vec(file, name)?;
1498            Ok(WeightMatrix::F32(Tensor::new(data, shape)))
1499        }
1500        other => match quant_kind_for(other) {
1501            Some(kind) => {
1502                let (mmap, range) = file.tensor_mapped_range(name)?;
1503                #[cfg(feature = "metal")]
1504                ferrox_metal::gpu::register_weight_mmap(Arc::clone(&mmap));
1505                Ok(WeightMatrix::Quantized {
1506                    data: WeightBytes::Mapped { mmap, range },
1507                    rows,
1508                    cols,
1509                    kind,
1510                })
1511            }
1512            None => Err(LoadError::UnsupportedDtype(name.to_string(), other)),
1513        },
1514    }
1515}
1516
1517/// Splits a packed 3D MoE expert tensor `blk.N.ffn_{gate,up,down}_exps.weight`
1518/// (shape `[n_experts, out_dim, in_dim]`) into per-expert `WeightMatrix`es,
1519/// slicing raw bytes directly (quantized tensors stay quantized; block
1520/// boundaries never cross expert boundaries since `in_dim` is a whole
1521/// number of quantization blocks). Matches llama.cpp/ik_llama.cpp layout
1522/// confirmed on real OLMoE and Qwen2-MoE GGUF checkpoints.
1523pub(crate) fn split_expert_tensor(
1524    file: &impl TensorSource,
1525    name: &str,
1526    n_experts: usize,
1527) -> Result<Vec<WeightMatrix>, LoadError> {
1528    let info = find_info(file, name)?;
1529    // Real raw shape is `[in_dim, out_dim, n_experts]` (ggml's
1530    // fastest-first `ne[]` order -- see `load_weight_matrix`'s doc
1531    // comment for the confirmed 2D case this generalizes from). `n_experts`
1532    // is the slowest-varying (last, i.e. outermost/most-major) dimension,
1533    // so each expert's `out_dim*in_dim` block is contiguous with experts
1534    // back-to-back in the mmap.
1535    if info.shape.len() != 3 || info.shape[2] as usize != n_experts {
1536        let file_experts = info.shape.last().map(|&d| d as usize).unwrap_or(0);
1537        return Err(LoadError::ExpertCountMismatch(
1538            name.to_string(),
1539            file_experts,
1540            n_experts,
1541        ));
1542    }
1543    let out_dim = info.shape[1] as usize;
1544    let in_dim = info.shape[0] as usize;
1545    let raw = file.tensor_bytes(name)?;
1546
1547    match info.dtype {
1548        GgmlType::F32 | GgmlType::F16 | GgmlType::BF16 => {
1549            let all = crate::loader::widen_plain_float(info.dtype, raw, name)?;
1550            let per_expert = out_dim * in_dim;
1551            Ok((0..n_experts)
1552                .map(|e| {
1553                    WeightMatrix::F32(Tensor::new(
1554                        all[e * per_expert..(e + 1) * per_expert].to_vec(),
1555                        vec![out_dim, in_dim],
1556                    ))
1557                })
1558                .collect())
1559        }
1560        other => match quant_kind_for(other) {
1561            Some(kind) => {
1562                let (mmap, full_range) = file.tensor_mapped_range(name)?;
1563                #[cfg(feature = "metal")]
1564                ferrox_metal::gpu::register_weight_mmap(Arc::clone(&mmap));
1565                let bytes_per_expert = raw.len() / n_experts;
1566                Ok((0..n_experts)
1567                    .map(|e| WeightMatrix::Quantized {
1568                        data: WeightBytes::Mapped {
1569                            mmap: Arc::clone(&mmap),
1570                            range: (full_range.start + e * bytes_per_expert)
1571                                ..(full_range.start + (e + 1) * bytes_per_expert),
1572                        },
1573                        rows: out_dim,
1574                        cols: in_dim,
1575                        kind,
1576                    })
1577                    .collect())
1578            }
1579            None => Err(LoadError::UnsupportedDtype(name.to_string(), other)),
1580        },
1581    }
1582}
1583
1584/// When every routed expert is mmap-backed with a Metal simdgroup-GEMM
1585/// kind and back-to-back slices, record the combined gate/up/down planes
1586/// for Metal packed MoE. Gate/up/down may differ in kind (e.g. Q4_K /
1587/// Q4_K / Q8_0) but must be uniform across experts per role.
1588#[cfg(feature = "metal")]
1589fn try_build_moe_packed_q4_planes(experts: &[ExpertWeights]) -> Option<MoePackedQ4Planes> {
1590    use ferrox_core::weight_matrix::{QuantKind, WeightBytes};
1591    use std::sync::Arc;
1592
1593    if experts.is_empty() {
1594        return None;
1595    }
1596
1597    fn mapped_sg(m: &WeightMatrix) -> Option<(WeightBytes, usize, &'static str)> {
1598        match m {
1599            WeightMatrix::Quantized {
1600                data: WeightBytes::Mapped { mmap, range },
1601                rows,
1602                kind,
1603                ..
1604            } => {
1605                let kind_str = match kind {
1606                    QuantKind::Q4_0 => "Q4_0",
1607                    QuantKind::Q5_0 => "Q5_0",
1608                    QuantKind::Q4K => "Q4_K",
1609                    QuantKind::Q5K => "Q5_K",
1610                    QuantKind::Q6K => "Q6_K",
1611                    QuantKind::Q8_0 => "Q8_0",
1612                    QuantKind::IQ4XS => "IQ4_XS",
1613                    _ => return None,
1614                };
1615                let _ = ferrox_metal::gpu::mul_mm_sg_meta(kind_str)?;
1616                Some((
1617                    WeightBytes::Mapped {
1618                        mmap: Arc::clone(mmap),
1619                        range: range.clone(),
1620                    },
1621                    *rows,
1622                    kind_str,
1623                ))
1624            }
1625            _ => None,
1626        }
1627    }
1628
1629    let (gate0, ffn_rows, gate_kind) = mapped_sg(&experts[0].gate)?;
1630    let (up0, up_rows, up_kind) = mapped_sg(&experts[0].up)?;
1631    let (down0, hidden_rows, down_kind) = mapped_sg(&experts[0].down)?;
1632    if up_rows != ffn_rows {
1633        return None;
1634    }
1635    let WeightBytes::Mapped {
1636        mmap: gate_mmap,
1637        range: gate0_range,
1638    } = &gate0
1639    else {
1640        return None;
1641    };
1642    let WeightBytes::Mapped {
1643        mmap: up_mmap,
1644        range: up0_range,
1645    } = &up0
1646    else {
1647        return None;
1648    };
1649    let WeightBytes::Mapped {
1650        mmap: down_mmap,
1651        range: down0_range,
1652    } = &down0
1653    else {
1654        return None;
1655    };
1656
1657    let gate_stride = gate0_range.len();
1658    let up_stride = up0_range.len();
1659    let down_stride = down0_range.len();
1660    if gate_stride == 0 || up_stride == 0 || down_stride == 0 {
1661        return None;
1662    }
1663
1664    let n = experts.len();
1665    for (i, ex) in experts.iter().enumerate().skip(1) {
1666        let (g, fr, gk) = mapped_sg(&ex.gate)?;
1667        let (u, ur, uk) = mapped_sg(&ex.up)?;
1668        let (d, hr, dk) = mapped_sg(&ex.down)?;
1669        if gk != gate_kind || uk != up_kind || dk != down_kind {
1670            return None;
1671        }
1672        let WeightBytes::Mapped { mmap, range } = &g else {
1673            return None;
1674        };
1675        if fr != ffn_rows {
1676            return None;
1677        }
1678        if !Arc::ptr_eq(mmap, gate_mmap)
1679            || range.len() != gate_stride
1680            || range.start != gate0_range.start + i * gate_stride
1681        {
1682            return None;
1683        }
1684        let WeightBytes::Mapped { mmap, range } = &u else {
1685            return None;
1686        };
1687        if ur != ffn_rows
1688            || !Arc::ptr_eq(mmap, up_mmap)
1689            || range.len() != up_stride
1690            || range.start != up0_range.start + i * up_stride
1691        {
1692            return None;
1693        }
1694        let WeightBytes::Mapped { mmap, range } = &d else {
1695            return None;
1696        };
1697        if hr != hidden_rows
1698            || !Arc::ptr_eq(mmap, down_mmap)
1699            || range.len() != down_stride
1700            || range.start != down0_range.start + i * down_stride
1701        {
1702            return None;
1703        }
1704    }
1705
1706    Some(MoePackedQ4Planes::new(
1707        WeightBytes::Mapped {
1708            mmap: Arc::clone(gate_mmap),
1709            range: gate0_range.start..gate0_range.start + n * gate_stride,
1710        },
1711        WeightBytes::Mapped {
1712            mmap: Arc::clone(up_mmap),
1713            range: up0_range.start..up0_range.start + n * up_stride,
1714        },
1715        WeightBytes::Mapped {
1716            mmap: Arc::clone(down_mmap),
1717            range: down0_range.start..down0_range.start + n * down_stride,
1718        },
1719        gate_stride,
1720        up_stride,
1721        down_stride,
1722        n,
1723        ffn_rows,
1724        hidden_rows,
1725        gate_kind,
1726        up_kind,
1727        down_kind,
1728    ))
1729}
1730
1731/// One matrix's place inside a store-backed expert's combined byte
1732/// buffer (gate bytes, then up, then down, concatenated by
1733/// `GgufExpertSource::read_expert`).
1734#[derive(Debug, Clone, Copy)]
1735pub struct StoredMatrixSpec {
1736    pub offset: usize,
1737    pub len: usize,
1738    pub rows: usize,
1739    pub cols: usize,
1740    pub kind: QuantKind,
1741}
1742
1743/// Byte-range layout of one store-backed routed expert.
1744#[derive(Debug, Clone, Copy)]
1745pub struct StoredExpertLayout {
1746    pub gate: StoredMatrixSpec,
1747    pub up: StoredMatrixSpec,
1748    pub down: StoredMatrixSpec,
1749}
1750
1751impl StoredExpertLayout {
1752    pub fn total_bytes(&self) -> usize {
1753        self.gate.len + self.up.len + self.down.len
1754    }
1755
1756    /// Builds temporary zero-copy `WeightMatrix` views over a leased
1757    /// buffer. Each view's `WeightBytes::Shared` clone of the lease's
1758    /// `Arc` keeps the cache entry pinned for the view's lifetime.
1759    pub fn materialize(&self, lease: &ferrox_core::expert_store::ExpertLease) -> ExpertWeights {
1760        let mk = |spec: &StoredMatrixSpec| WeightMatrix::Quantized {
1761            data: WeightBytes::Shared {
1762                buf: lease.shared_buf(),
1763                range: spec.offset..spec.offset + spec.len,
1764            },
1765            rows: spec.rows,
1766            cols: spec.cols,
1767            kind: spec.kind,
1768        };
1769        ExpertWeights {
1770            gate: mk(&self.gate),
1771            up: mk(&self.up),
1772            down: mk(&self.down),
1773        }
1774    }
1775}
1776
1777/// [`ExpertSource`] over a (possibly sharded) GGUF checkpoint: each
1778/// expert's gate/up/down byte ranges are read positionally from the
1779/// owning shard file and concatenated, so a store miss touches exactly
1780/// that expert's bytes -- no mmap of the expert region, no shared seek
1781/// cursor.
1782pub struct GgufExpertSource {
1783    files: Vec<std::fs::File>,
1784    /// (layer, expert) -> the three (file index, offset, len) segments
1785    /// in gate/up/down order.
1786    segments: std::collections::HashMap<ExpertKey, [(usize, u64, usize); 3]>,
1787}
1788
1789impl ExpertSource for GgufExpertSource {
1790    fn expert_len(&self, key: ExpertKey) -> Option<usize> {
1791        self.segments
1792            .get(&key)
1793            .map(|segs| segs.iter().map(|&(_, _, len)| len).sum())
1794    }
1795
1796    fn read_expert(&self, key: ExpertKey) -> std::io::Result<Vec<u8>> {
1797        let segs = self
1798            .segments
1799            .get(&key)
1800            .ok_or_else(|| std::io::Error::new(std::io::ErrorKind::NotFound, format!("{key:?}")))?;
1801        let total: usize = segs.iter().map(|&(_, _, len)| len).sum();
1802        let mut buf = vec![0u8; total];
1803        let mut written = 0;
1804        for &(fi, offset, len) in segs {
1805            let dst = &mut buf[written..written + len];
1806            #[cfg(unix)]
1807            {
1808                use std::os::unix::fs::FileExt;
1809                self.files[fi].read_exact_at(dst, offset)?;
1810            }
1811            #[cfg(not(unix))]
1812            {
1813                use std::io::{Read, Seek, SeekFrom};
1814                let mut f = &self.files[fi];
1815                f.seek(SeekFrom::Start(offset))?;
1816                f.read_exact(dst)?;
1817            }
1818            written += len;
1819        }
1820        Ok(buf)
1821    }
1822}
1823
1824/// Collects the per-expert `(file, offset, len)` segments and layout
1825/// for one packed 3D expert tensor -- the store-backed counterpart of
1826/// `split_expert_tensor`, sharing its shape/offset math. Only
1827/// quantized dtypes are supported (an F32/BF16 expert tensor keeps the
1828/// resident path; the store exists for the quantized multi-hundred-GB
1829/// case).
1830/// One packed 3D expert tensor's store-backed description: the owning
1831/// shard index, each expert's `(offset, len)` within that shard file,
1832/// and the matrix spec shared by every expert's slice.
1833struct StoredTensorSpecs {
1834    shard: usize,
1835    per_expert: Vec<(u64, usize)>,
1836    spec: StoredMatrixSpec,
1837}
1838
1839fn stored_expert_specs(
1840    file: &ShardedGguf,
1841    name: &str,
1842    n_experts: usize,
1843) -> Result<Option<StoredTensorSpecs>, LoadError> {
1844    let info = find_info(file, name)?;
1845    if info.shape.len() != 3 || info.shape[2] as usize != n_experts {
1846        let file_experts = info.shape.last().map(|&d| d as usize).unwrap_or(0);
1847        return Err(LoadError::ExpertCountMismatch(
1848            name.to_string(),
1849            file_experts,
1850            n_experts,
1851        ));
1852    }
1853    let out_dim = info.shape[1] as usize;
1854    let in_dim = info.shape[0] as usize;
1855    let Some(kind) = quant_kind_for(info.dtype) else {
1856        return Ok(None); // F32/BF16 (or unsupported): resident fallback
1857    };
1858    let shard = file
1859        .tensor_shard_index(name)
1860        .expect("find_info succeeded, shard index must exist");
1861    // The mmap range of a tensor within a GgufFile IS its byte offset
1862    // range within that shard file (the mmap covers the whole file).
1863    let (_, full_range) = file.tensor_mapped_range(name)?;
1864    let total_len = full_range.end - full_range.start;
1865    let bytes_per_expert = total_len / n_experts;
1866    let per_expert: Vec<(u64, usize)> = (0..n_experts)
1867        .map(|e| {
1868            (
1869                (full_range.start + e * bytes_per_expert) as u64,
1870                bytes_per_expert,
1871            )
1872        })
1873        .collect();
1874    let spec = StoredMatrixSpec {
1875        offset: 0, // caller assigns the position within the combined buffer
1876        len: bytes_per_expert,
1877        rows: out_dim,
1878        cols: in_dim,
1879        kind,
1880    };
1881    Ok(Some(StoredTensorSpecs {
1882        shard,
1883        per_expert,
1884        spec,
1885    }))
1886}
1887
1888impl Decoder {
1889    /// Loads real weights from `path` for the given `config`. `config`
1890    /// supplies the architecture shape (layer count, head counts, MoE
1891    /// topology); tensor names are resolved against it using the
1892    /// llama.cpp naming convention described in the module docs.
1893    ///
1894    /// A `config.moe.n_experts <= 1` model is treated as dense: expert
1895    /// weights are read from the plain `blk.N.ffn_{gate,up,down}.weight`
1896    /// tensor names rather than the packed 3D `_exps` variant.
1897    pub fn from_gguf(
1898        path: impl AsRef<std::path::Path>,
1899        config: ModelConfig,
1900    ) -> Result<Self, LoadError> {
1901        Self::from_gguf_with_expert_cache(path, config, None)
1902    }
1903
1904    /// Like `from_gguf`, but with `expert_cache_bytes: Some(budget)`
1905    /// routed experts are NOT loaded resident: each layer holds only
1906    /// byte-range layouts, and expert bytes are read on demand through
1907    /// one bounded, lease-protected `ExpertStore` shared by every
1908    /// layer (a single global byte budget; see
1909    /// `ferrox_core::expert_store`). Dense layers, shared experts,
1910    /// attention, embeddings, and the output head stay resident/mapped
1911    /// exactly as before -- only routed experts stream. Layers whose
1912    /// expert tensors are F32/BF16 fall back to resident loading (the
1913    /// store exists for the quantized case). Output is bit-identical
1914    /// to the resident path -- same bytes, same kernels -- pinned by
1915    /// the roundtrip suite's equivalence test.
1916    pub fn from_gguf_with_expert_cache(
1917        path: impl AsRef<std::path::Path>,
1918        mut config: ModelConfig,
1919        expert_cache_bytes: Option<u64>,
1920    ) -> Result<Self, LoadError> {
1921        let path = path.as_ref();
1922        let file = ShardedGguf::open(path)?;
1923
1924        // gpt-oss carries five per-layer tensors the generic GQA layer
1925        // structs have no home for, and reuses `post_attention_norm` for
1926        // a *different* norm slot than Gemma does. Both are decided by
1927        // the architecture string, so resolve them once here. See
1928        // `crate::decoder::GptOssWeights` and
1929        // `PRE_FFN_NORM_IS_POST_ATTENTION_NORM`.
1930        //
1931        // These used to be ONE flag, `arch == "gpt-oss"`, standing for
1932        // two unrelated facts. Splitting them is what let `seed_oss` --
1933        // which shares the norm slot and has none of the extra tensors
1934        // -- be admitted without also being handed attention sinks.
1935        let arch = file
1936            .metadata_str("general.architecture")
1937            .unwrap_or_default()
1938            .to_string();
1939        let is_gpt_oss = arch == "gpt-oss";
1940        let post_attn_norm_is_pre_ffn_norm = pre_ffn_norm_is_post_attention_norm(&arch);
1941        let mut gpt_oss_layers: Vec<crate::decoder::GptOssLayer> = Vec::new();
1942
1943        // One store for the whole model (keys are (layer, expert)),
1944        // built up-front with every stored expert's segments; created
1945        // only when the cache is enabled AND some layer can use it.
1946        let mut store_segments: std::collections::HashMap<ExpertKey, [(usize, u64, usize); 3]> =
1947            std::collections::HashMap::new();
1948        let mut stored_layouts: Vec<Option<Vec<StoredExpertLayout>>> = Vec::new();
1949
1950        // Loaded like any other weight matrix: a quantized embedding
1951        // table stays quantized (zero-copy mmap) and token lookup
1952        // dequantizes one row via `WeightMatrix::dequant_row`, instead
1953        // of the whole vocabulary tensor being widened to f32 up front.
1954        let embedding = load_weight_matrix(&file, "token_embd.weight")?;
1955
1956        let mut layers = Vec::with_capacity(config.n_layers);
1957        let mut refined_qk_norm = config.qk_norm_style;
1958        for l in 0..config.n_layers {
1959            let (q_proj, k_proj, v_proj) = load_qkv_projections(&file, l, &config)?;
1960            let q_norm = load_f32_vec_optional(&file, &format!("blk.{l}.attn_q_norm.weight"))?;
1961            let k_norm = load_f32_vec_optional(&file, &format!("blk.{l}.attn_k_norm.weight"))?;
1962            // Refine WholeVector vs PerHead from the first observed norm length.
1963            if let Some(ref w) = q_norm {
1964                if w.len() == config.head_dim {
1965                    refined_qk_norm = crate::capability::QkNormStyle::PerHead;
1966                } else if w.len() == config.n_heads * config.head_dim {
1967                    refined_qk_norm = crate::capability::QkNormStyle::WholeVector;
1968                } else {
1969                    return Err(LoadError::UnsupportedFeature(
1970                        config.name.to_string(),
1971                        format!(
1972                            "blk.{l}.attn_q_norm.weight length {} matches neither head_dim={} \
1973                             nor n_heads*head_dim={}",
1974                            w.len(),
1975                            config.head_dim,
1976                            config.n_heads * config.head_dim
1977                        ),
1978                    ));
1979                }
1980            }
1981            let attn = AttnWeights {
1982                q_proj,
1983                k_proj,
1984                v_proj,
1985                o_proj: load_weight_matrix(&file, &format!("blk.{l}.attn_output.weight"))?,
1986                norm_weight: load_f32_vec(&file, &format!("blk.{l}.attn_norm.weight"))?,
1987                q_norm,
1988                k_norm,
1989                // Qwen2/Qwen2-MoE-family real QKV bias (`attn_{q,k,v}.bias`,
1990                // real config `qkv_bias`, `o_proj` has none) -- see
1991                // `AttnWeights::q_bias`'s doc comment.
1992                q_bias: load_f32_vec_optional(&file, &format!("blk.{l}.attn_q.bias"))?,
1993                k_bias: load_f32_vec_optional(&file, &format!("blk.{l}.attn_k.bias"))?,
1994                v_bias: load_f32_vec_optional(&file, &format!("blk.{l}.attn_v.bias"))?,
1995                // gpt-oss ships `post_attention_norm` but applies it in
1996                // Gemma's *other* slot: llama.cpp's openai-moe graph
1997                // norms `ffn_inp` with it after the attention residual,
1998                // i.e. it is the pre-FFN norm, not a post-attention one.
1999                // It is read below into `MoeWeights::norm_weight`.
2000                post_attn_norm: if post_attn_norm_is_pre_ffn_norm {
2001                    None
2002                } else {
2003                    load_f32_vec_optional(&file, &format!("blk.{l}.post_attention_norm.weight"))?
2004                },
2005                post_ffn_norm: load_f32_vec_optional(
2006                    &file,
2007                    &format!("blk.{l}.post_ffw_norm.weight"),
2008                )?,
2009            };
2010
2011            // Leading dense layers (see ModelConfig::layer_is_dense's
2012            // doc comment) load from the plain dense tensor names
2013            // regardless of this model's global MoE topology, matching
2014            // the DeepSeek-2/3-family convention found in
2015            // ik_llama.cpp's source. A model with n_experts<=1
2016            // globally (the dense test fixture) is dense on every
2017            // layer either way.
2018            let is_dense_layer = config.layer_is_dense(l) || config.moe.n_experts <= 1;
2019            let n_experts = if is_dense_layer {
2020                1
2021            } else {
2022                config.moe.n_experts
2023            };
2024            let experts: ExpertBacking = if is_dense_layer {
2025                ExpertBacking::Resident(vec![load_dense_expert(&file, l, &config)?])
2026            } else {
2027                // Try store-backed layouts first when the cache is
2028                // enabled; fall back to resident when any of the three
2029                // tensors isn't a supported quantized dtype.
2030                let stored = if expert_cache_bytes.is_some() {
2031                    let g = stored_expert_specs(
2032                        &file,
2033                        &format!("blk.{l}.ffn_gate_exps.weight"),
2034                        n_experts,
2035                    )?;
2036                    let u = stored_expert_specs(
2037                        &file,
2038                        &format!("blk.{l}.ffn_up_exps.weight"),
2039                        n_experts,
2040                    )?;
2041                    let d = stored_expert_specs(
2042                        &file,
2043                        &format!("blk.{l}.ffn_down_exps.weight"),
2044                        n_experts,
2045                    )?;
2046                    match (g, u, d) {
2047                        (Some(gt), Some(ut), Some(dt)) => {
2048                            let mut layouts = Vec::with_capacity(n_experts);
2049                            for e in 0..n_experts {
2050                                let key = ExpertKey {
2051                                    layer: l as u32,
2052                                    expert: e as u32,
2053                                };
2054                                store_segments.insert(
2055                                    key,
2056                                    [
2057                                        (gt.shard, gt.per_expert[e].0, gt.per_expert[e].1),
2058                                        (ut.shard, ut.per_expert[e].0, ut.per_expert[e].1),
2059                                        (dt.shard, dt.per_expert[e].0, dt.per_expert[e].1),
2060                                    ],
2061                                );
2062                                let mut gate = gt.spec;
2063                                let mut up = ut.spec;
2064                                let mut down = dt.spec;
2065                                gate.offset = 0;
2066                                up.offset = gate.len;
2067                                down.offset = gate.len + up.len;
2068                                layouts.push(StoredExpertLayout { gate, up, down });
2069                            }
2070                            Some(layouts)
2071                        }
2072                        _ => None,
2073                    }
2074                } else {
2075                    None
2076                };
2077                match stored {
2078                    Some(layouts) => {
2079                        // Placeholder; the shared store is attached in a
2080                        // second pass below once every layer's segments
2081                        // are collected.
2082                        stored_layouts.push(Some(layouts));
2083                        ExpertBacking::Resident(Vec::new())
2084                    }
2085                    None => {
2086                        let gates = split_expert_tensor(
2087                            &file,
2088                            &format!("blk.{l}.ffn_gate_exps.weight"),
2089                            n_experts,
2090                        )?;
2091                        let ups = split_expert_tensor(
2092                            &file,
2093                            &format!("blk.{l}.ffn_up_exps.weight"),
2094                            n_experts,
2095                        )?;
2096                        let downs = split_expert_tensor(
2097                            &file,
2098                            &format!("blk.{l}.ffn_down_exps.weight"),
2099                            n_experts,
2100                        )?;
2101                        ExpertBacking::Resident(
2102                            gates
2103                                .into_iter()
2104                                .zip(ups)
2105                                .zip(downs)
2106                                .map(|((gate, up), down)| ExpertWeights { gate, up, down })
2107                                .collect(),
2108                        )
2109                    }
2110                }
2111            };
2112            if stored_layouts.len() < layers.len() + 1 {
2113                stored_layouts.push(None);
2114            }
2115
2116            let shared_experts: Vec<ExpertWeights> =
2117                if config.moe.n_shared_experts > 0 && !is_dense_layer {
2118                    vec![ExpertWeights {
2119                        gate: load_weight_matrix(&file, &format!("blk.{l}.ffn_gate_shexp.weight"))?,
2120                        up: load_weight_matrix(&file, &format!("blk.{l}.ffn_up_shexp.weight"))?,
2121                        down: load_weight_matrix(&file, &format!("blk.{l}.ffn_down_shexp.weight"))?,
2122                    }]
2123                } else {
2124                    Vec::new()
2125                };
2126
2127            let router = if !is_dense_layer {
2128                load_weight_matrix(&file, &format!("blk.{l}.ffn_gate_inp.weight"))?
2129            } else {
2130                // dense layer: no real router; a zero [1, hidden] matrix
2131                // always selects the single expert deterministically.
2132                WeightMatrix::F32(Tensor::zeros(vec![1, config.hidden_dim]))
2133            };
2134
2135            let n_for_counts = match &experts {
2136                ExpertBacking::Resident(v) if v.is_empty() => n_experts,
2137                other => other.n_experts(),
2138            };
2139            let activation_counts = (0..n_for_counts)
2140                .map(|_| std::sync::atomic::AtomicU64::new(0))
2141                .collect();
2142            // Qwen2-MoE-specific real tensor (`blk.N.ffn_gate_inp_shexp.weight`,
2143            // real on-disk shape `[hidden_dim]`, confirmed against
2144            // llama.cpp's real `qwen2moe.cpp`) -- see
2145            // `MoeWeights::shared_expert_gate`'s doc comment. Presence
2146            // of the tensor itself is the real signal (not an
2147            // architecture-name list): every other supported
2148            // architecture's checkpoints simply don't carry this
2149            // tensor, so this naturally stays `None` there.
2150            let shared_expert_gate = if is_dense_layer {
2151                None
2152            } else {
2153                load_f32_vec_optional(&file, &format!("blk.{l}.ffn_gate_inp_shexp.weight"))?
2154            };
2155            #[cfg(feature = "metal")]
2156            let packed_q4 = match &experts {
2157                ExpertBacking::Resident(v) if !v.is_empty() => try_build_moe_packed_q4_planes(v),
2158                _ => None,
2159            };
2160            // DeepSeek-V3's aux-loss-free selection bias. The on-disk
2161            // name carries no `ffn_` prefix -- llama.cpp's
2162            // `LLM_TENSOR_FFN_EXP_PROBS_B` maps to `blk.%d.exp_probs_b`
2163            // (`llama-arch.cpp:416`, `gguf-py/gguf/constants.py:1240`).
2164            // Optional: only the DeepSeek-V3-lineage MoE recipes carry
2165            // it, and this same generic loader serves OLMoE / Qwen2-MoE /
2166            // Mixtral, which do not.
2167            let exp_probs_bias = if is_dense_layer {
2168                None
2169            } else {
2170                load_f32_vec_optional(&file, &format!("blk.{l}.exp_probs_b.bias"))?
2171            };
2172            if let Some(bias) = &exp_probs_bias {
2173                if bias.len() != config.moe.n_experts {
2174                    return Err(LoadError::UnsupportedFeature(
2175                        arch.clone(),
2176                        format!(
2177                            "blk.{l}.exp_probs_b.bias has {} entries but the model has {} experts",
2178                            bias.len(),
2179                            config.moe.n_experts
2180                        ),
2181                    ));
2182                }
2183                // Grouped selection masks the *biased* scores before the
2184                // global top-k (`build_moe_ffn`, the `n_expert_groups > 1`
2185                // block). ferrox's `route_top_k_grouped` takes a fixed
2186                // count from every group instead, which is a different
2187                // algorithm, so combining the two here would be a guess.
2188                // Refuse rather than route wrongly.
2189                if config.moe.expert_group_count.is_some() {
2190                    return Err(LoadError::UnsupportedFeature(
2191                        arch.clone(),
2192                        format!(
2193                            "blk.{l}.exp_probs_b.bias together with expert groups \
2194                             ({:?}): llama.cpp masks the biased scores per group \
2195                             before a global top-k, which is not the per-group \
2196                             top-k ferrox implements",
2197                            config.moe.expert_group_count
2198                        ),
2199                    ));
2200                }
2201            }
2202            let moe = MoeWeights {
2203                router,
2204                experts,
2205                shared_experts,
2206                shared_expert_gate,
2207                exp_probs_bias,
2208                norm_weight: if post_attn_norm_is_pre_ffn_norm {
2209                    load_f32_vec(&file, &format!("blk.{l}.post_attention_norm.weight"))?
2210                } else {
2211                    load_f32_vec(&file, &format!("blk.{l}.ffn_norm.weight"))?
2212                },
2213                activation_counts,
2214                #[cfg(feature = "metal")]
2215                packed_q4,
2216            };
2217
2218            if is_gpt_oss {
2219                gpt_oss_layers.push(load_gpt_oss_layer(&file, l, &config)?);
2220            }
2221
2222            layers.push(LayerWeights { attn, moe });
2223        }
2224
2225        let final_norm = load_f32_vec(&file, "output_norm.weight")?;
2226        // Many small Llama/Gemma-family GGUFs tie the lm-head to
2227        // `token_embd.weight` and omit `output.weight` (llama.cpp
2228        // `llama_model_loader` falls back the same way). Prefer the
2229        // explicit head when present.
2230        let output_head = match load_weight_matrix(&file, "output.weight") {
2231            Ok(w) => w,
2232            Err(_) => load_weight_matrix(&file, "token_embd.weight")?,
2233        };
2234
2235        // Second pass: attach the one shared store to every
2236        // store-backed layer. Opening the shard files fresh (plain
2237        // `File` handles for positional reads, not mmaps) keeps the
2238        // stored experts' bytes out of the process's mapped footprint
2239        // entirely.
2240        if !store_segments.is_empty() {
2241            let budget = expert_cache_bytes
2242                .expect("store_segments only populated when a cache budget is set")
2243                as usize;
2244            let files: Result<Vec<std::fs::File>, std::io::Error> =
2245                file.shard_paths().iter().map(std::fs::File::open).collect();
2246            let files = files.map_err(GgufError::from)?;
2247            let store = std::sync::Arc::new(ExpertStore::new(
2248                GgufExpertSource {
2249                    files,
2250                    segments: store_segments,
2251                },
2252                budget,
2253            ));
2254            for (l, layer) in layers.iter_mut().enumerate() {
2255                if let Some(layouts) = stored_layouts.get_mut(l).and_then(Option::take) {
2256                    layer.moe.experts = ExpertBacking::Stored {
2257                        store: std::sync::Arc::clone(&store),
2258                        layouts,
2259                        layer: l as u32,
2260                    };
2261                }
2262            }
2263        }
2264
2265        config.qk_norm_style = refined_qk_norm;
2266
2267        let family = crate::capability::resolve_profile(
2268            file.metadata_str("general.architecture").unwrap_or("llama"),
2269        )
2270        .map(|p| p.family)
2271        .unwrap_or(crate::capability::DecoderFamily::StandardGqa);
2272        let memory_kind = crate::capability::resolve_profile(
2273            file.metadata_str("general.architecture").unwrap_or("llama"),
2274        )
2275        .map(|p| p.memory)
2276        .unwrap_or(crate::capability::MemoryKind::KvGqa);
2277        let execution_plan = crate::execution_plan::ExecutionPlan::from_config(
2278            &config,
2279            family,
2280            memory_kind,
2281            crate::execution_plan::ExecutionPlan::probe_metal_caps(),
2282        );
2283
2284        let decoder = Decoder {
2285            config,
2286            embedding,
2287            layers,
2288            final_norm,
2289            output_head,
2290            gpu_vram_budget_bytes: None,
2291            gpt_oss: if is_gpt_oss {
2292                Some(crate::decoder::GptOssWeights {
2293                    layers: gpt_oss_layers,
2294                })
2295            } else {
2296                None
2297            },
2298            qk_norm_after_rope: QK_NORM_AFTER_ROPE_ARCHITECTURES.contains(&arch.as_str()),
2299            #[cfg(feature = "metal")]
2300            metal_attn_kv: std::sync::Mutex::new(None),
2301            execution_plan,
2302            plan_cache: std::sync::Mutex::new(std::collections::HashMap::new()),
2303        };
2304        // Resolve every kernel the model will need while we still have a
2305        // load-time error path to report it on, then seal: from here a
2306        // lookup that misses is an unpredicted slow path and says so.
2307        decoder.probe_kernels();
2308        ferrox_core::kernel_registry::seal_or_error()
2309            .map_err(|e| LoadError::StrictKernels(e.to_string()))?;
2310        // `ModelConfig` is parsed from a *different* handle on the same
2311        // file (the CLI opens its own `GgufFile`, then hands the config
2312        // here), so the model-level tensors it consumed were recorded on
2313        // that handle, not this one. Replay them before the gate, or
2314        // every Llama-3.x checkpoint reads as carrying an unread
2315        // `rope_freqs.weight` it in fact uses on every RoPE call.
2316        for name in crate::config::MODEL_LEVEL_TENSORS_READ_BY_CONFIG {
2317            file.note_consumed(name);
2318        }
2319        assert_every_tensor_consumed(&file)?;
2320        Ok(decoder)
2321    }
2322}
2323
2324/// Tensor-name prefixes a text-generation load legitimately never
2325/// reads. Everything here is consumed by a *different* code path, not by
2326/// nothing: multimodal projector planes belong to `mmproj`, and the
2327/// per-shard split bookkeeping is metadata, not weights.
2328const IGNORED_TENSOR_PREFIXES: &[&str] = &["mm.", "v.", "mmproj.", "resampler.", "audio."];
2329
2330/// Fails the load when the checkpoint carries tensors this build never
2331/// looked at.
2332///
2333/// A tensor nobody reads is not a harmless extra: it is a term of the
2334/// real graph that ours is missing. gpt-oss ships `blk.N.attn_sinks`
2335/// and ferrox has no attention-sink code anywhere, so the file loads,
2336/// runs at full speed, and emits a different distribution than the model
2337/// it claims to be; the newer MoE recipes ship `ffn_exp_probs_b` the
2338/// same way. Both are silent today, and both are exactly what the
2339/// architecture registry cannot catch, because the architecture *string*
2340/// is one ferrox does support -- it is the checkpoint that carries more
2341/// than the registry entry promises.
2342///
2343/// This is deliberately the last check in the load: by here every loader
2344/// arm has had its chance to ask for what it needs, so what is left over
2345/// is what nothing in this build knows about.
2346///
2347/// `FERROX_ALLOW_UNKNOWN_TENSORS=1` downgrades it to a warning, for the
2348/// case where a human has decided the missing term does not matter (a
2349/// bias tensor of zeros, an auxiliary head that never runs). The default
2350/// is refusal: a wrong answer is worse than no answer.
2351pub fn assert_every_tensor_consumed(file: &ShardedGguf) -> Result<(), LoadError> {
2352    let mut left: Vec<String> = file
2353        .unconsumed_tensors()
2354        .into_iter()
2355        .filter(|n| !IGNORED_TENSOR_PREFIXES.iter().any(|p| n.starts_with(p)))
2356        .collect();
2357    if left.is_empty() {
2358        return Ok(());
2359    }
2360    left.sort();
2361    let shown = left.iter().take(8).cloned().collect::<Vec<_>>().join(", ");
2362    let listing = if left.len() > 8 {
2363        format!("{shown}, … (+{} more)", left.len() - 8)
2364    } else {
2365        shown
2366    };
2367    if matches!(
2368        std::env::var("FERROX_ALLOW_UNKNOWN_TENSORS")
2369            .ok()
2370            .as_deref(),
2371        Some("1") | Some("true") | Some("on")
2372    ) {
2373        eprintln!(
2374            "ferrox: WARNING -- {} tensor(s) in this checkpoint are never read \
2375             ({listing}); output may be wrong (FERROX_ALLOW_UNKNOWN_TENSORS=1)",
2376            left.len()
2377        );
2378        return Ok(());
2379    }
2380    Err(LoadError::UnconsumedTensors(left.len(), listing))
2381}
2382
2383#[cfg(test)]
2384mod tests {
2385
2386    /// A quantized 1-D tensor loads through the shared helper.
2387    ///
2388    /// This used to be six copies of `load_f32_vec`, and they had
2389    /// drifted badly: this one decoded twenty dtypes while the five
2390    /// architecture loaders decoded three (F32/F16/BF16). A quantizer
2391    /// that emits a Q8_0 norm or bias -- ordinary for aggressive
2392    /// quants -- loaded on the generic path and was rejected with
2393    /// `UnsupportedDtype` on GLM-5.2, Kimi, DeepSeek-MLA, Gemma-4 and
2394    /// the hybrid stack.
2395    ///
2396    /// This file's own comment predicted exactly that, about the same
2397    /// split one level down: "a dtype ferrox can decode should never be
2398    /// rejected here just because the *other* dispatch table below
2399    /// knows it -- that split is how a supported format turns into a
2400    /// load failure on the one checkpoint that uses it."
2401    #[test]
2402    fn a_quantized_one_dimensional_tensor_widens_through_the_shared_helper() {
2403        let values: Vec<f32> = (0..64).map(|i| (i as f32 - 32.0) * 0.25).collect();
2404        let quantized = ferrox_quant::quantize_q8_0(&values);
2405
2406        struct OneTensor {
2407            info: TensorInfo,
2408            bytes: Vec<u8>,
2409        }
2410        impl TensorSource for OneTensor {
2411            fn metadata(&self, _key: &str) -> Option<&ferrox_gguf::GgufValue> {
2412                None
2413            }
2414            fn find_tensor(&self, name: &str) -> Option<&TensorInfo> {
2415                (name == self.info.name).then_some(&self.info)
2416            }
2417            fn tensor_bytes(&self, _name: &str) -> Result<&[u8], GgufError> {
2418                Ok(&self.bytes)
2419            }
2420            fn tensor_mapped_range(
2421                &self,
2422                name: &str,
2423            ) -> Result<
2424                (
2425                    std::sync::Arc<ferrox_gguf::MmapHandle>,
2426                    std::ops::Range<usize>,
2427                ),
2428                GgufError,
2429            > {
2430                // Never reached: `load_f32_vec` widens from bytes.
2431                Err(GgufError::TensorNotFound(name.to_string()))
2432            }
2433        }
2434
2435        let source = OneTensor {
2436            info: TensorInfo {
2437                name: "blk.0.attn_norm.weight".to_string(),
2438                shape: vec![64],
2439                dtype: GgmlType::Q8_0,
2440                offset: 0,
2441            },
2442            bytes: quantized,
2443        };
2444
2445        let widened = load_f32_vec(&source, "blk.0.attn_norm.weight")
2446            .expect("a Q8_0 norm must load, not report an unsupported dtype");
2447        assert_eq!(widened.len(), values.len());
2448        for (got, want) in widened.iter().zip(values.iter()) {
2449            assert!(
2450                (got - want).abs() < 0.05,
2451                "q8_0 round trip: got {got}, want {want}"
2452            );
2453        }
2454    }
2455    use super::*;
2456    use byteorder::{LittleEndian, WriteBytesExt};
2457    use std::io::Write;
2458
2459    fn write_string(buf: &mut Vec<u8>, s: &str) {
2460        buf.write_u64::<LittleEndian>(s.len() as u64).unwrap();
2461        buf.write_all(s.as_bytes()).unwrap();
2462    }
2463
2464    fn write_kv_str(buf: &mut Vec<u8>, key: &str, val: &str) {
2465        write_string(buf, key);
2466        buf.write_u32::<LittleEndian>(8).unwrap(); // type = string
2467        write_string(buf, val);
2468    }
2469
2470    /// A minimal, tensor-free GGUF byte buffer declaring only
2471    /// `general.architecture` (no `{arch}.block_count` or any other
2472    /// hparam key) -- the shape a stripped-down or malformed file might
2473    /// take, and the exact case `ModelConfig::from_gguf` must reject
2474    /// loudly rather than silently default around.
2475    fn build_arch_only_gguf(arch: &str) -> Vec<u8> {
2476        let mut buf = Vec::new();
2477        buf.write_u32::<LittleEndian>(ferrox_gguf::GGUF_MAGIC)
2478            .unwrap();
2479        buf.write_u32::<LittleEndian>(3).unwrap(); // version
2480        buf.write_u64::<LittleEndian>(0).unwrap(); // tensor_count
2481        buf.write_u64::<LittleEndian>(1).unwrap(); // kv_count
2482        write_kv_str(&mut buf, "general.architecture", arch);
2483        buf
2484    }
2485
2486    #[test]
2487    fn model_config_from_gguf_fails_loudly_when_required_hparams_are_missing() {
2488        let tmp =
2489            std::env::temp_dir().join(format!("ferrox_test_arch_only_{}.gguf", std::process::id()));
2490        // Use a registered architecture so the failure is MissingHparam,
2491        // not UnsupportedArchitecture.
2492        std::fs::write(&tmp, build_arch_only_gguf("llama")).unwrap();
2493        let file = ferrox_gguf::GgufFile::open(&tmp).expect("minimal header must still parse");
2494        std::fs::remove_file(&tmp).ok();
2495
2496        match ModelConfig::from_gguf(&file) {
2497            Err(LoadError::MissingHparam(key)) => {
2498                assert_eq!(key, "llama.block_count");
2499            }
2500            other => panic!(
2501                "expected LoadError::MissingHparam for a file with no hparam keys, got {other:?}"
2502            ),
2503        }
2504    }
2505
2506    #[test]
2507    fn model_config_from_gguf_fails_closed_on_unknown_architecture() {
2508        let tmp = std::env::temp_dir().join(format!(
2509            "ferrox_test_unknown_arch_{}.gguf",
2510            std::process::id()
2511        ));
2512        std::fs::write(&tmp, build_arch_only_gguf("bogus-arch-with-no-hparams")).unwrap();
2513        let file = ferrox_gguf::GgufFile::open(&tmp).expect("minimal header must still parse");
2514        std::fs::remove_file(&tmp).ok();
2515
2516        match ModelConfig::from_gguf(&file) {
2517            Err(LoadError::UnsupportedArchitecture(arch)) => {
2518                assert_eq!(arch, "bogus-arch-with-no-hparams");
2519            }
2520            other => panic!(
2521                "expected LoadError::UnsupportedArchitecture for an unregistered arch, got {other:?}"
2522            ),
2523        }
2524    }
2525
2526    fn write_kv_f32(buf: &mut Vec<u8>, key: &str, val: f32) {
2527        write_string(buf, key);
2528        buf.write_u32::<LittleEndian>(6).unwrap(); // type = float32
2529        buf.write_f32::<LittleEndian>(val).unwrap();
2530    }
2531
2532    /// `arch` plus one f32 hparam, so a metadata-only feature gate can be
2533    /// exercised without building a whole checkpoint.
2534    fn build_arch_plus_f32_gguf(arch: &str, key: &str, val: f32) -> Vec<u8> {
2535        let mut buf = Vec::new();
2536        buf.write_u32::<LittleEndian>(ferrox_gguf::GGUF_MAGIC)
2537            .unwrap();
2538        buf.write_u32::<LittleEndian>(3).unwrap(); // version
2539        buf.write_u64::<LittleEndian>(0).unwrap(); // tensor_count
2540        buf.write_u64::<LittleEndian>(2).unwrap(); // kv_count
2541        write_kv_str(&mut buf, "general.architecture", arch);
2542        write_kv_f32(&mut buf, key, val);
2543        buf
2544    }
2545
2546    fn config_error_for(arch: &str, key: &str, val: f32, tag: &str) -> LoadError {
2547        let tmp = std::env::temp_dir().join(format!("ferrox_test_scale_{tag}.gguf"));
2548        std::fs::write(&tmp, build_arch_plus_f32_gguf(arch, key, val)).unwrap();
2549        let file = ferrox_gguf::GgufFile::open(&tmp).expect("minimal header must still parse");
2550        std::fs::remove_file(&tmp).ok();
2551        ModelConfig::from_gguf(&file).expect_err("must not succeed")
2552    }
2553
2554    /// Granite / MiniCPM / Command-R multipliers are hparams, not
2555    /// tensors, so `assert_every_tensor_consumed` cannot see them: a
2556    /// checkpoint declaring one loads, runs at full speed, and computes
2557    /// a differently-scaled graph than it was trained as. Refuse by name
2558    /// until the math lands.
2559    #[test]
2560    fn a_declared_multiplier_this_decoder_does_not_apply_is_refused_by_name() {
2561        for (key, val) in [
2562            ("granite.logit_scale", 6.0f32),
2563            ("granite.residual_scale", 0.22),
2564            ("granite.embedding_scale", 12.0),
2565            ("granite.attention.scale", 0.015_625),
2566        ] {
2567            let tag = key.replace('.', "_");
2568            match config_error_for("granite", key, val, &tag) {
2569                LoadError::UnsupportedFeature(arch, msg) => {
2570                    assert_eq!(arch, "granite");
2571                    assert!(msg.contains(key), "error must name the key: {msg}");
2572                }
2573                other => panic!("expected UnsupportedFeature for {key}, got {other:?}"),
2574            }
2575        }
2576    }
2577
2578    /// The gate must not fire on a multiplier that is a no-op. A file
2579    /// writing `residual_scale = 1.0` describes the graph ferrox already
2580    /// computes, and refusing it would be a false alarm. llama.cpp's
2581    /// `f_attention_scale` uses `0.0` rather than `1.0` as its "unset"
2582    /// sentinel, so the two are checked against their own no-op values.
2583    #[test]
2584    fn a_multiplier_that_is_a_no_op_is_not_refused() {
2585        for (key, val) in [
2586            ("granite.logit_scale", 1.0f32),
2587            ("granite.residual_scale", 1.0),
2588            ("granite.embedding_scale", 1.0),
2589            ("granite.attention.scale", 0.0),
2590        ] {
2591            let tag = format!("noop_{}", key.replace('.', "_"));
2592            // The file carries no `block_count`, so the load still fails
2593            // -- but on the *missing hparam*, having passed this gate.
2594            match config_error_for("granite", key, val, &tag) {
2595                LoadError::MissingHparam(k) => assert_eq!(k, "granite.block_count"),
2596                other => panic!("no-op {key}={val} must pass the scaling gate, got {other:?}"),
2597            }
2598        }
2599    }
2600
2601    /// One GGUF metadata value, in the three types these header-only
2602    /// fixtures need.
2603    enum Kv<'a> {
2604        Str(&'a str),
2605        U32(u32),
2606        F32(f32),
2607    }
2608
2609    /// A tensor-free GGUF carrying exactly `kvs` -- enough for
2610    /// `ModelConfig::from_gguf` to run without a single weight on disk.
2611    fn build_metadata_gguf(kvs: &[(&str, Kv)]) -> Vec<u8> {
2612        let mut buf = Vec::new();
2613        buf.write_u32::<LittleEndian>(ferrox_gguf::GGUF_MAGIC)
2614            .unwrap();
2615        buf.write_u32::<LittleEndian>(3).unwrap(); // version
2616        buf.write_u64::<LittleEndian>(0).unwrap(); // tensor_count
2617        buf.write_u64::<LittleEndian>(kvs.len() as u64).unwrap();
2618        for (k, v) in kvs {
2619            match v {
2620                Kv::Str(s) => write_kv_str(&mut buf, k, s),
2621                Kv::U32(n) => {
2622                    write_string(&mut buf, k);
2623                    buf.write_u32::<LittleEndian>(4).unwrap(); // type = uint32
2624                    buf.write_u32::<LittleEndian>(*n).unwrap();
2625                }
2626                Kv::F32(f) => write_kv_f32(&mut buf, k, *f),
2627            }
2628        }
2629        buf
2630    }
2631
2632    fn open_metadata_gguf(tag: &str, kvs: &[(&str, Kv)]) -> ferrox_gguf::GgufFile {
2633        let tmp = std::env::temp_dir().join(format!("ferrox_test_meta_{tag}.gguf"));
2634        std::fs::write(&tmp, build_metadata_gguf(kvs)).unwrap();
2635        let file = ferrox_gguf::GgufFile::open(&tmp).expect("header-only file must parse");
2636        std::fs::remove_file(&tmp).ok();
2637        file
2638    }
2639
2640    /// A minimal `llama` hparam set (64-wide single head, base 10000)
2641    /// plus whatever RoPE-scaling keys a test wants to add.
2642    fn llama_config_with(tag: &str, extra: &[(&str, Kv)]) -> ModelConfig {
2643        let mut kvs: Vec<(&str, Kv)> = vec![
2644            ("general.architecture", Kv::Str("llama")),
2645            ("llama.block_count", Kv::U32(1)),
2646            ("llama.embedding_length", Kv::U32(64)),
2647            ("llama.attention.head_count", Kv::U32(1)),
2648            ("llama.attention.head_count_kv", Kv::U32(1)),
2649            ("llama.attention.key_length", Kv::U32(64)),
2650            ("llama.rope.freq_base", Kv::F32(10_000.0)),
2651        ];
2652        for (k, v) in extra {
2653            kvs.push((
2654                k,
2655                match v {
2656                    Kv::Str(s) => Kv::Str(s),
2657                    Kv::U32(n) => Kv::U32(*n),
2658                    Kv::F32(f) => Kv::F32(*f),
2659                },
2660            ));
2661        }
2662        ModelConfig::from_gguf(&open_metadata_gguf(tag, &kvs)).expect("fixture must load")
2663    }
2664
2665    /// Builds a config for an arbitrary architecture tag, returning the
2666    /// error rather than unwrapping it.
2667    /// llama.cpp chooses the FFN gate activation PER ARCHITECTURE;
2668    /// ferrox chose it per family. Those are different partitions, and
2669    /// `grok` is where they disagree: `src/models/grok.cpp:165` passes
2670    /// `LLM_FFN_GELU` to `build_moe_ffn`, while `grok` is
2671    /// `DecoderFamily::StandardGqa` and so was handed SwiGLU -- a
2672    /// different FFN on every layer.
2673    ///
2674    /// Latent, because `grok` is not audited and refuses today. Pinned
2675    /// anyway: the failure mode is that auditing it later makes it
2676    /// silently wrong, and an audit is exactly when nobody thinks to
2677    /// re-check the activation.
2678    #[test]
2679    fn the_ffn_activation_follows_the_architecture_not_the_family() {
2680        use crate::capability::uses_geglu;
2681        use crate::config::FfnActivation;
2682
2683        assert!(uses_geglu("grok"), "grok's MoE FFN gate is GELU upstream");
2684        // Same family, SiLU upstream (`src/models/dbrx.cpp:122`), so the
2685        // family rule alone cannot be what selects grok.
2686        assert!(!uses_geglu("dbrx"));
2687        assert!(!uses_geglu("llama"));
2688
2689        // The Gemma lineage keeps its GELU through the FAMILY rule, so
2690        // the new per-architecture arm must not have displaced it.
2691        // gemma2/gemma3 only: `gemma` v1 is unaudited and refuses, so
2692        // it cannot be loaded to check its activation.
2693        for gemma in ["gemma2", "gemma3"] {
2694            assert!(
2695                !uses_geglu(gemma),
2696                "{gemma} is GELU via GemmaFamily; listing it here too \
2697                 would hide a later regression in the family rule"
2698            );
2699            assert_eq!(
2700                config_for_arch(gemma).expect("gemma loads").ffn_activation,
2701                FfnActivation::Gelu,
2702                "{gemma}"
2703            );
2704        }
2705
2706        // And a plain SwiGLU architecture stays SwiGLU.
2707        assert_eq!(
2708            config_for_arch("llama")
2709                .expect("llama loads")
2710                .ffn_activation,
2711            FfnActivation::Swiglu
2712        );
2713    }
2714
2715    /// The no-renormalise list is keyed on what llama.cpp's GRAPH does,
2716    /// not on what a GGUF says, because for these architectures the
2717    /// GGUF says nothing.
2718    ///
2719    /// `expert_weights_norm` is only written by converters that set it.
2720    /// `deepseek.cpp:145` passes `norm_w=false`, and
2721    /// `conversion/deepseek.py`'s `DeepseekModel` never writes the key
2722    /// -- only `DeepseekV2Model` does. So a real `deepseek` checkpoint
2723    /// carries no key at all and ferrox fell through to its default,
2724    /// renormalising the selected experts' softmax weights where
2725    /// llama.cpp leaves them alone.
2726    ///
2727    /// The same mistake made OLMoE emit garbage, which is why that list
2728    /// exists. This pins the membership so a later edit cannot quietly
2729    /// drop a name back into the renormalising default.
2730    #[test]
2731    fn the_architectures_llama_cpp_does_not_renormalise_are_pinned() {
2732        for arch in ["deepseek", "olmoe", "qwen2moe"] {
2733            assert!(
2734                NO_TOPK_RENORMALIZE_ARCHITECTURES.contains(&arch),
2735                "{arch} passes norm_w=false in llama.cpp and must not be renormalised"
2736            );
2737        }
2738        // `deepseek2` is a DIFFERENT architecture whose converter DOES
2739        // write the key, so it must not be on this list -- it gets its
2740        // answer from the file.
2741        assert!(!NO_TOPK_RENORMALIZE_ARCHITECTURES.contains(&"deepseek2"));
2742        assert!(!NO_TOPK_RENORMALIZE_ARCHITECTURES.contains(&"qwen3moe"));
2743    }
2744
2745    /// Every architecture llama.cpp defaults to SIGMOID gating must be
2746    /// on the list, because for these the GGUF carries no key to say so.
2747    ///
2748    /// Each of these reads `LLM_KV_EXPERT_GATING_FUNC` as optional and
2749    /// then sets SIGMOID when it is absent, so a converted checkpoint
2750    /// has nothing in it that would correct ferrox's softmax default.
2751    /// Same shape as the `deepseek` top-k renormalisation bug, and as
2752    /// `phi3`'s sliding window: the file is silent and the architecture
2753    /// decides.
2754    #[test]
2755    fn the_architectures_llama_cpp_defaults_to_sigmoid_gating_are_pinned() {
2756        for arch in ["afmoe", "deepseek2", "glm4moe", "laguna", "step35"] {
2757            assert!(
2758                SIGMOID_GATING_ARCHITECTURES.contains(&arch),
2759                "{arch} sets SIGMOID when the gating key is absent"
2760            );
2761        }
2762        // Architectures that HARDCODE softmax must stay off it, or the
2763        // fix becomes the opposite bug: `ernie4-5-moe.cpp:90` and
2764        // `qwen3moe` both gate with softmax unconditionally.
2765        for softmax in ["ernie4_5-moe", "qwen3moe", "olmoe", "llama"] {
2766            assert!(
2767                !SIGMOID_GATING_ARCHITECTURES.contains(&softmax),
2768                "{softmax} does not default to sigmoid"
2769            );
2770        }
2771    }
2772
2773    /// Every name in every architecture-keyed behaviour table is a name
2774    /// the catalog actually resolves, on the generic-GQA path.
2775    ///
2776    /// These five tables are the repo's dominant bug shape in its purest
2777    /// form: five lists of strings that have to agree with a sixth
2778    /// structure (`capability::architecture_catalog`) about what an
2779    /// architecture is called, with nothing checking it. A typo, a
2780    /// hyphen where the GGUF has an underscore, or a name that later
2781    /// moves to a dedicated stack all produce the same thing -- an entry
2782    /// that reads as coverage and can never fire. This repo has shipped
2783    /// exactly that once already, in `unsupported_feature_keys`, keyed
2784    /// on a GGUF spelling no converter writes.
2785    ///
2786    /// The generic-path check is the second half and the sharper one: a
2787    /// behaviour flag on an architecture that is `DedicatedOnly` or
2788    /// `Deferred` never reaches this loader, so it is dead text.
2789    ///
2790    /// Sabotage to confirm: add `"seedoss"` to any list below.
2791    #[test]
2792    fn every_architecture_keyed_behaviour_table_names_a_real_generic_row() {
2793        let tables: &[(&str, &[&str])] = &[
2794            ("SIGMOID_GATING_ARCHITECTURES", SIGMOID_GATING_ARCHITECTURES),
2795            (
2796                "NO_TOPK_RENORMALIZE_ARCHITECTURES",
2797                NO_TOPK_RENORMALIZE_ARCHITECTURES,
2798            ),
2799            (
2800                "PRE_FFN_NORM_IS_POST_ATTENTION_NORM",
2801                PRE_FFN_NORM_IS_POST_ATTENTION_NORM,
2802            ),
2803            ("LEADING_DENSE_KEY_IS_INERT", LEADING_DENSE_KEY_IS_INERT),
2804            (
2805                "QK_NORM_AFTER_ROPE_ARCHITECTURES",
2806                QK_NORM_AFTER_ROPE_ARCHITECTURES,
2807            ),
2808        ];
2809        for (table, names) in tables {
2810            for arch in *names {
2811                let profile = crate::capability::resolve_profile(arch).unwrap_or_else(|| {
2812                    panic!("{table} names `{arch}`, which the catalog does not have")
2813                });
2814                if matches!(profile.path, crate::capability::ArchPath::GenericGqa { .. }) {
2815                    continue;
2816                }
2817                // Not a generic row, so the entry cannot fire HERE.
2818                // That is allowed only when something else is named as
2819                // applying the behaviour instead. An unexplained dead
2820                // entry still fails, which is the whole point.
2821                let owner = DEDICATED_OWNS_ITS_BEHAVIOUR
2822                    .iter()
2823                    .find(|(name, _)| name == arch)
2824                    .map(|(_, owner)| *owner);
2825                assert!(
2826                    owner.is_some(),
2827                    "{table} names `{arch}`, which resolves to {:?} and never reaches this \
2828                     loader, so the entry cannot fire. Either drop it, or add it to \
2829                     DEDICATED_OWNS_ITS_BEHAVIOUR naming what applies the behaviour instead",
2830                    profile.path
2831                );
2832            }
2833        }
2834    }
2835
2836    /// The three tables that describe how a layer is BUILT, rather than
2837    /// how it is routed, only carry architectures that are audited.
2838    ///
2839    /// The distinction matters and is not pedantry. A routing default
2840    /// (`SIGMOID_GATING_ARCHITECTURES`, `NO_TOPK_RENORMALIZE_ARCHITECTURES`)
2841    /// is allowed to name an architecture that still refuses: it is
2842    /// written down ahead of time so a later admission inherits the
2843    /// right answer, and the tables say so. But the three below change
2844    /// which TENSOR a layer reads and in what order -- and each was
2845    /// added for exactly one architecture, whose fixture is the only
2846    /// thing proving the change is right. A fourth name appearing here
2847    /// without evidence would be a claim about a graph nobody read,
2848    /// carried by a list whose doc comment cites two.
2849    #[test]
2850    fn the_layer_shape_tables_only_name_audited_architectures() {
2851        for (table, names) in [
2852            (
2853                "PRE_FFN_NORM_IS_POST_ATTENTION_NORM",
2854                PRE_FFN_NORM_IS_POST_ATTENTION_NORM,
2855            ),
2856            ("LEADING_DENSE_KEY_IS_INERT", LEADING_DENSE_KEY_IS_INERT),
2857            (
2858                "QK_NORM_AFTER_ROPE_ARCHITECTURES",
2859                QK_NORM_AFTER_ROPE_ARCHITECTURES,
2860            ),
2861        ] {
2862            for arch in names {
2863                assert!(
2864                    crate::capability::is_audited_generic(arch),
2865                    "{table} names `{arch}`, which is not in AUDITED_GENERIC_GQA. Either it \
2866                     has a fixture proving the change is right -- audit it -- or the entry \
2867                     is a guess about a graph"
2868                );
2869            }
2870        }
2871    }
2872
2873    fn config_for_arch(arch: &'static str) -> Result<ModelConfig, LoadError> {
2874        // The per-arch hyperparameter keys are looked up by the arch's
2875        // own prefix, so they have to be built for the arch under test.
2876        let keys: Vec<String> = [
2877            "block_count",
2878            "embedding_length",
2879            "attention.head_count",
2880            "attention.head_count_kv",
2881            "attention.key_length",
2882        ]
2883        .iter()
2884        .map(|k| format!("{arch}.{k}"))
2885        .collect();
2886        let theta = format!("{arch}.rope.freq_base");
2887        let kvs: Vec<(&str, Kv)> = vec![
2888            ("general.architecture", Kv::Str(arch)),
2889            (keys[0].as_str(), Kv::U32(1)),
2890            (keys[1].as_str(), Kv::U32(64)),
2891            (keys[2].as_str(), Kv::U32(1)),
2892            (keys[3].as_str(), Kv::U32(1)),
2893            (keys[4].as_str(), Kv::U32(64)),
2894            (theta.as_str(), Kv::F32(10_000.0)),
2895        ];
2896        ModelConfig::from_gguf(&open_metadata_gguf(arch, &kvs))
2897    }
2898
2899    /// The generic path is OPT-IN, and this is what proves it.
2900    ///
2901    /// An architecture nobody has checked used to FALL ONTO generic GQA
2902    /// and run. Five did exactly that and computed the wrong thing for
2903    /// the life of the project. The refusal exists; nothing tested it,
2904    /// so a reordering or an unevidenced addition to
2905    /// `AUDITED_GENERIC_GQA` would have gone unnoticed.
2906    #[test]
2907    fn an_unaudited_generic_architecture_refuses_rather_than_guessing() {
2908        // `xverse` is on the generic path and is not in the audited
2909        // list: nobody has run a real one through ferrox. It replaced
2910        // `starcoder`, which was the example here until an audit found
2911        // starcoder REQUIRES a fused `attn_qkv.bias` and a learned
2912        // `position_embd` that the generic decoder has no slot for --
2913        // so it now refuses for a stronger reason than being unaudited,
2914        // and stopped being an example of this one.
2915        assert!(
2916            !crate::capability::is_audited_generic("xverse"),
2917            "this test needs an arch that is generic AND unaudited"
2918        );
2919        match config_for_arch("xverse") {
2920            Err(LoadError::UnauditedArchitecture(name, ..)) => assert_eq!(name, "xverse"),
2921            other => panic!("expected an unaudited refusal, got {other:?}"),
2922        }
2923    }
2924
2925    /// An architecture with evidence still loads, or the inversion would
2926    /// have turned every model off.
2927    #[test]
2928    fn an_audited_architecture_still_loads() {
2929        assert!(crate::capability::is_audited_generic("llama"));
2930        assert!(config_for_arch("llama").is_ok());
2931    }
2932
2933    /// A NAMED problem must outrank "unaudited".
2934    ///
2935    /// `gpt2` uses learned absolute position embeddings, and that is
2936    /// what its refusal should say. Reporting "unaudited" instead would
2937    /// be true and far less useful, and it is the ordering the loader's
2938    /// own comment claims. Nothing checked that claim.
2939    #[test]
2940    fn a_named_refusal_outranks_the_unaudited_one() {
2941        let err = config_for_arch("gpt2").expect_err("gpt2 must refuse");
2942        assert!(
2943            !matches!(err, LoadError::UnauditedArchitecture(..)),
2944            "gpt2 should report its own reason, not that nobody audited it: {err:?}"
2945        );
2946    }
2947
2948    /// A checkpoint that declares YaRN gets the per-band divisors the
2949    /// reference's `"yarn"` arm implies, folded into `rope_freqs` so the
2950    /// existing RoPE kernels apply them. Expected values are hand-derived
2951    /// from `_find_correction_dim` for this fixture (rotary width 64,
2952    /// base 10000, original context 131072): `low = 22`, `high = 35`.
2953    ///
2954    /// Before this, ferrox read neither `rope.scaling.type` nor
2955    /// `rope.scaling.factor`, so this file roped exactly like an
2956    /// unscaled one -- correct near position 0, progressively wrong
2957    /// further in.
2958    #[test]
2959    fn a_gguf_declaring_yarn_gets_its_rope_frequencies_rewritten() {
2960        let cfg = llama_config_with(
2961            "yarn",
2962            &[
2963                ("llama.rope.scaling.type", Kv::Str("yarn")),
2964                ("llama.rope.scaling.factor", Kv::F32(8.0)),
2965                (
2966                    "llama.rope.scaling.original_context_length",
2967                    Kv::U32(131_072),
2968                ),
2969            ],
2970        );
2971        let factors = cfg
2972            .rope_freqs
2973            .expect("a YaRN checkpoint must carry rewritten per-band frequencies")
2974            .full;
2975        assert_eq!(factors.len(), 32, "one divisor per rotation band");
2976        assert!(
2977            (factors[0] - 1.0).abs() < 1e-6,
2978            "the fastest band is left extrapolated, got {}",
2979            factors[0]
2980        );
2981        let ramp = (31.0 - 22.0) / (35.0 - 22.0);
2982        let want = 1.0 / (ramp / 8.0 + (1.0 - ramp));
2983        assert!(
2984            (factors[31] - want).abs() < 1e-4,
2985            "slowest band: got {}, reference {want}",
2986            factors[31]
2987        );
2988    }
2989
2990    /// The rewrite must not fire on a file that did not ask for it. A
2991    /// scaling type ferrox does not implement (`linear`, `longrope`) is
2992    /// left exactly as it was rather than being roped as YaRN, which
2993    /// would be a new kind of wrong rather than the current known one.
2994    /// `rope.scaling.type = "linear"` must actually scale.
2995    ///
2996    /// Rotating position `p/s` is the same as rotating `p` with every
2997    /// band's frequency divided by `s`, and `rope_freqs` is exactly a
2998    /// per-band frequency divisor, so a uniform vector of `s` expresses
2999    /// linear scaling with no new code on the RoPE paths.
3000    ///
3001    /// Before this, the scaling type was compared against "yarn" and
3002    /// anything else returned None, so such a file loaded and roped at
3003    /// unscaled positions: a different model, no error.
3004    #[test]
3005    fn linear_scaling_is_applied_as_a_uniform_frequency_divisor() {
3006        let cfg = llama_config_with(
3007            "linear",
3008            &[
3009                ("llama.rope.scaling.type", Kv::Str("linear")),
3010                ("llama.rope.scaling.factor", Kv::F32(4.0)),
3011            ],
3012        );
3013        let freqs = &cfg
3014            .rope_freqs
3015            .as_ref()
3016            .expect("linear scaling must produce frequency factors")
3017            .full;
3018        assert_eq!(freqs.len(), cfg.head_dim / 2, "one factor per rotated pair");
3019        assert!(
3020            freqs.iter().all(|f| (*f - 4.0).abs() < 1e-6),
3021            "linear scaling is uniform across bands, unlike YaRN: got {freqs:?}"
3022        );
3023    }
3024
3025    /// A factor that corrects nothing is not a correction.
3026    #[test]
3027    fn a_linear_factor_of_one_is_treated_as_absent() {
3028        assert!(llama_config_with(
3029            "linear_one",
3030            &[
3031                ("llama.rope.scaling.type", Kv::Str("linear")),
3032                ("llama.rope.scaling.factor", Kv::F32(1.0)),
3033            ],
3034        )
3035        .rope_freqs
3036        .is_none());
3037    }
3038
3039    #[test]
3040    fn a_gguf_without_yarn_scaling_keeps_its_rope_frequencies_untouched() {
3041        assert!(llama_config_with("noscale", &[]).rope_freqs.is_none());
3042        // Linear scaling is NOT "no scaling". It used to land here,
3043        // asserted as `is_none()`, on the reasoning that leaving
3044        // positions alone beat roping them wrong in a new way. Both are
3045        // wrong output: llama.cpp divides the positions by the factor.
3046        // See `linear_scaling_is_applied_as_a_uniform_frequency_divisor`.
3047        // YaRN with a no-op factor is not a correction either.
3048        assert!(llama_config_with(
3049            "yarn_factor_one",
3050            &[
3051                ("llama.rope.scaling.type", Kv::Str("yarn")),
3052                ("llama.rope.scaling.factor", Kv::F32(1.0)),
3053                (
3054                    "llama.rope.scaling.original_context_length",
3055                    Kv::U32(131_072),
3056                ),
3057            ],
3058        )
3059        .rope_freqs
3060        .is_none());
3061    }
3062
3063    /// The correction range is measured against the context the
3064    /// checkpoint was *trained* at, so a file that declares YaRN without
3065    /// `rope.scaling.original_context_length` leaves the rotation alone
3066    /// rather than inventing a trained length (`context_length` on such
3067    /// a file is the *extended* one, which would put the ramp in the
3068    /// wrong place at every band).
3069    #[test]
3070    fn yarn_without_an_original_context_length_is_not_guessed_at() {
3071        let cfg = llama_config_with(
3072            "yarn_noctx",
3073            &[
3074                ("llama.rope.scaling.type", Kv::Str("yarn")),
3075                ("llama.rope.scaling.factor", Kv::F32(8.0)),
3076            ],
3077        );
3078        assert!(cfg.rope_freqs.is_none());
3079    }
3080
3081    /// `general.sampling.*` is the checkpoint's own recommendation, and
3082    /// only the keys the file carries become one: a file naming just
3083    /// `top_k` must leave temperature and top_p to the server's
3084    /// defaults.
3085    #[test]
3086    fn gguf_sampling_metadata_is_read_as_the_checkpoints_recommendation() {
3087        use crate::sampling::RecommendedSampling;
3088        let full = RecommendedSampling::from_gguf(&open_metadata_gguf(
3089            "sampling_full",
3090            &[
3091                ("general.architecture", Kv::Str("llama")),
3092                ("general.sampling.temp", Kv::F32(1.0)),
3093                ("general.sampling.top_k", Kv::U32(20)),
3094                ("general.sampling.top_p", Kv::F32(0.95)),
3095            ],
3096        ));
3097        assert_eq!(
3098            full,
3099            RecommendedSampling {
3100                temperature: Some(1.0),
3101                top_p: Some(0.95),
3102                top_k: Some(20),
3103            }
3104        );
3105
3106        let partial = RecommendedSampling::from_gguf(&open_metadata_gguf(
3107            "sampling_partial",
3108            &[
3109                ("general.architecture", Kv::Str("llama")),
3110                ("general.sampling.top_k", Kv::U32(40)),
3111            ],
3112        ));
3113        assert_eq!(partial.top_k, Some(40));
3114        assert_eq!(partial.temperature, None);
3115        assert_eq!(partial.top_p, None);
3116    }
3117
3118    /// A converter that wrote `temp = 1` stores a GGUF integer, not a
3119    /// float. Dropping it would serve a checkpoint that asked for
3120    /// temperature 1.0 at the framework's greedy default -- the
3121    /// repetition-loop failure the recommendation exists to prevent.
3122    #[test]
3123    fn an_integer_valued_sampling_temp_is_still_a_recommendation() {
3124        let recommended = crate::sampling::RecommendedSampling::from_gguf(&open_metadata_gguf(
3125            "sampling_int_temp",
3126            &[
3127                ("general.architecture", Kv::Str("llama")),
3128                ("general.sampling.temp", Kv::U32(1)),
3129            ],
3130        ));
3131        assert_eq!(recommended.temperature, Some(1.0));
3132    }
3133
3134    /// The overwhelming majority of checkpoints recommend nothing, and
3135    /// those must keep ferrox's existing defaults exactly.
3136    #[test]
3137    fn a_gguf_without_sampling_metadata_recommends_nothing() {
3138        let recommended = crate::sampling::RecommendedSampling::from_gguf(&open_metadata_gguf(
3139            "sampling_absent",
3140            &[("general.architecture", Kv::Str("llama"))],
3141        ));
3142        assert!(recommended.is_empty());
3143    }
3144
3145    #[test]
3146    fn model_config_from_gguf_rejects_dedicated_architectures() {
3147        let tmp = std::env::temp_dir().join(format!(
3148            "ferrox_test_dedicated_arch_{}.gguf",
3149            std::process::id()
3150        ));
3151        std::fs::write(&tmp, build_arch_only_gguf("deepseek4")).unwrap();
3152        let file = ferrox_gguf::GgufFile::open(&tmp).expect("minimal header must still parse");
3153        std::fs::remove_file(&tmp).ok();
3154
3155        match ModelConfig::from_gguf(&file) {
3156            Err(LoadError::DedicatedArchitectureRequired(arch, _)) => {
3157                assert_eq!(arch, "deepseek4");
3158            }
3159            other => panic!(
3160                "expected LoadError::DedicatedArchitectureRequired for deepseek4, got {other:?}"
3161            ),
3162        }
3163    }
3164
3165    /// The same Q5_K block bytes cross-validated against an independent
3166    /// Python reference in `ferrox-quant`'s own tests, reused here for
3167    /// the same full-path proof as the Q6_K test below.
3168    #[rustfmt::skip]
3169    const Q5_K_TEST_BLOCK: [u8; 176] = [
3170        0x66, 0x2a, 0x66, 0x2a, 0x01, 0x01, 0x01, 0x01, 0x4f, 0x4b, 0x10, 0x12, 0x41, 0xe2, 0xc1,
3171        0xb1, 0x72, 0x2f, 0x20, 0x07, 0x31, 0x0c, 0x38, 0xb3, 0x9c, 0xb8, 0xad, 0x2f, 0x9a, 0xea,
3172        0x17, 0xd0, 0xee, 0x93, 0x9e, 0x3e, 0x74, 0xbb, 0x28, 0x18, 0x39, 0x25, 0xb6, 0x09, 0x18,
3173        0x29, 0x1c, 0x1d, 0x29, 0x41, 0x40, 0x0a, 0x74, 0x7d, 0xfd, 0x21, 0xdd, 0x6d, 0x45, 0x73,
3174        0x0e, 0x1e, 0xc0, 0x4a, 0xfc, 0xf3, 0x8e, 0x24, 0x6b, 0x34, 0x7d, 0xbe, 0x94, 0xde, 0x59,
3175        0x7a, 0x35, 0x30, 0x36, 0x0a, 0xf9, 0x4a, 0x9b, 0xa2, 0x26, 0x21, 0xa2, 0xfa, 0xdf, 0x4b,
3176        0x29, 0x64, 0x6f, 0xbb, 0xca, 0x0f, 0x3c, 0xda, 0x20, 0xf4, 0x93, 0x86, 0xab, 0x6e, 0xb9,
3177        0xe5, 0xd5, 0xa0, 0x82, 0xd6, 0x41, 0xff, 0x12, 0xbc, 0x34, 0xbb, 0xab, 0xb8, 0x20, 0x2f,
3178        0xbb, 0x5f, 0x0c, 0x10, 0xcf, 0x49, 0xc5, 0x86, 0x5c, 0xdf, 0xff, 0x78, 0x44, 0x26, 0x3b,
3179        0xc2, 0x23, 0x3d, 0x2b, 0xe9, 0x00, 0x12, 0xf8, 0xea, 0xe2, 0x9e, 0x5e, 0x50, 0x20, 0x9f,
3180        0x9d, 0x8d, 0x7d, 0x7f, 0xcc, 0x1d, 0x0e, 0x13, 0xf8, 0xc2, 0xf1, 0x3d, 0x08, 0x2f, 0x23,
3181        0x13, 0xac, 0x0d, 0xa7, 0xe7, 0x20, 0xa3, 0x90, 0xb7, 0xc8, 0x28,
3182    ];
3183
3184    fn build_single_q5_k_tensor_gguf() -> Vec<u8> {
3185        let mut buf = Vec::new();
3186        buf.write_u32::<LittleEndian>(ferrox_gguf::GGUF_MAGIC)
3187            .unwrap();
3188        buf.write_u32::<LittleEndian>(3).unwrap(); // version
3189        buf.write_u64::<LittleEndian>(1).unwrap(); // tensor_count
3190        buf.write_u64::<LittleEndian>(1).unwrap(); // kv_count
3191
3192        write_kv_str(&mut buf, "general.architecture", "ferrox-q5k-test");
3193
3194        write_string(&mut buf, "test.weight");
3195        buf.write_u32::<LittleEndian>(2).unwrap(); // n_dims
3196                                                   // Real GGUF ne[] order is fastest-varying-first, i.e. [cols,
3197                                                   // rows] -- reversed from the semantic [rows, cols] this tensor
3198                                                   // represents (1 row, 256 cols / 1 Q5_K block).
3199        buf.write_u64::<LittleEndian>(256).unwrap(); // cols (1 Q5_K block)
3200        buf.write_u64::<LittleEndian>(1).unwrap(); // rows
3201        buf.write_u32::<LittleEndian>(13).unwrap(); // dtype tag: Q5_K
3202        buf.write_u64::<LittleEndian>(0).unwrap(); // offset
3203
3204        while buf.len() % 32 != 0 {
3205            buf.push(0);
3206        }
3207        buf.extend_from_slice(&Q5_K_TEST_BLOCK);
3208        buf
3209    }
3210
3211    /// How far a fused dot may sit from an exact dequantized dot.
3212    ///
3213    /// Two regimes, and one fixed number cannot describe both. With
3214    /// `FERROX_CPU_INT_DOT` off the activation stays f32 and only
3215    /// rounding separates the two. With it on, the activation is
3216    /// quantized to int8 at `d = amax / 127`, which is the flag both
3217    /// binaries turn on by default and the reason the Q5_K and Q6_K
3218    /// cases failed against a flat `1e-2`.
3219    ///
3220    /// The bound grows with the L2 norm of the row, NOT the L1. Each
3221    /// element carries an independent rounding of up to `d/2`, so the
3222    /// dot's error is a sum of independent terms whose standard
3223    /// deviation is `d/sqrt(12) * ||w||_2`. Bounding by the worst case
3224    /// `d/2 * ||w||_1` instead assumes every rounding aligns with its
3225    /// weight's sign, which on this fixture gives 0.347 against a dot
3226    /// of 2.77: 12% of the value, loose enough that injecting a 5%
3227    /// error still passed. Measured here, the real error is 1.8 sigma,
3228    /// so four sigma keeps better than 2x headroom while still failing
3229    /// that 5% injection.
3230    fn fused_dot_tolerance(weights: &[f32], x: &[f32], exact_bound: f32) -> f32 {
3231        if !ferrox_core::weight_matrix::cpu_int_dot_enabled() {
3232            return exact_bound;
3233        }
3234        let amax = x.iter().fold(0.0f32, |a, v| a.max(v.abs()));
3235        let l2 = weights.iter().map(|w| w * w).sum::<f32>().sqrt();
3236        4.0 * (amax / 127.0) / 12f32.sqrt() * l2 + exact_bound
3237    }
3238
3239    #[test]
3240    fn load_weight_matrix_handles_a_real_on_disk_q5_k_tensor_end_to_end() {
3241        let tmp = std::env::temp_dir().join(format!(
3242            "ferrox_test_q5k_tensor_{}.gguf",
3243            std::process::id()
3244        ));
3245        std::fs::write(&tmp, build_single_q5_k_tensor_gguf()).unwrap();
3246        let file = ferrox_gguf::GgufFile::open(&tmp).expect("real Q5_K GGUF file must parse");
3247        std::fs::remove_file(&tmp).ok();
3248
3249        let matrix = load_weight_matrix(&file, "test.weight").expect("Q5_K tensor must load");
3250        assert_eq!(matrix.rows(), 1);
3251        assert_eq!(matrix.cols(), 256);
3252        match &matrix {
3253            WeightMatrix::Quantized { kind, data, .. } => {
3254                assert_eq!(*kind, QuantKind::Q5K);
3255                assert!(
3256                    data.is_mapped(),
3257                    "Q5_K tensors should take the zero-copy mmap path, same as Q8_0/Q4_0"
3258                );
3259            }
3260            _ => panic!("expected a Quantized matrix for a Q5_K tensor"),
3261        }
3262
3263        let expected = ferrox_quant::dequant_q5_k(&Q5_K_TEST_BLOCK).unwrap();
3264        let x: Vec<f32> = (0..256).map(|i| ((i as f32) * 0.013).sin()).collect();
3265        let expected_dot: f32 = expected.iter().zip(x.iter()).map(|(a, b)| a * b).sum();
3266
3267        let got = matrix.apply(&x);
3268        assert_eq!(got.len(), 1);
3269        assert!(
3270            (got[0] - expected_dot).abs() < fused_dot_tolerance(&expected, &x, 1e-2),
3271            "end-to-end loaded+applied Q5_K matrix diverged from direct dequant: got={} expected={}",
3272            got[0],
3273            expected_dot
3274        );
3275    }
3276
3277    /// The same Q6_K block bytes cross-validated against an independent
3278    /// Python reference in `ferrox-quant`'s own tests; reused here to
3279    /// prove the *full*
3280    /// path -- real on-disk GGUF bytes, parsed by `ferrox-gguf`, read
3281    /// through `GgufFile::tensor_mapped_range`, dispatched by
3282    /// `WeightMatrix::apply` to `ferrox_quant::dot_q6_k_f32` -- produces
3283    /// the same result as directly dequantizing those bytes, not just
3284    /// that the isolated kernel is correct in unit-test isolation.
3285    #[rustfmt::skip]
3286    const Q6_K_TEST_BLOCK: [u8; 210] = [
3287        0xe0, 0xa5, 0x40, 0x5c, 0x8d, 0x3a, 0x0a, 0x26, 0xfb, 0x4b, 0x6e, 0x9a, 0xdf, 0x3e, 0xa3,
3288        0xc4, 0xf8, 0x2b, 0x1d, 0x95, 0x76, 0x7d, 0x3b, 0xcd, 0xfd, 0xef, 0xc2, 0x0b, 0x07, 0x63,
3289        0x29, 0xfb, 0x81, 0x57, 0xbe, 0xbe, 0x06, 0xf7, 0x3a, 0x92, 0xc4, 0x43, 0xff, 0xad, 0xac,
3290        0x7e, 0x0f, 0x00, 0x2a, 0x4f, 0xf0, 0xf8, 0xa9, 0xfa, 0x3c, 0x90, 0x6d, 0x73, 0x2d, 0x5a,
3291        0xe6, 0xc6, 0x46, 0xf2, 0x0d, 0x55, 0x4c, 0x25, 0x38, 0x71, 0x2b, 0x35, 0x38, 0x82, 0x16,
3292        0x37, 0x5f, 0x32, 0x61, 0x02, 0xdd, 0x2f, 0x6f, 0x7b, 0x1f, 0xb4, 0x1a, 0x1b, 0x3e, 0x4f,
3293        0x11, 0xa3, 0x17, 0x40, 0x5a, 0x5f, 0x76, 0xcd, 0x19, 0x27, 0x9b, 0xc7, 0xc8, 0xf7, 0xf7,
3294        0xee, 0xf4, 0x86, 0xd9, 0xfd, 0xa7, 0xfe, 0x9e, 0xac, 0x70, 0x53, 0x5b, 0x76, 0xfb, 0x39,
3295        0xf8, 0x4b, 0x98, 0xfe, 0xd0, 0x06, 0x21, 0x4c, 0x4d, 0xbe, 0x10, 0x2b, 0x06, 0x65, 0xc9,
3296        0x5e, 0xf9, 0x95, 0x72, 0xae, 0x99, 0xd9, 0x7e, 0x15, 0xbd, 0x5e, 0x6d, 0xe8, 0x25, 0x8a,
3297        0xd5, 0x99, 0xc6, 0x6b, 0x69, 0xc7, 0x84, 0xc6, 0xa4, 0xf7, 0xb9, 0x6d, 0x68, 0x45, 0x0e,
3298        0x65, 0x69, 0xeb, 0xe6, 0xeb, 0xe9, 0x28, 0xa6, 0xb9, 0x96, 0xf2, 0xe8, 0xa7, 0x9b, 0x6e,
3299        0x79, 0x8a, 0x68, 0x65, 0x59, 0x98, 0x8b, 0x44, 0x41, 0x98, 0x9a, 0x56, 0x01, 0x01, 0x01,
3300        0x02, 0x01, 0x01, 0x01, 0x01, 0x02, 0x01, 0x02, 0x02, 0x01, 0x01, 0x01, 0x02, 0x1f, 0x25,
3301    ];
3302
3303    fn build_single_q6_k_tensor_gguf() -> Vec<u8> {
3304        let mut buf = Vec::new();
3305        buf.write_u32::<LittleEndian>(ferrox_gguf::GGUF_MAGIC)
3306            .unwrap();
3307        buf.write_u32::<LittleEndian>(3).unwrap(); // version
3308        buf.write_u64::<LittleEndian>(1).unwrap(); // tensor_count
3309        buf.write_u64::<LittleEndian>(1).unwrap(); // kv_count
3310
3311        write_kv_str(&mut buf, "general.architecture", "ferrox-q6k-test");
3312
3313        write_string(&mut buf, "test.weight");
3314        buf.write_u32::<LittleEndian>(2).unwrap(); // n_dims
3315                                                   // Real GGUF ne[] order is fastest-varying-first, i.e. [cols, rows].
3316        buf.write_u64::<LittleEndian>(256).unwrap(); // cols (1 Q6_K block)
3317        buf.write_u64::<LittleEndian>(1).unwrap(); // rows
3318        buf.write_u32::<LittleEndian>(14).unwrap(); // dtype tag: Q6_K
3319        buf.write_u64::<LittleEndian>(0).unwrap(); // offset
3320
3321        while buf.len() % 32 != 0 {
3322            buf.push(0);
3323        }
3324        buf.extend_from_slice(&Q6_K_TEST_BLOCK);
3325        buf
3326    }
3327
3328    #[test]
3329    fn load_weight_matrix_handles_a_real_on_disk_q6_k_tensor_end_to_end() {
3330        let tmp = std::env::temp_dir().join(format!(
3331            "ferrox_test_q6k_tensor_{}.gguf",
3332            std::process::id()
3333        ));
3334        std::fs::write(&tmp, build_single_q6_k_tensor_gguf()).unwrap();
3335        let file = ferrox_gguf::GgufFile::open(&tmp).expect("real Q6_K GGUF file must parse");
3336        std::fs::remove_file(&tmp).ok();
3337
3338        let matrix = load_weight_matrix(&file, "test.weight").expect("Q6_K tensor must load");
3339        assert_eq!(matrix.rows(), 1);
3340        assert_eq!(matrix.cols(), 256);
3341        match &matrix {
3342            WeightMatrix::Quantized { kind, data, .. } => {
3343                assert_eq!(*kind, QuantKind::Q6K);
3344                assert!(
3345                    data.is_mapped(),
3346                    "Q6_K tensors should take the zero-copy mmap path, same as Q8_0/Q4_0"
3347                );
3348            }
3349            _ => panic!("expected a Quantized matrix for a Q6_K tensor"),
3350        }
3351
3352        let expected = ferrox_quant::dequant_q6_k(&Q6_K_TEST_BLOCK).unwrap();
3353        let x: Vec<f32> = (0..256).map(|i| ((i as f32) * 0.013).sin()).collect();
3354        let expected_dot: f32 = expected.iter().zip(x.iter()).map(|(a, b)| a * b).sum();
3355
3356        let got = matrix.apply(&x);
3357        assert_eq!(got.len(), 1);
3358        assert!(
3359            (got[0] - expected_dot).abs() < fused_dot_tolerance(&expected, &x, 1e-2),
3360            "end-to-end loaded+applied Q6_K matrix diverged from direct dequant: got={} expected={}",
3361            got[0],
3362            expected_dot
3363        );
3364    }
3365
3366    fn build_single_bf16_tensor_gguf(rows: u64, cols: u64, values: &[f32]) -> Vec<u8> {
3367        let mut buf = Vec::new();
3368        buf.write_u32::<LittleEndian>(ferrox_gguf::GGUF_MAGIC)
3369            .unwrap();
3370        buf.write_u32::<LittleEndian>(3).unwrap(); // version
3371        buf.write_u64::<LittleEndian>(1).unwrap(); // tensor_count
3372        buf.write_u64::<LittleEndian>(1).unwrap(); // kv_count
3373
3374        write_kv_str(&mut buf, "general.architecture", "ferrox-bf16-test");
3375
3376        write_string(&mut buf, "test.weight");
3377        buf.write_u32::<LittleEndian>(2).unwrap(); // n_dims
3378                                                   // Real GGUF ne[] order is fastest-varying-first, i.e. [cols, rows].
3379        buf.write_u64::<LittleEndian>(cols).unwrap();
3380        buf.write_u64::<LittleEndian>(rows).unwrap();
3381        buf.write_u32::<LittleEndian>(30).unwrap(); // dtype tag: BF16
3382        buf.write_u64::<LittleEndian>(0).unwrap(); // offset
3383
3384        while buf.len() % 32 != 0 {
3385            buf.push(0);
3386        }
3387        for &v in values {
3388            // Real bf16 truncation (round-toward-zero, matching a real
3389            // writer closely enough for round-trip test purposes): top
3390            // 16 bits of the f32 bit pattern.
3391            let bf16_bits = (v.to_bits() >> 16) as u16;
3392            buf.extend_from_slice(&bf16_bits.to_le_bytes());
3393        }
3394        buf
3395    }
3396
3397    #[test]
3398    fn load_weight_matrix_handles_a_real_on_disk_bf16_tensor_end_to_end() {
3399        // Values with zero low-mantissa bits, so f32->bf16 truncation
3400        // is lossless and this is an exact-equality check.
3401        let values: Vec<f32> = vec![1.0, -2.5, 0.0, 4.0, -8.0, 16.0];
3402        let tmp = std::env::temp_dir().join(format!(
3403            "ferrox_test_bf16_tensor_{}.gguf",
3404            std::process::id()
3405        ));
3406        std::fs::write(&tmp, build_single_bf16_tensor_gguf(2, 3, &values)).unwrap();
3407        let file = ferrox_gguf::GgufFile::open(&tmp).expect("real BF16 GGUF file must parse");
3408        std::fs::remove_file(&tmp).ok();
3409
3410        let matrix = load_weight_matrix(&file, "test.weight").expect("BF16 tensor must load");
3411        assert_eq!(matrix.rows(), 2);
3412        assert_eq!(matrix.cols(), 3);
3413        match &matrix {
3414            WeightMatrix::F32(tensor) => {
3415                assert_eq!(tensor.data, values, "BF16 must widen to f32 exactly");
3416            }
3417            _ => panic!("expected an F32 matrix for a BF16 tensor (no fused dot kernel for it)"),
3418        }
3419    }
3420
3421    fn build_single_f16_tensor_gguf(rows: u64, cols: u64, values: &[f32]) -> Vec<u8> {
3422        let mut buf = Vec::new();
3423        buf.write_u32::<LittleEndian>(ferrox_gguf::GGUF_MAGIC)
3424            .unwrap();
3425        buf.write_u32::<LittleEndian>(3).unwrap(); // version
3426        buf.write_u64::<LittleEndian>(1).unwrap(); // tensor_count
3427        buf.write_u64::<LittleEndian>(1).unwrap(); // kv_count
3428
3429        write_kv_str(&mut buf, "general.architecture", "ferrox-f16-test");
3430
3431        write_string(&mut buf, "test.weight");
3432        buf.write_u32::<LittleEndian>(2).unwrap(); // n_dims
3433        buf.write_u64::<LittleEndian>(cols).unwrap();
3434        buf.write_u64::<LittleEndian>(rows).unwrap();
3435        buf.write_u32::<LittleEndian>(1).unwrap(); // dtype tag: F16
3436        buf.write_u64::<LittleEndian>(0).unwrap(); // offset
3437
3438        while buf.len() % 32 != 0 {
3439            buf.push(0);
3440        }
3441        for &v in values {
3442            buf.extend_from_slice(&half::f16::from_f32(v).to_le_bytes());
3443        }
3444        buf
3445    }
3446
3447    /// `GgmlType::F16` was parsed and sized but had no dequant arm in any
3448    /// of the seven loaders, so every `*-f16.gguf` was a hard
3449    /// `UnsupportedDtype`. Values are exactly representable in f16, so
3450    /// this is an exact-equality check.
3451    #[test]
3452    fn load_weight_matrix_handles_a_real_on_disk_f16_tensor_end_to_end() {
3453        let values: Vec<f32> = vec![1.0, -2.5, 0.0, 4.0, -8.0, 16.0];
3454        let tmp = std::env::temp_dir().join(format!(
3455            "ferrox_test_f16_tensor_{}.gguf",
3456            std::process::id()
3457        ));
3458        std::fs::write(&tmp, build_single_f16_tensor_gguf(2, 3, &values)).unwrap();
3459        let file = ferrox_gguf::GgufFile::open(&tmp).expect("real F16 GGUF file must parse");
3460        std::fs::remove_file(&tmp).ok();
3461
3462        let matrix = load_weight_matrix(&file, "test.weight").expect("F16 tensor must load");
3463        assert_eq!(matrix.rows(), 2);
3464        assert_eq!(matrix.cols(), 3);
3465        match &matrix {
3466            WeightMatrix::F32(tensor) => {
3467                assert_eq!(tensor.data, values, "F16 must widen to f32 exactly");
3468            }
3469            _ => panic!("expected an F32 matrix for an F16 tensor (no fused dot kernel for it)"),
3470        }
3471
3472        // The same tensor read as a plain vector (norm weights, biases and
3473        // the router all take this path, not `load_weight_matrix`).
3474        let tmp =
3475            std::env::temp_dir().join(format!("ferrox_test_f16_vec_{}.gguf", std::process::id()));
3476        std::fs::write(&tmp, build_single_f16_tensor_gguf(2, 3, &values)).unwrap();
3477        let file = ferrox_gguf::GgufFile::open(&tmp).expect("real F16 GGUF file must parse");
3478        std::fs::remove_file(&tmp).ok();
3479        assert_eq!(load_f32_vec(&file, "test.weight").unwrap(), values);
3480    }
3481
3482    fn build_single_q5_1_tensor_gguf() -> Vec<u8> {
3483        let mut buf = Vec::new();
3484        buf.write_u32::<LittleEndian>(ferrox_gguf::GGUF_MAGIC)
3485            .unwrap();
3486        buf.write_u32::<LittleEndian>(3).unwrap(); // version
3487        buf.write_u64::<LittleEndian>(1).unwrap(); // tensor_count
3488        buf.write_u64::<LittleEndian>(1).unwrap(); // kv_count
3489
3490        write_kv_str(&mut buf, "general.architecture", "ferrox-q5-1-test");
3491
3492        write_string(&mut buf, "test.weight");
3493        buf.write_u32::<LittleEndian>(2).unwrap(); // n_dims
3494                                                   // Real GGUF ne[] order is fastest-varying-first, i.e. [cols, rows].
3495        buf.write_u64::<LittleEndian>(32).unwrap(); // cols (1 Q5_1 block)
3496        buf.write_u64::<LittleEndian>(1).unwrap(); // rows
3497        buf.write_u32::<LittleEndian>(7).unwrap(); // dtype tag: Q5_1
3498        buf.write_u64::<LittleEndian>(0).unwrap(); // offset
3499
3500        while buf.len() % 32 != 0 {
3501            buf.push(0);
3502        }
3503        // d=0.25 (f16 0x3400), m=1.5 (f16 0x3E00) -- both exact in f16,
3504        // hand-verified bit patterns to avoid pulling in the `half`
3505        // crate just for two test constants. qh varied, qs a real
3506        // (non-degenerate) pattern.
3507        buf.extend_from_slice(&0x3400u16.to_le_bytes());
3508        buf.extend_from_slice(&0x3E00u16.to_le_bytes());
3509        buf.extend_from_slice(&[0x9au8, 0x3c, 0xf0, 0x0f]);
3510        buf.extend_from_slice(&(0..16u8).map(|i| i | ((15 - i) << 4)).collect::<Vec<u8>>());
3511        buf
3512    }
3513
3514    #[test]
3515    fn load_weight_matrix_handles_a_real_on_disk_q5_1_tensor_end_to_end() {
3516        let tmp = std::env::temp_dir().join(format!(
3517            "ferrox_test_q5_1_tensor_{}.gguf",
3518            std::process::id()
3519        ));
3520        std::fs::write(&tmp, build_single_q5_1_tensor_gguf()).unwrap();
3521        let file = ferrox_gguf::GgufFile::open(&tmp).expect("real Q5_1 GGUF file must parse");
3522        std::fs::remove_file(&tmp).ok();
3523
3524        let matrix = load_weight_matrix(&file, "test.weight").expect("Q5_1 tensor must load");
3525        assert_eq!(matrix.rows(), 1);
3526        assert_eq!(matrix.cols(), 32);
3527        let raw = file.tensor_bytes("test.weight").unwrap();
3528        let expected = ferrox_quant::dequant_q5_1(raw).unwrap();
3529        match &matrix {
3530            WeightMatrix::Quantized { kind, data, .. } => {
3531                assert_eq!(*kind, QuantKind::Q5_1);
3532                assert!(data.is_mapped());
3533            }
3534            _ => panic!("expected a Quantized matrix for a Q5_1 tensor"),
3535        }
3536
3537        let x: Vec<f32> = (0..32).map(|i| ((i as f32) * 0.017).cos()).collect();
3538        let expected_dot: f32 = expected.iter().zip(x.iter()).map(|(a, b)| a * b).sum();
3539        let got = matrix.apply(&x);
3540        assert_eq!(got.len(), 1);
3541        assert!(
3542            (got[0] - expected_dot).abs() < 1e-2,
3543            "end-to-end loaded+applied Q5_1 matrix diverged from direct dequant: got={} expected={}",
3544            got[0],
3545            expected_dot
3546        );
3547    }
3548
3549    // Same bytes as ferrox-quant's own Q3_K_TEST_BLOCK (Python-cross-
3550    // validated there); duplicated here to build a real on-disk GGUF
3551    // file, matching this file's existing per-format test convention
3552    // (see Q6_K_TEST_BLOCK above).
3553    const Q3_K_TEST_BLOCK: [u8; 110] = [
3554        0x56, 0xf2, 0xb4, 0x2b, 0xd5, 0x6f, 0x51, 0x71, 0x3c, 0x0a, 0xb9, 0x1d, 0xd0, 0xb9, 0x3b,
3555        0xb3, 0x0f, 0xff, 0x8c, 0xb2, 0x83, 0x3a, 0x3d, 0x24, 0xb1, 0x12, 0x56, 0xe3, 0x23, 0x54,
3556        0xf2, 0xfa, 0x7f, 0xdf, 0x31, 0xe1, 0x18, 0x26, 0x6e, 0xcd, 0x5b, 0x38, 0xee, 0xbd, 0x9f,
3557        0x8c, 0x57, 0x47, 0x0b, 0x11, 0xcb, 0xfb, 0xb4, 0x83, 0xa0, 0x4e, 0x0b, 0xd4, 0xa7, 0x85,
3558        0xe0, 0x60, 0xf3, 0xb3, 0xe3, 0x95, 0x43, 0xc6, 0x05, 0x05, 0x77, 0x53, 0xed, 0x23, 0xcc,
3559        0x6a, 0x0e, 0x89, 0xa1, 0x79, 0x85, 0xf6, 0x6e, 0x5a, 0x23, 0x63, 0xbe, 0x53, 0xfa, 0xa2,
3560        0x2b, 0xe9, 0xcd, 0xce, 0xf8, 0x3d, 0x6f, 0xd0, 0x42, 0x6e, 0x3b, 0x7f, 0x23, 0x26, 0xd3,
3561        0xb9, 0x18, 0xbf, 0xa4, 0x34,
3562    ];
3563
3564    fn build_single_q3_k_tensor_gguf() -> Vec<u8> {
3565        let mut buf = Vec::new();
3566        buf.write_u32::<LittleEndian>(ferrox_gguf::GGUF_MAGIC)
3567            .unwrap();
3568        buf.write_u32::<LittleEndian>(3).unwrap(); // version
3569        buf.write_u64::<LittleEndian>(1).unwrap(); // tensor_count
3570        buf.write_u64::<LittleEndian>(1).unwrap(); // kv_count
3571
3572        write_kv_str(&mut buf, "general.architecture", "ferrox-q3k-test");
3573
3574        write_string(&mut buf, "test.weight");
3575        buf.write_u32::<LittleEndian>(2).unwrap(); // n_dims
3576                                                   // Real GGUF ne[] order is fastest-varying-first, i.e. [cols, rows].
3577        buf.write_u64::<LittleEndian>(256).unwrap(); // cols (1 Q3_K block)
3578        buf.write_u64::<LittleEndian>(1).unwrap(); // rows
3579        buf.write_u32::<LittleEndian>(11).unwrap(); // dtype tag: Q3_K
3580        buf.write_u64::<LittleEndian>(0).unwrap(); // offset
3581
3582        while buf.len() % 32 != 0 {
3583            buf.push(0);
3584        }
3585        buf.extend_from_slice(&Q3_K_TEST_BLOCK);
3586        buf
3587    }
3588
3589    #[test]
3590    fn load_weight_matrix_handles_a_real_on_disk_q3_k_tensor_end_to_end() {
3591        let tmp = std::env::temp_dir().join(format!(
3592            "ferrox_test_q3k_tensor_{}.gguf",
3593            std::process::id()
3594        ));
3595        std::fs::write(&tmp, build_single_q3_k_tensor_gguf()).unwrap();
3596        let file = ferrox_gguf::GgufFile::open(&tmp).expect("real Q3_K GGUF file must parse");
3597        std::fs::remove_file(&tmp).ok();
3598
3599        let matrix = load_weight_matrix(&file, "test.weight").expect("Q3_K tensor must load");
3600        assert_eq!(matrix.rows(), 1);
3601        assert_eq!(matrix.cols(), 256);
3602        match &matrix {
3603            WeightMatrix::Quantized { kind, data, .. } => {
3604                assert_eq!(*kind, QuantKind::Q3K);
3605                assert!(data.is_mapped());
3606            }
3607            _ => panic!("expected a Quantized matrix for a Q3_K tensor"),
3608        }
3609
3610        let expected = ferrox_quant::dequant_q3_k(&Q3_K_TEST_BLOCK).unwrap();
3611        let x: Vec<f32> = (0..256).map(|i| ((i as f32) * 0.013).sin()).collect();
3612        let expected_dot: f32 = expected.iter().zip(x.iter()).map(|(a, b)| a * b).sum();
3613
3614        let got = matrix.apply(&x);
3615        assert_eq!(got.len(), 1);
3616        assert!(
3617            (got[0] - expected_dot).abs() < fused_dot_tolerance(&expected, &x, 1e-1),
3618            "end-to-end loaded+applied Q3_K matrix diverged from direct dequant: got={} expected={}",
3619            got[0],
3620            expected_dot
3621        );
3622    }
3623
3624    // Same bytes as ferrox-quant's own IQ4_XS_TEST_BLOCK (Python-cross-
3625    // validated there); duplicated here to build a real on-disk GGUF
3626    // file, matching this file's existing per-format test convention.
3627    const IQ4_XS_TEST_BLOCK: [u8; 136] = [
3628        0x5c, 0x33, 0xb4, 0x39, 0xd1, 0x64, 0x97, 0x82, 0xcb, 0xbd, 0x88, 0x95, 0xf3, 0x60, 0x2a,
3629        0xb5, 0xe7, 0x24, 0xd3, 0xee, 0xfe, 0x71, 0x13, 0xbe, 0x70, 0x84, 0x48, 0x79, 0x7b, 0x3e,
3630        0xf0, 0x55, 0xdc, 0xb2, 0xb2, 0xde, 0x32, 0xa1, 0x5b, 0x02, 0x01, 0xdc, 0x2a, 0xbb, 0xf7,
3631        0x0b, 0x8a, 0x88, 0xdd, 0x0b, 0x02, 0x7e, 0x5e, 0x76, 0x87, 0x30, 0x1e, 0x1c, 0xcf, 0x48,
3632        0xd7, 0x61, 0xf3, 0x51, 0x52, 0x17, 0x98, 0x0a, 0x87, 0xcf, 0x02, 0x91, 0xc8, 0xee, 0xc0,
3633        0x91, 0x69, 0x2a, 0x4f, 0x64, 0x68, 0xa7, 0xb2, 0xe6, 0x98, 0x21, 0x81, 0x75, 0x53, 0x2a,
3634        0x8d, 0x12, 0xae, 0xe0, 0xea, 0x0c, 0x75, 0xff, 0x22, 0x5e, 0x25, 0x19, 0xda, 0x2e, 0x51,
3635        0x4e, 0x81, 0xdc, 0x0e, 0x78, 0x86, 0xd7, 0x58, 0xb5, 0xb7, 0xf6, 0x45, 0xa9, 0x0a, 0x83,
3636        0xfd, 0x2a, 0x12, 0x7d, 0xf0, 0x12, 0x97, 0xe2, 0xfe, 0xf4, 0xd0, 0xa2, 0x11, 0x14, 0x78,
3637        0xdb,
3638    ];
3639
3640    fn build_single_iq4_xs_tensor_gguf() -> Vec<u8> {
3641        let mut buf = Vec::new();
3642        buf.write_u32::<LittleEndian>(ferrox_gguf::GGUF_MAGIC)
3643            .unwrap();
3644        buf.write_u32::<LittleEndian>(3).unwrap(); // version
3645        buf.write_u64::<LittleEndian>(1).unwrap(); // tensor_count
3646        buf.write_u64::<LittleEndian>(1).unwrap(); // kv_count
3647
3648        write_kv_str(&mut buf, "general.architecture", "ferrox-iq4xs-test");
3649
3650        write_string(&mut buf, "test.weight");
3651        buf.write_u32::<LittleEndian>(2).unwrap(); // n_dims
3652                                                   // Real GGUF ne[] order is fastest-varying-first, i.e. [cols, rows].
3653        buf.write_u64::<LittleEndian>(256).unwrap(); // cols (1 IQ4_XS block)
3654        buf.write_u64::<LittleEndian>(1).unwrap(); // rows
3655        buf.write_u32::<LittleEndian>(23).unwrap(); // dtype tag: IQ4_XS
3656        buf.write_u64::<LittleEndian>(0).unwrap(); // offset
3657
3658        while buf.len() % 32 != 0 {
3659            buf.push(0);
3660        }
3661        buf.extend_from_slice(&IQ4_XS_TEST_BLOCK);
3662        buf
3663    }
3664
3665    #[test]
3666    fn load_weight_matrix_handles_a_real_on_disk_iq4_xs_tensor_end_to_end() {
3667        let tmp = std::env::temp_dir().join(format!(
3668            "ferrox_test_iq4xs_tensor_{}.gguf",
3669            std::process::id()
3670        ));
3671        std::fs::write(&tmp, build_single_iq4_xs_tensor_gguf()).unwrap();
3672        let file = ferrox_gguf::GgufFile::open(&tmp).expect("real IQ4_XS GGUF file must parse");
3673        std::fs::remove_file(&tmp).ok();
3674
3675        let matrix = load_weight_matrix(&file, "test.weight").expect("IQ4_XS tensor must load");
3676        assert_eq!(matrix.rows(), 1);
3677        assert_eq!(matrix.cols(), 256);
3678        match &matrix {
3679            WeightMatrix::Quantized { kind, data, .. } => {
3680                assert_eq!(*kind, QuantKind::IQ4XS);
3681                assert!(data.is_mapped());
3682            }
3683            _ => panic!("expected a Quantized matrix for an IQ4_XS tensor"),
3684        }
3685
3686        let expected = ferrox_quant::dequant_iq4_xs(&IQ4_XS_TEST_BLOCK).unwrap();
3687        let x: Vec<f32> = (0..256).map(|i| ((i as f32) * 0.013).sin()).collect();
3688        let expected_dot: f32 = expected.iter().zip(x.iter()).map(|(a, b)| a * b).sum();
3689
3690        let got = matrix.apply(&x);
3691        assert_eq!(got.len(), 1);
3692        assert!(
3693            (got[0] - expected_dot).abs() < 1e-1,
3694            "end-to-end loaded+applied IQ4_XS matrix diverged from direct dequant: got={} expected={}",
3695            got[0],
3696            expected_dot
3697        );
3698    }
3699
3700    // Same bytes as ferrox-quant's own IQ low-bit test blocks
3701    // (Python-cross-validated there against the real compiled ggml
3702    // implementation), duplicated as literals for the same reason as
3703    // IQ4_XS_TEST_BLOCK above.
3704    const IQ1_S_TEST_BLOCK: [u8; 50] = [
3705        0x0a, 0x2f, 0xfa, 0x06, 0x1e, 0x37, 0x6f, 0xe3, 0x62, 0xd0, 0xb6, 0xa4, 0x25, 0xae, 0x76,
3706        0x14, 0x72, 0x5b, 0xfa, 0x05, 0xd1, 0xf1, 0x2a, 0x4c, 0xad, 0x29, 0xae, 0xf4, 0xcf, 0x0c,
3707        0x96, 0x51, 0x58, 0x03, 0x6d, 0xd3, 0x10, 0x92, 0x70, 0xff, 0x61, 0x58, 0xc8, 0x30, 0x25,
3708        0x64, 0x49, 0x85, 0xc0, 0x24,
3709    ];
3710    const IQ2_XXS_TEST_BLOCK: [u8; 66] = [
3711        0x29, 0x30, 0xd9, 0x33, 0x95, 0x4c, 0x08, 0x1e, 0xad, 0x79, 0x49, 0xf2, 0x8d, 0x5f, 0x93,
3712        0xea, 0x78, 0x18, 0x98, 0xb9, 0x94, 0x14, 0xad, 0xce, 0xca, 0x1d, 0xab, 0x81, 0x53, 0x4a,
3713        0x68, 0xd0, 0x59, 0x96, 0x36, 0x5d, 0xbe, 0x20, 0xc4, 0xff, 0xe4, 0x2c, 0xcd, 0x2f, 0x4f,
3714        0x4f, 0x67, 0x53, 0xc6, 0xd5, 0xa2, 0xfb, 0xc7, 0xf3, 0xe2, 0x6b, 0xf1, 0x99, 0x23, 0x1e,
3715        0x2d, 0x5e, 0x8c, 0x78, 0xc2, 0x31,
3716    ];
3717    const IQ3_XXS_TEST_BLOCK: [u8; 98] = [
3718        0x71, 0x31, 0x16, 0x0a, 0x79, 0x04, 0x5d, 0x87, 0xae, 0x2a, 0x4a, 0x43, 0xfd, 0x02, 0xba,
3719        0x6c, 0x10, 0x42, 0x80, 0xe5, 0x1d, 0x08, 0x22, 0xcb, 0x21, 0x54, 0xf9, 0xaa, 0x8e, 0xc2,
3720        0xf2, 0x34, 0x66, 0x1e, 0x2a, 0xef, 0x19, 0xae, 0x48, 0x47, 0x29, 0xa0, 0x72, 0xd1, 0x31,
3721        0xc0, 0x65, 0x49, 0xde, 0x79, 0x32, 0xe6, 0x4d, 0xb6, 0x55, 0x3f, 0x4d, 0xf1, 0x18, 0xbb,
3722        0x18, 0x59, 0x4c, 0x31, 0xa3, 0xb2, 0x34, 0xdd, 0xf6, 0x4a, 0x91, 0x51, 0x3f, 0x3e, 0x40,
3723        0x69, 0xad, 0xbf, 0x1a, 0xd0, 0x05, 0xfb, 0xbe, 0x8b, 0x0b, 0xdd, 0xdf, 0x7d, 0x94, 0x74,
3724        0x92, 0x3e, 0xff, 0x04, 0x2a, 0xc4, 0xea, 0xc9,
3725    ];
3726
3727    #[rustfmt::skip]
3728    const MXFP4_GGUF_TEST_BLOCKS: [u8; 68] = [0x79, 0xb4, 0x8d, 0xe2, 0x62, 0x5d, 0xbb, 0x9d, 0x54, 0xe6, 0xdb, 0x94, 0x59, 0x7d, 0x28, 0xf9, 0x79, 0x7a, 0xfc, 0xc1, 0xfa, 0x1e, 0x53, 0x5b, 0x0e, 0xc2, 0x5a, 0x2f, 0x0c, 0x82, 0x4d, 0xcb, 0x11, 0x28, 0x7b, 0x7c, 0xb6, 0x45, 0xe0, 0xb0, 0x52, 0x40, 0x51, 0xec, 0x30, 0x1a, 0xd2, 0x17, 0xf3, 0xbb, 0xfc, 0x7c, 0x8f, 0xf0, 0x67, 0x83, 0x88, 0x9d, 0x79, 0xdb, 0xf4, 0x45, 0x29, 0x78, 0xe6, 0xf4, 0x99, 0xea];
3729
3730    /// A live ggml type this build has no kernel for must be REFUSED BY
3731    /// NAME at execution, having been sized correctly at parse.
3732    ///
3733    /// Before `TQ2_0` was recognized, tag 35 was `Other(35)`, which had
3734    /// no block layout: the tensor's size was unknown, so `tensor_bytes`
3735    /// could not even hand back the row, and the error named a number.
3736    /// Now the file parses, the tensor measures 66 bytes per 256
3737    /// elements, and the stop happens where it belongs -- at the point
3738    /// something wants to multiply by it -- naming `TQ2_0`.
3739    #[test]
3740    fn a_recognized_but_unimplemented_ggml_type_refuses_by_name_after_sizing_correctly() {
3741        // 256 elements of TQ2_0 = one 66-byte block.
3742        let block = pseudo_iq_block(66, 0x0720_5eed);
3743        let tmp =
3744            std::env::temp_dir().join(format!("ferrox_test_tq2_0_{}.gguf", std::process::id()));
3745        std::fs::write(
3746            &tmp,
3747            build_single_iq_lowbit_tensor_gguf("tq2test", 35, 256, &block),
3748        )
3749        .unwrap();
3750        let file = ferrox_gguf::GgufFile::open(&tmp).expect("a TQ2_0 file must still parse");
3751        std::fs::remove_file(&tmp).ok();
3752
3753        // Sized, not zero: the size estimate is right even though the
3754        // kernel is missing.
3755        let info = file.find_tensor("test.weight").expect("tensor present");
3756        assert_eq!(info.dtype, GgmlType::TQ2_0);
3757        assert_eq!(info.byte_len(), Some(66));
3758        assert_eq!(
3759            file.tensor_bytes("test.weight").map(<[u8]>::len).ok(),
3760            Some(66)
3761        );
3762
3763        match load_weight_matrix(&file, "test.weight") {
3764            Err(LoadError::UnsupportedDtype(name, GgmlType::TQ2_0)) => {
3765                assert_eq!(name, "test.weight");
3766            }
3767            Err(other) => panic!("TQ2_0 must be refused by name, got {other:?}"),
3768            Ok(_) => panic!("TQ2_0 must be refused, not loaded as some other kind"),
3769        }
3770    }
3771
3772    /// An MXFP4 norm/bias must widen, not be refused.
3773    ///
3774    /// `load_weight_matrix` accepts MXFP4 as a 2-D weight and
3775    /// `load_moe_expert_matrices` accepts it as an expert tensor, and
3776    /// `WeightMatrix::dequant` calls `dequant_mxfp4_gguf` on both. One
3777    /// missing arm in `widen_plain_float` made the *1-D* tensors of the
3778    /// exact same dtype a hard `UnsupportedDtype` -- the split that
3779    /// turns a supported format into a load failure on the one
3780    /// checkpoint that uses it.
3781    #[test]
3782    fn an_mxfp4_one_dimensional_tensor_widens_instead_of_being_refused() {
3783        let expected = ferrox_quant::dequant_mxfp4_gguf(&MXFP4_GGUF_TEST_BLOCKS)
3784            .expect("the fixture blocks must dequantize");
3785        let cols = expected.len();
3786        let tmp = std::env::temp_dir().join(format!(
3787            "ferrox_test_mxfp4_norm_{}.gguf",
3788            std::process::id()
3789        ));
3790        std::fs::write(
3791            &tmp,
3792            build_single_iq_lowbit_tensor_gguf(
3793                "mxfp4norm",
3794                39,
3795                cols as u64,
3796                &MXFP4_GGUF_TEST_BLOCKS,
3797            ),
3798        )
3799        .unwrap();
3800        let file = ferrox_gguf::GgufFile::open(&tmp).expect("file must parse");
3801        std::fs::remove_file(&tmp).ok();
3802
3803        let got = load_f32_vec(&file, "test.weight")
3804            .expect("an MXFP4 norm must load, not report an unsupported dtype");
3805        assert_eq!(got, expected);
3806
3807        // Same arm, reached directly: `widen_plain_float` is the shared
3808        // helper the six architecture loaders call, so its table is the
3809        // one that has to know MXFP4.
3810        let direct = widen_plain_float(GgmlType::MXFP4, &MXFP4_GGUF_TEST_BLOCKS, "test.weight")
3811            .expect("widen_plain_float must widen MXFP4");
3812        assert_eq!(direct, expected);
3813
3814        // And the refusal still works for a dtype that genuinely has no
3815        // widening path, so this test cannot pass by making everything
3816        // succeed.
3817        match widen_plain_float(GgmlType::TQ2_0, &MXFP4_GGUF_TEST_BLOCKS, "test.weight") {
3818            Err(LoadError::UnsupportedDtype(name, GgmlType::TQ2_0)) => {
3819                assert_eq!(name, "test.weight");
3820            }
3821            other => panic!("TQ2_0 must be refused by name, got {other:?}"),
3822        }
3823    }
3824
3825    fn build_single_iq_lowbit_tensor_gguf(
3826        arch: &str,
3827        tag: u32,
3828        cols: u64,
3829        block: &[u8],
3830    ) -> Vec<u8> {
3831        let mut buf = Vec::new();
3832        buf.write_u32::<LittleEndian>(ferrox_gguf::GGUF_MAGIC)
3833            .unwrap();
3834        buf.write_u32::<LittleEndian>(3).unwrap(); // version
3835        buf.write_u64::<LittleEndian>(1).unwrap(); // tensor_count
3836        buf.write_u64::<LittleEndian>(1).unwrap(); // kv_count
3837        write_kv_str(&mut buf, "general.architecture", arch);
3838        write_string(&mut buf, "test.weight");
3839        buf.write_u32::<LittleEndian>(2).unwrap(); // n_dims
3840        buf.write_u64::<LittleEndian>(cols).unwrap();
3841        buf.write_u64::<LittleEndian>(1).unwrap(); // rows
3842        buf.write_u32::<LittleEndian>(tag).unwrap();
3843        buf.write_u64::<LittleEndian>(0).unwrap(); // offset
3844        while buf.len() % 32 != 0 {
3845            buf.push(0);
3846        }
3847        buf.extend_from_slice(block);
3848        buf
3849    }
3850
3851    /// A structurally valid block of `len` bytes for any of the
3852    /// codebook-grid formats: every bit pattern is a legal code in all
3853    /// of them (the grid indices are bounded by their own bit widths),
3854    /// so a deterministic byte fill is a real block, not a fixture that
3855    /// happens to avoid the interesting paths. Only the f16 scale needs
3856    /// pinning, and only so the comparison below can't be NaN-vs-NaN.
3857    fn pseudo_iq_block(len: usize, seed: u32) -> Vec<u8> {
3858        let mut s = seed;
3859        let mut out = Vec::with_capacity(len);
3860        for _ in 0..len {
3861            s ^= s << 13;
3862            s ^= s >> 17;
3863            s ^= s << 5;
3864            out.push((s >> 24) as u8);
3865        }
3866        out
3867    }
3868
3869    /// End-to-end load+apply for the codebook-grid low-bit formats the
3870    /// published Dynamic GGUFs are built from: a real on-disk tensor of
3871    /// each type must load zero-copy as the right `QuantKind` and
3872    /// produce the same matvec result as dequantizing the block
3873    /// directly. That is the property this test exists for -- the
3874    /// *values* are pinned against real ggml in `ferrox-quant`; what
3875    /// can only break here is the tag -> kind -> block-stride chain,
3876    /// and a wrong stride silently reads the neighbouring row.
3877    /// Dtype tags (19/29/16/17/22/18/21/39) verified against ggml.h's
3878    /// enum ggml_type.
3879    #[test]
3880    fn load_weight_matrix_handles_real_on_disk_iq_lowbit_tensors_end_to_end() {
3881        type DequantFn = fn(&[u8]) -> Result<Vec<f32>, ferrox_quant::QuantError>;
3882        // IQ1_M carries no f16 scale field; its scale is reassembled
3883        // from the four scale words' top nibbles, and the top nibble of
3884        // the last one supplies the f16 sign + high exponent bits.
3885        // Pinning it to 0x2 keeps the exponent out of the all-ones
3886        // NaN/Inf pattern whatever the rest of the fill does. The other
3887        // three do carry a leading f16 `d`, pinned for the same reason.
3888        let mut iq1m = pseudo_iq_block(ferrox_quant::IQ1_M_BLOCK_BYTES, 0x2907_31A0);
3889        iq1m[55] = (iq1m[55] & 0x0F) | 0x20;
3890        let mut iq2xs = pseudo_iq_block(ferrox_quant::IQ2_XS_BLOCK_BYTES, 0x2107_31A1);
3891        let mut iq2s = pseudo_iq_block(ferrox_quant::IQ2_S_BLOCK_BYTES, 0x2207_31A2);
3892        let mut iq3s = pseudo_iq_block(ferrox_quant::IQ3_S_BLOCK_BYTES, 0x2307_31A3);
3893        for blk in [&mut iq2xs, &mut iq2s, &mut iq3s] {
3894            blk[0..2].copy_from_slice(&half::f16::from_f32(0.115).to_le_bytes());
3895        }
3896        let cases: [(&str, u32, &[u8], QuantKind, DequantFn); 8] = [
3897            (
3898                "iq1s",
3899                19,
3900                &IQ1_S_TEST_BLOCK,
3901                QuantKind::IQ1S,
3902                ferrox_quant::dequant_iq1_s,
3903            ),
3904            (
3905                "iq1m",
3906                29,
3907                &iq1m,
3908                QuantKind::IQ1M,
3909                ferrox_quant::dequant_iq1_m,
3910            ),
3911            (
3912                "iq2xxs",
3913                16,
3914                &IQ2_XXS_TEST_BLOCK,
3915                QuantKind::IQ2XXS,
3916                ferrox_quant::dequant_iq2_xxs,
3917            ),
3918            (
3919                "iq2xs",
3920                17,
3921                &iq2xs,
3922                QuantKind::IQ2XS,
3923                ferrox_quant::dequant_iq2_xs,
3924            ),
3925            (
3926                "iq2s",
3927                22,
3928                &iq2s,
3929                QuantKind::IQ2S,
3930                ferrox_quant::dequant_iq2_s,
3931            ),
3932            (
3933                "iq3xxs",
3934                18,
3935                &IQ3_XXS_TEST_BLOCK,
3936                QuantKind::IQ3XXS,
3937                ferrox_quant::dequant_iq3_xxs,
3938            ),
3939            (
3940                "iq3s",
3941                21,
3942                &iq3s,
3943                QuantKind::IQ3S,
3944                ferrox_quant::dequant_iq3_s,
3945            ),
3946            (
3947                "mxfp4_gguf",
3948                39,
3949                &MXFP4_GGUF_TEST_BLOCKS,
3950                QuantKind::Mxfp4Gguf,
3951                ferrox_quant::dequant_mxfp4_gguf,
3952            ),
3953        ];
3954        for (name, tag, block, kind, dequant) in cases {
3955            let expected = dequant(block).unwrap();
3956            let cols = expected.len();
3957            let tmp = std::env::temp_dir().join(format!("ferrox_test_{name}_tensor.gguf"));
3958            std::fs::write(
3959                &tmp,
3960                build_single_iq_lowbit_tensor_gguf(name, tag, cols as u64, block),
3961            )
3962            .unwrap();
3963            let file = ferrox_gguf::GgufFile::open(&tmp).expect("file must parse");
3964            std::fs::remove_file(&tmp).ok();
3965
3966            let matrix =
3967                load_weight_matrix(&file, "test.weight").expect("low-bit tensor must load");
3968            assert_eq!((matrix.rows(), matrix.cols()), (1, cols), "{name}");
3969            match &matrix {
3970                WeightMatrix::Quantized { kind: k, data, .. } => {
3971                    assert_eq!(*k, kind, "{name}");
3972                    assert!(data.is_mapped(), "{name} must load zero-copy");
3973                }
3974                _ => panic!("expected a Quantized matrix for {name}"),
3975            }
3976
3977            let x: Vec<f32> = (0..cols).map(|i| ((i as f32) * 0.013).sin()).collect();
3978            let expected_dot: f32 = expected.iter().zip(x.iter()).map(|(a, b)| a * b).sum();
3979            let got = matrix.apply(&x);
3980            assert!(
3981                (got[0] - expected_dot).abs() < 1e-1,
3982                "{name}: loaded+applied diverged from direct dequant: got={} expected={}",
3983                got[0],
3984                expected_dot
3985            );
3986        }
3987    }
3988
3989    #[test]
3990    fn qwen2moe_disables_topk_renorm() {
3991        assert!(
3992            NO_TOPK_RENORMALIZE_ARCHITECTURES.contains(&"qwen2moe"),
3993            "qwen2moe must have norm_topk_prob=false (llama.cpp build_moe_ffn norm_w=false)"
3994        );
3995    }
3996
3997    /// An architecture that uses no RoPE must not reach the generic
3998    /// decoder, which rotates unconditionally.
3999    ///
4000    /// All five of these were admitted as `GenericGqa { rope: Neox }`.
4001    /// Nothing downstream could have caught it: `bloom` and `refact`
4002    /// hardcode their ALiBi slope in `load_arch_hparams` with no GGUF
4003    /// key, so the metadata gates above see nothing, and `mpt` carries
4004    /// no tensor the generic loader fails to consume, so
4005    /// `assert_every_tensor_consumed` sees nothing either. It would have
4006    /// loaded, run at full speed, and answered from rotated positions.
4007    #[test]
4008    fn an_architecture_with_no_rope_is_refused_by_name() {
4009        for arch in ["gpt2", "mpt", "refact", "bloom", "jais"] {
4010            let file = open_metadata_gguf(
4011                &format!("norope_{arch}"),
4012                &[("general.architecture", Kv::Str(arch))],
4013            );
4014            match ModelConfig::from_gguf(&file) {
4015                Err(LoadError::DedicatedArchitectureRequired(got, reason)) => {
4016                    assert_eq!(got, arch);
4017                    assert!(
4018                        reason.contains("ALiBi") || reason.contains("position embeddings"),
4019                        "{arch}: the refusal must name what is missing, got {reason:?}"
4020                    );
4021                }
4022                other => panic!("{arch} must be refused, got {other:?}"),
4023            }
4024        }
4025    }
4026
4027    /// Baichuan is one `general.architecture` string covering two
4028    /// positional schemes, and llama.cpp picks between them on the layer
4029    /// count alone (`src/models/baichuan.cpp:11-14`, with its own "TODO:
4030    /// become GGUF KV parameter"). So the 13B is the MiniCPM case: no
4031    /// key to gate on and no tensor to miss.
4032    #[test]
4033    fn baichuan_13b_is_refused_because_it_uses_alibi_and_the_7b_is_not() {
4034        let thirteen_b = open_metadata_gguf(
4035            "baichuan13b",
4036            &[
4037                ("general.architecture", Kv::Str("baichuan")),
4038                ("baichuan.block_count", Kv::U32(40)),
4039            ],
4040        );
4041        match ModelConfig::from_gguf(&thirteen_b) {
4042            Err(LoadError::UnsupportedFeature(arch, msg)) => {
4043                assert_eq!(arch, "baichuan");
4044                assert!(msg.contains("ALiBi"), "{msg}");
4045                assert!(
4046                    msg.contains("40"),
4047                    "the refusal must name the layer count: {msg}"
4048                );
4049            }
4050            other => panic!("Baichuan-13B must be refused, got {other:?}"),
4051        }
4052
4053        // The 7B rotates exactly as the generic decoder does, so it must
4054        // pass this gate. It still fails later, on the next missing
4055        // hparam, which is what proves the gate let it through.
4056        let seven_b = open_metadata_gguf(
4057            "baichuan7b",
4058            &[
4059                ("general.architecture", Kv::Str("baichuan")),
4060                ("baichuan.block_count", Kv::U32(32)),
4061            ],
4062        );
4063        match ModelConfig::from_gguf(&seven_b) {
4064            Err(LoadError::MissingHparam(key)) => assert_eq!(key, "baichuan.embedding_length"),
4065            other => panic!("Baichuan-7B must pass the ALiBi gate, got {other:?}"),
4066        }
4067    }
4068
4069    /// The hyper-parameters a real Gemma-3 GGUF header carries for one
4070    /// size. `block_count` is the field llama.cpp's `LLM_TYPE_27B`
4071    /// switch reads (`gemma3.cpp:20-28`), so it is never a free
4072    /// parameter here.
4073    ///
4074    /// `linear_factor` adds the pair `conversion/base.py:1222-1230`
4075    /// writes from `rope_parameters["full_attention"]` -- and only from
4076    /// there: its own comment is "TODO: Handle sliding_attention
4077    /// similarly when models start implementing it", so the sliding
4078    /// layers get no scaling key at all.
4079    fn gemma3_config(
4080        tag: &str,
4081        n_layers: u32,
4082        hidden_dim: u32,
4083        n_heads: u32,
4084        head_dim: u32,
4085        linear_factor: Option<f32>,
4086    ) -> ModelConfig {
4087        let mut kvs: Vec<(&str, Kv)> = vec![
4088            ("general.architecture", Kv::Str("gemma3")),
4089            ("gemma3.block_count", Kv::U32(n_layers)),
4090            ("gemma3.embedding_length", Kv::U32(hidden_dim)),
4091            ("gemma3.attention.head_count", Kv::U32(n_heads)),
4092            ("gemma3.attention.head_count_kv", Kv::U32(n_heads)),
4093            ("gemma3.attention.key_length", Kv::U32(head_dim)),
4094            ("gemma3.attention.value_length", Kv::U32(head_dim)),
4095            // Global layers rotate at 1e6; the sliding ones fall back to
4096            // llama.cpp's `rope_freq_base_train_swa` default of 10000,
4097            // because `gemma3.cpp:11` reads only the BASE key.
4098            ("gemma3.rope.freq_base", Kv::F32(1_000_000.0)),
4099            ("gemma3.attention.sliding_window", Kv::U32(1024)),
4100            ("gemma3.attention.sliding_window_pattern", Kv::U32(6)),
4101        ];
4102        if let Some(factor) = linear_factor {
4103            kvs.push(("gemma3.rope.scaling.type", Kv::Str("linear")));
4104            kvs.push(("gemma3.rope.scaling.factor", Kv::F32(factor)));
4105        }
4106        ModelConfig::from_gguf(&open_metadata_gguf(tag, &kvs)).expect("gemma3 fixture must load")
4107    }
4108
4109    /// The 27B attention scale reaches `ModelConfig`, and no other
4110    /// Gemma-3 size acquires one.
4111    ///
4112    /// `capability::attention_scale_override` is where the arithmetic is
4113    /// checked; this pins that the LOADER calls it with this file's own
4114    /// numbers. That step is the one that shipped broken: the function
4115    /// did not exist and `attention_scale` was the literal `None`, under
4116    /// a comment naming the exception. A helper nobody calls looks
4117    /// exactly like a fix.
4118    #[test]
4119    fn a_gemma3_27b_header_sets_the_attention_scale_and_no_smaller_size_does() {
4120        // Gemma-3-27B: 62 layers, n_embd 5376, 32 heads, head_dim 128.
4121        let big = gemma3_config("g3_27b_scale", 62, 5376, 32, 128, Some(8.0));
4122        let want = 1.0f32 / (5376.0f32 / 32.0).sqrt();
4123        let got = big
4124            .attention_scale
4125            .expect("Gemma-3-27B is llama.cpp's LLM_TYPE_27B");
4126        assert!(
4127            (got - want).abs() < 1e-7,
4128            "want 1/sqrt(168) = {want}, got {got}"
4129        );
4130        // The bug's magnitude: scores were sqrt(168/128) = 1.146x too
4131        // large without this.
4132        let kernel = 1.0f32 / 128.0f32.sqrt();
4133        assert!((kernel / got - (168.0f32 / 128.0).sqrt()).abs() < 1e-5);
4134
4135        // Gemma-3-1B and -4B take llama.cpp's other branch, which is the
4136        // scale the attention kernels already apply. A `Some` here would
4137        // double-scale them.
4138        for (tag, n_layers, hidden, heads) in
4139            [("g3_1b_scale", 26, 1152, 4), ("g3_4b_scale", 34, 2560, 8)]
4140        {
4141            let cfg = gemma3_config(tag, n_layers, hidden, heads, 256, None);
4142            assert_eq!(
4143                cfg.attention_scale, None,
4144                "{tag} must keep the kernels' own 1/sqrt(head_dim)"
4145            );
4146        }
4147    }
4148
4149    /// Gemma-3's declared linear scaling reaches the FULL-ATTENTION
4150    /// layers only, and the sliding ones rope unscaled.
4151    ///
4152    /// `gemma3.cpp:11` reads `LLM_KV_ROPE_FREQ_BASE_SWA` and nothing
4153    /// else, so `rope_freq_scale_train_swa` keeps its `1.0f` default
4154    /// (`src/llama-hparams.h:129`) while `get_rope_freq_scale`
4155    /// (`llama-model.cpp:2033-2035`) hands the trained scale to the full
4156    /// layers. The converter agrees: `conversion/base.py:1222-1230`
4157    /// takes the factor from `rope_parameters["full_attention"]` and
4158    /// writes nothing for the sliding half.
4159    ///
4160    /// ferrox folded the factor into ONE global `rope_freqs` vector, so
4161    /// Gemma-3-4B/12B/27B rotated five layers in six at `p/8`.
4162    #[test]
4163    fn gemma3_linear_scaling_reaches_the_full_layers_and_not_the_sliding_ones() {
4164        // Gemma-3-4B: 34 layers, head_dim 256, `rope_scaling: linear 8`.
4165        let cfg = gemma3_config("g3_4b_rope", 34, 2560, 8, 256, Some(8.0));
4166        let freqs = cfg
4167            .rope_freqs
4168            .as_ref()
4169            .expect("declared linear scaling must produce per-band divisors");
4170        assert!(
4171            freqs.full.iter().all(|f| (*f - 8.0).abs() < 1e-6),
4172            "full-attention layers divide every band by the trained factor: {:?}",
4173            freqs.full
4174        );
4175        let swa = freqs
4176            .swa
4177            .as_ref()
4178            .expect("gemma3 does not assign rope_freq_scale_train_swa, so 1.0 applies");
4179        assert!(
4180            swa.iter().all(|f| (*f - 1.0).abs() < 1e-6),
4181            "sliding layers rope at the raw position: {swa:?}"
4182        );
4183        assert_eq!(swa.len(), freqs.full.len(), "one divisor per rotated pair");
4184
4185        // Period 6, last-dense (`capability::default_swa_layout`), so
4186        // layer 5 is the full-attention one and 0..=4 slide. The phase
4187        // matters: getting it wrong swaps which five-sixths are wrong.
4188        assert!(cfg.layer_sliding_window(0).is_some());
4189        assert!(cfg.layer_sliding_window(5).is_none());
4190        assert_eq!(cfg.layer_rope_freqs(0), Some(&[1.0f32; 128][..]));
4191        assert_eq!(cfg.layer_rope_freqs(5), Some(&[8.0f32; 128][..]));
4192        assert_eq!(cfg.layer_rope_theta(0), 10_000.0);
4193        assert_eq!(cfg.layer_rope_theta(5), 1_000_000.0);
4194        assert!(
4195            cfg.rope_freqs_vary_by_layer(),
4196            "the fused Metal stacks take one divisor slice for a whole run \
4197             and so must refuse this model"
4198        );
4199
4200        // Gemma-3-1B declares no scaling at all -- the audited fixture,
4201        // and the reason this was invisible. Nothing to split, so no
4202        // per-layer set and no Metal refusal.
4203        let plain = gemma3_config("g3_1b_rope", 26, 1152, 4, 256, None);
4204        assert!(plain.rope_freqs.is_none());
4205        assert!(!plain.rope_freqs_vary_by_layer());
4206    }
4207
4208    /// Gemma-2 is the counter-case, and it is why the SWA scale needs
4209    /// its own table rather than reusing `swa_rope_base_follows_model`.
4210    ///
4211    /// `gemma2.cpp:10-11` assigns BOTH `rope_freq_base_train_swa` and
4212    /// `rope_freq_scale_train_swa` from the model's trained values, so
4213    /// its sliding layers keep the declared scaling. Splitting them here
4214    /// would be the same bug pointed the other way.
4215    #[test]
4216    fn gemma2_sliding_layers_inherit_the_trained_rope_scale() {
4217        let cfg = ModelConfig::from_gguf(&open_metadata_gguf(
4218            "g2_rope",
4219            &[
4220                ("general.architecture", Kv::Str("gemma2")),
4221                ("gemma2.block_count", Kv::U32(26)),
4222                ("gemma2.embedding_length", Kv::U32(2304)),
4223                ("gemma2.attention.head_count", Kv::U32(8)),
4224                ("gemma2.attention.head_count_kv", Kv::U32(4)),
4225                ("gemma2.attention.key_length", Kv::U32(256)),
4226                ("gemma2.attention.value_length", Kv::U32(256)),
4227                ("gemma2.rope.freq_base", Kv::F32(10_000.0)),
4228                ("gemma2.attention.sliding_window", Kv::U32(4096)),
4229                ("gemma2.rope.scaling.type", Kv::Str("linear")),
4230                ("gemma2.rope.scaling.factor", Kv::F32(8.0)),
4231            ],
4232        ))
4233        .expect("gemma2 fixture must load");
4234
4235        let freqs = cfg.rope_freqs.as_ref().expect("linear scaling declared");
4236        assert_eq!(
4237            freqs.swa, None,
4238            "gemma2.cpp:11 assigns rope_freq_scale_train_swa from the trained scale"
4239        );
4240        assert!(!cfg.rope_freqs_vary_by_layer());
4241        // Period 2, last-dense: layer 0 slides, layer 1 does not, and
4242        // both get the same divisors.
4243        assert!(cfg.layer_sliding_window(0).is_some());
4244        assert!(cfg.layer_sliding_window(1).is_none());
4245        assert_eq!(cfg.layer_rope_freqs(0), cfg.layer_rope_freqs(1));
4246
4247        // The two tables really are different: this is the pair that
4248        // must not be collapsed into one.
4249        assert!(crate::capability::swa_rope_scale_follows_model("gemma2"));
4250        assert!(!crate::capability::swa_rope_scale_follows_model("gemma3"));
4251        for arch in ["olmo2", "laguna"] {
4252            assert!(
4253                crate::capability::swa_rope_base_follows_model(arch),
4254                "{arch} seeds the SWA base from the model"
4255            );
4256            assert!(
4257                !crate::capability::swa_rope_scale_follows_model(arch),
4258                "{arch} pins the SWA scale to 1.0 (olmo2.cpp:14, laguna.cpp:48)"
4259            );
4260        }
4261    }
4262}