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