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::{QuantKind, WeightBytes, WeightMatrix};
28use ferrox_gguf::{GgmlType, GgufError, GgufValue, ShardedGguf, TensorInfo, TensorSource};
29use ferrox_moe::{ExpertWeights, GatingFunction, MoeLayerConfig};
30use std::sync::Arc;
31use thiserror::Error;
32
33use crate::config::ModelConfig;
34#[cfg(feature = "metal")]
35use crate::decoder::MoePackedQ4Planes;
36use crate::decoder::{AttnWeights, Decoder, ExpertBacking, LayerWeights, MoeWeights};
37
38#[derive(Debug, Error)]
39pub enum LoadError {
40    #[error(transparent)]
41    Gguf(#[from] GgufError),
42    #[error(transparent)]
43    Shard(#[from] ferrox_gguf::ShardError),
44    #[error("tensor '{0}' has unsupported dtype {1:?}")]
45    UnsupportedDtype(String, GgmlType),
46    #[error(
47        "MoE tensor '{0}' is not 3D or its expert count {1} does not match config n_experts {2}"
48    )]
49    ExpertCountMismatch(String, usize, usize),
50    #[error("GGUF file is missing required hparam metadata key '{0}'")]
51    MissingHparam(String),
52    /// `general.architecture` is not in the capability registry — refuse
53    /// to guess RoPE/gating rather than emit fluent-but-wrong logits.
54    #[error(
55        "unsupported GGUF architecture '{0}': not in ferrox's capability registry \
56         (unknown required features fail closed; see ferrox_models::capability)"
57    )]
58    UnsupportedArchitecture(String),
59    /// Architecture exists but must not use the generic GQA decoder.
60    #[error("architecture '{0}' cannot use the generic Decoder: {1}")]
61    DedicatedArchitectureRequired(String, &'static str),
62    /// Metadata advertises a feature the generic decoder does not implement.
63    #[error("architecture '{0}' requires unimplemented feature: {1}")]
64    UnsupportedFeature(String, String),
65    /// The checkpoint carries per-block tensors this build never reads,
66    /// i.e. weights that contribute to the real graph and would simply
67    /// be missing from ours. See [`assert_every_tensor_consumed`].
68    #[error(
69        "checkpoint carries {0} tensor(s) this build never reads, so its graph is not the one \
70         ferrox would run: {1}. This is a missing feature, not a corrupt file. Override with \
71         FERROX_ALLOW_UNKNOWN_TENSORS=1 to load anyway and accept wrong output."
72    )]
73    UnconsumedTensors(usize, String),
74    /// `FERROX_STRICT_KERNELS=1` and the model has weights with no
75    /// kernel on the selected accelerator, i.e. it would run, correctly,
76    /// on a silently slower path. Refusing is the point: a benchmark or
77    /// CI run must not be able to publish a number taken off the
78    /// backend it claims. See [`ferrox_core::kernel_registry`].
79    #[error("{0}")]
80    StrictKernels(String),
81}
82
83/// Architecture-family name strings (GGUF's `general.architecture` value)
84/// known, from reading ik_llama.cpp's `llama-hparams.cpp`
85/// (`LLM_ARCH_DEEPSEEK2`, `LLM_ARCH_GLM4_MOE` cases), to default to
86/// sigmoid MoE gating with post-selection renormalization rather than
87/// softmax. See docs/MODELS.md for the citations behind this list.
88const SIGMOID_GATING_ARCHITECTURES: &[&str] = &["deepseek2", "glm4moe"];
89
90/// Architecture-family names whose real reference implementation skips
91/// renormalizing top-k softmax routing weights after selection (GGUF
92/// carries no metadata key for this -- it's hardcoded per-architecture in
93/// both the real HF `transformers` model code and llama.cpp's
94/// `build_moe_ffn` call sites, not read from the file). Confirmed for
95/// `olmoe` against `OlmoeTopKRouter.forward` in
96/// `transformers/models/olmoe/modeling_olmoe.py` (`config.norm_topk_prob`
97/// is `false` in the real published config.json) and llama.cpp's
98/// `src/models/olmoe.cpp` (`build_moe_ffn(..., false, ...,
99/// LLAMA_EXPERT_GATING_FUNC_TYPE_SOFTMAX, ...)`). See
100/// `MoeLayerConfig::norm_topk_prob`'s doc comment for why this matters:
101/// getting it wrong silently produces wrong generation output even
102/// though the file loads and shape-validates fine.
103// Architectures whose reference graphs pass `norm_w=false` to
104// `build_moe_ffn` (llama.cpp) / `norm_topk_prob=false` in HF config.
105// Qwen2-MoE: `.scratch/llama.cpp/src/models/qwen2moe.cpp` — Softmax +
106// `false` for the norm_topk slot. Renormalizing top-k weights made
107// Qwen1.5-MoE greedy decode emit garbage despite shared-expert load.
108const NO_TOPK_RENORMALIZE_ARCHITECTURES: &[&str] = &["olmoe", "qwen2moe"];
109
110fn metadata_u64_any(file: &impl TensorSource, keys: &[String]) -> Option<u64> {
111    keys.iter().find_map(|k| file.metadata_u64(k))
112}
113
114fn metadata_f32_any(file: &impl TensorSource, keys: &[String]) -> Option<f32> {
115    keys.iter()
116        .find_map(|k| file.metadata(k).and_then(GgufValue::as_f32))
117}
118
119impl ModelConfig {
120    /// Derives a `ModelConfig` from a real GGUF file's own hyperparameter
121    /// metadata, following llama.cpp's `general.architecture`-prefixed key
122    /// convention (`{arch}.block_count`, `{arch}.embedding_length`,
123    /// `{arch}.attention.head_count`, `{arch}.expert_count`, ...) rather
124    /// than requiring a hand-written preset to already match the file's
125    /// shape exactly. This is what lets `ferrox-server` (and `ferrox
126    /// run-real`) load an arbitrary checkpoint, not just the three
127    /// hand-tuned presets in `config.rs`.
128    ///
129    /// Fields with no corresponding metadata key fall back to widely-used
130    /// llama.cpp defaults (documented inline) and are listed in the
131    /// returned config's `best_effort_fields`, following the same
132    /// confirmed-vs-estimated discipline as the hand-written presets.
133    pub fn from_gguf(file: &impl TensorSource) -> Result<Self, LoadError> {
134        let arch = file
135            .metadata_str("general.architecture")
136            .ok_or_else(|| LoadError::MissingHparam("general.architecture".to_string()))?
137            .to_string();
138        let arch_profile = crate::capability::resolve_profile(&arch)
139            .ok_or_else(|| LoadError::UnsupportedArchitecture(arch.clone()))?;
140        let rope_layout = match arch_profile.path {
141            crate::capability::ArchPath::GenericGqa { rope }
142            | crate::capability::ArchPath::TestFixture { rope } => rope,
143            crate::capability::ArchPath::DedicatedOnly { reason } => {
144                return Err(LoadError::DedicatedArchitectureRequired(
145                    arch.clone(),
146                    reason,
147                ));
148            }
149            crate::capability::ArchPath::Deferred { reason } => {
150                return Err(LoadError::UnsupportedFeature(
151                    arch.clone(),
152                    format!("architecture deferred from Ferrox text-generation scope: {reason}"),
153                ));
154            }
155        };
156        let qk_norm_style = arch_profile.qk_norm;
157        for (meta_key, feature) in crate::capability::unsupported_feature_keys(&arch) {
158            if let Some(v) = metadata_f32_any(file, std::slice::from_ref(&meta_key)) {
159                if v > 0.0 {
160                    return Err(LoadError::UnsupportedFeature(
161                        arch.clone(),
162                        format!("{feature} (metadata {meta_key}={v})"),
163                    ));
164                }
165            }
166            if let Some(v) = metadata_u64_any(file, std::slice::from_ref(&meta_key)) {
167                if v > 0 {
168                    return Err(LoadError::UnsupportedFeature(
169                        arch.clone(),
170                        feature.to_string(),
171                    ));
172                }
173            }
174        }
175        let key = |suffix: &str| format!("{arch}.{suffix}");
176
177        let name: &'static str = Box::leak(
178            file.metadata_str("general.name")
179                .unwrap_or(&arch)
180                .to_string()
181                .into_boxed_str(),
182        );
183
184        let n_layers =
185            file.metadata_u64(&key("block_count"))
186                .ok_or_else(|| LoadError::MissingHparam(key("block_count")))? as usize;
187        let hidden_dim = file
188            .metadata_u64(&key("embedding_length"))
189            .ok_or_else(|| LoadError::MissingHparam(key("embedding_length")))?
190            as usize;
191        let n_heads = file
192            .metadata_u64(&key("attention.head_count"))
193            .ok_or_else(|| LoadError::MissingHparam(key("attention.head_count")))?
194            as usize;
195
196        let mut best_effort_fields: Vec<&'static str> = Vec::new();
197
198        let n_kv_heads = file
199            .metadata_u64(&key("attention.head_count_kv"))
200            .map(|v| v as usize)
201            .unwrap_or_else(|| {
202                best_effort_fields.push("n_kv_heads (no attention.head_count_kv key; assumed equal to n_heads, i.e. plain MHA)");
203                n_heads
204            });
205        let head_dim = file
206            .metadata_u64(&key("attention.key_length"))
207            .map(|v| v as usize)
208            .unwrap_or_else(|| {
209                best_effort_fields.push(
210                    "head_dim (no attention.key_length key; derived as hidden_dim / n_heads)",
211                );
212                hidden_dim / n_heads
213            });
214        let v_head_dim = file
215            .metadata_u64(&key("attention.value_length"))
216            .map(|v| v as usize)
217            .unwrap_or(head_dim);
218        if v_head_dim != head_dim {
219            return Err(LoadError::UnsupportedFeature(
220                arch.clone(),
221                format!(
222                    "split K/V head dims (key_length={head_dim}, value_length={v_head_dim}); \
223                     generic decoder requires equal head dims"
224                ),
225            ));
226        }
227        let vocab_size = file
228            .metadata("tokenizer.ggml.tokens")
229            .and_then(|v| match v {
230                GgufValue::Array(items) => Some(items.len()),
231                _ => None,
232            })
233            .or_else(|| file.metadata_u64(&key("vocab_size")).map(|v| v as usize))
234            .unwrap_or_else(|| {
235                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)");
236                // `output.weight`'s real raw shape is `[hidden_dim,
237                // vocab_size]` (ggml's fastest-first `ne[]` order --
238                // see `load_weight_matrix`'s doc comment), so vocab_size
239                // is the *last* element, not the first.
240                file.find_tensor("output.weight")
241                    .and_then(|t| t.shape.last().copied())
242                    .unwrap_or(0) as usize
243            });
244        let rope_theta = metadata_f32_any(file, &[key("rope.freq_base")]).unwrap_or_else(|| {
245            best_effort_fields.push("rope_theta (no rope.freq_base key; defaulted to 10000.0)");
246            10000.0
247        });
248        let rms_norm_eps = metadata_f32_any(
249            file,
250            &[
251                key("attention.layer_norm_rms_epsilon"),
252                key("attention.layer_norm_epsilon"),
253            ],
254        )
255        .unwrap_or_else(|| {
256            best_effort_fields
257                .push("rms_norm_eps (no layer_norm_rms_epsilon key; defaulted to 1e-5)");
258            1e-5
259        });
260
261        let n_experts = metadata_u64_any(file, &[key("expert_count")]).unwrap_or(0) as usize;
262        let is_moe = n_experts > 1;
263
264        let n_experts_active = if is_moe {
265            metadata_u64_any(file, &[key("expert_used_count")]).unwrap_or_else(|| {
266                best_effort_fields
267                    .push("moe.n_experts_active (no expert_used_count key; defaulted to 2)");
268                2
269            }) as usize
270        } else {
271            1
272        };
273        // Prefer the GGUF hparam when present. Qwen2MoE (and some other
274        // HF→GGUF exports) omit `expert_shared_count` but still ship
275        // `blk.N.ffn_{gate,up,down}_shexp.weight` — without a tensor-
276        // presence fallback those weights are silently dropped and the
277        // model runs with a large chunk of active FFN missing.
278        let n_shared_experts = match metadata_u64_any(file, &[key("expert_shared_count")]) {
279            Some(n) => n as usize,
280            None if is_moe && file.find_tensor("blk.0.ffn_gate_shexp.weight").is_some() => {
281                best_effort_fields.push(
282                    "moe.n_shared_experts (no expert_shared_count; inferred 1 from blk.0.ffn_gate_shexp.weight)",
283                );
284                1
285            }
286            None => 0,
287        };
288        // MoE GGUFs often only set `feed_forward_length` (OLMoE=1024,
289        // Qwen2-MoE=5632 for the shared expert). `expert_feed_forward_length`
290        // is optional. llama.cpp `qwen2moe.cpp` uses
291        // `n_ff_exp = n_ff_exp ? n_ff_exp : n_ff / n_expert_used` (1408 for
292        // Qwen1.5-MoE); the shared expert keeps the full `n_ff` (5632).
293        let feed_forward_length = metadata_u64_any(file, &[key("feed_forward_length")]);
294        let expert_ffn_dim = metadata_u64_any(file, &[key("expert_feed_forward_length")])
295            .or_else(|| {
296                feed_forward_length.map(|ff| {
297                    if is_moe && n_experts_active > 0 {
298                        ff / n_experts_active as u64
299                    } else {
300                        ff
301                    }
302                })
303            })
304            .unwrap_or_else(|| {
305                best_effort_fields.push(
306                    "moe.expert_ffn_dim (no expert_feed_forward_length/feed_forward_length; defaulted to 4x hidden_dim)",
307                );
308                (hidden_dim * 4) as u64
309            }) as usize;
310        let n_dense_leading_layers =
311            metadata_u64_any(file, &[key("leading_dense_block_count")]).unwrap_or(0) as usize;
312
313        // ik_llama.cpp's real gating-function hparam
314        // (LLM_KV_EXPERT_GATING_FUNC: 1=softmax, 2=sigmoid) if the file
315        // carries it; otherwise fall back to the same architecture-name
316        // convention the hand-written presets in config.rs use (see
317        // docs/MODELS.md for the citations behind that list).
318        let gating = match metadata_u64_any(file, &[key("expert_gating_func")]) {
319            Some(2) => GatingFunction::Sigmoid,
320            Some(1) => GatingFunction::Softmax,
321            _ => {
322                if SIGMOID_GATING_ARCHITECTURES.contains(&arch.as_str()) {
323                    GatingFunction::Sigmoid
324                } else {
325                    if is_moe {
326                        best_effort_fields.push(
327                            "moe.gating (no expert_gating_func key and architecture not in the known-sigmoid list; defaulted to softmax)",
328                        );
329                    }
330                    GatingFunction::Softmax
331                }
332            }
333        };
334
335        // See `NO_TOPK_RENORMALIZE_ARCHITECTURES`'s doc comment: no GGUF
336        // metadata key exists for this, so it's an architecture-name
337        // lookup, the same convention `gating`'s fallback above uses.
338        let norm_topk_prob = !NO_TOPK_RENORMALIZE_ARCHITECTURES.contains(&arch.as_str());
339        if is_moe && matches!(gating, GatingFunction::Softmax) {
340            best_effort_fields.push(
341                "moe.norm_topk_prob (no GGUF metadata key exists for this; defaulted by architecture-name lookup against NO_TOPK_RENORMALIZE_ARCHITECTURES)",
342            );
343        }
344
345        // Real GGUF key (`{arch}.attention.sliding_window`, confirmed
346        // against `gguf-py/gguf/constants.py`'s real
347        // `LLM_KV_ATTENTION_SLIDING_WINDOW`). Some checkpoints
348        // (confirmed for real published Qwen1.5-MoE/Qwen2-MoE GGUFs)
349        // carry a nonzero window value even when the model's own
350        // config disables sliding-window attention entirely
351        // (`use_sliding_window: false`) -- llama.cpp's own convention
352        // is that a window of 0 means "unused," so only a real nonzero
353        // value here is treated as active.
354        let sliding_window = metadata_u64_any(file, &[key("attention.sliding_window")])
355            .map(|v| v as usize)
356            .filter(|&w| w > 0);
357
358        // Gemma alternating SWA period (`attention.sliding_window_pattern`).
359        // llama.cpp: gemma2 defaults period=2, gemma3 defaults period=6 when
360        // the pattern key is absent. A missing key must NOT mean "all SWA".
361        let swa_pattern = metadata_u64_any(file, &[key("attention.sliding_window_pattern")])
362            .map(|v| v as usize)
363            .filter(|&p| p > 1)
364            .or_else(|| {
365                sliding_window?;
366                // llama.cpp hardcodes the period per architecture and
367                // only lets the metadata key override it, so a missing
368                // key is *not* "every layer windowed" — see
369                // `capability::default_swa_pattern`.
370                crate::capability::default_swa_pattern(&arch).or(
371                    // Any Gemma variant not named in the table keeps the
372                    // gemma3+ period rather than going uniform.
373                    match arch_profile.family {
374                        crate::capability::DecoderFamily::GemmaFamily => Some(6),
375                        _ => None,
376                    },
377                )
378            });
379
380        let attn_logit_softcap = metadata_f32_any(
381            file,
382            &[
383                key("attention.logit_softcapping"),
384                key("attn_logit_softcapping"),
385            ],
386        )
387        .filter(|&v| v > 0.0);
388        let final_logit_softcap =
389            metadata_f32_any(file, &[key("final_logit_softcapping")]).filter(|&v| v > 0.0);
390
391        // Gemma: embeddings are scaled by sqrt(hidden_dim) at input.
392        let embedding_scale = if matches!(
393            arch_profile.family,
394            crate::capability::DecoderFamily::GemmaFamily
395        ) {
396            Some((hidden_dim as f32).sqrt())
397        } else {
398            None
399        };
400
401        // Gemma's f_attention_scale equals 1/sqrt(n_embd_head_k) for non-27B,
402        // which is already what `causal_gqa_attention` applies. Do not also
403        // pre-scale Q (that double-scales scores vs llama.cpp's
404        // `build_attn(..., 1.0f)` after an explicit Q scale).
405        let attention_scale = None;
406
407        // SWA-layer RoPE base. `llama_hparams` defaults it to 10000 and
408        // the Gemma-3 lineage relies on that default; the architectures
409        // in `swa_rope_base_follows_model` instead seed it from the
410        // model's own base before the key can override.
411        let rope_theta_swa = if sliding_window.is_some() {
412            let fallback = if crate::capability::swa_rope_base_follows_model(&arch) {
413                rope_theta
414            } else {
415                10_000.0
416            };
417            Some(
418                metadata_f32_any(
419                    file,
420                    &[key("rope.freq_base_swa"), key("rope_freq_base_swa")],
421                )
422                .unwrap_or(fallback),
423            )
424        } else {
425            None
426        };
427
428        let ffn_activation = match arch_profile.family {
429            crate::capability::DecoderFamily::GemmaFamily => crate::config::FfnActivation::Gelu,
430            crate::capability::DecoderFamily::PhiFamily => {
431                crate::config::FfnActivation::SwigluFused
432            }
433            _ => crate::config::FfnActivation::Swiglu,
434        };
435
436        // Llama 3/3.1/3.2's real per-band RoPE frequency correction: one
437        // model-level tensor (`TENSOR_NOT_REQUIRED`, `TENSOR_DUPLICATED`
438        // for every layer but the first in the real llama.cpp source --
439        // i.e. every layer shares this same array), not per-layer. See
440        // `ferrox_core::attention::apply_rope_with_freq_factors`'s doc
441        // comment for why this matters.
442        let rope_freqs = load_f32_vec_optional(file, "rope_freqs.weight")?;
443
444        // Phi-3/Phi-4 LongRoPE: two per-band factor tensors instead of
445        // Llama's single `rope_freqs.weight`, selected by context size
446        // (llama.cpp `llama_model::get_rope_factors`: `rope_freqs` wins if
447        // present, else `rope_long` when the run's context exceeds
448        // `rope.scaling.original_context_length`, else `rope_short`).
449        //
450        // The selection here uses the checkpoint's own advertised context
451        // length, which is what llama.cpp defaults `n_ctx` to. A run that
452        // caps the context below `original_context_length` should use the
453        // short set; ferrox's config is built before the context size is
454        // known, so that case is not yet handled — recorded as a
455        // best-effort field rather than silently assumed correct.
456        let rope_orig_ctx = metadata_u64_any(file, &[key("rope.scaling.original_context_length")])
457            .map(|v| v as usize);
458        // `rope_freqs.weight` outranks the LongRoPE pair (llama.cpp
459        // `get_rope_factors` checks it first), so a checkpoint carrying
460        // it never populates these and the runtime re-pick below cannot
461        // overwrite a Llama-3 correction with a Phi one.
462        let (rope_freqs_long, rope_freqs_short) = if rope_freqs.is_some() {
463            (None, None)
464        } else {
465            (
466                load_f32_vec_optional(file, "rope_factors_long.weight")?,
467                load_f32_vec_optional(file, "rope_factors_short.weight")?,
468            )
469        };
470        // Provisional pick from the checkpoint's own advertised context;
471        // `ModelConfig::apply_runtime_context` re-picks once the run's
472        // `--ctx-size` is known, which is the number llama.cpp decides on.
473        let rope_freqs = match (rope_freqs, rope_orig_ctx) {
474            (Some(f), _) => Some(f),
475            (None, Some(orig)) => {
476                let model_ctx = metadata_u64_any(file, &[key("context_length")])
477                    .unwrap_or(orig as u64) as usize;
478                if model_ctx > orig {
479                    rope_freqs_long.clone().or_else(|| rope_freqs_short.clone())
480                } else {
481                    rope_freqs_short.clone().or_else(|| rope_freqs_long.clone())
482                }
483            }
484            (None, None) => None,
485        };
486
487        // Partial rotary: only when the file says the rotary width is
488        // narrower than a head. Equal values mean "whole head", which is
489        // the same thing as `None` and stays `None` so nothing downstream
490        // has to special-case it.
491        let rope_dim = metadata_u64_any(file, &[key("rope.dimension_count")])
492            .map(|d| d as usize)
493            .filter(|d| *d > 0 && *d < head_dim);
494
495        // See `ModelConfig::rope_attn_factor`.
496        let rope_attn_factor = metadata_f32_any(file, &[key("rope.scaling.attn_factor")])
497            .filter(|f| f.is_finite() && *f > 0.0)
498            .unwrap_or(1.0);
499
500        // RoPE layout comes from the capability registry above (fail-
501        // closed). Getting this wrong for `llama` (needs Norm) was the
502        // real root cause of the Llama-3.1-8B early-stop/wrong-logits bug.
503
504        if best_effort_fields.is_empty() {
505            best_effort_fields.push(
506                "none -- every field above was read directly from this file's own GGUF metadata",
507            );
508        }
509
510        Ok(ModelConfig {
511            name,
512            n_layers,
513            hidden_dim,
514            n_heads,
515            n_kv_heads,
516            head_dim,
517            vocab_size,
518            rope_theta,
519            rms_norm_eps,
520            // No GGUF file encodes a hybrid KDA/Gated-MLA attention
521            // topology today; every real checkpoint loaded this way
522            // runs the standard Gqa path.
523            attention: crate::config::AttentionKind::Gqa,
524            sliding_window,
525            swa_pattern,
526            moe: MoeLayerConfig {
527                n_experts: n_experts.max(1),
528                n_experts_active,
529                n_shared_experts,
530                hidden_dim,
531                expert_ffn_dim,
532                gating,
533                norm_topk_prob,
534                expert_group_count: metadata_u64_any(file, &[key("expert_group_count")])
535                    .map(|v| v as usize)
536                    .filter(|&c| c > 1),
537                expert_group_used_count: metadata_u64_any(file, &[key("expert_group_used_count")])
538                    .map(|v| v as usize)
539                    .filter(|&c| c > 0),
540            },
541            n_dense_leading_layers,
542            rope_freqs,
543            rope_layout,
544            qk_norm_style,
545            attn_logit_softcap,
546            final_logit_softcap,
547            embedding_scale,
548            attention_scale,
549            rope_attn_factor,
550            rope_dim,
551            rope_freqs_long,
552            rope_freqs_short,
553            rope_orig_ctx,
554            rope_theta_swa,
555            ffn_activation,
556            best_effort_fields: Box::leak(best_effort_fields.into_boxed_slice()),
557        })
558    }
559}
560
561fn find_info<'a>(file: &'a impl TensorSource, name: &str) -> Result<&'a TensorInfo, LoadError> {
562    file.find_tensor(name)
563        .ok_or_else(|| LoadError::Gguf(GgufError::TensorNotFound(name.to_string())))
564}
565
566/// Maps a GGUF tensor's on-disk dtype to the `QuantKind` `WeightMatrix`
567/// uses to pick a fused dequant+dot kernel, or `None` for dtypes with
568/// no quantized kernel (F32, or a dtype not yet implemented at all).
569fn quant_kind_for(dtype: GgmlType) -> Option<QuantKind> {
570    match dtype {
571        GgmlType::Q8_0 => Some(QuantKind::Q8_0),
572        GgmlType::Q4_0 => Some(QuantKind::Q4_0),
573        GgmlType::Q4K => Some(QuantKind::Q4K),
574        GgmlType::Q5K => Some(QuantKind::Q5K),
575        GgmlType::Q6K => Some(QuantKind::Q6K),
576        GgmlType::Q2K => Some(QuantKind::Q2K),
577        GgmlType::Q3K => Some(QuantKind::Q3K),
578        GgmlType::Q4_1 => Some(QuantKind::Q4_1),
579        GgmlType::Q5_0 => Some(QuantKind::Q5_0),
580        GgmlType::Q5_1 => Some(QuantKind::Q5_1),
581        GgmlType::Q8_1 => Some(QuantKind::Q8_1),
582        GgmlType::IQ4NL => Some(QuantKind::IQ4NL),
583        GgmlType::IQ4XS => Some(QuantKind::IQ4XS),
584        GgmlType::IQ2XS => Some(QuantKind::IQ2XS),
585        GgmlType::IQ2S => Some(QuantKind::IQ2S),
586        GgmlType::IQ3S => Some(QuantKind::IQ3S),
587        GgmlType::IQ1M => Some(QuantKind::IQ1M),
588        GgmlType::IQ1S => Some(QuantKind::IQ1S),
589        GgmlType::IQ2XXS => Some(QuantKind::IQ2XXS),
590        GgmlType::IQ3XXS => Some(QuantKind::IQ3XXS),
591        GgmlType::MXFP4 => Some(QuantKind::Mxfp4Gguf),
592        _ => None,
593    }
594}
595
596/// Like `load_f32_vec`, but for tensors that only exist on some
597/// checkpoints (e.g. `attn_q_norm`/`attn_k_norm` -- OLMoE-style
598/// per-projection QK-RMSNorm applied to the full q_proj/k_proj output
599/// before RoPE, confirmed against `OlmoeAttention.forward` in
600/// `transformers/models/olmoe/modeling_olmoe.py`: `q_norm(q_proj(x))`,
601/// `k_norm(k_proj(x))`, both plain RMSNorm over the whole projected
602/// width, not per-head). Absent for every other preset/fixture this
603/// loader already handles -- `None` there is correct, not a missing
604/// feature.
605/// Loads the five gpt-oss-only tensors for one layer.
606///
607/// Every one of them is **required**: a gpt-oss checkpoint that is
608/// missing any of these is not a gpt-oss checkpoint ferrox can run, and
609/// quietly substituting zeros would reintroduce exactly the
610/// silently-wrong-graph failure this path exists to remove. The lengths
611/// are asserted against the config for the same reason — a bias of the
612/// wrong width would otherwise be applied to a `zip`-truncated prefix
613/// and produce a plausible, wrong answer.
614///
615/// Shapes follow `src/models/openai-moe.cpp::load_arch_tensors`:
616/// `attn_sinks {n_head}`, `attn_output.bias {n_embd}`,
617/// `ffn_gate_inp.bias {n_expert}`, `ffn_{gate,up}_exps.bias
618/// {n_ff_exp, n_expert}`, `ffn_down_exps.bias {n_embd, n_expert}`.
619/// GGUF stores the fastest dimension first, so the 2-D bias tensors
620/// arrive expert-major and split by simple chunking.
621fn load_gpt_oss_layer(
622    file: &impl TensorSource,
623    l: usize,
624    config: &ModelConfig,
625) -> Result<crate::decoder::GptOssLayer, LoadError> {
626    let n_experts = config.moe.n_experts;
627    let ff = config.moe.expert_ffn_dim;
628
629    let want = |name: &str, got: usize, expect: usize| -> Result<(), LoadError> {
630        if got == expect {
631            Ok(())
632        } else {
633            Err(LoadError::UnsupportedFeature(
634                config.name.to_string(),
635                format!("{name} has {got} elements, expected {expect}"),
636            ))
637        }
638    };
639
640    let attn_sinks = load_f32_vec(file, &format!("blk.{l}.attn_sinks.weight"))?;
641    want(
642        &format!("blk.{l}.attn_sinks.weight"),
643        attn_sinks.len(),
644        config.n_heads,
645    )?;
646    let o_bias = load_f32_vec(file, &format!("blk.{l}.attn_output.bias"))?;
647    want(
648        &format!("blk.{l}.attn_output.bias"),
649        o_bias.len(),
650        config.hidden_dim,
651    )?;
652    let router_bias = load_f32_vec(file, &format!("blk.{l}.ffn_gate_inp.bias"))?;
653    want(
654        &format!("blk.{l}.ffn_gate_inp.bias"),
655        router_bias.len(),
656        n_experts,
657    )?;
658
659    let gate_b = load_f32_vec(file, &format!("blk.{l}.ffn_gate_exps.bias"))?;
660    want(
661        &format!("blk.{l}.ffn_gate_exps.bias"),
662        gate_b.len(),
663        n_experts * ff,
664    )?;
665    let up_b = load_f32_vec(file, &format!("blk.{l}.ffn_up_exps.bias"))?;
666    want(
667        &format!("blk.{l}.ffn_up_exps.bias"),
668        up_b.len(),
669        n_experts * ff,
670    )?;
671    let down_b = load_f32_vec(file, &format!("blk.{l}.ffn_down_exps.bias"))?;
672    want(
673        &format!("blk.{l}.ffn_down_exps.bias"),
674        down_b.len(),
675        n_experts * config.hidden_dim,
676    )?;
677
678    let expert_bias = (0..n_experts)
679        .map(|e| ferrox_moe::ExpertBias {
680            gate: gate_b[e * ff..(e + 1) * ff].to_vec(),
681            up: up_b[e * ff..(e + 1) * ff].to_vec(),
682            down: down_b[e * config.hidden_dim..(e + 1) * config.hidden_dim].to_vec(),
683        })
684        .collect();
685
686    Ok(crate::decoder::GptOssLayer {
687        attn_sinks,
688        o_bias,
689        router_bias,
690        expert_bias,
691    })
692}
693
694fn load_f32_vec_optional(
695    file: &impl TensorSource,
696    name: &str,
697) -> Result<Option<Vec<f32>>, LoadError> {
698    if file.find_tensor(name).is_none() {
699        return Ok(None);
700    }
701    Ok(Some(load_f32_vec(file, name)?))
702}
703
704/// Slice `n` rows starting at `start` out of a quantized matrix without
705/// dequantizing: every `Quantized` kind stores one interleaved block
706/// buffer per row (fixed `row_bytes`), so a row range is a contiguous
707/// byte range. Mapped sources stay zero-copy (sub-range of the same
708/// mmap); other backings get an owned copy. Returns `None` for non-
709/// quantized matrices (F32 / MXFP4) — callers fall back to dequant.
710fn slice_quantized_rows(m: &WeightMatrix, start: usize, n: usize) -> Option<WeightMatrix> {
711    let WeightMatrix::Quantized {
712        data,
713        rows,
714        cols,
715        kind,
716    } = m
717    else {
718        return None;
719    };
720    let total = data.len();
721    if *rows == 0 || total % *rows != 0 || start + n > *rows {
722        return None;
723    }
724    let row_bytes = total / *rows;
725    let (b0, b1) = (start * row_bytes, (start + n) * row_bytes);
726    let bytes = match data {
727        WeightBytes::Mapped { mmap, range } => WeightBytes::Mapped {
728            mmap: mmap.clone(),
729            range: range.start + b0..range.start + b1,
730        },
731        other => WeightBytes::Owned(other.as_slice()[b0..b1].to_vec()),
732    };
733    Some(WeightMatrix::Quantized {
734        data: bytes,
735        rows: n,
736        cols: *cols,
737        kind: *kind,
738    })
739}
740
741/// Loads Q/K/V projections: prefers split `attn_{q,k,v}.weight`, falls
742/// back to fused `attn_qkv.weight` (Phi-3 / some Qwen GGUFs) by
743/// slicing quantized rows (zero-copy for mmapped GGUFs; dequant only
744/// for non-quantized storage). Mirrors llama.cpp `create_tensor_qkv`.
745fn load_qkv_projections(
746    file: &impl TensorSource,
747    layer: usize,
748    config: &ModelConfig,
749) -> Result<(WeightMatrix, WeightMatrix, WeightMatrix), LoadError> {
750    let q_name = format!("blk.{layer}.attn_q.weight");
751    let k_name = format!("blk.{layer}.attn_k.weight");
752    let v_name = format!("blk.{layer}.attn_v.weight");
753    let fused_name = format!("blk.{layer}.attn_qkv.weight");
754
755    if file.find_tensor(&q_name).is_some() {
756        return Ok((
757            load_weight_matrix(file, &q_name)?,
758            load_weight_matrix(file, &k_name)?,
759            load_weight_matrix(file, &v_name)?,
760        ));
761    }
762    if file.find_tensor(&fused_name).is_none() {
763        return Err(LoadError::Gguf(GgufError::TensorNotFound(q_name)));
764    }
765
766    let fused = load_weight_matrix(file, &fused_name)?;
767    let q_rows = config.n_heads * config.head_dim;
768    let kv_rows = config.n_kv_heads * config.head_dim;
769    let expected = q_rows + 2 * kv_rows;
770    if fused.rows() != expected {
771        // Phi-3 sometimes stores Q as full n_embd (== q_rows when MHA).
772        return Err(LoadError::UnsupportedFeature(
773            config.name.to_string(),
774            format!(
775                "{fused_name} has {} rows; expected q+k+v = {} \
776                 (n_heads*head_dim + 2*n_kv_heads*head_dim)",
777                fused.rows(),
778                expected
779            ),
780        ));
781    }
782    let cols = fused.cols();
783    // Quantized fused tensor: split by row ranges without dequantizing,
784    // keeping Q/K/V on the quantized (Metal-capable) matvec path.
785    if let (Some(q), Some(k), Some(v)) = (
786        slice_quantized_rows(&fused, 0, q_rows),
787        slice_quantized_rows(&fused, q_rows, kv_rows),
788        slice_quantized_rows(&fused, q_rows + kv_rows, kv_rows),
789    ) {
790        return Ok((q, k, v));
791    }
792    // Non-quantized storage: dequant once and split.
793    let mut full = Vec::with_capacity(fused.rows() * cols);
794    for r in 0..fused.rows() {
795        full.extend_from_slice(&fused.dequant_row(r));
796    }
797    let q = WeightMatrix::F32(Tensor::new(
798        full[..q_rows * cols].to_vec(),
799        vec![q_rows, cols],
800    ));
801    let k = WeightMatrix::F32(Tensor::new(
802        full[q_rows * cols..(q_rows + kv_rows) * cols].to_vec(),
803        vec![kv_rows, cols],
804    ));
805    let v = WeightMatrix::F32(Tensor::new(
806        full[(q_rows + kv_rows) * cols..].to_vec(),
807        vec![kv_rows, cols],
808    ));
809    Ok((q, k, v))
810}
811
812/// Dense-layer FFN tensors: standard gate/up/down, or Phi-3 fused
813/// `ffn_up` with `2 * expert_ffn_dim` rows and no separate gate.
814fn load_dense_expert(
815    file: &impl TensorSource,
816    layer: usize,
817    config: &ModelConfig,
818) -> Result<ExpertWeights, LoadError> {
819    let gate_name = format!("blk.{layer}.ffn_gate.weight");
820    let up_name = format!("blk.{layer}.ffn_up.weight");
821    let down_name = format!("blk.{layer}.ffn_down.weight");
822    if file.find_tensor(&gate_name).is_some() {
823        return Ok(ExpertWeights {
824            gate: load_weight_matrix(file, &gate_name)?,
825            up: load_weight_matrix(file, &up_name)?,
826            down: load_weight_matrix(file, &down_name)?,
827        });
828    }
829    // Phi-3 fused SwiGLU: up is [hidden, 2*ff], first half gate, second up.
830    let fused = load_weight_matrix(file, &up_name)?;
831    let ff = config.moe.expert_ffn_dim;
832    if fused.rows() != 2 * ff {
833        return Err(LoadError::UnsupportedFeature(
834            config.name.to_string(),
835            format!(
836                "{up_name} has {} rows without a companion ffn_gate; \
837                 expected fused SwiGLU with 2*ffn_dim = {} rows",
838                fused.rows(),
839                2 * ff
840            ),
841        ));
842    }
843    let cols = fused.cols();
844    // Quantized fused gate+up: split by rows, no dequant (Metal-capable).
845    if let (Some(gate), Some(up)) = (
846        slice_quantized_rows(&fused, 0, ff),
847        slice_quantized_rows(&fused, ff, ff),
848    ) {
849        return Ok(ExpertWeights {
850            gate,
851            up,
852            down: load_weight_matrix(file, &down_name)?,
853        });
854    }
855    let mut full = Vec::with_capacity(fused.rows() * cols);
856    for r in 0..fused.rows() {
857        full.extend_from_slice(&fused.dequant_row(r));
858    }
859    let gate = WeightMatrix::F32(Tensor::new(full[..ff * cols].to_vec(), vec![ff, cols]));
860    let up = WeightMatrix::F32(Tensor::new(full[ff * cols..].to_vec(), vec![ff, cols]));
861    Ok(ExpertWeights {
862        gate,
863        up,
864        down: load_weight_matrix(file, &down_name)?,
865    })
866}
867
868/// Widen a raw plain-float tensor (`F32` / `F16` / `BF16`) to `f32`.
869///
870/// The three unquantized element types are handled identically at every
871/// call site (eager widening to an owned buffer -- none of them has a
872/// block structure a fused dot kernel could exploit), and each of the
873/// seven GGUF loaders used to inline the same two-way match. F16 had no
874/// arm in any of them, which made every `*-f16.gguf` a hard
875/// `UnsupportedDtype` even though the type was parsed and sized.
876pub(crate) fn widen_plain_float(
877    dtype: GgmlType,
878    raw: &[u8],
879    name: &str,
880) -> Result<Vec<f32>, LoadError> {
881    match dtype {
882        GgmlType::F32 => {
883            let mut out = Vec::with_capacity(raw.len() / 4);
884            for chunk in raw.chunks_exact(4) {
885                out.push(f32::from_le_bytes(chunk.try_into().unwrap()));
886            }
887            Ok(out)
888        }
889        GgmlType::F16 => ferrox_quant::dequant_f16(raw)
890            .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::F16)),
891        GgmlType::BF16 => ferrox_quant::dequant_bf16(raw)
892            .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::BF16)),
893        other => Err(LoadError::UnsupportedDtype(name.to_string(), other)),
894    }
895}
896
897fn load_f32_vec(file: &impl TensorSource, name: &str) -> Result<Vec<f32>, LoadError> {
898    let info = find_info(file, name)?;
899    let raw = file.tensor_bytes(name)?;
900    match info.dtype {
901        GgmlType::F32 | GgmlType::F16 | GgmlType::BF16 => widen_plain_float(info.dtype, raw, name),
902        GgmlType::Q8_0 => ferrox_quant::dequant_q8_0(raw)
903            .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::Q8_0)),
904        GgmlType::Q4_0 => ferrox_quant::dequant_q4_0(raw)
905            .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::Q4_0)),
906        GgmlType::Q4K => ferrox_quant::dequant_q4_k(raw)
907            .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::Q4K)),
908        GgmlType::Q5K => ferrox_quant::dequant_q5_k(raw)
909            .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::Q5K)),
910        GgmlType::Q6K => ferrox_quant::dequant_q6_k(raw)
911            .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::Q6K)),
912        GgmlType::Q2K => ferrox_quant::dequant_q2_k(raw)
913            .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::Q2K)),
914        GgmlType::Q3K => ferrox_quant::dequant_q3_k(raw)
915            .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::Q3K)),
916        GgmlType::Q4_1 => ferrox_quant::dequant_q4_1(raw)
917            .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::Q4_1)),
918        GgmlType::Q5_0 => ferrox_quant::dequant_q5_0(raw)
919            .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::Q5_0)),
920        GgmlType::Q5_1 => ferrox_quant::dequant_q5_1(raw)
921            .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::Q5_1)),
922        GgmlType::Q8_1 => ferrox_quant::dequant_q8_1(raw)
923            .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::Q8_1)),
924        GgmlType::IQ4NL => ferrox_quant::dequant_iq4_nl(raw)
925            .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::IQ4NL)),
926        GgmlType::IQ4XS => ferrox_quant::dequant_iq4_xs(raw)
927            .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::IQ4XS)),
928        // The codebook-grid tiers. Rare on the 1-D tensors this
929        // function widens (norms and biases are almost always F32),
930        // but a dtype ferrox can decode should never be rejected here
931        // just because the *other* dispatch table below knows it --
932        // that split is how a supported format turns into a load
933        // failure on the one checkpoint that uses it.
934        GgmlType::IQ1S => ferrox_quant::dequant_iq1_s(raw)
935            .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::IQ1S)),
936        GgmlType::IQ1M => ferrox_quant::dequant_iq1_m(raw)
937            .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::IQ1M)),
938        GgmlType::IQ2XXS => ferrox_quant::dequant_iq2_xxs(raw)
939            .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::IQ2XXS)),
940        GgmlType::IQ2XS => ferrox_quant::dequant_iq2_xs(raw)
941            .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::IQ2XS)),
942        GgmlType::IQ2S => ferrox_quant::dequant_iq2_s(raw)
943            .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::IQ2S)),
944        GgmlType::IQ3XXS => ferrox_quant::dequant_iq3_xxs(raw)
945            .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::IQ3XXS)),
946        GgmlType::IQ3S => ferrox_quant::dequant_iq3_s(raw)
947            .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::IQ3S)),
948        other => Err(LoadError::UnsupportedDtype(name.to_string(), other)),
949    }
950}
951
952/// Loads a 2D weight matrix, keeping Q8_0/Q4_0 tensors quantized (raw
953/// bytes copied out, never dequantized) and only expanding truly F32
954/// tensors. This is the memory- and bandwidth-saving path: for a
955/// multi-billion-parameter checkpoint the difference between this and
956/// "dequant everything on load" is the difference between fitting in
957/// RAM and not.
958fn load_weight_matrix(file: &impl TensorSource, name: &str) -> Result<WeightMatrix, LoadError> {
959    let info = find_info(file, name)?;
960    // GGUF's on-disk `ne[]` shape array is fastest-varying-dimension-first
961    // (ggml convention), i.e. `[in_features, out_features]` for a 2D
962    // weight matrix -- the *reverse* of the row-major `[rows, cols]` =
963    // `[out_features, in_features]` order `WeightMatrix`/`matmul_f32`
964    // need. Reversed here once so every consumer below gets the correct
965    // orientation. Before this reversal existed, every 2D tensor in an
966    // externally-produced GGUF file was silently loaded transposed -- a
967    // real bug found by running a real downloaded checkpoint
968    // (TinyLlama-1.1B-Chat, e.g. `attn_k.weight`'s real raw shape is
969    // `[2048, 256]` = `[hidden_dim, kv_dim]` = `[in, out]`) -- found
970    // as a real transposition bug affecting every externally-produced
971    // GGUF file, caught by serving a real downloaded checkpoint.
972    let shape: Vec<usize> = info.shape.iter().rev().map(|&d| d as usize).collect();
973    let (rows, cols) = match shape.as_slice() {
974        [r, c] => (*r, *c),
975        other => {
976            return Err(LoadError::UnsupportedDtype(
977                format!("{name} (expected 2D, got shape {other:?})"),
978                info.dtype,
979            ))
980        }
981    };
982
983    match info.dtype {
984        // BF16 has no block/scale structure to keep quantized-in-place
985        // the way Q4_0/Q8_0/K-quants do -- there's no fused dot kernel
986        // that would make sense for a plain narrowed float, so it's
987        // eagerly widened to an owned f32 Tensor exactly like F32
988        // tensors already are.
989        GgmlType::F32 | GgmlType::F16 | GgmlType::BF16 => {
990            let data = load_f32_vec(file, name)?;
991            Ok(WeightMatrix::F32(Tensor::new(data, shape)))
992        }
993        other => match quant_kind_for(other) {
994            Some(kind) => {
995                let (mmap, range) = file.tensor_mapped_range(name)?;
996                #[cfg(feature = "metal")]
997                ferrox_metal::gpu::register_weight_mmap(Arc::clone(&mmap));
998                Ok(WeightMatrix::Quantized {
999                    data: WeightBytes::Mapped { mmap, range },
1000                    rows,
1001                    cols,
1002                    kind,
1003                })
1004            }
1005            None => Err(LoadError::UnsupportedDtype(name.to_string(), other)),
1006        },
1007    }
1008}
1009
1010/// Splits a packed 3D MoE expert tensor `blk.N.ffn_{gate,up,down}_exps.weight`
1011/// (shape `[n_experts, out_dim, in_dim]`) into per-expert `WeightMatrix`es,
1012/// slicing raw bytes directly (quantized tensors stay quantized; block
1013/// boundaries never cross expert boundaries since `in_dim` is a whole
1014/// number of quantization blocks). Matches llama.cpp/ik_llama.cpp layout
1015/// confirmed on real OLMoE and Qwen2-MoE GGUF checkpoints.
1016fn split_expert_tensor(
1017    file: &impl TensorSource,
1018    name: &str,
1019    n_experts: usize,
1020) -> Result<Vec<WeightMatrix>, LoadError> {
1021    let info = find_info(file, name)?;
1022    // Real raw shape is `[in_dim, out_dim, n_experts]` (ggml's
1023    // fastest-first `ne[]` order -- see `load_weight_matrix`'s doc
1024    // comment for the confirmed 2D case this generalizes from). `n_experts`
1025    // is the slowest-varying (last, i.e. outermost/most-major) dimension,
1026    // so each expert's `out_dim*in_dim` block is contiguous with experts
1027    // back-to-back in the mmap.
1028    if info.shape.len() != 3 || info.shape[2] as usize != n_experts {
1029        let file_experts = info.shape.last().map(|&d| d as usize).unwrap_or(0);
1030        return Err(LoadError::ExpertCountMismatch(
1031            name.to_string(),
1032            file_experts,
1033            n_experts,
1034        ));
1035    }
1036    let out_dim = info.shape[1] as usize;
1037    let in_dim = info.shape[0] as usize;
1038    let raw = file.tensor_bytes(name)?;
1039
1040    match info.dtype {
1041        GgmlType::F32 | GgmlType::F16 | GgmlType::BF16 => {
1042            let all = crate::loader::widen_plain_float(info.dtype, raw, name)?;
1043            let per_expert = out_dim * in_dim;
1044            Ok((0..n_experts)
1045                .map(|e| {
1046                    WeightMatrix::F32(Tensor::new(
1047                        all[e * per_expert..(e + 1) * per_expert].to_vec(),
1048                        vec![out_dim, in_dim],
1049                    ))
1050                })
1051                .collect())
1052        }
1053        other => match quant_kind_for(other) {
1054            Some(kind) => {
1055                let (mmap, full_range) = file.tensor_mapped_range(name)?;
1056                #[cfg(feature = "metal")]
1057                ferrox_metal::gpu::register_weight_mmap(Arc::clone(&mmap));
1058                let bytes_per_expert = raw.len() / n_experts;
1059                Ok((0..n_experts)
1060                    .map(|e| WeightMatrix::Quantized {
1061                        data: WeightBytes::Mapped {
1062                            mmap: Arc::clone(&mmap),
1063                            range: (full_range.start + e * bytes_per_expert)
1064                                ..(full_range.start + (e + 1) * bytes_per_expert),
1065                        },
1066                        rows: out_dim,
1067                        cols: in_dim,
1068                        kind,
1069                    })
1070                    .collect())
1071            }
1072            None => Err(LoadError::UnsupportedDtype(name.to_string(), other)),
1073        },
1074    }
1075}
1076
1077/// When every routed expert is mmap-backed with a Metal simdgroup-GEMM
1078/// kind and back-to-back slices, record the combined gate/up/down planes
1079/// for Metal packed MoE. Gate/up/down may differ in kind (e.g. Q4_K /
1080/// Q4_K / Q8_0) but must be uniform across experts per role.
1081#[cfg(feature = "metal")]
1082fn try_build_moe_packed_q4_planes(experts: &[ExpertWeights]) -> Option<MoePackedQ4Planes> {
1083    use ferrox_core::weight_matrix::{QuantKind, WeightBytes};
1084    use std::sync::Arc;
1085
1086    if experts.is_empty() {
1087        return None;
1088    }
1089
1090    fn mapped_sg(m: &WeightMatrix) -> Option<(WeightBytes, usize, &'static str)> {
1091        match m {
1092            WeightMatrix::Quantized {
1093                data: WeightBytes::Mapped { mmap, range },
1094                rows,
1095                kind,
1096                ..
1097            } => {
1098                let kind_str = match kind {
1099                    QuantKind::Q4_0 => "Q4_0",
1100                    QuantKind::Q5_0 => "Q5_0",
1101                    QuantKind::Q4K => "Q4_K",
1102                    QuantKind::Q5K => "Q5_K",
1103                    QuantKind::Q6K => "Q6_K",
1104                    QuantKind::Q8_0 => "Q8_0",
1105                    QuantKind::IQ4XS => "IQ4_XS",
1106                    _ => return None,
1107                };
1108                let _ = ferrox_metal::gpu::mul_mm_sg_meta(kind_str)?;
1109                Some((
1110                    WeightBytes::Mapped {
1111                        mmap: Arc::clone(mmap),
1112                        range: range.clone(),
1113                    },
1114                    *rows,
1115                    kind_str,
1116                ))
1117            }
1118            _ => None,
1119        }
1120    }
1121
1122    let (gate0, ffn_rows, gate_kind) = mapped_sg(&experts[0].gate)?;
1123    let (up0, up_rows, up_kind) = mapped_sg(&experts[0].up)?;
1124    let (down0, hidden_rows, down_kind) = mapped_sg(&experts[0].down)?;
1125    if up_rows != ffn_rows {
1126        return None;
1127    }
1128    let WeightBytes::Mapped {
1129        mmap: gate_mmap,
1130        range: gate0_range,
1131    } = &gate0
1132    else {
1133        return None;
1134    };
1135    let WeightBytes::Mapped {
1136        mmap: up_mmap,
1137        range: up0_range,
1138    } = &up0
1139    else {
1140        return None;
1141    };
1142    let WeightBytes::Mapped {
1143        mmap: down_mmap,
1144        range: down0_range,
1145    } = &down0
1146    else {
1147        return None;
1148    };
1149
1150    let gate_stride = gate0_range.len();
1151    let up_stride = up0_range.len();
1152    let down_stride = down0_range.len();
1153    if gate_stride == 0 || up_stride == 0 || down_stride == 0 {
1154        return None;
1155    }
1156
1157    let n = experts.len();
1158    for (i, ex) in experts.iter().enumerate().skip(1) {
1159        let (g, fr, gk) = mapped_sg(&ex.gate)?;
1160        let (u, ur, uk) = mapped_sg(&ex.up)?;
1161        let (d, hr, dk) = mapped_sg(&ex.down)?;
1162        if gk != gate_kind || uk != up_kind || dk != down_kind {
1163            return None;
1164        }
1165        let WeightBytes::Mapped { mmap, range } = &g else {
1166            return None;
1167        };
1168        if fr != ffn_rows {
1169            return None;
1170        }
1171        if !Arc::ptr_eq(mmap, gate_mmap)
1172            || range.len() != gate_stride
1173            || range.start != gate0_range.start + i * gate_stride
1174        {
1175            return None;
1176        }
1177        let WeightBytes::Mapped { mmap, range } = &u else {
1178            return None;
1179        };
1180        if ur != ffn_rows
1181            || !Arc::ptr_eq(mmap, up_mmap)
1182            || range.len() != up_stride
1183            || range.start != up0_range.start + i * up_stride
1184        {
1185            return None;
1186        }
1187        let WeightBytes::Mapped { mmap, range } = &d else {
1188            return None;
1189        };
1190        if hr != hidden_rows
1191            || !Arc::ptr_eq(mmap, down_mmap)
1192            || range.len() != down_stride
1193            || range.start != down0_range.start + i * down_stride
1194        {
1195            return None;
1196        }
1197    }
1198
1199    Some(MoePackedQ4Planes::new(
1200        WeightBytes::Mapped {
1201            mmap: Arc::clone(gate_mmap),
1202            range: gate0_range.start..gate0_range.start + n * gate_stride,
1203        },
1204        WeightBytes::Mapped {
1205            mmap: Arc::clone(up_mmap),
1206            range: up0_range.start..up0_range.start + n * up_stride,
1207        },
1208        WeightBytes::Mapped {
1209            mmap: Arc::clone(down_mmap),
1210            range: down0_range.start..down0_range.start + n * down_stride,
1211        },
1212        gate_stride,
1213        up_stride,
1214        down_stride,
1215        n,
1216        ffn_rows,
1217        hidden_rows,
1218        gate_kind,
1219        up_kind,
1220        down_kind,
1221    ))
1222}
1223
1224/// One matrix's place inside a store-backed expert's combined byte
1225/// buffer (gate bytes, then up, then down, concatenated by
1226/// `GgufExpertSource::read_expert`).
1227#[derive(Debug, Clone, Copy)]
1228pub struct StoredMatrixSpec {
1229    pub offset: usize,
1230    pub len: usize,
1231    pub rows: usize,
1232    pub cols: usize,
1233    pub kind: QuantKind,
1234}
1235
1236/// Byte-range layout of one store-backed routed expert.
1237#[derive(Debug, Clone, Copy)]
1238pub struct StoredExpertLayout {
1239    pub gate: StoredMatrixSpec,
1240    pub up: StoredMatrixSpec,
1241    pub down: StoredMatrixSpec,
1242}
1243
1244impl StoredExpertLayout {
1245    pub fn total_bytes(&self) -> usize {
1246        self.gate.len + self.up.len + self.down.len
1247    }
1248
1249    /// Builds temporary zero-copy `WeightMatrix` views over a leased
1250    /// buffer. Each view's `WeightBytes::Shared` clone of the lease's
1251    /// `Arc` keeps the cache entry pinned for the view's lifetime.
1252    pub fn materialize(&self, lease: &ferrox_core::expert_store::ExpertLease) -> ExpertWeights {
1253        let mk = |spec: &StoredMatrixSpec| WeightMatrix::Quantized {
1254            data: WeightBytes::Shared {
1255                buf: lease.shared_buf(),
1256                range: spec.offset..spec.offset + spec.len,
1257            },
1258            rows: spec.rows,
1259            cols: spec.cols,
1260            kind: spec.kind,
1261        };
1262        ExpertWeights {
1263            gate: mk(&self.gate),
1264            up: mk(&self.up),
1265            down: mk(&self.down),
1266        }
1267    }
1268}
1269
1270/// [`ExpertSource`] over a (possibly sharded) GGUF checkpoint: each
1271/// expert's gate/up/down byte ranges are read positionally from the
1272/// owning shard file and concatenated, so a store miss touches exactly
1273/// that expert's bytes -- no mmap of the expert region, no shared seek
1274/// cursor.
1275pub struct GgufExpertSource {
1276    files: Vec<std::fs::File>,
1277    /// (layer, expert) -> the three (file index, offset, len) segments
1278    /// in gate/up/down order.
1279    segments: std::collections::HashMap<ExpertKey, [(usize, u64, usize); 3]>,
1280}
1281
1282impl ExpertSource for GgufExpertSource {
1283    fn expert_len(&self, key: ExpertKey) -> Option<usize> {
1284        self.segments
1285            .get(&key)
1286            .map(|segs| segs.iter().map(|&(_, _, len)| len).sum())
1287    }
1288
1289    fn read_expert(&self, key: ExpertKey) -> std::io::Result<Vec<u8>> {
1290        let segs = self
1291            .segments
1292            .get(&key)
1293            .ok_or_else(|| std::io::Error::new(std::io::ErrorKind::NotFound, format!("{key:?}")))?;
1294        let total: usize = segs.iter().map(|&(_, _, len)| len).sum();
1295        let mut buf = vec![0u8; total];
1296        let mut written = 0;
1297        for &(fi, offset, len) in segs {
1298            let dst = &mut buf[written..written + len];
1299            #[cfg(unix)]
1300            {
1301                use std::os::unix::fs::FileExt;
1302                self.files[fi].read_exact_at(dst, offset)?;
1303            }
1304            #[cfg(not(unix))]
1305            {
1306                use std::io::{Read, Seek, SeekFrom};
1307                let mut f = &self.files[fi];
1308                f.seek(SeekFrom::Start(offset))?;
1309                f.read_exact(dst)?;
1310            }
1311            written += len;
1312        }
1313        Ok(buf)
1314    }
1315}
1316
1317/// Collects the per-expert `(file, offset, len)` segments and layout
1318/// for one packed 3D expert tensor -- the store-backed counterpart of
1319/// `split_expert_tensor`, sharing its shape/offset math. Only
1320/// quantized dtypes are supported (an F32/BF16 expert tensor keeps the
1321/// resident path; the store exists for the quantized multi-hundred-GB
1322/// case).
1323/// One packed 3D expert tensor's store-backed description: the owning
1324/// shard index, each expert's `(offset, len)` within that shard file,
1325/// and the matrix spec shared by every expert's slice.
1326struct StoredTensorSpecs {
1327    shard: usize,
1328    per_expert: Vec<(u64, usize)>,
1329    spec: StoredMatrixSpec,
1330}
1331
1332fn stored_expert_specs(
1333    file: &ShardedGguf,
1334    name: &str,
1335    n_experts: usize,
1336) -> Result<Option<StoredTensorSpecs>, LoadError> {
1337    let info = find_info(file, name)?;
1338    if info.shape.len() != 3 || info.shape[2] as usize != n_experts {
1339        let file_experts = info.shape.last().map(|&d| d as usize).unwrap_or(0);
1340        return Err(LoadError::ExpertCountMismatch(
1341            name.to_string(),
1342            file_experts,
1343            n_experts,
1344        ));
1345    }
1346    let out_dim = info.shape[1] as usize;
1347    let in_dim = info.shape[0] as usize;
1348    let Some(kind) = quant_kind_for(info.dtype) else {
1349        return Ok(None); // F32/BF16 (or unsupported): resident fallback
1350    };
1351    let shard = file
1352        .tensor_shard_index(name)
1353        .expect("find_info succeeded, shard index must exist");
1354    // The mmap range of a tensor within a GgufFile IS its byte offset
1355    // range within that shard file (the mmap covers the whole file).
1356    let (_, full_range) = file.tensor_mapped_range(name)?;
1357    let total_len = full_range.end - full_range.start;
1358    let bytes_per_expert = total_len / n_experts;
1359    let per_expert: Vec<(u64, usize)> = (0..n_experts)
1360        .map(|e| {
1361            (
1362                (full_range.start + e * bytes_per_expert) as u64,
1363                bytes_per_expert,
1364            )
1365        })
1366        .collect();
1367    let spec = StoredMatrixSpec {
1368        offset: 0, // caller assigns the position within the combined buffer
1369        len: bytes_per_expert,
1370        rows: out_dim,
1371        cols: in_dim,
1372        kind,
1373    };
1374    Ok(Some(StoredTensorSpecs {
1375        shard,
1376        per_expert,
1377        spec,
1378    }))
1379}
1380
1381impl Decoder {
1382    /// Loads real weights from `path` for the given `config`. `config`
1383    /// supplies the architecture shape (layer count, head counts, MoE
1384    /// topology); tensor names are resolved against it using the
1385    /// llama.cpp naming convention described in the module docs.
1386    ///
1387    /// A `config.moe.n_experts <= 1` model is treated as dense: expert
1388    /// weights are read from the plain `blk.N.ffn_{gate,up,down}.weight`
1389    /// tensor names rather than the packed 3D `_exps` variant.
1390    pub fn from_gguf(
1391        path: impl AsRef<std::path::Path>,
1392        config: ModelConfig,
1393    ) -> Result<Self, LoadError> {
1394        Self::from_gguf_with_expert_cache(path, config, None)
1395    }
1396
1397    /// Like `from_gguf`, but with `expert_cache_bytes: Some(budget)`
1398    /// routed experts are NOT loaded resident: each layer holds only
1399    /// byte-range layouts, and expert bytes are read on demand through
1400    /// one bounded, lease-protected `ExpertStore` shared by every
1401    /// layer (a single global byte budget; see
1402    /// `ferrox_core::expert_store`). Dense layers, shared experts,
1403    /// attention, embeddings, and the output head stay resident/mapped
1404    /// exactly as before -- only routed experts stream. Layers whose
1405    /// expert tensors are F32/BF16 fall back to resident loading (the
1406    /// store exists for the quantized case). Output is bit-identical
1407    /// to the resident path -- same bytes, same kernels -- pinned by
1408    /// the roundtrip suite's equivalence test.
1409    pub fn from_gguf_with_expert_cache(
1410        path: impl AsRef<std::path::Path>,
1411        mut config: ModelConfig,
1412        expert_cache_bytes: Option<u64>,
1413    ) -> Result<Self, LoadError> {
1414        let path = path.as_ref();
1415        let file = ShardedGguf::open(path)?;
1416
1417        // gpt-oss carries five per-layer tensors the generic GQA layer
1418        // structs have no home for, and reuses `post_attention_norm` for
1419        // a *different* norm slot than Gemma does. Both are decided by
1420        // the architecture string, so resolve it once here. See
1421        // `crate::decoder::GptOssWeights`.
1422        let arch = file
1423            .metadata_str("general.architecture")
1424            .unwrap_or_default()
1425            .to_string();
1426        let is_gpt_oss = arch == "gpt-oss";
1427        let mut gpt_oss_layers: Vec<crate::decoder::GptOssLayer> = Vec::new();
1428
1429        // One store for the whole model (keys are (layer, expert)),
1430        // built up-front with every stored expert's segments; created
1431        // only when the cache is enabled AND some layer can use it.
1432        let mut store_segments: std::collections::HashMap<ExpertKey, [(usize, u64, usize); 3]> =
1433            std::collections::HashMap::new();
1434        let mut stored_layouts: Vec<Option<Vec<StoredExpertLayout>>> = Vec::new();
1435
1436        // Loaded like any other weight matrix: a quantized embedding
1437        // table stays quantized (zero-copy mmap) and token lookup
1438        // dequantizes one row via `WeightMatrix::dequant_row`, instead
1439        // of the whole vocabulary tensor being widened to f32 up front.
1440        let embedding = load_weight_matrix(&file, "token_embd.weight")?;
1441
1442        let mut layers = Vec::with_capacity(config.n_layers);
1443        let mut refined_qk_norm = config.qk_norm_style;
1444        for l in 0..config.n_layers {
1445            let (q_proj, k_proj, v_proj) = load_qkv_projections(&file, l, &config)?;
1446            let q_norm = load_f32_vec_optional(&file, &format!("blk.{l}.attn_q_norm.weight"))?;
1447            let k_norm = load_f32_vec_optional(&file, &format!("blk.{l}.attn_k_norm.weight"))?;
1448            // Refine WholeVector vs PerHead from the first observed norm length.
1449            if let Some(ref w) = q_norm {
1450                if w.len() == config.head_dim {
1451                    refined_qk_norm = crate::capability::QkNormStyle::PerHead;
1452                } else if w.len() == config.n_heads * config.head_dim {
1453                    refined_qk_norm = crate::capability::QkNormStyle::WholeVector;
1454                } else {
1455                    return Err(LoadError::UnsupportedFeature(
1456                        config.name.to_string(),
1457                        format!(
1458                            "blk.{l}.attn_q_norm.weight length {} matches neither head_dim={} \
1459                             nor n_heads*head_dim={}",
1460                            w.len(),
1461                            config.head_dim,
1462                            config.n_heads * config.head_dim
1463                        ),
1464                    ));
1465                }
1466            }
1467            let attn = AttnWeights {
1468                q_proj,
1469                k_proj,
1470                v_proj,
1471                o_proj: load_weight_matrix(&file, &format!("blk.{l}.attn_output.weight"))?,
1472                norm_weight: load_f32_vec(&file, &format!("blk.{l}.attn_norm.weight"))?,
1473                q_norm,
1474                k_norm,
1475                // Qwen2/Qwen2-MoE-family real QKV bias (`attn_{q,k,v}.bias`,
1476                // real config `qkv_bias`, `o_proj` has none) -- see
1477                // `AttnWeights::q_bias`'s doc comment.
1478                q_bias: load_f32_vec_optional(&file, &format!("blk.{l}.attn_q.bias"))?,
1479                k_bias: load_f32_vec_optional(&file, &format!("blk.{l}.attn_k.bias"))?,
1480                v_bias: load_f32_vec_optional(&file, &format!("blk.{l}.attn_v.bias"))?,
1481                // gpt-oss ships `post_attention_norm` but applies it in
1482                // Gemma's *other* slot: llama.cpp's openai-moe graph
1483                // norms `ffn_inp` with it after the attention residual,
1484                // i.e. it is the pre-FFN norm, not a post-attention one.
1485                // It is read below into `MoeWeights::norm_weight`.
1486                post_attn_norm: if is_gpt_oss {
1487                    None
1488                } else {
1489                    load_f32_vec_optional(&file, &format!("blk.{l}.post_attention_norm.weight"))?
1490                },
1491                post_ffn_norm: load_f32_vec_optional(
1492                    &file,
1493                    &format!("blk.{l}.post_ffw_norm.weight"),
1494                )?,
1495            };
1496
1497            // Leading dense layers (see ModelConfig::layer_is_dense's
1498            // doc comment) load from the plain dense tensor names
1499            // regardless of this model's global MoE topology, matching
1500            // the DeepSeek-2/3-family convention found in
1501            // ik_llama.cpp's source. A model with n_experts<=1
1502            // globally (the dense test fixture) is dense on every
1503            // layer either way.
1504            let is_dense_layer = config.layer_is_dense(l) || config.moe.n_experts <= 1;
1505            let n_experts = if is_dense_layer {
1506                1
1507            } else {
1508                config.moe.n_experts
1509            };
1510            let experts: ExpertBacking = if is_dense_layer {
1511                ExpertBacking::Resident(vec![load_dense_expert(&file, l, &config)?])
1512            } else {
1513                // Try store-backed layouts first when the cache is
1514                // enabled; fall back to resident when any of the three
1515                // tensors isn't a supported quantized dtype.
1516                let stored = if expert_cache_bytes.is_some() {
1517                    let g = stored_expert_specs(
1518                        &file,
1519                        &format!("blk.{l}.ffn_gate_exps.weight"),
1520                        n_experts,
1521                    )?;
1522                    let u = stored_expert_specs(
1523                        &file,
1524                        &format!("blk.{l}.ffn_up_exps.weight"),
1525                        n_experts,
1526                    )?;
1527                    let d = stored_expert_specs(
1528                        &file,
1529                        &format!("blk.{l}.ffn_down_exps.weight"),
1530                        n_experts,
1531                    )?;
1532                    match (g, u, d) {
1533                        (Some(gt), Some(ut), Some(dt)) => {
1534                            let mut layouts = Vec::with_capacity(n_experts);
1535                            for e in 0..n_experts {
1536                                let key = ExpertKey {
1537                                    layer: l as u32,
1538                                    expert: e as u32,
1539                                };
1540                                store_segments.insert(
1541                                    key,
1542                                    [
1543                                        (gt.shard, gt.per_expert[e].0, gt.per_expert[e].1),
1544                                        (ut.shard, ut.per_expert[e].0, ut.per_expert[e].1),
1545                                        (dt.shard, dt.per_expert[e].0, dt.per_expert[e].1),
1546                                    ],
1547                                );
1548                                let mut gate = gt.spec;
1549                                let mut up = ut.spec;
1550                                let mut down = dt.spec;
1551                                gate.offset = 0;
1552                                up.offset = gate.len;
1553                                down.offset = gate.len + up.len;
1554                                layouts.push(StoredExpertLayout { gate, up, down });
1555                            }
1556                            Some(layouts)
1557                        }
1558                        _ => None,
1559                    }
1560                } else {
1561                    None
1562                };
1563                match stored {
1564                    Some(layouts) => {
1565                        // Placeholder; the shared store is attached in a
1566                        // second pass below once every layer's segments
1567                        // are collected.
1568                        stored_layouts.push(Some(layouts));
1569                        ExpertBacking::Resident(Vec::new())
1570                    }
1571                    None => {
1572                        let gates = split_expert_tensor(
1573                            &file,
1574                            &format!("blk.{l}.ffn_gate_exps.weight"),
1575                            n_experts,
1576                        )?;
1577                        let ups = split_expert_tensor(
1578                            &file,
1579                            &format!("blk.{l}.ffn_up_exps.weight"),
1580                            n_experts,
1581                        )?;
1582                        let downs = split_expert_tensor(
1583                            &file,
1584                            &format!("blk.{l}.ffn_down_exps.weight"),
1585                            n_experts,
1586                        )?;
1587                        ExpertBacking::Resident(
1588                            gates
1589                                .into_iter()
1590                                .zip(ups)
1591                                .zip(downs)
1592                                .map(|((gate, up), down)| ExpertWeights { gate, up, down })
1593                                .collect(),
1594                        )
1595                    }
1596                }
1597            };
1598            if stored_layouts.len() < layers.len() + 1 {
1599                stored_layouts.push(None);
1600            }
1601
1602            let shared_experts: Vec<ExpertWeights> =
1603                if config.moe.n_shared_experts > 0 && !is_dense_layer {
1604                    vec![ExpertWeights {
1605                        gate: load_weight_matrix(&file, &format!("blk.{l}.ffn_gate_shexp.weight"))?,
1606                        up: load_weight_matrix(&file, &format!("blk.{l}.ffn_up_shexp.weight"))?,
1607                        down: load_weight_matrix(&file, &format!("blk.{l}.ffn_down_shexp.weight"))?,
1608                    }]
1609                } else {
1610                    Vec::new()
1611                };
1612
1613            let router = if !is_dense_layer {
1614                load_weight_matrix(&file, &format!("blk.{l}.ffn_gate_inp.weight"))?
1615            } else {
1616                // dense layer: no real router; a zero [1, hidden] matrix
1617                // always selects the single expert deterministically.
1618                WeightMatrix::F32(Tensor::zeros(vec![1, config.hidden_dim]))
1619            };
1620
1621            let n_for_counts = match &experts {
1622                ExpertBacking::Resident(v) if v.is_empty() => n_experts,
1623                other => other.n_experts(),
1624            };
1625            let activation_counts = (0..n_for_counts)
1626                .map(|_| std::sync::atomic::AtomicU64::new(0))
1627                .collect();
1628            // Qwen2-MoE-specific real tensor (`blk.N.ffn_gate_inp_shexp.weight`,
1629            // real on-disk shape `[hidden_dim]`, confirmed against
1630            // llama.cpp's real `qwen2moe.cpp`) -- see
1631            // `MoeWeights::shared_expert_gate`'s doc comment. Presence
1632            // of the tensor itself is the real signal (not an
1633            // architecture-name list): every other supported
1634            // architecture's checkpoints simply don't carry this
1635            // tensor, so this naturally stays `None` there.
1636            let shared_expert_gate = if is_dense_layer {
1637                None
1638            } else {
1639                load_f32_vec_optional(&file, &format!("blk.{l}.ffn_gate_inp_shexp.weight"))?
1640            };
1641            #[cfg(feature = "metal")]
1642            let packed_q4 = match &experts {
1643                ExpertBacking::Resident(v) if !v.is_empty() => try_build_moe_packed_q4_planes(v),
1644                _ => None,
1645            };
1646            let moe = MoeWeights {
1647                router,
1648                experts,
1649                shared_experts,
1650                shared_expert_gate,
1651                norm_weight: if is_gpt_oss {
1652                    load_f32_vec(&file, &format!("blk.{l}.post_attention_norm.weight"))?
1653                } else {
1654                    load_f32_vec(&file, &format!("blk.{l}.ffn_norm.weight"))?
1655                },
1656                activation_counts,
1657                #[cfg(feature = "metal")]
1658                packed_q4,
1659            };
1660
1661            if is_gpt_oss {
1662                gpt_oss_layers.push(load_gpt_oss_layer(&file, l, &config)?);
1663            }
1664
1665            layers.push(LayerWeights { attn, moe });
1666        }
1667
1668        let final_norm = load_f32_vec(&file, "output_norm.weight")?;
1669        // Many small Llama/Gemma-family GGUFs tie the lm-head to
1670        // `token_embd.weight` and omit `output.weight` (llama.cpp
1671        // `llama_model_loader` falls back the same way). Prefer the
1672        // explicit head when present.
1673        let output_head = match load_weight_matrix(&file, "output.weight") {
1674            Ok(w) => w,
1675            Err(_) => load_weight_matrix(&file, "token_embd.weight")?,
1676        };
1677
1678        // Second pass: attach the one shared store to every
1679        // store-backed layer. Opening the shard files fresh (plain
1680        // `File` handles for positional reads, not mmaps) keeps the
1681        // stored experts' bytes out of the process's mapped footprint
1682        // entirely.
1683        if !store_segments.is_empty() {
1684            let budget = expert_cache_bytes
1685                .expect("store_segments only populated when a cache budget is set")
1686                as usize;
1687            let files: Result<Vec<std::fs::File>, std::io::Error> =
1688                file.shard_paths().iter().map(std::fs::File::open).collect();
1689            let files = files.map_err(GgufError::from)?;
1690            let store = std::sync::Arc::new(ExpertStore::new(
1691                GgufExpertSource {
1692                    files,
1693                    segments: store_segments,
1694                },
1695                budget,
1696            ));
1697            for (l, layer) in layers.iter_mut().enumerate() {
1698                if let Some(layouts) = stored_layouts.get_mut(l).and_then(Option::take) {
1699                    layer.moe.experts = ExpertBacking::Stored {
1700                        store: std::sync::Arc::clone(&store),
1701                        layouts,
1702                        layer: l as u32,
1703                    };
1704                }
1705            }
1706        }
1707
1708        config.qk_norm_style = refined_qk_norm;
1709
1710        let family = crate::capability::resolve_profile(
1711            file.metadata_str("general.architecture").unwrap_or("llama"),
1712        )
1713        .map(|p| p.family)
1714        .unwrap_or(crate::capability::DecoderFamily::StandardGqa);
1715        let memory_kind = crate::capability::resolve_profile(
1716            file.metadata_str("general.architecture").unwrap_or("llama"),
1717        )
1718        .map(|p| p.memory)
1719        .unwrap_or(crate::capability::MemoryKind::KvGqa);
1720        let execution_plan = crate::execution_plan::ExecutionPlan::from_config(
1721            &config,
1722            family,
1723            memory_kind,
1724            crate::execution_plan::ExecutionPlan::probe_metal_caps(),
1725        );
1726
1727        let decoder = Decoder {
1728            config,
1729            embedding,
1730            layers,
1731            final_norm,
1732            output_head,
1733            gpu_vram_budget_bytes: None,
1734            gpt_oss: if is_gpt_oss {
1735                Some(crate::decoder::GptOssWeights {
1736                    layers: gpt_oss_layers,
1737                })
1738            } else {
1739                None
1740            },
1741            #[cfg(feature = "metal")]
1742            metal_attn_kv: std::sync::Mutex::new(None),
1743            execution_plan,
1744            plan_cache: std::sync::Mutex::new(std::collections::HashMap::new()),
1745        };
1746        // Resolve every kernel the model will need while we still have a
1747        // load-time error path to report it on, then seal: from here a
1748        // lookup that misses is an unpredicted slow path and says so.
1749        decoder.probe_kernels();
1750        ferrox_core::kernel_registry::seal_or_error()
1751            .map_err(|e| LoadError::StrictKernels(e.to_string()))?;
1752        // `ModelConfig` is parsed from a *different* handle on the same
1753        // file (the CLI opens its own `GgufFile`, then hands the config
1754        // here), so the model-level tensors it consumed were recorded on
1755        // that handle, not this one. Replay them before the gate, or
1756        // every Llama-3.x checkpoint reads as carrying an unread
1757        // `rope_freqs.weight` it in fact uses on every RoPE call.
1758        for name in crate::config::MODEL_LEVEL_TENSORS_READ_BY_CONFIG {
1759            file.note_consumed(name);
1760        }
1761        assert_every_tensor_consumed(&file)?;
1762        Ok(decoder)
1763    }
1764}
1765
1766/// Tensor-name prefixes a text-generation load legitimately never
1767/// reads. Everything here is consumed by a *different* code path, not by
1768/// nothing: multimodal projector planes belong to `mmproj`, and the
1769/// per-shard split bookkeeping is metadata, not weights.
1770const IGNORED_TENSOR_PREFIXES: &[&str] = &["mm.", "v.", "mmproj.", "resampler.", "audio."];
1771
1772/// Fails the load when the checkpoint carries tensors this build never
1773/// looked at.
1774///
1775/// A tensor nobody reads is not a harmless extra: it is a term of the
1776/// real graph that ours is missing. gpt-oss ships `blk.N.attn_sinks`
1777/// and ferrox has no attention-sink code anywhere, so the file loads,
1778/// runs at full speed, and emits a different distribution than the model
1779/// it claims to be; the newer MoE recipes ship `ffn_exp_probs_b` the
1780/// same way. Both are silent today, and both are exactly what the
1781/// architecture registry cannot catch, because the architecture *string*
1782/// is one ferrox does support — it is the checkpoint that carries more
1783/// than the registry entry promises.
1784///
1785/// This is deliberately the last check in the load: by here every loader
1786/// arm has had its chance to ask for what it needs, so what is left over
1787/// is what nothing in this build knows about.
1788///
1789/// `FERROX_ALLOW_UNKNOWN_TENSORS=1` downgrades it to a warning, for the
1790/// case where a human has decided the missing term does not matter (a
1791/// bias tensor of zeros, an auxiliary head that never runs). The default
1792/// is refusal: a wrong answer is worse than no answer.
1793pub fn assert_every_tensor_consumed(file: &ShardedGguf) -> Result<(), LoadError> {
1794    let mut left: Vec<String> = file
1795        .unconsumed_tensors()
1796        .into_iter()
1797        .filter(|n| !IGNORED_TENSOR_PREFIXES.iter().any(|p| n.starts_with(p)))
1798        .collect();
1799    if left.is_empty() {
1800        return Ok(());
1801    }
1802    left.sort();
1803    let shown = left.iter().take(8).cloned().collect::<Vec<_>>().join(", ");
1804    let listing = if left.len() > 8 {
1805        format!("{shown}, … (+{} more)", left.len() - 8)
1806    } else {
1807        shown
1808    };
1809    if matches!(
1810        std::env::var("FERROX_ALLOW_UNKNOWN_TENSORS")
1811            .ok()
1812            .as_deref(),
1813        Some("1") | Some("true") | Some("on")
1814    ) {
1815        eprintln!(
1816            "ferrox: WARNING — {} tensor(s) in this checkpoint are never read \
1817             ({listing}); output may be wrong (FERROX_ALLOW_UNKNOWN_TENSORS=1)",
1818            left.len()
1819        );
1820        return Ok(());
1821    }
1822    Err(LoadError::UnconsumedTensors(left.len(), listing))
1823}
1824
1825#[cfg(test)]
1826mod tests {
1827    use super::*;
1828    use byteorder::{LittleEndian, WriteBytesExt};
1829    use std::io::Write;
1830
1831    fn write_string(buf: &mut Vec<u8>, s: &str) {
1832        buf.write_u64::<LittleEndian>(s.len() as u64).unwrap();
1833        buf.write_all(s.as_bytes()).unwrap();
1834    }
1835
1836    fn write_kv_str(buf: &mut Vec<u8>, key: &str, val: &str) {
1837        write_string(buf, key);
1838        buf.write_u32::<LittleEndian>(8).unwrap(); // type = string
1839        write_string(buf, val);
1840    }
1841
1842    /// A minimal, tensor-free GGUF byte buffer declaring only
1843    /// `general.architecture` (no `{arch}.block_count` or any other
1844    /// hparam key) -- the shape a stripped-down or malformed file might
1845    /// take, and the exact case `ModelConfig::from_gguf` must reject
1846    /// loudly rather than silently default around.
1847    fn build_arch_only_gguf(arch: &str) -> Vec<u8> {
1848        let mut buf = Vec::new();
1849        buf.write_u32::<LittleEndian>(ferrox_gguf::GGUF_MAGIC)
1850            .unwrap();
1851        buf.write_u32::<LittleEndian>(3).unwrap(); // version
1852        buf.write_u64::<LittleEndian>(0).unwrap(); // tensor_count
1853        buf.write_u64::<LittleEndian>(1).unwrap(); // kv_count
1854        write_kv_str(&mut buf, "general.architecture", arch);
1855        buf
1856    }
1857
1858    #[test]
1859    fn model_config_from_gguf_fails_loudly_when_required_hparams_are_missing() {
1860        let tmp = std::env::temp_dir().join("ferrox_test_arch_only.gguf");
1861        // Use a registered architecture so the failure is MissingHparam,
1862        // not UnsupportedArchitecture.
1863        std::fs::write(&tmp, build_arch_only_gguf("llama")).unwrap();
1864        let file = ferrox_gguf::GgufFile::open(&tmp).expect("minimal header must still parse");
1865        std::fs::remove_file(&tmp).ok();
1866
1867        match ModelConfig::from_gguf(&file) {
1868            Err(LoadError::MissingHparam(key)) => {
1869                assert_eq!(key, "llama.block_count");
1870            }
1871            other => panic!(
1872                "expected LoadError::MissingHparam for a file with no hparam keys, got {other:?}"
1873            ),
1874        }
1875    }
1876
1877    #[test]
1878    fn model_config_from_gguf_fails_closed_on_unknown_architecture() {
1879        let tmp = std::env::temp_dir().join("ferrox_test_unknown_arch.gguf");
1880        std::fs::write(&tmp, build_arch_only_gguf("bogus-arch-with-no-hparams")).unwrap();
1881        let file = ferrox_gguf::GgufFile::open(&tmp).expect("minimal header must still parse");
1882        std::fs::remove_file(&tmp).ok();
1883
1884        match ModelConfig::from_gguf(&file) {
1885            Err(LoadError::UnsupportedArchitecture(arch)) => {
1886                assert_eq!(arch, "bogus-arch-with-no-hparams");
1887            }
1888            other => panic!(
1889                "expected LoadError::UnsupportedArchitecture for an unregistered arch, got {other:?}"
1890            ),
1891        }
1892    }
1893
1894    #[test]
1895    fn model_config_from_gguf_rejects_dedicated_architectures() {
1896        let tmp = std::env::temp_dir().join("ferrox_test_dedicated_arch.gguf");
1897        std::fs::write(&tmp, build_arch_only_gguf("deepseek4")).unwrap();
1898        let file = ferrox_gguf::GgufFile::open(&tmp).expect("minimal header must still parse");
1899        std::fs::remove_file(&tmp).ok();
1900
1901        match ModelConfig::from_gguf(&file) {
1902            Err(LoadError::DedicatedArchitectureRequired(arch, _)) => {
1903                assert_eq!(arch, "deepseek4");
1904            }
1905            other => panic!(
1906                "expected LoadError::DedicatedArchitectureRequired for deepseek4, got {other:?}"
1907            ),
1908        }
1909    }
1910
1911    /// The same Q5_K block bytes cross-validated against an independent
1912    /// Python reference in `ferrox-quant`'s own tests, reused here for
1913    /// the same full-path proof as the Q6_K test below.
1914    #[rustfmt::skip]
1915    const Q5_K_TEST_BLOCK: [u8; 176] = [
1916        0x66, 0x2a, 0x66, 0x2a, 0x01, 0x01, 0x01, 0x01, 0x4f, 0x4b, 0x10, 0x12, 0x41, 0xe2, 0xc1,
1917        0xb1, 0x72, 0x2f, 0x20, 0x07, 0x31, 0x0c, 0x38, 0xb3, 0x9c, 0xb8, 0xad, 0x2f, 0x9a, 0xea,
1918        0x17, 0xd0, 0xee, 0x93, 0x9e, 0x3e, 0x74, 0xbb, 0x28, 0x18, 0x39, 0x25, 0xb6, 0x09, 0x18,
1919        0x29, 0x1c, 0x1d, 0x29, 0x41, 0x40, 0x0a, 0x74, 0x7d, 0xfd, 0x21, 0xdd, 0x6d, 0x45, 0x73,
1920        0x0e, 0x1e, 0xc0, 0x4a, 0xfc, 0xf3, 0x8e, 0x24, 0x6b, 0x34, 0x7d, 0xbe, 0x94, 0xde, 0x59,
1921        0x7a, 0x35, 0x30, 0x36, 0x0a, 0xf9, 0x4a, 0x9b, 0xa2, 0x26, 0x21, 0xa2, 0xfa, 0xdf, 0x4b,
1922        0x29, 0x64, 0x6f, 0xbb, 0xca, 0x0f, 0x3c, 0xda, 0x20, 0xf4, 0x93, 0x86, 0xab, 0x6e, 0xb9,
1923        0xe5, 0xd5, 0xa0, 0x82, 0xd6, 0x41, 0xff, 0x12, 0xbc, 0x34, 0xbb, 0xab, 0xb8, 0x20, 0x2f,
1924        0xbb, 0x5f, 0x0c, 0x10, 0xcf, 0x49, 0xc5, 0x86, 0x5c, 0xdf, 0xff, 0x78, 0x44, 0x26, 0x3b,
1925        0xc2, 0x23, 0x3d, 0x2b, 0xe9, 0x00, 0x12, 0xf8, 0xea, 0xe2, 0x9e, 0x5e, 0x50, 0x20, 0x9f,
1926        0x9d, 0x8d, 0x7d, 0x7f, 0xcc, 0x1d, 0x0e, 0x13, 0xf8, 0xc2, 0xf1, 0x3d, 0x08, 0x2f, 0x23,
1927        0x13, 0xac, 0x0d, 0xa7, 0xe7, 0x20, 0xa3, 0x90, 0xb7, 0xc8, 0x28,
1928    ];
1929
1930    fn build_single_q5_k_tensor_gguf() -> Vec<u8> {
1931        let mut buf = Vec::new();
1932        buf.write_u32::<LittleEndian>(ferrox_gguf::GGUF_MAGIC)
1933            .unwrap();
1934        buf.write_u32::<LittleEndian>(3).unwrap(); // version
1935        buf.write_u64::<LittleEndian>(1).unwrap(); // tensor_count
1936        buf.write_u64::<LittleEndian>(1).unwrap(); // kv_count
1937
1938        write_kv_str(&mut buf, "general.architecture", "ferrox-q5k-test");
1939
1940        write_string(&mut buf, "test.weight");
1941        buf.write_u32::<LittleEndian>(2).unwrap(); // n_dims
1942                                                   // Real GGUF ne[] order is fastest-varying-first, i.e. [cols,
1943                                                   // rows] -- reversed from the semantic [rows, cols] this tensor
1944                                                   // represents (1 row, 256 cols / 1 Q5_K block).
1945        buf.write_u64::<LittleEndian>(256).unwrap(); // cols (1 Q5_K block)
1946        buf.write_u64::<LittleEndian>(1).unwrap(); // rows
1947        buf.write_u32::<LittleEndian>(13).unwrap(); // dtype tag: Q5_K
1948        buf.write_u64::<LittleEndian>(0).unwrap(); // offset
1949
1950        while buf.len() % 32 != 0 {
1951            buf.push(0);
1952        }
1953        buf.extend_from_slice(&Q5_K_TEST_BLOCK);
1954        buf
1955    }
1956
1957    #[test]
1958    fn load_weight_matrix_handles_a_real_on_disk_q5_k_tensor_end_to_end() {
1959        let tmp = std::env::temp_dir().join("ferrox_test_q5k_tensor.gguf");
1960        std::fs::write(&tmp, build_single_q5_k_tensor_gguf()).unwrap();
1961        let file = ferrox_gguf::GgufFile::open(&tmp).expect("real Q5_K GGUF file must parse");
1962        std::fs::remove_file(&tmp).ok();
1963
1964        let matrix = load_weight_matrix(&file, "test.weight").expect("Q5_K tensor must load");
1965        assert_eq!(matrix.rows(), 1);
1966        assert_eq!(matrix.cols(), 256);
1967        match &matrix {
1968            WeightMatrix::Quantized { kind, data, .. } => {
1969                assert_eq!(*kind, QuantKind::Q5K);
1970                assert!(
1971                    data.is_mapped(),
1972                    "Q5_K tensors should take the zero-copy mmap path, same as Q8_0/Q4_0"
1973                );
1974            }
1975            _ => panic!("expected a Quantized matrix for a Q5_K tensor"),
1976        }
1977
1978        let expected = ferrox_quant::dequant_q5_k(&Q5_K_TEST_BLOCK).unwrap();
1979        let x: Vec<f32> = (0..256).map(|i| ((i as f32) * 0.013).sin()).collect();
1980        let expected_dot: f32 = expected.iter().zip(x.iter()).map(|(a, b)| a * b).sum();
1981
1982        let got = matrix.apply(&x);
1983        assert_eq!(got.len(), 1);
1984        assert!(
1985            (got[0] - expected_dot).abs() < 1e-2,
1986            "end-to-end loaded+applied Q5_K matrix diverged from direct dequant: got={} expected={}",
1987            got[0],
1988            expected_dot
1989        );
1990    }
1991
1992    /// The same Q6_K block bytes cross-validated against an independent
1993    /// Python reference in `ferrox-quant`'s own tests; reused here to
1994    /// prove the *full*
1995    /// path -- real on-disk GGUF bytes, parsed by `ferrox-gguf`, read
1996    /// through `GgufFile::tensor_mapped_range`, dispatched by
1997    /// `WeightMatrix::apply` to `ferrox_quant::dot_q6_k_f32` -- produces
1998    /// the same result as directly dequantizing those bytes, not just
1999    /// that the isolated kernel is correct in unit-test isolation.
2000    #[rustfmt::skip]
2001    const Q6_K_TEST_BLOCK: [u8; 210] = [
2002        0xe0, 0xa5, 0x40, 0x5c, 0x8d, 0x3a, 0x0a, 0x26, 0xfb, 0x4b, 0x6e, 0x9a, 0xdf, 0x3e, 0xa3,
2003        0xc4, 0xf8, 0x2b, 0x1d, 0x95, 0x76, 0x7d, 0x3b, 0xcd, 0xfd, 0xef, 0xc2, 0x0b, 0x07, 0x63,
2004        0x29, 0xfb, 0x81, 0x57, 0xbe, 0xbe, 0x06, 0xf7, 0x3a, 0x92, 0xc4, 0x43, 0xff, 0xad, 0xac,
2005        0x7e, 0x0f, 0x00, 0x2a, 0x4f, 0xf0, 0xf8, 0xa9, 0xfa, 0x3c, 0x90, 0x6d, 0x73, 0x2d, 0x5a,
2006        0xe6, 0xc6, 0x46, 0xf2, 0x0d, 0x55, 0x4c, 0x25, 0x38, 0x71, 0x2b, 0x35, 0x38, 0x82, 0x16,
2007        0x37, 0x5f, 0x32, 0x61, 0x02, 0xdd, 0x2f, 0x6f, 0x7b, 0x1f, 0xb4, 0x1a, 0x1b, 0x3e, 0x4f,
2008        0x11, 0xa3, 0x17, 0x40, 0x5a, 0x5f, 0x76, 0xcd, 0x19, 0x27, 0x9b, 0xc7, 0xc8, 0xf7, 0xf7,
2009        0xee, 0xf4, 0x86, 0xd9, 0xfd, 0xa7, 0xfe, 0x9e, 0xac, 0x70, 0x53, 0x5b, 0x76, 0xfb, 0x39,
2010        0xf8, 0x4b, 0x98, 0xfe, 0xd0, 0x06, 0x21, 0x4c, 0x4d, 0xbe, 0x10, 0x2b, 0x06, 0x65, 0xc9,
2011        0x5e, 0xf9, 0x95, 0x72, 0xae, 0x99, 0xd9, 0x7e, 0x15, 0xbd, 0x5e, 0x6d, 0xe8, 0x25, 0x8a,
2012        0xd5, 0x99, 0xc6, 0x6b, 0x69, 0xc7, 0x84, 0xc6, 0xa4, 0xf7, 0xb9, 0x6d, 0x68, 0x45, 0x0e,
2013        0x65, 0x69, 0xeb, 0xe6, 0xeb, 0xe9, 0x28, 0xa6, 0xb9, 0x96, 0xf2, 0xe8, 0xa7, 0x9b, 0x6e,
2014        0x79, 0x8a, 0x68, 0x65, 0x59, 0x98, 0x8b, 0x44, 0x41, 0x98, 0x9a, 0x56, 0x01, 0x01, 0x01,
2015        0x02, 0x01, 0x01, 0x01, 0x01, 0x02, 0x01, 0x02, 0x02, 0x01, 0x01, 0x01, 0x02, 0x1f, 0x25,
2016    ];
2017
2018    fn build_single_q6_k_tensor_gguf() -> Vec<u8> {
2019        let mut buf = Vec::new();
2020        buf.write_u32::<LittleEndian>(ferrox_gguf::GGUF_MAGIC)
2021            .unwrap();
2022        buf.write_u32::<LittleEndian>(3).unwrap(); // version
2023        buf.write_u64::<LittleEndian>(1).unwrap(); // tensor_count
2024        buf.write_u64::<LittleEndian>(1).unwrap(); // kv_count
2025
2026        write_kv_str(&mut buf, "general.architecture", "ferrox-q6k-test");
2027
2028        write_string(&mut buf, "test.weight");
2029        buf.write_u32::<LittleEndian>(2).unwrap(); // n_dims
2030                                                   // Real GGUF ne[] order is fastest-varying-first, i.e. [cols, rows].
2031        buf.write_u64::<LittleEndian>(256).unwrap(); // cols (1 Q6_K block)
2032        buf.write_u64::<LittleEndian>(1).unwrap(); // rows
2033        buf.write_u32::<LittleEndian>(14).unwrap(); // dtype tag: Q6_K
2034        buf.write_u64::<LittleEndian>(0).unwrap(); // offset
2035
2036        while buf.len() % 32 != 0 {
2037            buf.push(0);
2038        }
2039        buf.extend_from_slice(&Q6_K_TEST_BLOCK);
2040        buf
2041    }
2042
2043    #[test]
2044    fn load_weight_matrix_handles_a_real_on_disk_q6_k_tensor_end_to_end() {
2045        let tmp = std::env::temp_dir().join("ferrox_test_q6k_tensor.gguf");
2046        std::fs::write(&tmp, build_single_q6_k_tensor_gguf()).unwrap();
2047        let file = ferrox_gguf::GgufFile::open(&tmp).expect("real Q6_K GGUF file must parse");
2048        std::fs::remove_file(&tmp).ok();
2049
2050        let matrix = load_weight_matrix(&file, "test.weight").expect("Q6_K tensor must load");
2051        assert_eq!(matrix.rows(), 1);
2052        assert_eq!(matrix.cols(), 256);
2053        match &matrix {
2054            WeightMatrix::Quantized { kind, data, .. } => {
2055                assert_eq!(*kind, QuantKind::Q6K);
2056                assert!(
2057                    data.is_mapped(),
2058                    "Q6_K tensors should take the zero-copy mmap path, same as Q8_0/Q4_0"
2059                );
2060            }
2061            _ => panic!("expected a Quantized matrix for a Q6_K tensor"),
2062        }
2063
2064        let expected = ferrox_quant::dequant_q6_k(&Q6_K_TEST_BLOCK).unwrap();
2065        let x: Vec<f32> = (0..256).map(|i| ((i as f32) * 0.013).sin()).collect();
2066        let expected_dot: f32 = expected.iter().zip(x.iter()).map(|(a, b)| a * b).sum();
2067
2068        let got = matrix.apply(&x);
2069        assert_eq!(got.len(), 1);
2070        assert!(
2071            (got[0] - expected_dot).abs() < 1e-2,
2072            "end-to-end loaded+applied Q6_K matrix diverged from direct dequant: got={} expected={}",
2073            got[0],
2074            expected_dot
2075        );
2076    }
2077
2078    fn build_single_bf16_tensor_gguf(rows: u64, cols: u64, values: &[f32]) -> Vec<u8> {
2079        let mut buf = Vec::new();
2080        buf.write_u32::<LittleEndian>(ferrox_gguf::GGUF_MAGIC)
2081            .unwrap();
2082        buf.write_u32::<LittleEndian>(3).unwrap(); // version
2083        buf.write_u64::<LittleEndian>(1).unwrap(); // tensor_count
2084        buf.write_u64::<LittleEndian>(1).unwrap(); // kv_count
2085
2086        write_kv_str(&mut buf, "general.architecture", "ferrox-bf16-test");
2087
2088        write_string(&mut buf, "test.weight");
2089        buf.write_u32::<LittleEndian>(2).unwrap(); // n_dims
2090                                                   // Real GGUF ne[] order is fastest-varying-first, i.e. [cols, rows].
2091        buf.write_u64::<LittleEndian>(cols).unwrap();
2092        buf.write_u64::<LittleEndian>(rows).unwrap();
2093        buf.write_u32::<LittleEndian>(30).unwrap(); // dtype tag: BF16
2094        buf.write_u64::<LittleEndian>(0).unwrap(); // offset
2095
2096        while buf.len() % 32 != 0 {
2097            buf.push(0);
2098        }
2099        for &v in values {
2100            // Real bf16 truncation (round-toward-zero, matching a real
2101            // writer closely enough for round-trip test purposes): top
2102            // 16 bits of the f32 bit pattern.
2103            let bf16_bits = (v.to_bits() >> 16) as u16;
2104            buf.extend_from_slice(&bf16_bits.to_le_bytes());
2105        }
2106        buf
2107    }
2108
2109    #[test]
2110    fn load_weight_matrix_handles_a_real_on_disk_bf16_tensor_end_to_end() {
2111        // Values with zero low-mantissa bits, so f32->bf16 truncation
2112        // is lossless and this is an exact-equality check.
2113        let values: Vec<f32> = vec![1.0, -2.5, 0.0, 4.0, -8.0, 16.0];
2114        let tmp = std::env::temp_dir().join("ferrox_test_bf16_tensor.gguf");
2115        std::fs::write(&tmp, build_single_bf16_tensor_gguf(2, 3, &values)).unwrap();
2116        let file = ferrox_gguf::GgufFile::open(&tmp).expect("real BF16 GGUF file must parse");
2117        std::fs::remove_file(&tmp).ok();
2118
2119        let matrix = load_weight_matrix(&file, "test.weight").expect("BF16 tensor must load");
2120        assert_eq!(matrix.rows(), 2);
2121        assert_eq!(matrix.cols(), 3);
2122        match &matrix {
2123            WeightMatrix::F32(tensor) => {
2124                assert_eq!(tensor.data, values, "BF16 must widen to f32 exactly");
2125            }
2126            _ => panic!("expected an F32 matrix for a BF16 tensor (no fused dot kernel for it)"),
2127        }
2128    }
2129
2130    fn build_single_f16_tensor_gguf(rows: u64, cols: u64, values: &[f32]) -> Vec<u8> {
2131        let mut buf = Vec::new();
2132        buf.write_u32::<LittleEndian>(ferrox_gguf::GGUF_MAGIC)
2133            .unwrap();
2134        buf.write_u32::<LittleEndian>(3).unwrap(); // version
2135        buf.write_u64::<LittleEndian>(1).unwrap(); // tensor_count
2136        buf.write_u64::<LittleEndian>(1).unwrap(); // kv_count
2137
2138        write_kv_str(&mut buf, "general.architecture", "ferrox-f16-test");
2139
2140        write_string(&mut buf, "test.weight");
2141        buf.write_u32::<LittleEndian>(2).unwrap(); // n_dims
2142        buf.write_u64::<LittleEndian>(cols).unwrap();
2143        buf.write_u64::<LittleEndian>(rows).unwrap();
2144        buf.write_u32::<LittleEndian>(1).unwrap(); // dtype tag: F16
2145        buf.write_u64::<LittleEndian>(0).unwrap(); // offset
2146
2147        while buf.len() % 32 != 0 {
2148            buf.push(0);
2149        }
2150        for &v in values {
2151            buf.extend_from_slice(&half::f16::from_f32(v).to_le_bytes());
2152        }
2153        buf
2154    }
2155
2156    /// `GgmlType::F16` was parsed and sized but had no dequant arm in any
2157    /// of the seven loaders, so every `*-f16.gguf` was a hard
2158    /// `UnsupportedDtype`. Values are exactly representable in f16, so
2159    /// this is an exact-equality check.
2160    #[test]
2161    fn load_weight_matrix_handles_a_real_on_disk_f16_tensor_end_to_end() {
2162        let values: Vec<f32> = vec![1.0, -2.5, 0.0, 4.0, -8.0, 16.0];
2163        let tmp = std::env::temp_dir().join("ferrox_test_f16_tensor.gguf");
2164        std::fs::write(&tmp, build_single_f16_tensor_gguf(2, 3, &values)).unwrap();
2165        let file = ferrox_gguf::GgufFile::open(&tmp).expect("real F16 GGUF file must parse");
2166        std::fs::remove_file(&tmp).ok();
2167
2168        let matrix = load_weight_matrix(&file, "test.weight").expect("F16 tensor must load");
2169        assert_eq!(matrix.rows(), 2);
2170        assert_eq!(matrix.cols(), 3);
2171        match &matrix {
2172            WeightMatrix::F32(tensor) => {
2173                assert_eq!(tensor.data, values, "F16 must widen to f32 exactly");
2174            }
2175            _ => panic!("expected an F32 matrix for an F16 tensor (no fused dot kernel for it)"),
2176        }
2177
2178        // The same tensor read as a plain vector (norm weights, biases and
2179        // the router all take this path, not `load_weight_matrix`).
2180        let tmp = std::env::temp_dir().join("ferrox_test_f16_vec.gguf");
2181        std::fs::write(&tmp, build_single_f16_tensor_gguf(2, 3, &values)).unwrap();
2182        let file = ferrox_gguf::GgufFile::open(&tmp).expect("real F16 GGUF file must parse");
2183        std::fs::remove_file(&tmp).ok();
2184        assert_eq!(load_f32_vec(&file, "test.weight").unwrap(), values);
2185    }
2186
2187    fn build_single_q5_1_tensor_gguf() -> Vec<u8> {
2188        let mut buf = Vec::new();
2189        buf.write_u32::<LittleEndian>(ferrox_gguf::GGUF_MAGIC)
2190            .unwrap();
2191        buf.write_u32::<LittleEndian>(3).unwrap(); // version
2192        buf.write_u64::<LittleEndian>(1).unwrap(); // tensor_count
2193        buf.write_u64::<LittleEndian>(1).unwrap(); // kv_count
2194
2195        write_kv_str(&mut buf, "general.architecture", "ferrox-q5-1-test");
2196
2197        write_string(&mut buf, "test.weight");
2198        buf.write_u32::<LittleEndian>(2).unwrap(); // n_dims
2199                                                   // Real GGUF ne[] order is fastest-varying-first, i.e. [cols, rows].
2200        buf.write_u64::<LittleEndian>(32).unwrap(); // cols (1 Q5_1 block)
2201        buf.write_u64::<LittleEndian>(1).unwrap(); // rows
2202        buf.write_u32::<LittleEndian>(7).unwrap(); // dtype tag: Q5_1
2203        buf.write_u64::<LittleEndian>(0).unwrap(); // offset
2204
2205        while buf.len() % 32 != 0 {
2206            buf.push(0);
2207        }
2208        // d=0.25 (f16 0x3400), m=1.5 (f16 0x3E00) -- both exact in f16,
2209        // hand-verified bit patterns to avoid pulling in the `half`
2210        // crate just for two test constants. qh varied, qs a real
2211        // (non-degenerate) pattern.
2212        buf.extend_from_slice(&0x3400u16.to_le_bytes());
2213        buf.extend_from_slice(&0x3E00u16.to_le_bytes());
2214        buf.extend_from_slice(&[0x9au8, 0x3c, 0xf0, 0x0f]);
2215        buf.extend_from_slice(&(0..16u8).map(|i| i | ((15 - i) << 4)).collect::<Vec<u8>>());
2216        buf
2217    }
2218
2219    #[test]
2220    fn load_weight_matrix_handles_a_real_on_disk_q5_1_tensor_end_to_end() {
2221        let tmp = std::env::temp_dir().join("ferrox_test_q5_1_tensor.gguf");
2222        std::fs::write(&tmp, build_single_q5_1_tensor_gguf()).unwrap();
2223        let file = ferrox_gguf::GgufFile::open(&tmp).expect("real Q5_1 GGUF file must parse");
2224        std::fs::remove_file(&tmp).ok();
2225
2226        let matrix = load_weight_matrix(&file, "test.weight").expect("Q5_1 tensor must load");
2227        assert_eq!(matrix.rows(), 1);
2228        assert_eq!(matrix.cols(), 32);
2229        let raw = file.tensor_bytes("test.weight").unwrap();
2230        let expected = ferrox_quant::dequant_q5_1(raw).unwrap();
2231        match &matrix {
2232            WeightMatrix::Quantized { kind, data, .. } => {
2233                assert_eq!(*kind, QuantKind::Q5_1);
2234                assert!(data.is_mapped());
2235            }
2236            _ => panic!("expected a Quantized matrix for a Q5_1 tensor"),
2237        }
2238
2239        let x: Vec<f32> = (0..32).map(|i| ((i as f32) * 0.017).cos()).collect();
2240        let expected_dot: f32 = expected.iter().zip(x.iter()).map(|(a, b)| a * b).sum();
2241        let got = matrix.apply(&x);
2242        assert_eq!(got.len(), 1);
2243        assert!(
2244            (got[0] - expected_dot).abs() < 1e-2,
2245            "end-to-end loaded+applied Q5_1 matrix diverged from direct dequant: got={} expected={}",
2246            got[0],
2247            expected_dot
2248        );
2249    }
2250
2251    // Same bytes as ferrox-quant's own Q3_K_TEST_BLOCK (Python-cross-
2252    // validated there); duplicated here to build a real on-disk GGUF
2253    // file, matching this file's existing per-format test convention
2254    // (see Q6_K_TEST_BLOCK above).
2255    const Q3_K_TEST_BLOCK: [u8; 110] = [
2256        0x56, 0xf2, 0xb4, 0x2b, 0xd5, 0x6f, 0x51, 0x71, 0x3c, 0x0a, 0xb9, 0x1d, 0xd0, 0xb9, 0x3b,
2257        0xb3, 0x0f, 0xff, 0x8c, 0xb2, 0x83, 0x3a, 0x3d, 0x24, 0xb1, 0x12, 0x56, 0xe3, 0x23, 0x54,
2258        0xf2, 0xfa, 0x7f, 0xdf, 0x31, 0xe1, 0x18, 0x26, 0x6e, 0xcd, 0x5b, 0x38, 0xee, 0xbd, 0x9f,
2259        0x8c, 0x57, 0x47, 0x0b, 0x11, 0xcb, 0xfb, 0xb4, 0x83, 0xa0, 0x4e, 0x0b, 0xd4, 0xa7, 0x85,
2260        0xe0, 0x60, 0xf3, 0xb3, 0xe3, 0x95, 0x43, 0xc6, 0x05, 0x05, 0x77, 0x53, 0xed, 0x23, 0xcc,
2261        0x6a, 0x0e, 0x89, 0xa1, 0x79, 0x85, 0xf6, 0x6e, 0x5a, 0x23, 0x63, 0xbe, 0x53, 0xfa, 0xa2,
2262        0x2b, 0xe9, 0xcd, 0xce, 0xf8, 0x3d, 0x6f, 0xd0, 0x42, 0x6e, 0x3b, 0x7f, 0x23, 0x26, 0xd3,
2263        0xb9, 0x18, 0xbf, 0xa4, 0x34,
2264    ];
2265
2266    fn build_single_q3_k_tensor_gguf() -> Vec<u8> {
2267        let mut buf = Vec::new();
2268        buf.write_u32::<LittleEndian>(ferrox_gguf::GGUF_MAGIC)
2269            .unwrap();
2270        buf.write_u32::<LittleEndian>(3).unwrap(); // version
2271        buf.write_u64::<LittleEndian>(1).unwrap(); // tensor_count
2272        buf.write_u64::<LittleEndian>(1).unwrap(); // kv_count
2273
2274        write_kv_str(&mut buf, "general.architecture", "ferrox-q3k-test");
2275
2276        write_string(&mut buf, "test.weight");
2277        buf.write_u32::<LittleEndian>(2).unwrap(); // n_dims
2278                                                   // Real GGUF ne[] order is fastest-varying-first, i.e. [cols, rows].
2279        buf.write_u64::<LittleEndian>(256).unwrap(); // cols (1 Q3_K block)
2280        buf.write_u64::<LittleEndian>(1).unwrap(); // rows
2281        buf.write_u32::<LittleEndian>(11).unwrap(); // dtype tag: Q3_K
2282        buf.write_u64::<LittleEndian>(0).unwrap(); // offset
2283
2284        while buf.len() % 32 != 0 {
2285            buf.push(0);
2286        }
2287        buf.extend_from_slice(&Q3_K_TEST_BLOCK);
2288        buf
2289    }
2290
2291    #[test]
2292    fn load_weight_matrix_handles_a_real_on_disk_q3_k_tensor_end_to_end() {
2293        let tmp = std::env::temp_dir().join("ferrox_test_q3k_tensor.gguf");
2294        std::fs::write(&tmp, build_single_q3_k_tensor_gguf()).unwrap();
2295        let file = ferrox_gguf::GgufFile::open(&tmp).expect("real Q3_K GGUF file must parse");
2296        std::fs::remove_file(&tmp).ok();
2297
2298        let matrix = load_weight_matrix(&file, "test.weight").expect("Q3_K tensor must load");
2299        assert_eq!(matrix.rows(), 1);
2300        assert_eq!(matrix.cols(), 256);
2301        match &matrix {
2302            WeightMatrix::Quantized { kind, data, .. } => {
2303                assert_eq!(*kind, QuantKind::Q3K);
2304                assert!(data.is_mapped());
2305            }
2306            _ => panic!("expected a Quantized matrix for a Q3_K tensor"),
2307        }
2308
2309        let expected = ferrox_quant::dequant_q3_k(&Q3_K_TEST_BLOCK).unwrap();
2310        let x: Vec<f32> = (0..256).map(|i| ((i as f32) * 0.013).sin()).collect();
2311        let expected_dot: f32 = expected.iter().zip(x.iter()).map(|(a, b)| a * b).sum();
2312
2313        let got = matrix.apply(&x);
2314        assert_eq!(got.len(), 1);
2315        assert!(
2316            (got[0] - expected_dot).abs() < 1e-1,
2317            "end-to-end loaded+applied Q3_K matrix diverged from direct dequant: got={} expected={}",
2318            got[0],
2319            expected_dot
2320        );
2321    }
2322
2323    // Same bytes as ferrox-quant's own IQ4_XS_TEST_BLOCK (Python-cross-
2324    // validated there); duplicated here to build a real on-disk GGUF
2325    // file, matching this file's existing per-format test convention.
2326    const IQ4_XS_TEST_BLOCK: [u8; 136] = [
2327        0x5c, 0x33, 0xb4, 0x39, 0xd1, 0x64, 0x97, 0x82, 0xcb, 0xbd, 0x88, 0x95, 0xf3, 0x60, 0x2a,
2328        0xb5, 0xe7, 0x24, 0xd3, 0xee, 0xfe, 0x71, 0x13, 0xbe, 0x70, 0x84, 0x48, 0x79, 0x7b, 0x3e,
2329        0xf0, 0x55, 0xdc, 0xb2, 0xb2, 0xde, 0x32, 0xa1, 0x5b, 0x02, 0x01, 0xdc, 0x2a, 0xbb, 0xf7,
2330        0x0b, 0x8a, 0x88, 0xdd, 0x0b, 0x02, 0x7e, 0x5e, 0x76, 0x87, 0x30, 0x1e, 0x1c, 0xcf, 0x48,
2331        0xd7, 0x61, 0xf3, 0x51, 0x52, 0x17, 0x98, 0x0a, 0x87, 0xcf, 0x02, 0x91, 0xc8, 0xee, 0xc0,
2332        0x91, 0x69, 0x2a, 0x4f, 0x64, 0x68, 0xa7, 0xb2, 0xe6, 0x98, 0x21, 0x81, 0x75, 0x53, 0x2a,
2333        0x8d, 0x12, 0xae, 0xe0, 0xea, 0x0c, 0x75, 0xff, 0x22, 0x5e, 0x25, 0x19, 0xda, 0x2e, 0x51,
2334        0x4e, 0x81, 0xdc, 0x0e, 0x78, 0x86, 0xd7, 0x58, 0xb5, 0xb7, 0xf6, 0x45, 0xa9, 0x0a, 0x83,
2335        0xfd, 0x2a, 0x12, 0x7d, 0xf0, 0x12, 0x97, 0xe2, 0xfe, 0xf4, 0xd0, 0xa2, 0x11, 0x14, 0x78,
2336        0xdb,
2337    ];
2338
2339    fn build_single_iq4_xs_tensor_gguf() -> Vec<u8> {
2340        let mut buf = Vec::new();
2341        buf.write_u32::<LittleEndian>(ferrox_gguf::GGUF_MAGIC)
2342            .unwrap();
2343        buf.write_u32::<LittleEndian>(3).unwrap(); // version
2344        buf.write_u64::<LittleEndian>(1).unwrap(); // tensor_count
2345        buf.write_u64::<LittleEndian>(1).unwrap(); // kv_count
2346
2347        write_kv_str(&mut buf, "general.architecture", "ferrox-iq4xs-test");
2348
2349        write_string(&mut buf, "test.weight");
2350        buf.write_u32::<LittleEndian>(2).unwrap(); // n_dims
2351                                                   // Real GGUF ne[] order is fastest-varying-first, i.e. [cols, rows].
2352        buf.write_u64::<LittleEndian>(256).unwrap(); // cols (1 IQ4_XS block)
2353        buf.write_u64::<LittleEndian>(1).unwrap(); // rows
2354        buf.write_u32::<LittleEndian>(23).unwrap(); // dtype tag: IQ4_XS
2355        buf.write_u64::<LittleEndian>(0).unwrap(); // offset
2356
2357        while buf.len() % 32 != 0 {
2358            buf.push(0);
2359        }
2360        buf.extend_from_slice(&IQ4_XS_TEST_BLOCK);
2361        buf
2362    }
2363
2364    #[test]
2365    fn load_weight_matrix_handles_a_real_on_disk_iq4_xs_tensor_end_to_end() {
2366        let tmp = std::env::temp_dir().join("ferrox_test_iq4xs_tensor.gguf");
2367        std::fs::write(&tmp, build_single_iq4_xs_tensor_gguf()).unwrap();
2368        let file = ferrox_gguf::GgufFile::open(&tmp).expect("real IQ4_XS GGUF file must parse");
2369        std::fs::remove_file(&tmp).ok();
2370
2371        let matrix = load_weight_matrix(&file, "test.weight").expect("IQ4_XS tensor must load");
2372        assert_eq!(matrix.rows(), 1);
2373        assert_eq!(matrix.cols(), 256);
2374        match &matrix {
2375            WeightMatrix::Quantized { kind, data, .. } => {
2376                assert_eq!(*kind, QuantKind::IQ4XS);
2377                assert!(data.is_mapped());
2378            }
2379            _ => panic!("expected a Quantized matrix for an IQ4_XS tensor"),
2380        }
2381
2382        let expected = ferrox_quant::dequant_iq4_xs(&IQ4_XS_TEST_BLOCK).unwrap();
2383        let x: Vec<f32> = (0..256).map(|i| ((i as f32) * 0.013).sin()).collect();
2384        let expected_dot: f32 = expected.iter().zip(x.iter()).map(|(a, b)| a * b).sum();
2385
2386        let got = matrix.apply(&x);
2387        assert_eq!(got.len(), 1);
2388        assert!(
2389            (got[0] - expected_dot).abs() < 1e-1,
2390            "end-to-end loaded+applied IQ4_XS matrix diverged from direct dequant: got={} expected={}",
2391            got[0],
2392            expected_dot
2393        );
2394    }
2395
2396    // Same bytes as ferrox-quant's own IQ low-bit test blocks
2397    // (Python-cross-validated there against the real compiled ggml
2398    // implementation), duplicated as literals for the same reason as
2399    // IQ4_XS_TEST_BLOCK above.
2400    const IQ1_S_TEST_BLOCK: [u8; 50] = [
2401        0x0a, 0x2f, 0xfa, 0x06, 0x1e, 0x37, 0x6f, 0xe3, 0x62, 0xd0, 0xb6, 0xa4, 0x25, 0xae, 0x76,
2402        0x14, 0x72, 0x5b, 0xfa, 0x05, 0xd1, 0xf1, 0x2a, 0x4c, 0xad, 0x29, 0xae, 0xf4, 0xcf, 0x0c,
2403        0x96, 0x51, 0x58, 0x03, 0x6d, 0xd3, 0x10, 0x92, 0x70, 0xff, 0x61, 0x58, 0xc8, 0x30, 0x25,
2404        0x64, 0x49, 0x85, 0xc0, 0x24,
2405    ];
2406    const IQ2_XXS_TEST_BLOCK: [u8; 66] = [
2407        0x29, 0x30, 0xd9, 0x33, 0x95, 0x4c, 0x08, 0x1e, 0xad, 0x79, 0x49, 0xf2, 0x8d, 0x5f, 0x93,
2408        0xea, 0x78, 0x18, 0x98, 0xb9, 0x94, 0x14, 0xad, 0xce, 0xca, 0x1d, 0xab, 0x81, 0x53, 0x4a,
2409        0x68, 0xd0, 0x59, 0x96, 0x36, 0x5d, 0xbe, 0x20, 0xc4, 0xff, 0xe4, 0x2c, 0xcd, 0x2f, 0x4f,
2410        0x4f, 0x67, 0x53, 0xc6, 0xd5, 0xa2, 0xfb, 0xc7, 0xf3, 0xe2, 0x6b, 0xf1, 0x99, 0x23, 0x1e,
2411        0x2d, 0x5e, 0x8c, 0x78, 0xc2, 0x31,
2412    ];
2413    const IQ3_XXS_TEST_BLOCK: [u8; 98] = [
2414        0x71, 0x31, 0x16, 0x0a, 0x79, 0x04, 0x5d, 0x87, 0xae, 0x2a, 0x4a, 0x43, 0xfd, 0x02, 0xba,
2415        0x6c, 0x10, 0x42, 0x80, 0xe5, 0x1d, 0x08, 0x22, 0xcb, 0x21, 0x54, 0xf9, 0xaa, 0x8e, 0xc2,
2416        0xf2, 0x34, 0x66, 0x1e, 0x2a, 0xef, 0x19, 0xae, 0x48, 0x47, 0x29, 0xa0, 0x72, 0xd1, 0x31,
2417        0xc0, 0x65, 0x49, 0xde, 0x79, 0x32, 0xe6, 0x4d, 0xb6, 0x55, 0x3f, 0x4d, 0xf1, 0x18, 0xbb,
2418        0x18, 0x59, 0x4c, 0x31, 0xa3, 0xb2, 0x34, 0xdd, 0xf6, 0x4a, 0x91, 0x51, 0x3f, 0x3e, 0x40,
2419        0x69, 0xad, 0xbf, 0x1a, 0xd0, 0x05, 0xfb, 0xbe, 0x8b, 0x0b, 0xdd, 0xdf, 0x7d, 0x94, 0x74,
2420        0x92, 0x3e, 0xff, 0x04, 0x2a, 0xc4, 0xea, 0xc9,
2421    ];
2422
2423    #[rustfmt::skip]
2424    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];
2425
2426    fn build_single_iq_lowbit_tensor_gguf(
2427        arch: &str,
2428        tag: u32,
2429        cols: u64,
2430        block: &[u8],
2431    ) -> Vec<u8> {
2432        let mut buf = Vec::new();
2433        buf.write_u32::<LittleEndian>(ferrox_gguf::GGUF_MAGIC)
2434            .unwrap();
2435        buf.write_u32::<LittleEndian>(3).unwrap(); // version
2436        buf.write_u64::<LittleEndian>(1).unwrap(); // tensor_count
2437        buf.write_u64::<LittleEndian>(1).unwrap(); // kv_count
2438        write_kv_str(&mut buf, "general.architecture", arch);
2439        write_string(&mut buf, "test.weight");
2440        buf.write_u32::<LittleEndian>(2).unwrap(); // n_dims
2441        buf.write_u64::<LittleEndian>(cols).unwrap();
2442        buf.write_u64::<LittleEndian>(1).unwrap(); // rows
2443        buf.write_u32::<LittleEndian>(tag).unwrap();
2444        buf.write_u64::<LittleEndian>(0).unwrap(); // offset
2445        while buf.len() % 32 != 0 {
2446            buf.push(0);
2447        }
2448        buf.extend_from_slice(block);
2449        buf
2450    }
2451
2452    /// A structurally valid block of `len` bytes for any of the
2453    /// codebook-grid formats: every bit pattern is a legal code in all
2454    /// of them (the grid indices are bounded by their own bit widths),
2455    /// so a deterministic byte fill is a real block, not a fixture that
2456    /// happens to avoid the interesting paths. Only the f16 scale needs
2457    /// pinning, and only so the comparison below can't be NaN-vs-NaN.
2458    fn pseudo_iq_block(len: usize, seed: u32) -> Vec<u8> {
2459        let mut s = seed;
2460        let mut out = Vec::with_capacity(len);
2461        for _ in 0..len {
2462            s ^= s << 13;
2463            s ^= s >> 17;
2464            s ^= s << 5;
2465            out.push((s >> 24) as u8);
2466        }
2467        out
2468    }
2469
2470    /// End-to-end load+apply for the codebook-grid low-bit formats the
2471    /// published Dynamic GGUFs are built from: a real on-disk tensor of
2472    /// each type must load zero-copy as the right `QuantKind` and
2473    /// produce the same matvec result as dequantizing the block
2474    /// directly. That is the property this test exists for -- the
2475    /// *values* are pinned against real ggml in `ferrox-quant`; what
2476    /// can only break here is the tag -> kind -> block-stride chain,
2477    /// and a wrong stride silently reads the neighbouring row.
2478    /// Dtype tags (19/29/16/17/22/18/21/39) verified against ggml.h's
2479    /// enum ggml_type.
2480    #[test]
2481    fn load_weight_matrix_handles_real_on_disk_iq_lowbit_tensors_end_to_end() {
2482        type DequantFn = fn(&[u8]) -> Result<Vec<f32>, ferrox_quant::QuantError>;
2483        // IQ1_M carries no f16 scale field; its scale is reassembled
2484        // from the four scale words' top nibbles, and the top nibble of
2485        // the last one supplies the f16 sign + high exponent bits.
2486        // Pinning it to 0x2 keeps the exponent out of the all-ones
2487        // NaN/Inf pattern whatever the rest of the fill does. The other
2488        // three do carry a leading f16 `d`, pinned for the same reason.
2489        let mut iq1m = pseudo_iq_block(ferrox_quant::IQ1_M_BLOCK_BYTES, 0x2907_31A0);
2490        iq1m[55] = (iq1m[55] & 0x0F) | 0x20;
2491        let mut iq2xs = pseudo_iq_block(ferrox_quant::IQ2_XS_BLOCK_BYTES, 0x2107_31A1);
2492        let mut iq2s = pseudo_iq_block(ferrox_quant::IQ2_S_BLOCK_BYTES, 0x2207_31A2);
2493        let mut iq3s = pseudo_iq_block(ferrox_quant::IQ3_S_BLOCK_BYTES, 0x2307_31A3);
2494        for blk in [&mut iq2xs, &mut iq2s, &mut iq3s] {
2495            blk[0..2].copy_from_slice(&half::f16::from_f32(0.115).to_le_bytes());
2496        }
2497        let cases: [(&str, u32, &[u8], QuantKind, DequantFn); 8] = [
2498            (
2499                "iq1s",
2500                19,
2501                &IQ1_S_TEST_BLOCK,
2502                QuantKind::IQ1S,
2503                ferrox_quant::dequant_iq1_s,
2504            ),
2505            (
2506                "iq1m",
2507                29,
2508                &iq1m,
2509                QuantKind::IQ1M,
2510                ferrox_quant::dequant_iq1_m,
2511            ),
2512            (
2513                "iq2xxs",
2514                16,
2515                &IQ2_XXS_TEST_BLOCK,
2516                QuantKind::IQ2XXS,
2517                ferrox_quant::dequant_iq2_xxs,
2518            ),
2519            (
2520                "iq2xs",
2521                17,
2522                &iq2xs,
2523                QuantKind::IQ2XS,
2524                ferrox_quant::dequant_iq2_xs,
2525            ),
2526            (
2527                "iq2s",
2528                22,
2529                &iq2s,
2530                QuantKind::IQ2S,
2531                ferrox_quant::dequant_iq2_s,
2532            ),
2533            (
2534                "iq3xxs",
2535                18,
2536                &IQ3_XXS_TEST_BLOCK,
2537                QuantKind::IQ3XXS,
2538                ferrox_quant::dequant_iq3_xxs,
2539            ),
2540            (
2541                "iq3s",
2542                21,
2543                &iq3s,
2544                QuantKind::IQ3S,
2545                ferrox_quant::dequant_iq3_s,
2546            ),
2547            (
2548                "mxfp4_gguf",
2549                39,
2550                &MXFP4_GGUF_TEST_BLOCKS,
2551                QuantKind::Mxfp4Gguf,
2552                ferrox_quant::dequant_mxfp4_gguf,
2553            ),
2554        ];
2555        for (name, tag, block, kind, dequant) in cases {
2556            let expected = dequant(block).unwrap();
2557            let cols = expected.len();
2558            let tmp = std::env::temp_dir().join(format!("ferrox_test_{name}_tensor.gguf"));
2559            std::fs::write(
2560                &tmp,
2561                build_single_iq_lowbit_tensor_gguf(name, tag, cols as u64, block),
2562            )
2563            .unwrap();
2564            let file = ferrox_gguf::GgufFile::open(&tmp).expect("file must parse");
2565            std::fs::remove_file(&tmp).ok();
2566
2567            let matrix =
2568                load_weight_matrix(&file, "test.weight").expect("low-bit tensor must load");
2569            assert_eq!((matrix.rows(), matrix.cols()), (1, cols), "{name}");
2570            match &matrix {
2571                WeightMatrix::Quantized { kind: k, data, .. } => {
2572                    assert_eq!(*k, kind, "{name}");
2573                    assert!(data.is_mapped(), "{name} must load zero-copy");
2574                }
2575                _ => panic!("expected a Quantized matrix for {name}"),
2576            }
2577
2578            let x: Vec<f32> = (0..cols).map(|i| ((i as f32) * 0.013).sin()).collect();
2579            let expected_dot: f32 = expected.iter().zip(x.iter()).map(|(a, b)| a * b).sum();
2580            let got = matrix.apply(&x);
2581            assert!(
2582                (got[0] - expected_dot).abs() < 1e-1,
2583                "{name}: loaded+applied diverged from direct dequant: got={} expected={}",
2584                got[0],
2585                expected_dot
2586            );
2587        }
2588    }
2589
2590    #[test]
2591    fn qwen2moe_disables_topk_renorm() {
2592        assert!(
2593            NO_TOPK_RENORMALIZE_ARCHITECTURES.contains(&"qwen2moe"),
2594            "qwen2moe must have norm_topk_prob=false (llama.cpp build_moe_ffn norm_w=false)"
2595        );
2596    }
2597}