Skip to main content

ferrox_models/
loader.rs

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