Skip to main content

ferrox_models/
loader.rs

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