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