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