Skip to main content

frink_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, frink 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 frink-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/frink-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 frink_core::expert_store::{ExpertKey, ExpertSource, ExpertStore};
26use frink_core::tensor::Tensor;
27use frink_core::weight_matrix::quant_kind_for;
28use frink_core::weight_matrix::{QuantKind, WeightBytes, WeightMatrix};
29use frink_gguf::{GgmlType, GgufError, GgufValue, ShardedGguf, TensorInfo, TensorSource};
30use frink_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};
38use crate::norm::NormOp;
39
40#[derive(Debug, Error)]
41pub enum LoadError {
42    #[error(transparent)]
43    Gguf(#[from] GgufError),
44    #[error(transparent)]
45    Shard(#[from] frink_gguf::ShardError),
46    #[error("tensor '{0}' has unsupported dtype {1:?}")]
47    UnsupportedDtype(String, GgmlType),
48    #[error(
49        "MoE tensor '{0}' is not 3D or its expert count {1} does not match config n_experts {2}"
50    )]
51    ExpertCountMismatch(String, usize, usize),
52    #[error("GGUF file is missing required hparam metadata key '{0}'")]
53    MissingHparam(String),
54    /// `general.architecture` is not in the capability registry -- refuse
55    /// to guess RoPE/gating rather than emit fluent-but-wrong logits.
56    #[error(
57        "unsupported GGUF architecture '{0}': not in frink's capability registry \
58         (unknown required features fail closed; see frink_models::capability)"
59    )]
60    UnsupportedArchitecture(String),
61    /// Architecture exists but must not use the generic GQA decoder.
62    #[error("architecture '{0}' cannot use the generic Decoder: {1}")]
63    DedicatedArchitectureRequired(String, &'static str),
64    /// Metadata advertises a feature the generic decoder does not implement.
65    #[error("architecture '{0}' requires unimplemented feature: {1}")]
66    UnsupportedFeature(String, String),
67    #[error(
68        "architecture '{0}' has never been verified against llama.cpp. It would run on \
69         frink's shared generic-GQA path, which ASSUMES plain GQA with {1:?} RoPE and no \
70         ALiBi, no learned position embeddings and no per-layer rope skipping. That \
71         assumption has already been wrong for gpt2, mpt, refact, bloom and jais, each of \
72         which loaded clean and answered as a different model. {2} Set \
73         FRINK_ALLOW_UNAUDITED_ARCH=1 to run it anyway and compare the output against \
74         llama.cpp yourself"
75    )]
76    UnauditedArchitecture(String, crate::config::RopeLayout, String),
77    /// The checkpoint carries per-block tensors this build never reads,
78    /// i.e. weights that contribute to the real graph and would simply
79    /// be missing from ours. See [`assert_every_tensor_consumed`].
80    #[error(
81        "checkpoint carries {0} tensor(s) this build never reads, so its graph is not the one \
82         frink would run: {1}. This is a missing feature, not a corrupt file. Override with \
83         FRINK_ALLOW_UNKNOWN_TENSORS=1 to load anyway and accept wrong output."
84    )]
85    UnconsumedTensors(usize, String),
86    /// `FRINK_STRICT_KERNELS=1` and the model has weights with no
87    /// kernel on the selected accelerator, i.e. it would run, correctly,
88    /// on a silently slower path. Refusing is the point: a benchmark or
89    /// CI run must not be able to publish a number taken off the
90    /// backend it claims. See [`frink_core::kernel_registry`].
91    #[error("{0}")]
92    StrictKernels(String),
93}
94
95/// Architecture-family name strings (GGUF's `general.architecture` value)
96/// known, from reading ik_llama.cpp's `llama-hparams.cpp`
97/// (`LLM_ARCH_DEEPSEEK2`, `LLM_ARCH_GLM4_MOE` cases), to default to
98/// sigmoid MoE gating with post-selection renormalization rather than
99/// softmax. Every member's citation is inline here; `docs/MODELS.md`
100/// carries none and the pointer that used to send readers there was
101/// dangling.
102/// `afmoe`, `laguna` and `step35` added 2026-09-01 by the
103/// unaudited-refusal triage's gating sweep. Each reads
104/// `LLM_KV_EXPERT_GATING_FUNC` as OPTIONAL and then, when the key is
105/// absent, sets `LLAMA_EXPERT_GATING_FUNC_TYPE_SIGMOID`
106/// (`afmoe.cpp:29-30`, `laguna.cpp:55-56`, `step35.cpp:19-20`). Frink
107/// fell back to softmax for all three.
108///
109/// This is the `deepseek` shape a third, fourth and fifth time: a
110/// default that is right for most architectures and silently wrong for
111/// one, where the GGUF carries no key to correct it. Nothing is live
112/// today -- all three are `NewCode` for other reasons and refuse before
113/// reaching here -- but the list is what a later admission would trust.
114const SIGMOID_GATING_ARCHITECTURES: &[&str] = &[
115    "afmoe",
116    // cohere2moe.cpp:27-29: the key read optional, SIGMOID when absent.
117    "cohere2moe",
118    "deepseek2",
119    "glm4moe",
120    "laguna",
121    "step35",
122];
123
124/// Architectures whose graph passes a gating LITERAL into
125/// `build_moe_ffn`, so the file's `expert_gating_func` is never read:
126/// the literal wins even over a key that says otherwise.
127///
128/// Measured 2026-09-12 by parsing every `build_moe_ffn(` call's
129/// arguments in all 155 `src/models/*.cpp`: three graphs pass
130/// `LLAMA_EXPERT_GATING_FUNC_TYPE_SIGMOID` (`llama4.cpp`, `mimo2.cpp:227`,
131/// `nemotron-h.cpp`), twenty-six pass `_SOFTMAX`, nineteen pass
132/// `hparams.expert_gating_func`. Only `mimo2` of the three is on this
133/// loader. The twenty-six softmax literals are not tabled: every
134/// converter for them writes no key or writes SOFTMAX, so the key and
135/// the literal agree on every real file, and a table of twenty-six
136/// hand-copied rows would be a bigger risk than the hand-written file
137/// it guards against. `conversion/mimo.py` writes SIGMOID from
138/// `scoring_func`, so on a real MiMo file the two agree too; the row
139/// exists because the literal is what llama.cpp runs.
140const GATING_LITERAL_ARCHITECTURES: &[(&str, GatingFunction)] = &[
141    ("mimo2", GatingFunction::Sigmoid),
142    // `nemotron-h.cpp:218`: the SIGMOID literal; the converter writes no
143    // `expert_gating_func` (`conversion/nemotron.py:238-250`).
144    ("nemotron_h_moe", GatingFunction::Sigmoid),
145    // `llama4.cpp:230`: the SIGMOID literal, with `norm_w = false` at
146    // `:228` (`NO_TOPK_RENORMALIZE_ARCHITECTURES`); the converter
147    // writes no key (`conversion/llama.py:374-394`).
148    ("llama4", GatingFunction::Sigmoid),
149];
150/// The names alone, for the cross-table test.
151#[cfg(test)]
152const GATING_LITERAL_NAMES: &[&str] = &["mimo2", "nemotron_h_moe", "llama4"];
153
154/// Architectures whose `load_arch_hparams` reads
155/// `{arch}.expert_weights_scale` (`LLM_KV_EXPERT_WEIGHTS_SCALE`) -- and
156/// so the only ones whose `build_moe_ffn` call sees a nonzero
157/// `hparams.expert_weights_scale`. On every other architecture the key
158/// is dead metadata upstream: the field stays 0 and the multiply is
159/// skipped, whatever the file says.
160///
161/// Measured 2026-09-12: `grep -l LLM_KV_EXPERT_WEIGHTS_SCALE
162/// src/models/*.cpp` is twenty graphs; these are the eight on this
163/// loader (`deepseek2` / `deepseek32` / `deepseek2ocr` / `deepseek4` /
164/// `glm-dsa` / `glm4-moe` / `kimi-linear` / `minimax-m3` / `dflash` /
165/// `nemotron-h` / `hy-v3` are on other engines, refused, or unknown
166/// here; `cohere2moe.cpp:20` joined on 2026-09-14). Found by `mimo2`'s fixture: `mimo2.cpp` reads the
167/// key nowhere, libllama ran the fixture unscaled, and frink -- which
168/// honoured the key for any architecture -- scaled it by 2.5.
169const EXPERT_WEIGHTS_SCALE_READERS: &[&str] = &[
170    "afmoe",
171    "bailingmoe",
172    "bailingmoe2",
173    // `cohere2moe.cpp:19-20` read both the norm and the scale.
174    "cohere2moe",
175    "deepseek",
176    "dots1",
177    "exaone-moe",
178    // `glm4-moe.cpp:13-14` read both the scale and the norm.
179    "glm4moe",
180    "laguna",
181    // `nemotron-h.cpp:19` (`routed_scaling_factor`, 2.5 on Nemotron-3 Nano).
182    "nemotron_h_moe",
183    "step35",
184];
185
186/// The same for `{arch}.expert_weights_norm` (`LLM_KV_EXPERT_WEIGHTS_NORM`):
187/// eighteen graphs read it upstream, seven on this loader; every other
188/// graph passes `norm_w` as a LITERAL into `build_moe_ffn`, and the
189/// literal is what `NO_TOPK_RENORMALIZE_ARCHITECTURES` and its default
190/// transcribe. `deepseek` reads the scale but not the norm
191/// (`deepseek.cpp` passes `false`).
192const EXPERT_WEIGHTS_NORM_READERS: &[&str] = &[
193    "afmoe",
194    "bailingmoe",
195    "bailingmoe2",
196    "cohere2moe",
197    "dots1",
198    "exaone-moe",
199    "glm4moe",
200    "laguna",
201    // `nemotron-h.cpp:18` (`norm_topk_prob`).
202    "nemotron_h_moe",
203    "step35",
204];
205
206/// Names that appear in a behaviour table above but are `DedicatedOnly`
207/// or `Deferred`, together with the module that actually applies the
208/// behaviour for them.
209///
210/// Two true things were in conflict here, and deleting either would
211/// have lost one. `SIGMOID_GATING_ARCHITECTURES` records a fact about
212/// llama.cpp (these architectures default to sigmoid when the GGUF
213/// carries no `expert_gating_func`), and a test pins it as such. The
214/// cross-table test records a different fact: an entry for an
215/// architecture that never reaches THIS loader cannot fire, and a gate
216/// that cannot fire is worse than no gate because it reads as coverage.
217///
218/// Both hold. `deepseek2` is genuinely sigmoid-gated and genuinely
219/// never arrives here. So the resolution is not to drop a name
220/// from either place, it is to say out loud who owns it instead, and to
221/// make an unexplained dead entry still fail.
222///
223/// Adding a name here is a claim that the named module applies the
224/// behaviour. It is checked no further than that, so it is the one line
225/// in this file to be suspicious of.
226/// Test-only: it asserts a relationship rather than driving one, and a
227/// production reader would have to be told that.
228#[cfg(test)]
229const DEDICATED_OWNS_ITS_BEHAVIOUR: &[(&str, &str)] = &[
230    // `mla_gguf_loader` reads `expert_gating_func` and falls back to
231    // Sigmoid itself, so deepseek2's gating is decided there.
232    ("deepseek2", "mla_gguf_loader"),
233    // `glm4moe` was here while it was refused; it is a generic-path
234    // row now (2026-09-12) and the sigmoid default is live in THIS
235    // loader.
236];
237
238/// Architecture-family names whose real reference implementation skips
239/// renormalizing top-k softmax routing weights after selection (GGUF
240/// carries no metadata key for this -- it's hardcoded per-architecture in
241/// both the real HF `transformers` model code and llama.cpp's
242/// `build_moe_ffn` call sites, not read from the file). Confirmed for
243/// `olmoe` against `OlmoeTopKRouter.forward` in
244/// `transformers/models/olmoe/modeling_olmoe.py` (`config.norm_topk_prob`
245/// is `false` in the real published config.json) and llama.cpp's
246/// `src/models/olmoe.cpp` (`build_moe_ffn(..., false, ...,
247/// LLAMA_EXPERT_GATING_FUNC_TYPE_SOFTMAX, ...)`). See
248/// `MoeLayerConfig::norm_topk_prob`'s doc comment for why this matters:
249/// getting it wrong silently produces wrong generation output even
250/// though the file loads and shape-validates fine.
251// Architectures whose reference graphs pass `norm_w=false` to
252// `build_moe_ffn` (llama.cpp) / `norm_topk_prob=false` in HF config.
253// Qwen2-MoE: `.scratch/llama.cpp/src/models/qwen2moe.cpp` -- Softmax +
254// `false` for the norm_topk slot. Renormalizing top-k weights made
255// Qwen1.5-MoE greedy decode emit garbage despite shared-expert load.
256// `deepseek` (V1) added 2026-09-01 by the unaudited-refusal triage.
257// `src/models/deepseek.cpp:145-155` passes `norm_w=false`, and
258// `conversion/deepseek.py`'s `DeepseekModel` never writes
259// `{arch}.expert_weights_norm` -- only `DeepseekV2Model` does -- so no
260// real `deepseek` GGUF carries the key to override the default with.
261// Frink therefore renormalised where llama.cpp does not. Same class of
262// bug as the OLMoE one above, and latent only because `deepseek` is
263// unaudited and refuses first.
264// `jamba.cpp:164` passes `norm_w = false` and its converter writes no
265// `expert_weights_norm` (`conversion/jamba.py:24-54`).
266// `llama4.cpp:228` passes `false` beside its SIGMOID literal
267// (`GATING_LITERAL_ARCHITECTURES`): the top-k sigmoid scores weight
268// the experts unrenormalised.
269const NO_TOPK_RENORMALIZE_ARCHITECTURES: &[&str] =
270    &["deepseek", "jamba", "llama4", "olmoe", "qwen2moe"];
271
272/// Architectures whose `{arch}.feed_forward_length` counts the gate and
273/// the up projection TOGETHER, so each FFN matrix is half as wide as the
274/// key says.
275///
276/// Qwen-1 (`QWenLMHeadModel`, GGUF string `qwen` -- not `qwen2` and not
277/// `qwen3`) is the only one. HF's `QWenMLP` sets
278/// `ff_dim_in = config.intermediate_size // 2` and builds `w1` and `w2`
279/// at that width; `conversion/qwen.py`'s `QwenModel` inherits the base
280/// `set_gguf_parameters`, which writes `intermediate_size` through
281/// unchanged (`conversion/base.py:1206`); and `src/models/qwen.cpp:33-35`
282/// therefore creates `ffn_gate`, `ffn_up` and `ffn_down` at `n_ff / 2`.
283///
284/// **This costs no logits and is still worth fixing.** frink loads the
285/// dense FFN by tensor NAME and uses each matrix's own shape, so the
286/// forward pass was always right; what was wrong was `expert_ffn_dim`,
287/// which is what every memory estimate and `frink inspect-plan` row
288/// prices the FFN from. That is this repo's dominant bug shape --
289/// `ModelConfig` and the weights disagreeing about one number with
290/// nothing comparing them -- so
291/// `the_declared_ffn_width_matches_the_matrices_that_load`
292/// (tests/one_match_arm_graphs.rs) now compares them.
293const FFN_LENGTH_COUNTS_GATE_AND_UP: &[&str] = &["qwen"];
294
295// The norm-slot lists (`PRE_FFN_NORM_IS_POST_ATTENTION_NORM` and its
296// siblings) live in `crate::norm_sites`, beside the table that reads
297// them; the tests below still walk them.
298
299/// Architectures whose checkpoints carry `{arch}.leading_dense_block_count`
300/// while their reference graph never branches on it: **every** layer is
301/// MoE regardless of what the key says.
302///
303/// `bailingmoe` is the case this list exists for.
304/// `src/models/bailingmoe.cpp:5` reads
305/// `LLM_KV_LEADING_DENSE_BLOCK_COUNT` into `n_layer_dense_lead` and then
306/// `load_arch_tensors` creates `ffn_gate_inp`, the expert tensors and
307/// the shared-expert tensors unconditionally for every layer (:39-54 --
308/// there is no `if (i < n_layer_dense_lead)` anywhere in the file) and
309/// the graph has no dense branch either (:119-152). Meanwhile
310/// `conversion/bailingmoe.py:27` writes `first_k_dense_replace` into the
311/// key verbatim, so real Ling checkpoints DO carry a nonzero value.
312///
313/// Frink's `ModelConfig::layer_is_dense` does branch on it, so without
314/// this list frink looks for `blk.0.ffn_gate.weight` on a layer that
315/// only ships experts and dies on a missing tensor. That is a load
316/// failure rather than wrong logits, which is why it stayed latent.
317///
318/// Do not read this as "the key is meaningless": for `deepseek`,
319/// `dots1`, `glm4moe` and every other leading-dense architecture the key
320/// is load-bearing and must be honoured. Membership here is a statement
321/// about ONE architecture's graph, checked in that graph.
322const LEADING_DENSE_KEY_IS_INERT: &[&str] = &["bailingmoe"];
323
324/// Architectures whose reference graph applies `attn_q_norm` /
325/// `attn_k_norm` AFTER `ggml_rope_ext`, not before it.
326///
327/// There is no GGUF key for this. llama.cpp writes the order into each
328/// hand-written graph, so the only place it can come from is the
329/// architecture string, and getting it wrong changes every layer's
330/// attention scores without changing a single tensor shape.
331///
332/// - `maincoder`: `src/models/maincoder.cpp:78-90` ropes Q and K, then
333///   norms them at `:92` and `:95`.
334/// - `hunyuan-moe`: `src/models/hunyuan-moe.cpp:93,104` rope, `:110,115`
335///   norm.
336///
337/// - `hunyuan-dense`: it has no graph of its own --
338///   `src/models/models.h:1830-1834` derives `llama_model_hunyuan_dense`
339///   from `llama_model_hunyuan_vl` and reuses its graph -- so the lines
340///   are `hunyuan-vl.cpp:56-66` (rope) then `:73-81` (norm). Its second
341///   blocker, the NTK-alpha RoPE base rescale, is implemented too; see
342///   [`crate::rope_ntk_alpha`].
343///
344/// The audited majority is the other way round -- `qwen3moe.cpp:99,108`
345/// and `bailingmoe2.cpp:123-135` both norm first -- which is why the
346/// decoder's default is "before" and this list is the exception.
347const QK_NORM_AFTER_ROPE_ARCHITECTURES: &[&str] =
348    &["hunyuan-dense", "hunyuan-moe", "maincoder", "talkie"];
349
350fn metadata_u64_any(file: &impl TensorSource, keys: &[String]) -> Option<u64> {
351    keys.iter().find_map(|k| file.metadata_u64(k))
352}
353
354fn metadata_f32_any(file: &impl TensorSource, keys: &[String]) -> Option<f32> {
355    keys.iter()
356        .find_map(|k| file.metadata(k).and_then(GgufValue::as_f32))
357}
358
359impl ModelConfig {
360    /// Derives a `ModelConfig` from a real GGUF file's own hyperparameter
361    /// metadata, following llama.cpp's `general.architecture`-prefixed key
362    /// convention (`{arch}.block_count`, `{arch}.embedding_length`,
363    /// `{arch}.attention.head_count`, `{arch}.expert_count`, ...) rather
364    /// than requiring a hand-written preset to already match the file's
365    /// shape exactly. This is what lets `frink-server` (and `frink
366    /// run-real`) load an arbitrary checkpoint, not just the three
367    /// hand-tuned presets in `config.rs`.
368    ///
369    /// Fields with no corresponding metadata key fall back to widely-used
370    /// llama.cpp defaults (documented inline) and are listed in the
371    /// returned config's `best_effort_fields`, following the same
372    /// confirmed-vs-estimated discipline as the hand-written presets.
373    pub fn from_gguf(file: &impl TensorSource) -> Result<Self, LoadError> {
374        let arch = file
375            .metadata_str("general.architecture")
376            .ok_or_else(|| LoadError::MissingHparam("general.architecture".to_string()))?
377            .to_string();
378        let arch_profile = crate::capability::resolve_profile(&arch)
379            .ok_or_else(|| LoadError::UnsupportedArchitecture(arch.clone()))?;
380        let rope_layout = match arch_profile.path {
381            crate::capability::ArchPath::GenericGqa { rope }
382            | crate::capability::ArchPath::TestFixture { rope } => rope,
383            crate::capability::ArchPath::DedicatedOnly { reason } => {
384                return Err(LoadError::DedicatedArchitectureRequired(
385                    arch.clone(),
386                    reason,
387                ));
388            }
389            crate::capability::ArchPath::Deferred { reason } => {
390                return Err(LoadError::UnsupportedFeature(
391                    arch.clone(),
392                    format!("architecture deferred from Frink text-generation scope: {reason}"),
393                ));
394            }
395        };
396        let qk_norm_style = arch_profile.qk_norm;
397        // A vision export's text tower declaring M-RoPE sections on an
398        // architecture whose text rotation is NORM (`crate::mrope`).
399        if let Some(reason) = crate::mrope::mrope_refusal(file, &arch) {
400            return Err(LoadError::UnsupportedFeature(arch.clone(), reason));
401        }
402        for (meta_key, feature) in crate::capability::unsupported_feature_keys(&arch) {
403            if let Some(v) = metadata_f32_any(file, std::slice::from_ref(&meta_key)) {
404                if v > 0.0 {
405                    return Err(LoadError::UnsupportedFeature(
406                        arch.clone(),
407                        format!("{feature} (metadata {meta_key}={v})"),
408                    ));
409                }
410            }
411            if let Some(v) = metadata_u64_any(file, std::slice::from_ref(&meta_key)) {
412                if v > 0 {
413                    return Err(LoadError::UnsupportedFeature(
414                        arch.clone(),
415                        feature.to_string(),
416                    ));
417                }
418            }
419        }
420        // Metadata-declared multipliers the generic decoder does not
421        // apply. Unlike the tensor-consumption gate, nothing about these
422        // is visible in the weights, so a Granite checkpoint would load
423        // and answer at the wrong scale. See
424        // `capability::unsupported_scaling_keys`.
425        for (meta_key, feature, no_op) in crate::capability::unsupported_scaling_keys(&arch) {
426            if let Some(v) = metadata_f32_any(file, std::slice::from_ref(&meta_key)) {
427                if (v - no_op).abs() > 1e-6 {
428                    return Err(LoadError::UnsupportedFeature(
429                        arch.clone(),
430                        format!("{feature} (metadata {meta_key}={v})"),
431                    ));
432                }
433            }
434        }
435        let key = |suffix: &str| format!("{arch}.{suffix}");
436
437        let name: &'static str = Box::leak(
438            file.metadata_str("general.name")
439                .unwrap_or(&arch)
440                .to_string()
441                .into_boxed_str(),
442        );
443
444        let block_count =
445            file.metadata_u64(&key("block_count"))
446                .ok_or_else(|| LoadError::MissingHparam(key("block_count")))? as usize;
447        // llama.cpp's `n_layer()` is `block_count` MINUS the NextN/MTP
448        // blocks the converter appended inside it, for the graphs that
449        // read `nextn_predict_layers` (`crate::mtp_blocks`). `n_layers`
450        // is the trunk from here on; `block_count` is handed ONLY to the
451        // two things llama.cpp decides before it has read the key --
452        // `exaone4.cpp:4`'s layer-count gate and the per-layer array
453        // lengths -- and nowhere else.
454        let trunk = crate::mtp_blocks::trunk_layers(file, &arch, block_count)?;
455        // Nanbeige's `num_loops`: the trunk is the PHYSICAL count and
456        // `n_layers` the logical one from here on (`crate::layer_loops`);
457        // the per-layer arrays below are read at physical length and
458        // replicated per pass, as `nanbeige.cpp:24-26` replicate them.
459        let layer_loops = crate::layer_loops::read_layer_loops(file, &arch, trunk.n_layers)?;
460        let n_layers = layer_loops
461            .map(|l| l.logical_layers())
462            .unwrap_or(trunk.n_layers);
463        // Baichuan-13B (block_count 40) used to be refused HERE: one
464        // architecture string, two positional schemes, decided by layer
465        // count with no key (`baichuan.cpp:11-14`). It is served now
466        // through `crate::alibi` (the bias) and `crate::rope_layers`
467        // (no rotation), both keyed on the same layer count.
468        // EXAONE-4 32B used to be refused HERE, on the same shape:
469        // `exaone4.cpp:4-14` switches the whole SWA machinery on inside
470        // `if (hparams.n_layer() == 64)` and :116 then ropes only the
471        // sliding layers, so its full-attention layers get no rotation
472        // at all and no GGUF key says so. That is now IMPLEMENTED rather
473        // than refused -- `capability::swa_disabled_by_arch` carries the
474        // layer-count gate and `crate::rope_layers` the per-layer
475        // rotation rule it feeds -- so the two EXAONE-4 sizes are one
476        // code path with two answers instead of one running and one
477        // stopping. `tests/no_rope_layer_graphs.rs` has a 64-layer
478        // fixture against libllama's own logits.
479        let hidden_dim = file
480            .metadata_u64(&key("embedding_length"))
481            .ok_or_else(|| LoadError::MissingHparam(key("embedding_length")))?
482            as usize;
483        // Scalar OR per-layer array, as llama.cpp reads all three
484        // (`get_key_or_arr`, llama-model.cpp:1149-1158). The scalars
485        // below are the WIDEST layer's; `ModelConfig::layer_shape` is
486        // what a layer body reads. See `crate::layer_shapes`.
487        let heads_per_layer =
488            crate::layer_shapes::read_u64_trunk_layers(file, &key("attention.head_count"), &trunk)?
489                .ok_or_else(|| LoadError::MissingHparam(key("attention.head_count")))?;
490        let n_heads = heads_per_layer.iter().copied().max().unwrap_or(0) as usize;
491
492        let mut best_effort_fields: Vec<&'static str> = Vec::new();
493
494        let kv_heads_per_layer = match crate::layer_shapes::read_u64_trunk_layers(
495            file,
496            &key("attention.head_count_kv"),
497            &trunk,
498        )? {
499            Some(v) => v,
500            None => {
501                best_effort_fields.push("n_kv_heads (no attention.head_count_kv key; assumed equal to n_heads, i.e. plain MHA)");
502                heads_per_layer.clone()
503            }
504        };
505        let n_kv_heads = kv_heads_per_layer.iter().copied().max().unwrap_or(0) as usize;
506        let head_dim = match file.metadata_u64(&key("attention.key_length")) {
507            Some(v) => v as usize,
508            None => {
509                // llama.cpp derives it from LAYER 0's head count
510                // (`n_embd / n_head()`, llama-model.cpp:1195), which on
511                // a file whose layer 0 has none is a division by zero
512                // there and a refusal here.
513                let h0 = heads_per_layer.first().copied().unwrap_or(0) as usize;
514                // A pure recurrent model has no heads and no head width
515                // (`layer_shapes::PURE_RECURRENT`); nothing reads one.
516                match hidden_dim.checked_div(h0) {
517                    _ if h0 == 0 && crate::layer_shapes::pure_recurrent_block(&arch).is_some() => 0,
518                    None => {
519                        return Err(LoadError::MissingHparam(format!(
520                            "{} (layer 0 declares head_count 0, so it cannot be derived as \
521                             hidden_dim / n_heads)",
522                            key("attention.key_length")
523                        )));
524                    }
525                    Some(derived) => {
526                        best_effort_fields.push(
527                            "head_dim (no attention.key_length key; derived as hidden_dim / n_heads)",
528                        );
529                        derived
530                    }
531                }
532            }
533        };
534        let v_head_dim = crate::kv_head_dims::resolve_v_head_dim(
535            &arch,
536            head_dim,
537            file.metadata_u64(&key("attention.value_length"))
538                .map(|v| v as usize),
539        )?;
540        // `Some` only when it differs: see `ModelConfig::v_head_dim`.
541        let v_head_dim = (v_head_dim != head_dim).then_some(v_head_dim);
542        let vocab_size = file
543            .metadata("tokenizer.ggml.tokens")
544            .and_then(|v| match v {
545                GgufValue::Array(items) => Some(items.len()),
546                _ => None,
547            })
548            .or_else(|| file.metadata_u64(&key("vocab_size")).map(|v| v as usize))
549            .unwrap_or_else(|| {
550                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)");
551                // `output.weight`'s real raw shape is `[hidden_dim,
552                // vocab_size]` (ggml's fastest-first `ne[]` order --
553                // see `load_weight_matrix`'s doc comment), so vocab_size
554                // is the *last* element, not the first.
555                file.find_tensor("output.weight")
556                    .and_then(|t| t.shape.last().copied())
557                    .unwrap_or(0) as usize
558            });
559        let rope_theta = metadata_f32_any(file, &[key("rope.freq_base")]).unwrap_or_else(|| {
560            best_effort_fields.push("rope_theta (no rope.freq_base key; defaulted to 10000.0)");
561            10000.0
562        });
563        // NTK-alpha: `{arch}.rope.scaling.alpha` is read for every
564        // architecture (llama-model.cpp:1186) and APPLIED by two
565        // (`hunyuan-vl.cpp:8-12`, inherited by `hunyuan-dense`). The
566        // list and the arithmetic live together in one module so the
567        // key's readers and its appliers cannot drift apart -- see
568        // `crate::rope_ntk_alpha`, which also records why a converted
569        // `hunyuan-dense` file carries the already-scaled base instead.
570        let rope_theta = crate::rope_ntk_alpha::ntk_alpha_scaled_rope_base(
571            &arch,
572            rope_theta,
573            head_dim,
574            metadata_f32_any(file, &[key("rope.scaling.alpha")]),
575        );
576        // The norm FUNCTION is the architecture's, except where the file
577        // decides it (`crate::norm::NORM_BY_RMS_EPS_KEY`): a present and
578        // nonzero RMS epsilon means RMSNorm there, and a zero one is
579        // llama.cpp's "absent", so it is dropped before the epsilon
580        // itself is read below.
581        let declared_rms_eps = metadata_f32_any(file, &[key("attention.layer_norm_rms_epsilon")])
582            .filter(|eps| {
583                *eps != 0.0
584                    || !crate::norm::NORM_BY_RMS_EPS_KEY
585                        .iter()
586                        .any(|(a, _)| *a == arch)
587            });
588        let norm_function = crate::norm::norm_function_for_file(&arch, declared_rms_eps);
589        let rms_norm_eps = declared_rms_eps
590            .or_else(|| metadata_f32_any(file, &[key("attention.layer_norm_epsilon")]))
591            .unwrap_or_else(|| {
592                best_effort_fields
593                    .push("rms_norm_eps (no layer_norm_rms_epsilon key; defaulted to 1e-5)");
594                1e-5
595            });
596
597        let n_experts = metadata_u64_any(file, &[key("expert_count")]).unwrap_or(0) as usize;
598        let is_moe = n_experts > 1;
599
600        // `expert_used_count` is scalar OR an array at `block_count`
601        // length: `llama-model.cpp:1266` reads it with `get_key_or_arr`
602        // in the COMMON loader, for every architecture, and
603        // `gguf_writer.py:869-873` writes whichever it is handed.
604        // `conversion/nemotron.py:574` hands it a LIST (Nemotron-H
605        // Puzzle, one entry per block), and `nemotron_h` is an
606        // architecture frink serves -- so before 2026-09-19 such a
607        // file read no scalar here, fell into the default, and routed
608        // top-2 on every layer whatever the file said. A uniform array
609        // is that one value; a varying one needs a per-layer top-k the
610        // MoE layer does not have and stops by name rather than
611        // picking a number.
612        let n_experts_active = if is_moe {
613            match crate::layer_shapes::read_u64_per_layer(
614                file,
615                &key("expert_used_count"),
616                // `n_layer_all`, i.e. `block_count` including any MTP
617                // blocks, which is the length llama.cpp asks for at
618                // `llama-model.cpp:1266` -- BEFORE `n_layer()` drops
619                // them.
620                block_count,
621            )? {
622                Some(per_layer) => {
623                    let first = per_layer[0];
624                    if per_layer.iter().any(|v| *v != first) {
625                        return Err(LoadError::UnsupportedFeature(
626                            key("expert_used_count"),
627                            format!(
628                                "a PER-LAYER expert count ({per_layer:?}). llama.cpp reads this \
629                                 key with `get_key_or_arr` for every architecture \
630                                 (llama-model.cpp:1266) and routes layer `il` to \
631                                 `n_expert_used_arr[il]` experts; frink carries one top-k for \
632                                 the model, so it would route every layer to {first} and answer \
633                                 something else. conversion/nemotron.py:574 writes the array for \
634                                 Nemotron-H Puzzle"
635                            ),
636                        ));
637                    }
638                    first as usize
639                }
640                None => {
641                    best_effort_fields
642                        .push("moe.n_experts_active (no expert_used_count key; defaulted to 2)");
643                    2
644                }
645            }
646        } else {
647            1
648        };
649        // Read here, ahead of the shared-expert inference below, because
650        // the tensor that inference probes lives on the first MoE
651        // layer, not on layer 0.
652        let n_dense_leading_layers = if LEADING_DENSE_KEY_IS_INERT.contains(&arch.as_str()) {
653            0
654        } else {
655            metadata_u64_any(file, &[key("leading_dense_block_count")]).unwrap_or(0) as usize
656        };
657        // Prefer the GGUF hparam when present. Qwen2MoE (and some other
658        // HF→GGUF exports) omit `expert_shared_count` but still ship
659        // `blk.N.ffn_{gate,up,down}_shexp.weight` -- without a tensor-
660        // presence fallback those weights are silently dropped and the
661        // model runs with a large chunk of active FFN missing.
662        //
663        // The probe is the FIRST MoE LAYER, not `blk.0`: a leading-dense
664        // model has no shared expert on layer 0, and probing there
665        // answered 0 for every such file. `laguna` is the case that
666        // found it -- `laguna.cpp:20` assigns `n_expert_shared = 1`
667        // before reading the key, `conversion/laguna.py` never writes
668        // the key, and its layer 0 is dense (:105), so a real Laguna
669        // export loaded with its three REQUIRED `_shexp` tensors
670        // (:138-140) unread on every MoE layer.
671        // The interleave step the loader honours (`crate::moe_interleave`,
672        // `llama4.cpp:64`), read here because the shared-expert probe
673        // below needs the first layer it makes MoE.
674        let moe_interleave_step = crate::moe_interleave::interleave_step(
675            &arch,
676            metadata_u64_any(file, &[key("interleave_moe_layer_step")]),
677            n_experts,
678        )
679        .map_err(|reason| LoadError::UnsupportedFeature(arch.clone(), reason))?;
680        let first_moe_layer = (n_dense_leading_layers..n_layers)
681            .find(|&il| !moe_interleave_step.is_some_and(|step| !(il + 1).is_multiple_of(step)))
682            .unwrap_or(n_layers.saturating_sub(1));
683        let shexp_probe = format!("blk.{first_moe_layer}.ffn_gate_shexp.weight");
684        let n_shared_experts = match metadata_u64_any(file, &[key("expert_shared_count")]) {
685            Some(n) => n as usize,
686            None if is_moe && file.find_tensor(&shexp_probe).is_some() => {
687                best_effort_fields.push(
688                    "moe.n_shared_experts (no expert_shared_count; inferred 1 from the first \
689                     MoE layer's ffn_gate_shexp.weight)",
690                );
691                1
692            }
693            None => 0,
694        };
695        // MoE GGUFs often only set `feed_forward_length` (OLMoE=1024,
696        // Qwen2-MoE=5632 for the shared expert). `expert_feed_forward_length`
697        // is optional. llama.cpp `qwen2moe.cpp` uses
698        // `n_ff_exp = n_ff_exp ? n_ff_exp : n_ff / n_expert_used` (1408 for
699        // Qwen1.5-MoE); the shared expert keeps the full `n_ff` (5632).
700        let ffn_per_layer =
701            crate::layer_shapes::read_u64_trunk_layers(file, &key("feed_forward_length"), &trunk)?
702                // Qwen-1 declares gate and up as one number; see
703                // `FFN_LENGTH_COUNTS_GATE_AND_UP`.
704                .map(|v| {
705                    if FFN_LENGTH_COUNTS_GATE_AND_UP.contains(&arch.as_str()) {
706                        v.into_iter().map(|ff| ff / 2).collect()
707                    } else {
708                        v
709                    }
710                });
711        let feed_forward_length = ffn_per_layer.as_ref().and_then(|v| v.iter().copied().max());
712        // Scalar OR an array, exactly as `expert_used_count` above:
713        // llama.cpp reads it with `get_key_or_arr` (`maple.cpp:6`,
714        // `dots3note.cpp:11`, `nemotron-h.cpp`), and
715        // `conversion/nemotron.py:573` writes a LIST for Nemotron-H
716        // Puzzle -- an architecture frink serves. Read as a scalar
717        // alone, an array answered `None` here and the fallback below
718        // silently sized every expert at `feed_forward_length /
719        // n_experts_used`, which is a different FFN and loads without
720        // complaint when the tensors happen to be that wide.
721        let expert_ffn_per_layer = crate::layer_shapes::read_u64_per_layer(
722            file,
723            &key("expert_feed_forward_length"),
724            block_count,
725        )?;
726        if let Some(per_layer) = expert_ffn_per_layer.as_ref() {
727            let first = per_layer[0];
728            if per_layer.iter().any(|v| *v != first) {
729                return Err(LoadError::UnsupportedFeature(
730                    key("expert_feed_forward_length"),
731                    format!(
732                        "a PER-LAYER expert FFN width ({per_layer:?}). llama.cpp sizes the \
733                         expert tensors from layer 0's entry and `LayerShapes` carries one \
734                         expert width for the model, so frink would build every layer at \
735                         {first} and read the others' weights at the wrong stride"
736                    ),
737                ));
738            }
739        }
740        let expert_ffn_dim = expert_ffn_per_layer
741            .map(|v| v[0])
742            .or_else(|| {
743                feed_forward_length.map(|ff| {
744                    if is_moe && n_experts_active > 0 {
745                        ff / n_experts_active as u64
746                    } else {
747                        ff
748                    }
749                })
750            })
751            .unwrap_or_else(|| {
752                best_effort_fields.push(
753                    "moe.expert_ffn_dim (no expert_feed_forward_length/feed_forward_length; defaulted to 4x hidden_dim)",
754                );
755                (hidden_dim * 4) as u64
756            }) as usize;
757        // `{arch}.attention.rope_pattern`: one entry per layer, nonzero
758        // meaning "this layer rotates" (`llama-hparams.cpp:333-343`).
759        // Read only where llama.cpp reads it, because
760        // `llama-model.cpp:1314` seeds the array with 1 for every
761        // architecture and only `granite-swa.cpp:43` reads it back --
762        // so honouring it elsewhere would answer differently from
763        // upstream on a file that carries it as dead metadata.
764        let rope_pattern: Option<std::sync::Arc<[bool]>> =
765            if crate::rope_layers::reads_rope_pattern(&arch) {
766                crate::layer_shapes::read_u64_per_layer(
767                    file,
768                    &key("attention.rope_pattern"),
769                    block_count,
770                )?
771                .map(|v| v.into_iter().map(|x| x != 0).collect())
772            } else {
773                None
774            };
775
776        // Qwen3.5's recurrent layers come from two keys, not from the
777        // head counts (`crate::gdn::recurrent_layers`).
778        let recurrent_layers =
779            crate::gdn::recurrent_layers(file, &arch, trunk.block_count, n_layers)?;
780        let layer_shapes = crate::layer_shapes::LayerShapes::resolve(
781            &arch,
782            &heads_per_layer,
783            &kv_heads_per_layer,
784            ffn_per_layer.as_deref(),
785            expert_ffn_dim,
786            recurrent_layers.as_ref(),
787        )?
788        // `nanbeige.cpp:24-26` copies each physical layer's shape
789        // arrays to every logical slot; HRM-Text's two stacks are
790        // uniform and its arrays are scalars, so the replication is
791        // spelled for the one schedule that needs it rather than
792        // divided out of the other's counts.
793        .replicated(match layer_loops {
794            Some(crate::layer_loops::LayerLoops::Repeat { n_loops, .. }) => n_loops,
795            _ => 1,
796        });
797        // The OTHER half of llama.cpp's dense-vs-MoE rule.
798        // `ModelConfig::layer_is_dense` implements the leading-dense
799        // prefix and not the `(il + 1) % n_moe_layer_step == 0` at
800        // `src/models/ernie4-5-moe.cpp:64`, so a file whose step would
801        // change the answer stops here rather than looking for expert
802        // tensors on a layer that stores dense ones. `moe_interleave`
803        // records what building the fixture found: llama.cpp cannot load
804        // such a file either, because its own tensor loader has no step
805        // in it.
806        if let Some(reason) = crate::moe_interleave::interleave_step_refusal(
807            &arch,
808            metadata_u64_any(file, &[key("interleave_moe_layer_step")]),
809        ) {
810            return Err(LoadError::UnsupportedFeature(arch.clone(), reason));
811        }
812
813        // ik_llama.cpp's real gating-function hparam
814        // (LLM_KV_EXPERT_GATING_FUNC: 1=softmax, 2=sigmoid) if the file
815        // carries it; otherwise fall back to the same architecture-name
816        // convention the hand-written presets in config.rs use (see
817        // docs/MODELS.md for the citations behind that list).
818        let gating_literal = GATING_LITERAL_ARCHITECTURES
819            .iter()
820            .find(|(name, _)| *name == arch)
821            .map(|(_, g)| *g);
822        let gating = match (
823            gating_literal,
824            metadata_u64_any(file, &[key("expert_gating_func")]),
825        ) {
826            (Some(literal), _) => literal,
827            (None, Some(2)) => GatingFunction::Sigmoid,
828            (None, Some(1)) => GatingFunction::Softmax,
829            (None, _) => {
830                if SIGMOID_GATING_ARCHITECTURES.contains(&arch.as_str()) {
831                    GatingFunction::Sigmoid
832                } else {
833                    if is_moe {
834                        best_effort_fields.push(
835                            "moe.gating (no expert_gating_func key and architecture not in the known-sigmoid list; defaulted to softmax)",
836                        );
837                    }
838                    GatingFunction::Softmax
839                }
840            }
841        };
842
843        // `{arch}.expert_weights_norm` (llama.cpp
844        // `LLM_KV_EXPERT_WEIGHTS_NORM`) is the real metadata key for
845        // whether the selected experts' weights are renormalised. Most
846        // checkpoints do not carry it, which is why the fallback below
847        // exists at all -- but when one does, the file's own answer wins
848        // over an architecture-name guess.
849        // The key only where llama.cpp reads it (`EXPERT_WEIGHTS_NORM_READERS`);
850        // everywhere else the graph's literal, which the table below
851        // transcribes, whatever the file says.
852        let norm_key = if EXPERT_WEIGHTS_NORM_READERS.contains(&arch.as_str()) {
853            file.metadata_bool(&key("expert_weights_norm"))
854        } else {
855            None
856        };
857        let norm_topk_prob = match norm_key {
858            Some(v) => v,
859            None => {
860                // See `NO_TOPK_RENORMALIZE_ARCHITECTURES`'s doc comment:
861                // an architecture-name lookup, the same convention
862                // `gating`'s fallback above uses.
863                if is_moe && matches!(gating, GatingFunction::Softmax) {
864                    best_effort_fields.push(
865                        "moe.norm_topk_prob (no expert_weights_norm key; defaulted by architecture-name lookup against NO_TOPK_RENORMALIZE_ARCHITECTURES)",
866                    );
867                }
868                !NO_TOPK_RENORMALIZE_ARCHITECTURES.contains(&arch.as_str())
869            }
870        };
871
872        // `{arch}.expert_weights_scale` (`LLM_KV_EXPERT_WEIGHTS_SCALE`).
873        // llama.cpp's `build_moe_ffn` skips the multiply for both 0.0 and
874        // 1.0, so both mean "no scaling" and both land on 1.0 here.
875        // And only where llama.cpp reads it (`EXPERT_WEIGHTS_SCALE_READERS`).
876        let expert_weights_scale = if EXPERT_WEIGHTS_SCALE_READERS.contains(&arch.as_str()) {
877            metadata_f32_any(file, &[key("expert_weights_scale")])
878                .filter(|s| *s != 0.0)
879                .unwrap_or(1.0)
880        } else {
881            1.0
882        };
883
884        // Real GGUF key (`{arch}.attention.sliding_window`, confirmed
885        // against `gguf-py/gguf/constants.py`'s real
886        // `LLM_KV_ATTENTION_SLIDING_WINDOW`). Some checkpoints
887        // (confirmed for real published Qwen1.5-MoE/Qwen2-MoE GGUFs)
888        // carry a nonzero window value even when the model's own
889        // config disables sliding-window attention entirely
890        // (`use_sliding_window: false`) -- llama.cpp's own convention
891        // is that a window of 0 means "unused," so only a real nonzero
892        // value here is treated as active.
893        let declared_window = metadata_u64_any(file, &[key("attention.sliding_window")]);
894        let sliding_window = declared_window
895            .map(|v| v as usize)
896            .filter(|&w| w > 0)
897            // `phi3` declares a window that llama.cpp deliberately does
898            // NOT honour -- see `capability::swa_disabled_by_arch`. This
899            // has to drop the window rather than pick a period, because
900            // upstream is declining to use the file's value, not
901            // choosing a different one.
902            //
903            // `block_count`, NOT `n_layers`: `exaone4.cpp:4` tests
904            // `n_layer() == 64` at a point where `n_layer_nextn` has not
905            // been read yet (`:18`), so a 64-trunk EXAONE-4 with an MTP
906            // block appended sees 65 there and gets no window.
907            //
908            // `smallthinker` declares a window that llama.cpp REPLACES:
909            // `smallthinker.cpp:8` assigns `n_swa = 4096` on the branch
910            // the file's nonzero value selected. One table decides all
911            // three answers (`capability::swa_window_override`), so a
912            // row cannot be dropped by one reader and pinned by another.
913            .and_then(
914                |w| match crate::capability::swa_window_override(&arch, trunk.block_count) {
915                    crate::capability::SwaWindowOverride::Honour => Some(w),
916                    crate::capability::SwaWindowOverride::Drop => None,
917                    crate::capability::SwaWindowOverride::Pin(pinned) => Some(pinned),
918                },
919            );
920
921        // A CHUNKED window (`crate::chunked_swa`): the literal chunk on
922        // the branch the file takes, whatever nonzero value it declares
923        // and whether it declares one at all; a declared ZERO is the
924        // branch libllama aborts on, refused there by name.
925        let swa_chunked = crate::chunked_swa::chunked_window(&arch, declared_window)?;
926        let sliding_window = swa_chunked.or(sliding_window);
927        let swa_chunked = swa_chunked.is_some();
928
929        // A window llama.cpp REQUIRES (`crate::swa_geometry::
930        // window_required`): without it the file does not load upstream,
931        // and here it would run with no layer rotated.
932        if let (None, Some(line)) = (sliding_window, crate::swa_geometry::window_required(&arch)) {
933            return Err(LoadError::UnsupportedFeature(
934                arch.clone(),
935                format!(
936                    "`{arch}.attention.sliding_window` is absent or zero, and llama.cpp reads it \
937                     as a REQUIRED key for this architecture (src/models/{line}); every real \
938                     export writes it, and a file without it does not load upstream"
939                ),
940            ));
941        }
942
943        // A window on a short-conv architecture: `lfm2.cpp:24-29`
944        // honours it on the ATTENTION layers alone (`is_swa_impl[il] =
945        // !is_recr_impl[il]`), a per-layer answer `crate::swa_layers`
946        // has no variant for, and one that would also arm eviction
947        // against the history the conv indexes by row
948        // (`crate::shortconv`). No published export writes the key.
949        if let (Some(w), true) = (
950            sliding_window,
951            crate::shortconv::is_shortconv_architecture(&arch),
952        ) {
953            return Err(LoadError::UnsupportedFeature(
954                arch.clone(),
955                format!(
956                    "`{arch}.attention.sliding_window` {w}: lfm2.cpp:24-29 windows the attention \
957                     layers and not the conv layers, which `swa_layers` cannot yet spell, and a \
958                     window on a conv layer would evict the history its convolution reads \
959                     (`crate::shortconv`); no published LFM2 export writes the key"
960                ),
961            ));
962        }
963
964        // Three graphs rope their SLIDING layers with the scaling
965        // switched off -- freq_scale = 1, ext_factor = 0, attn_factor =
966        // 1 -- while the full-attention layers use the model's:
967        // `olmo2` (Olmo-3), `mellum` and `laguna` (Laguna-XS.2), each
968        // at the lines `crate::swa_geometry` cites. frink's
969        // `RopeFreqs` already keeps their sliding layers' divisors
970        // unscaled, but `rope_attn_factor` is one value for the whole
971        // model, so honouring the file would mean rotating half the
972        // layers at a magnitude the checkpoint never trained at.
973        //
974        // A window with NO scaling is not this case and is not refused:
975        // both branches then reduce to the same plain RoPE, and the
976        // difference is masking alone, which frink implements.
977        if let (Some(lines), true) = (
978            crate::swa_geometry::swa_layers_unscaled_rope(&arch),
979            sliding_window.is_some(),
980        ) {
981            let scaling_type = file
982                .metadata_str(&key("rope.scaling.type"))
983                .unwrap_or("none")
984                .to_string();
985            if !scaling_type.eq_ignore_ascii_case("none") {
986                return Err(LoadError::UnsupportedFeature(
987                    arch.clone(),
988                    format!(
989                        "this {arch} checkpoint declares BOTH a sliding window and \
990                         rope.scaling.type = \"{scaling_type}\". llama.cpp ropes the \
991                         sliding layers with the scaling switched off (freq_scale = 1, \
992                         ext_factor = 0, attn_factor = 1; {lines}) and the \
993                         full-attention layers with it on, and frink carries one RoPE \
994                         scaling for the whole model. A {arch} file with a window and no \
995                         scaling, or with scaling and no window, is unaffected"
996                    ),
997                ));
998            }
999        }
1000
1001        // WHICH LAYERS SLIDE (`attention.sliding_window_pattern`).
1002        //
1003        // llama.cpp seeds a period per architecture (gemma2 2, gemma3
1004        // 6, exaone4 4, ...) and only then reads the key, so a missing
1005        // key must NOT mean "all SWA": the seed is
1006        // `capability::default_swa_layout`, with the gemma3+ period for
1007        // any Gemma variant the table does not name. The phase is a
1008        // property of the architecture -- `dense_first` is an argument
1009        // to `set_swa_pattern`, not a GGUF key -- so it comes from the
1010        // seed either way.
1011        //
1012        // The key itself is a scalar period OR a per-layer bool array,
1013        // and which of the two an architecture's graph honours -- and
1014        // what it does with the other -- is `crate::swa_layers`'s
1015        // table, transcribed from the `get_key_or_arr` overload each
1016        // `load_arch_hparams` calls. The array used to be REFUSED here
1017        // for every architecture, which stopped every real EXAONE-4
1018        // 32B, EXAONE-MoE and Olmo-3 export at the door over a value
1019        // llama.cpp never reads for them.
1020        let swa_seed = crate::capability::default_swa_layout(&arch).or(match arch_profile.family {
1021            crate::capability::DecoderFamily::GemmaFamily => Some(crate::capability::SwaPattern {
1022                period: 6,
1023                dense_first: false,
1024            }),
1025            _ => None,
1026        });
1027        let swa_layers = match sliding_window {
1028            // No window: no graph consults `is_swa`, and reading the
1029            // key would only refuse a file over a value nothing uses.
1030            None => crate::swa_layers::SwaLayers::All,
1031            Some(_) => crate::swa_layers::read_swa_layers(
1032                file,
1033                &arch,
1034                &key("attention.sliding_window_pattern"),
1035                &trunk,
1036                swa_seed,
1037            )?,
1038        };
1039
1040        // The metadata-declared scalar multipliers, resolved once for
1041        // whichever subset this architecture's reference graph applies.
1042        // See `crate::scalar_multipliers`; the keys the graph does NOT
1043        // apply were already refused above, by a list derived from the
1044        // same table. Read first because its `defaults` also seed the
1045        // softcap below: `grok.cpp:5-12` assigns all of them in one
1046        // place, and so does this.
1047        let multiplier_support = crate::scalar_multipliers::multiplier_support(&arch);
1048
1049        // The file's softcap, then the architecture's default for a
1050        // file that declares none (`grok.cpp:9`), then llama.cpp's own
1051        // "off". `> 0.0` is what every graph tests before applying one.
1052        let attn_logit_softcap = metadata_f32_any(
1053            file,
1054            &[
1055                key("attention.logit_softcapping"),
1056                key("attn_logit_softcapping"),
1057            ],
1058        )
1059        .or(multiplier_support.defaults.attn_logit_softcap())
1060        .filter(|&v| v > 0.0);
1061        let final_logit_softcap =
1062            metadata_f32_any(file, &[key("final_logit_softcapping")]).filter(|&v| v > 0.0);
1063
1064        let declared = crate::scalar_multipliers::DeclaredMultipliers {
1065            logit: metadata_f32_any(file, &[key("logit_scale")]),
1066            residual: metadata_f32_any(file, &[key("residual_scale")]),
1067            embedding: metadata_f32_any(file, &[key("embedding_scale")]),
1068            // Exactly the spelling this architecture's graph reads --
1069            // `attention.scale` for Granite, `attention.output_scale`
1070            // for Grok -- and nothing for the rest. The other spelling
1071            // was refused above, by the same table.
1072            attention: multiplier_support
1073                .attention
1074                .suffix()
1075                .and_then(|suffix| metadata_f32_any(file, &[key(suffix)])),
1076        };
1077        let multipliers = crate::scalar_multipliers::resolve(
1078            multiplier_support,
1079            declared,
1080            // `n_layer` and `n_embd` are here for MiniCPM's defaults
1081            // (`minicpm.cpp:6-7`), which are computed from the model's
1082            // own shape rather than declared: an older MiniCPM export
1083            // carries none of the three keys and is still scaled by all
1084            // three.
1085            crate::scalar_multipliers::MultiplierDims {
1086                head_dim,
1087                n_layer: n_layers,
1088                n_embd: hidden_dim,
1089            },
1090        )
1091        .map_err(|e| LoadError::UnsupportedFeature(arch.clone(), e.message(&arch)))?;
1092
1093        // Gemma and afmoe: embeddings are scaled by sqrt(hidden_dim) at
1094        // input. That is ARITHMETIC, not a key -- those graphs read no
1095        // `embedding_scale` at all -- so it comes from the table and a
1096        // file declaring the key on one of them is refused above rather
1097        // than honoured. Granite's comes out of `{arch}.embedding_scale`.
1098        let embedding_scale =
1099            if crate::capability::embeddings_scaled_by_sqrt_n_embd(&arch, arch_profile.family) {
1100                Some((hidden_dim as f32).sqrt())
1101            } else {
1102                multipliers.embedding_scale
1103            };
1104
1105        // llama.cpp's `f_attention_scale`, and ONLY where it differs from
1106        // the `1/sqrt(head_dim)` frink's attention kernels already
1107        // apply -- `Some` here means "pre-scale Q", so restating the
1108        // kernels' own scale would double-scale every score.
1109        //
1110        // For Gemma-2 and Gemma-3 that difference is real at 27B and
1111        // nowhere else (`capability::attention_scale_override` carries
1112        // the llama.cpp lines). This used to be a hardcoded `None` under
1113        // a comment that NAMED the 27B exception without implementing
1114        // it, so Gemma-2-27B scored 1.061x and Gemma-3-27B 1.146x too
1115        // large on every layer: a sharper softmax than the trained one,
1116        // fluent and wrong, with no error.
1117        //
1118        // Granite reaches the same slot from the file's own
1119        // `{arch}.attention.scale` (`granite.cpp:225`, whose `0.0f`
1120        // sentinel means "use the kernels' scale"). The two sources
1121        // cannot both be live on one architecture: `attention_scale_override`
1122        // covers the architectures that COMPUTE the scale and
1123        // `scalar_multipliers` the ones that READ it, and no llama.cpp
1124        // architecture does both. `.or` rather than a match because the
1125        // computed one is the one that cannot be turned off by a file.
1126        let attention_scale = crate::capability::attention_scale_override(
1127            &arch, n_layers, hidden_dim, n_heads, head_dim,
1128        )
1129        .or(multipliers.attention_scale);
1130
1131        // Granite reads `{arch}.rope.scaling.finetuned` as a switch for
1132        // RoPE itself, not as a note about the scaling: a file declaring
1133        // it false runs UNROTATED in llama.cpp (every Granite-4.0 hybrid
1134        // export), which is `RopeLayers::Never` below
1135        // (`crate::rope_finetuned`).
1136        let rope_switched_off = crate::rope_finetuned::unrotated(
1137            &arch,
1138            file.metadata(&key("rope.scaling.finetuned"))
1139                .and_then(GgufValue::as_bool),
1140        );
1141
1142        // OLMo-1 and DBRX clamp Q, K and V by `{arch}.attention.clamp_kqv`
1143        // inside the shared `build_qkv`. Resolved here for the
1144        // architectures whose loader reads the key, REQUIRED where
1145        // llama.cpp's is (`dbrx.cpp:5`), and applied by the one helper
1146        // every host body shares (`decoder/qkv_bias.rs`). See
1147        // `crate::clamp_kqv`, which also records that both converters
1148        // really write this key.
1149        let clamp_kqv = crate::clamp_kqv::resolve_clamp(
1150            &arch,
1151            metadata_f32_any(file, &[key("attention.clamp_kqv")]),
1152        )
1153        .map_err(|e| LoadError::UnsupportedFeature(arch.clone(), e.message(&arch)))?;
1154
1155        // SWA-layer RoPE base. `llama_hparams` defaults it to 10000 and
1156        // the Gemma-3 lineage relies on that default; the architectures
1157        // in `swa_rope_base_follows_model` instead seed it from the
1158        // model's own base before the key can override.
1159        let rope_theta_swa = if sliding_window.is_some() {
1160            let fallback = if crate::capability::swa_rope_base_follows_model(&arch) {
1161                rope_theta
1162            } else {
1163                10_000.0
1164            };
1165            Some(
1166                metadata_f32_any(
1167                    file,
1168                    &[key("rope.freq_base_swa"), key("rope_freq_base_swa")],
1169                )
1170                .unwrap_or(fallback),
1171            )
1172        } else {
1173            None
1174        };
1175
1176        let ffn_activation = match arch_profile.family {
1177            // Per-ARCHITECTURE first, because llama.cpp's choice is per
1178            // architecture and the family partition does not match it:
1179            // `grok` is StandardGqa and passes `LLM_FFN_GELU`.
1180            _ if crate::capability::uses_geglu(&arch) => crate::config::FfnActivation::Gelu,
1181            _ if crate::capability::uses_relu_sqr(&arch) => crate::config::FfnActivation::ReluSqr,
1182            _ if crate::capability::uses_gelu_ungated(&arch) => {
1183                crate::config::FfnActivation::GeluUngated
1184            }
1185            // The GATED ReLU (`ggml_reglu_split`), a real gate tensor:
1186            // NOT the row above, which aliases gate to up.
1187            _ if crate::capability::uses_reglu(&arch) => crate::config::FfnActivation::Reglu,
1188            // The four per-layer arrays travel IN the variant, read as
1189            // `apertus.cpp:6-9` reads them (`crate::act_layers`).
1190            _ if crate::act_layers::uses_xielu(&arch) => crate::config::FfnActivation::Xielu(
1191                crate::act_layers::read_xielu_layers(file, trunk.n_layers)?,
1192            ),
1193            // The two clamp arrays, read as `step35.cpp:28-29` read them
1194            // (optional; a file with neither is plain SwiGLU).
1195            _ if crate::act_layers::reads_swiglu_clamps(&arch) => {
1196                match crate::act_layers::read_swiglu_clamps(file, &arch, &trunk)? {
1197                    Some(clamps) => crate::config::FfnActivation::SwigluClamped(clamps),
1198                    None => crate::config::FfnActivation::Swiglu,
1199                }
1200            }
1201            crate::capability::DecoderFamily::GemmaFamily => crate::config::FfnActivation::Gelu,
1202            crate::capability::DecoderFamily::PhiFamily => {
1203                crate::config::FfnActivation::SwigluFused
1204            }
1205            _ => crate::config::FfnActivation::Swiglu,
1206        };
1207
1208        // Llama 3/3.1/3.2's real per-band RoPE frequency correction: one
1209        // model-level tensor (`TENSOR_NOT_REQUIRED`, `TENSOR_DUPLICATED`
1210        // for every layer but the first in the real llama.cpp source --
1211        // i.e. every layer shares this same array), not per-layer. See
1212        // `frink_core::attention::apply_rope_with_freq_factors`'s doc
1213        // comment for why this matters.
1214        let rope_freqs = load_f32_vec_optional(file, "rope_freqs.weight")?;
1215
1216        // Phi-3/Phi-4 LongRoPE: two per-band factor tensors instead of
1217        // Llama's single `rope_freqs.weight`, selected by context size
1218        // (llama.cpp `llama_model::get_rope_factors`: `rope_freqs` wins if
1219        // present, else `rope_long` when the run's context exceeds
1220        // `rope.scaling.original_context_length`, else `rope_short`).
1221        //
1222        // Provisional pick from the checkpoint's advertised context length
1223        // (llama.cpp's default `n_ctx`). The definitive pick happens in
1224        // `ModelConfig::apply_runtime_context`, called from `frink run`
1225        // (`--ctx-size`) and from `verify_engine::load_and_tokenize`
1226        // (`n_tokens + 8`, matching `tools/llama_logits.c`).
1227        let rope_orig_ctx = metadata_u64_any(file, &[key("rope.scaling.original_context_length")])
1228            .map(|v| v as usize);
1229
1230        // The per-position attention temperature (`crate::attn_temperature`).
1231        // `mistral3.cpp:15` floors it on `hparams.n_ctx_orig_yarn`, which
1232        // `llama-model.cpp:1164-1165` seeds from `context_length` BEFORE
1233        // the YaRN key overrides it -- so a Ministral file with no YaRN
1234        // key floors on its context length, and the resolver is handed
1235        // that value rather than the key. Before this existed the key
1236        // loaded and was silently dropped on the one generic-path
1237        // architecture whose graph applies it.
1238        let attn_temperature = crate::attn_temperature::resolve_attn_temperature(
1239            &arch,
1240            crate::attn_temperature::DeclaredTemperature {
1241                scale: metadata_f32_any(file, &[key("attention.temperature_scale")]),
1242                length: metadata_u64_any(file, &[key("attention.temperature_length")]),
1243                n_ctx_orig_yarn: rope_orig_ctx
1244                    .map(|v| v as u64)
1245                    .or_else(|| metadata_u64_any(file, &[key("context_length")])),
1246            },
1247        )
1248        .map_err(|e| LoadError::UnsupportedFeature(arch.clone(), e.message(&arch)))?;
1249        // `rope_freqs.weight` outranks the LongRoPE pair (llama.cpp
1250        // `get_rope_factors` checks it first), so a checkpoint carrying
1251        // it never populates these and the runtime re-pick below cannot
1252        // overwrite a Llama-3 correction with a Phi one.
1253        let (rope_freqs_long, rope_freqs_short) = if rope_freqs.is_some() {
1254            (None, None)
1255        } else {
1256            (
1257                load_f32_vec_optional(file, "rope_factors_long.weight")?,
1258                load_f32_vec_optional(file, "rope_factors_short.weight")?,
1259            )
1260        };
1261        // Provisional pick from the checkpoint's own advertised context;
1262        // `ModelConfig::apply_runtime_context` re-picks once the run's
1263        // `--ctx-size` is known, which is the number llama.cpp decides on.
1264        let rope_freqs = match (rope_freqs, rope_orig_ctx) {
1265            (Some(f), _) => Some(f),
1266            (None, Some(orig)) => {
1267                let model_ctx = metadata_u64_any(file, &[key("context_length")])
1268                    .unwrap_or(orig as u64) as usize;
1269                if model_ctx > orig {
1270                    rope_freqs_long.clone().or_else(|| rope_freqs_short.clone())
1271                } else {
1272                    rope_freqs_short.clone().or_else(|| rope_freqs_long.clone())
1273                }
1274            }
1275            (None, None) => None,
1276        };
1277
1278        // Partial rotary: the file's `rope.dimension_count`, or the head
1279        // width when absent -- llama.cpp's seeded `n_rot_full`
1280        // (`llama-model.cpp:1200-1202`).
1281        let rope_dim_seeded = metadata_u64_any(file, &[key("rope.dimension_count")])
1282            .map(|d| d as usize)
1283            .filter(|d| *d > 0)
1284            .unwrap_or(head_dim);
1285
1286        // The sliding layers' OWN rotary and head widths
1287        // (`llama-model.cpp:1215-1223`). The rotary one is honoured --
1288        // `crate::swa_geometry` resolves the key and step35's halving
1289        // into the two widths `ModelConfig::layer_rope` hands out -- and
1290        // the head ones are refused, since frink carries one head width
1291        // in every cache. Only a model with a sliding layer reads the
1292        // keys; on one that has none they are dead metadata, as they
1293        // are upstream (`n_rot(il)` never takes the `_swa` branch). The
1294        // halving is not a key and applies regardless.
1295        let geometry = crate::swa_geometry::SwaGeometry {
1296            rope_dim_swa: metadata_u64_any(file, &[key("rope.dimension_count_swa")]),
1297            key_length_swa: metadata_u64_any(file, &[key("attention.key_length_swa")]),
1298            value_length_swa: metadata_u64_any(file, &[key("attention.value_length_swa")]),
1299            rope_dim_full: rope_dim_seeded as u64,
1300            head_dim: head_dim as u64,
1301        };
1302        let widths = if sliding_window.is_some()
1303            || crate::swa_geometry::full_layers_rotate_half(&arch).is_some()
1304        {
1305            if let Some(reason) = crate::swa_geometry::swa_geometry_refusal(&arch, geometry) {
1306                return Err(LoadError::UnsupportedFeature(arch.clone(), reason));
1307            }
1308            crate::swa_geometry::rotary_widths(&arch, geometry)
1309        } else {
1310            crate::swa_geometry::RotaryWidths {
1311                full: Some(rope_dim_seeded).filter(|d| *d < head_dim),
1312                swa: None,
1313            }
1314        };
1315        // Equal values mean "whole head", which is the same thing as
1316        // `None` and stays `None` so nothing downstream has to
1317        // special-case it.
1318        let rope_dim = widths.full;
1319        let rope_dim_swa = widths.swa;
1320
1321        // See `ModelConfig::rope_attn_factor`. `mut` because YaRN's
1322        // magnitude term is folded into it below.
1323        let mut rope_attn_factor = metadata_f32_any(file, &[key("rope.scaling.attn_factor")])
1324            .filter(|f| f.is_finite() && *f > 0.0)
1325            .unwrap_or(1.0);
1326
1327        // YaRN long-context scaling. `rope.scaling.attn_factor` above is
1328        // only YaRN's *magnitude* term (ggml `rope_yarn`'s `mscale`); the
1329        // frequency half -- which bands get interpolated toward the
1330        // trained context and which stay extrapolated -- lives in
1331        // `rope.scaling.type` + `rope.scaling.factor`, and frink read
1332        // neither before this. A YaRN checkpoint was therefore roped as
1333        // if it declared no scaling at all: right near position 0 and
1334        // progressively wrong further in, i.e. the failure that reads as
1335        // long-prompt quality decay rather than as a bug.
1336        //
1337        // The rewrite is folded into `rope_freqs`, the same per-band
1338        // divisor array Llama-3's `rope_freqs.weight` supplies (ggml
1339        // divides each band's theta by it), so it rides the existing CPU
1340        // and Metal RoPE paths unchanged. When a file carries both, the
1341        // two corrections compose by multiplication, as they do in
1342        // llama.cpp (`ggml_rope_cache_init` divides by `freq_factors`
1343        // *and then* runs `rope_yarn`).
1344        // Linear scaling, which was silently DROPPED before this.
1345        //
1346        // `rope.scaling.type = "linear"` with factor s means rotating
1347        // position `p/s` instead of `p`. Since the angle is `p * freq`,
1348        // that is exactly `p * (freq / s)`, and `rope_freqs` already
1349        // divides each band's frequency. So a uniform vector of `s`
1350        // expresses it exactly and rides the existing CPU and Metal RoPE
1351        // paths unchanged, the same way YaRN does below.
1352        //
1353        // Before this, the type was compared against "yarn" and anything
1354        // else returned None, so a checkpoint declaring linear scaling
1355        // with factor 4 loaded and roped at UNSCALED positions where
1356        // llama.cpp divides them by 4. It answered as a different model
1357        // with no error. Affects the long-context community rescales
1358        // (`*-16k`, `*-32k` Llama-2 derivatives).
1359
1360        // The file's own per-band factors, BEFORE any position-scaling
1361        // fold. That is what a sliding layer uses on an architecture
1362        // whose SWA layers do not inherit the trained scale -- llama.cpp
1363        // keeps the two apart as `freq_factors` (a tensor, the same for
1364        // every layer) and `freq_scale` (per layer,
1365        // `llama-model.cpp:2033`), while frink folds them into one
1366        // vector. See `config::RopeFreqs`.
1367        let rope_freqs_unscaled = rope_freqs.clone();
1368
1369        let rope_freqs = match linear_scaling_from_gguf(file, &arch) {
1370            None => rope_freqs,
1371            Some(factor) => {
1372                let rotary_dim = rope_dim.unwrap_or(head_dim);
1373                if rotary_dim == 0 || !rotary_dim.is_multiple_of(2) {
1374                    best_effort_fields.push(
1375                        "rope_freqs (linear scaling declared but the rotary width is odd; \
1376                         scaling not applied)",
1377                    );
1378                    rope_freqs
1379                } else {
1380                    let linear = vec![factor; rotary_dim / 2];
1381                    match rope_freqs {
1382                        None => Some(linear),
1383                        // Compose by multiplication, as a file carrying
1384                        // its own `rope_freqs.weight` tensor and a
1385                        // declared linear factor means both.
1386                        Some(own) if own.len() == linear.len() => {
1387                            Some(own.iter().zip(linear.iter()).map(|(a, b)| a * b).collect())
1388                        }
1389                        Some(own) => {
1390                            best_effort_fields.push(
1391                                "rope_freqs (linear scaling declared but the file's own \
1392                                 rope_freqs tensor has a different width; scaling not applied)",
1393                            );
1394                            Some(own)
1395                        }
1396                    }
1397                }
1398            }
1399        };
1400        let rope_freqs = match yarn_scaling_from_gguf(file, &arch, rope_orig_ctx) {
1401            None => rope_freqs,
1402            Some(scaling) => {
1403                // YaRN's MAGNITUDE half (`crate::yarn_magnitude`):
1404                // llama.cpp multiplies the rotated channels of q and k
1405                // by `get_mscale(factor, 1) / get_mscale(factor,
1406                // log_mul)` on top of `rope.scaling.attn_factor`
1407                // (`llama-context.cpp:196-231` with ggml's `rope_yarn`
1408                // term cancelled), and frink applied only the key.
1409                // Folded into the same field so it reaches the CPU
1410                // helper and the Metal `mscale` uniform through one
1411                // value. Gated on the same `Some(scaling)` as the
1412                // frequency half, so a file frink does not rewrite
1413                // (no `original_context_length`) takes neither half.
1414                rope_attn_factor *= crate::yarn_magnitude::yarn_attn_magnitude(
1415                    scaling.factor,
1416                    crate::yarn_magnitude::yarn_log_mul_for(
1417                        &arch,
1418                        metadata_f32_any(file, &[key("rope.scaling.yarn_log_multiplier")]),
1419                    ),
1420                );
1421                let rotary_dim = rope_dim.unwrap_or(head_dim);
1422                if rotary_dim == 0 || !rotary_dim.is_multiple_of(2) {
1423                    best_effort_fields.push(
1424                        "rope_freqs (YaRN declared but the rotary width is odd; scaling not applied)",
1425                    );
1426                    rope_freqs
1427                } else {
1428                    let yarn =
1429                        frink_core::attention::yarn_freq_factors(scaling, rotary_dim, rope_theta);
1430                    match rope_freqs {
1431                        None => Some(yarn),
1432                        Some(own) if own.len() == yarn.len() => {
1433                            Some(own.iter().zip(yarn.iter()).map(|(a, b)| a * b).collect())
1434                        }
1435                        Some(own) => {
1436                            best_effort_fields.push(
1437                                "rope_freqs (YaRN declared alongside a per-band factor tensor of a \
1438                                 different width; the file's own tensor is used unscaled)",
1439                            );
1440                            Some(own)
1441                        }
1442                    }
1443                }
1444            }
1445        };
1446
1447        // The SWA half of the split. llama.cpp defaults
1448        // `rope_freq_scale_train_swa` to `1.0f`
1449        // (`src/llama-hparams.h:129`) and only the architectures in
1450        // `swa_rope_scale_follows_model` assign it from
1451        // `rope_freq_scale_train`; `get_rope_freq_scale`
1452        // (`llama-model.cpp:2033-2035`) then picks between them per
1453        // layer. `gemma3.cpp` is not on that list and its converter
1454        // writes the FULL-ATTENTION factor
1455        // (`conversion/base.py:1222-1230`), so a Gemma-3 4B/12B/27B was
1456        // rotating five layers in six at `p/8` where llama.cpp rotates
1457        // at `p`.
1458        //
1459        // "No scaling" is spelled as an all-ones divisor vector, which
1460        // is what dividing by nothing is, so the sliding layers need no
1461        // second code path anywhere downstream.
1462        // With TWO rotary widths, one divisor vector cannot serve both
1463        // kinds of layer. `step35.cpp:247` passes NO factors to its
1464        // sliding layers (`crate::swa_geometry::swa_layers_drop_rope_
1465        // factors`), so for it the full layers take the first
1466        // `rope_dim/2` bands of the tensor -- ggml reads only that many
1467        // -- and the sliding layers divide by nothing at their own
1468        // width; every other architecture is refused by name, because
1469        // nothing upstream says which layers would take which.
1470        if rope_freqs.is_some() {
1471            if let Some(reason) =
1472                crate::swa_geometry::two_widths_with_factors_refusal(&arch, widths)
1473            {
1474                return Err(LoadError::UnsupportedFeature(arch.clone(), reason));
1475            }
1476        }
1477        let rope_freqs = rope_freqs
1478            .map(|full| -> Result<crate::config::RopeFreqs, LoadError> {
1479                if let Some(swa_width) = rope_dim_swa {
1480                    let full_width = rope_dim.unwrap_or(head_dim);
1481                    if full.len() < full_width / 2 {
1482                        return Err(LoadError::UnsupportedFeature(
1483                            arch.clone(),
1484                            format!(
1485                                "rope_freqs.weight has {} bands; the full-attention layers rotate \
1486                                 {full_width} dims and need {}",
1487                                full.len(),
1488                                full_width / 2
1489                            ),
1490                        ));
1491                    }
1492                    let full: Vec<f32> = full[..full_width / 2].to_vec();
1493                    return Ok(crate::config::RopeFreqs {
1494                        full,
1495                        swa: Some(vec![1.0; swa_width / 2]),
1496                    });
1497                }
1498                let swa = (sliding_window.is_some()
1499                    && !crate::capability::swa_rope_scale_follows_model(&arch))
1500                .then(|| rope_freqs_unscaled.unwrap_or_else(|| vec![1.0; full.len()]))
1501                .filter(|swa| *swa != full);
1502                Ok(crate::config::RopeFreqs { full, swa })
1503            })
1504            .transpose()?;
1505
1506        // RoPE layout comes from the capability registry above (fail-
1507        // closed). Getting this wrong for `llama` (needs Norm) was the
1508        // real root cause of the Llama-3.1-8B early-stop/wrong-logits bug.
1509
1510        if best_effort_fields.is_empty() {
1511            best_effort_fields.push(
1512                "none -- every field above was read directly from this file's own GGUF metadata",
1513            );
1514        }
1515
1516        // LAST, deliberately. The generic path is a GUESS, so it has to
1517        // be opted into rather than fallen onto: it assumes plain GQA
1518        // with no ALiBi, no learned position embeddings and no
1519        // per-layer rope skipping, and that assumption was already
1520        // wrong for gpt2, mpt, refact, bloom and jais.
1521        //
1522        // But it runs AFTER every architecture-specific refusal, so a
1523        // checkpoint with a NAMED problem still reports that problem.
1524        // Checking first would have replaced "this uses ALiBi" with
1525        // "this is unaudited", which is true and much less useful.
1526        if matches!(
1527            arch_profile.path,
1528            crate::capability::ArchPath::GenericGqa { .. }
1529        ) && !crate::capability::is_audited_generic(&arch)
1530            && !matches!(
1531                std::env::var("FRINK_ALLOW_UNAUDITED_ARCH").ok().as_deref(),
1532                Some("1") | Some("true") | Some("on")
1533            )
1534        {
1535            return Err(LoadError::UnauditedArchitecture(
1536                arch.clone(),
1537                rope_layout,
1538                crate::capability::unaudited_refusal_detail(&arch),
1539            ));
1540        }
1541
1542        Ok(ModelConfig {
1543            name,
1544            n_layers,
1545            n_mtp_blocks: trunk.n_mtp_blocks,
1546            layer_loops,
1547            skip_stream: crate::skip_stream::has_skip_stream(&arch),
1548            parallel_ssm: crate::mamba2::parallel_with_attention(&arch),
1549            swa_chunked,
1550            weightless_qk_norm: crate::weightless_qk_norm::weightless_qk_norm(&arch, n_experts),
1551            hidden_dim,
1552            n_heads,
1553            n_kv_heads,
1554            head_dim,
1555            v_head_dim,
1556            vocab_size,
1557            rope_theta,
1558            rms_norm_eps,
1559            // `crate::norm::POST_NORM_EPS_LITERAL`: the architecture's, which
1560            // is the model's for all but one graph of 155.
1561            post_norm_eps: crate::norm::post_norm_eps(&arch, rms_norm_eps),
1562            // No GGUF file encodes a hybrid KDA/Gated-MLA attention
1563            // topology today; every real checkpoint loaded this way
1564            // runs the standard Gqa path.
1565            attention: crate::config::AttentionKind::Gqa,
1566            sliding_window,
1567            swa_layers,
1568            // llama.cpp's per-layer `use_rope`. Fed the POST-gate window
1569            // answer (`sliding_window`, not the raw key), because
1570            // `exaone4` decides both off the same layer count and the
1571            // two must not be able to disagree.
1572            rope_layers: if rope_switched_off {
1573                crate::rope_layers::RopeLayers::Never
1574            } else if let Some(mask) = rope_pattern.clone() {
1575                // The FILE's answer, for the one architecture that
1576                // reads the key (`rope_layers::ROPE_PATTERN_READERS`).
1577                crate::rope_layers::RopeLayers::FileMask(mask)
1578            } else {
1579                crate::rope_layers::rope_layers(
1580                    &arch,
1581                    n_layers,
1582                    sliding_window.is_some(),
1583                    n_dense_leading_layers,
1584                )
1585            },
1586            router_input: crate::router_input::router_input(&arch),
1587            block_sub_norms: crate::sub_norms::block_sub_norms(&arch),
1588            parallel_residual: crate::parallel_residual::model_has_parallel_layer(
1589                file, &arch, n_layers,
1590            ),
1591            learned_positions: crate::position_embd::learned_positions(&arch),
1592            attn_value_scale: crate::attn_value_scale::resolve_attn_value_scale(
1593                &arch,
1594                file.metadata_f32(&key("attention.value_scale")),
1595            ),
1596            alibi_max_bias: crate::alibi::max_alibi_bias(
1597                &arch,
1598                n_layers,
1599                file.metadata_f32(&key("attention.max_alibi_bias")),
1600            ),
1601            layer_shapes,
1602            moe: MoeLayerConfig {
1603                n_experts: n_experts.max(1),
1604                n_experts_active,
1605                n_shared_experts,
1606                hidden_dim,
1607                expert_ffn_dim,
1608                gating,
1609                norm_topk_prob,
1610                expert_group_count: metadata_u64_any(file, &[key("expert_group_count")])
1611                    .map(|v| v as usize)
1612                    .filter(|&c| c > 1),
1613                expert_group_used_count: metadata_u64_any(file, &[key("expert_group_used_count")])
1614                    .map(|v| v as usize)
1615                    .filter(|&c| c > 0),
1616                expert_weights_scale,
1617                routed_weight_before_ffn: crate::routed_weight_site::weight_before_ffn(&arch),
1618            },
1619            n_dense_leading_layers,
1620            moe_interleave_step,
1621            norm_function,
1622            rope_freqs,
1623            rope_layout,
1624            qk_norm_style,
1625            attn_logit_softcap,
1626            final_logit_softcap,
1627            embedding_scale,
1628            residual_scale: multipliers.residual_scale,
1629            normed_residual_scale: multipliers.normed_residual_scale,
1630            clamp_kqv,
1631            attn_temperature,
1632            logit_multiplier: multipliers.logit_multiplier,
1633            attention_scale,
1634            rope_attn_factor,
1635            rope_dim,
1636            rope_dim_swa,
1637            rope_freqs_long,
1638            rope_freqs_short,
1639            rope_orig_ctx,
1640            rope_theta_swa,
1641            ffn_activation,
1642            best_effort_fields: Box::leak(best_effort_fields.into_boxed_slice()),
1643        })
1644    }
1645}
1646
1647impl crate::sampling::RecommendedSampling {
1648    /// The sampling a GGUF recommends for itself, from the
1649    /// `general.sampling.*` metadata keys llama.cpp's converter writes
1650    /// when the source checkpoint carried a `generation_config.json`.
1651    ///
1652    /// This is the GGUF half of FreeToken's `load_generation_sampling`
1653    /// (`python/freetoken/utils/hf.py:92`), which checks the GGUF
1654    /// metadata *first* and only falls back to a `generation_config.json`
1655    /// sidecar for non-GGUF checkpoints -- a GGUF is a single file and
1656    /// has no sidecar to read.
1657    ///
1658    /// Key names are llama.cpp's own (`general.sampling.temp`, not
1659    /// `temperature`). Each key is independent: a file that names only
1660    /// `top_k` recommends only `top_k`, and the two fields it did not
1661    /// mention stay `None` so the server's own defaults keep speaking
1662    /// for them.
1663    ///
1664    /// `temp` / `top_p` are read as float *or* integer, because a
1665    /// converter that wrote `temp = 1` stores a GGUF integer and
1666    /// dropping that value would silently serve the checkpoint greedy --
1667    /// the exact repetition-loop failure the recommendation exists to
1668    /// prevent.
1669    pub fn from_gguf(file: &impl TensorSource) -> Self {
1670        let number = |k: &str| -> Option<f32> {
1671            file.metadata(k)
1672                .and_then(|v| v.as_f32().or_else(|| v.as_u64().map(|u| u as f32)))
1673        };
1674        crate::sampling::RecommendedSampling {
1675            temperature: number("general.sampling.temp"),
1676            top_p: number("general.sampling.top_p"),
1677            top_k: file
1678                .metadata("general.sampling.top_k")
1679                .and_then(|v| v.as_u64())
1680                .map(|v| v as usize),
1681        }
1682    }
1683}
1684
1685/// The `linear` RoPE scaling factor, if this file declares one.
1686///
1687/// Deliberately separate from [`yarn_scaling_from_gguf`]: YaRN needs an
1688/// original context length and per-band betas, and linear needs neither.
1689/// Any factor at or below one is not a correction, and is treated as
1690/// absent rather than applied as a no-op.
1691fn linear_scaling_from_gguf(file: &impl TensorSource, arch: &str) -> Option<f32> {
1692    let key = |suffix: &str| format!("{arch}.{suffix}");
1693    let scaling_type = file.metadata_str(&key("rope.scaling.type"))?;
1694    if !scaling_type.eq_ignore_ascii_case("linear") {
1695        return None;
1696    }
1697    metadata_f32_any(file, &[key("rope.scaling.factor")]).filter(|f| f.is_finite() && *f > 1.0)
1698}
1699
1700/// The YaRN RoPE scaling a GGUF declares, or `None` when this file
1701/// declares none that changes the rotation.
1702///
1703/// llama.cpp's key names (`llama-arch.cpp`
1704/// `LLM_KV_ROPE_SCALING_TYPE` / `_FACTOR`): `<arch>.rope.scaling.type`
1705/// is a string (`"none"`, `"linear"`, `"yarn"`, `"longrope"`) and
1706/// `<arch>.rope.scaling.factor` the ratio of served to trained context.
1707/// `beta_fast` / `beta_slow` are read from both the plain and the
1708/// `yarn_`-prefixed spelling and otherwise fall back to the reference's
1709/// own defaults (32.0 / 1.0), which is what a real checkpoint relies on
1710/// -- almost none of them write those two keys.
1711///
1712/// `None` is returned for every case where applying YaRN would be a
1713/// guess or a no-op rather than a correction, so that no checkpoint's
1714/// rotation moves without the file having asked for it:
1715///
1716/// * a scaling type other than `yarn` (`linear` divides positions,
1717///   `longrope` rides the `rope_factors_long`/`_short` tensors this
1718///   loader already reads -- neither is this rewrite, and treating them
1719///   as YaRN would rope them wrong in a *new* way instead of leaving
1720///   them as they are),
1721/// * a missing, non-finite or `<= 1.0` factor (the reference's own
1722///   `get_mscale` treats `scale <= 1` as unscaled, and a factor of 1.0
1723///   makes every band's divisor exactly 1.0 anyway),
1724/// * a missing `rope.scaling.original_context_length` -- the trained
1725///   context is what the correction range is measured against, and
1726///   inventing one (say, from `context_length`, which on a YaRN file is
1727///   the *extended* length) would put the ramp in the wrong place and
1728///   quietly rope the checkpoint at frequencies nobody trained.
1729pub(crate) fn yarn_scaling_from_gguf(
1730    file: &impl TensorSource,
1731    arch: &str,
1732    orig_ctx: Option<usize>,
1733) -> Option<frink_core::attention::YarnScaling> {
1734    let key = |suffix: &str| format!("{arch}.{suffix}");
1735    let scaling_type = file.metadata_str(&key("rope.scaling.type"))?;
1736    if !scaling_type.eq_ignore_ascii_case("yarn") {
1737        return None;
1738    }
1739    let factor = metadata_f32_any(file, &[key("rope.scaling.factor")])
1740        .filter(|f| f.is_finite() && *f > 1.0)?;
1741    let orig_max_pos = orig_ctx?;
1742    let beta = |suffix: &str, default: f32| -> f32 {
1743        metadata_f32_any(
1744            file,
1745            &[
1746                key(&format!("rope.scaling.{suffix}")),
1747                key(&format!("rope.scaling.yarn_{suffix}")),
1748            ],
1749        )
1750        .filter(|v| v.is_finite() && *v > 0.0)
1751        .unwrap_or(default)
1752    };
1753    // `llama-hparams.h:137` seeds `yarn_beta_fast = 32.0f` for every
1754    // architecture and `grok.cpp:5` reseeds it to 8.0 before the key is
1755    // read; the table that holds Grok's other defaults holds that one
1756    // too, so it is not a second literal here.
1757    let beta_fast_default = crate::scalar_multipliers::multiplier_support(arch)
1758        .defaults
1759        .yarn_beta_fast()
1760        .unwrap_or(32.0);
1761    Some(frink_core::attention::YarnScaling {
1762        factor,
1763        beta_fast: beta("beta_fast", beta_fast_default),
1764        beta_slow: beta("beta_slow", 1.0),
1765        orig_max_pos,
1766        // No GGUF key carries the reference's `truncate` flag, and its
1767        // default is `true`; a file that wanted the fractional range
1768        // would have no way to say so here.
1769        truncate: true,
1770    })
1771}
1772
1773pub(crate) fn find_info<'a>(
1774    file: &'a impl TensorSource,
1775    name: &str,
1776) -> Result<&'a TensorInfo, LoadError> {
1777    file.find_tensor(name)
1778        .ok_or_else(|| LoadError::Gguf(GgufError::TensorNotFound(name.to_string())))
1779}
1780
1781/// Like `load_f32_vec`, but for tensors that only exist on some
1782/// checkpoints (e.g. `attn_q_norm`/`attn_k_norm` -- OLMoE-style
1783/// per-projection QK-RMSNorm applied to the full q_proj/k_proj output
1784/// before RoPE, confirmed against `OlmoeAttention.forward` in
1785/// `transformers/models/olmoe/modeling_olmoe.py`: `q_norm(q_proj(x))`,
1786/// `k_norm(k_proj(x))`, both plain RMSNorm over the whole projected
1787/// width, not per-head). Absent for every other preset/fixture this
1788/// loader already handles -- `None` there is correct, not a missing
1789/// feature.
1790/// Loads the four gpt-oss-only side-table tensors for one layer, and
1791/// checks that the fifth, the attention sinks, was loaded onto the
1792/// layer's [`AttnWeights`] by the generic tensor-presence read.
1793///
1794/// Every one of them is **required**: a gpt-oss checkpoint that is
1795/// missing any of these is not a gpt-oss checkpoint frink can run, and
1796/// quietly substituting zeros would reintroduce exactly the
1797/// silently-wrong-graph failure this path exists to remove. The lengths
1798/// are asserted against the config for the same reason -- a bias of the
1799/// wrong width would otherwise be applied to a `zip`-truncated prefix
1800/// and produce a plausible, wrong answer.
1801///
1802/// Shapes follow `src/models/openai-moe.cpp::load_arch_tensors`:
1803/// `attn_sinks {n_head}` (`:44`, flags `0`, so REQUIRED there too),
1804/// `attn_output.bias {n_embd}`, `ffn_gate_inp.bias {n_expert}`,
1805/// `ffn_{gate,up}_exps.bias {n_ff_exp, n_expert}`,
1806/// `ffn_down_exps.bias {n_embd, n_expert}`. GGUF stores the fastest
1807/// dimension first, so the 2-D bias tensors arrive expert-major and
1808/// split by simple chunking.
1809fn load_gpt_oss_layer(
1810    file: &impl TensorSource,
1811    l: usize,
1812    config: &ModelConfig,
1813    sinks_loaded: bool,
1814) -> Result<crate::decoder::GptOssLayer, LoadError> {
1815    let n_experts = config.moe.n_experts;
1816    let ff = config.moe.expert_ffn_dim;
1817
1818    let want = |name: &str, got: usize, expect: usize| -> Result<(), LoadError> {
1819        if got == expect {
1820            Ok(())
1821        } else {
1822            Err(LoadError::UnsupportedFeature(
1823                config.name.to_string(),
1824                format!("{name} has {got} elements, expected {expect}"),
1825            ))
1826        }
1827    };
1828
1829    if !sinks_loaded {
1830        return Err(LoadError::UnsupportedFeature(
1831            config.name.to_string(),
1832            format!(
1833                "blk.{l}.attn_sinks.weight is missing; gpt-oss requires it \
1834                 (src/models/openai-moe.cpp:44) and llama.cpp refuses the file without it"
1835            ),
1836        ));
1837    }
1838    // `attn_output.bias` is `AttnWeights::o_bias` now, read by
1839    // `crate::proj_bias` (gpt-oss is a REQUIRED row of its table).
1840    let router_bias = load_f32_vec(file, &format!("blk.{l}.ffn_gate_inp.bias"))?;
1841    want(
1842        &format!("blk.{l}.ffn_gate_inp.bias"),
1843        router_bias.len(),
1844        n_experts,
1845    )?;
1846
1847    let gate_b = load_f32_vec(file, &format!("blk.{l}.ffn_gate_exps.bias"))?;
1848    want(
1849        &format!("blk.{l}.ffn_gate_exps.bias"),
1850        gate_b.len(),
1851        n_experts * ff,
1852    )?;
1853    let up_b = load_f32_vec(file, &format!("blk.{l}.ffn_up_exps.bias"))?;
1854    want(
1855        &format!("blk.{l}.ffn_up_exps.bias"),
1856        up_b.len(),
1857        n_experts * ff,
1858    )?;
1859    let down_b = load_f32_vec(file, &format!("blk.{l}.ffn_down_exps.bias"))?;
1860    want(
1861        &format!("blk.{l}.ffn_down_exps.bias"),
1862        down_b.len(),
1863        n_experts * config.hidden_dim,
1864    )?;
1865
1866    let expert_bias = (0..n_experts)
1867        .map(|e| frink_moe::ExpertBias {
1868            gate: gate_b[e * ff..(e + 1) * ff].to_vec(),
1869            up: up_b[e * ff..(e + 1) * ff].to_vec(),
1870            down: down_b[e * config.hidden_dim..(e + 1) * config.hidden_dim].to_vec(),
1871        })
1872        .collect();
1873
1874    Ok(crate::decoder::GptOssLayer {
1875        router_bias,
1876        expert_bias,
1877    })
1878}
1879
1880/// `blk.N.attn_sinks.weight` when the file carries it, checked to be
1881/// one logit per query head of THIS layer (`{n_head}` in every graph
1882/// that creates it: `openai-moe.cpp:44`, `mimo2.cpp:58`).
1883///
1884/// Optional here because that is what the tensor's consumers make it:
1885/// `build_attn_mha` takes a nullable `sinks` and `mimo2.cpp:58` creates
1886/// it `TENSOR_NOT_REQUIRED`. gpt-oss, which requires it, checks the
1887/// result where its side table loads.
1888fn load_attn_sinks(
1889    file: &impl TensorSource,
1890    l: usize,
1891    n_heads: usize,
1892) -> Result<Option<Vec<f32>>, LoadError> {
1893    let name = format!("blk.{l}.attn_sinks.weight");
1894    let Some(sinks) = load_f32_vec_optional(file, &name)? else {
1895        return Ok(None);
1896    };
1897    if sinks.len() != n_heads {
1898        return Err(LoadError::UnsupportedFeature(
1899            name,
1900            format!(
1901                "attention sinks are one logit per query head; this layer has {n_heads} heads \
1902                 and the tensor {} entries",
1903                sinks.len()
1904            ),
1905        ));
1906    }
1907    Ok(Some(sinks))
1908}
1909
1910pub(crate) fn load_f32_vec_optional(
1911    file: &impl TensorSource,
1912    name: &str,
1913) -> Result<Option<Vec<f32>>, LoadError> {
1914    if file.find_tensor(name).is_none() {
1915        return Ok(None);
1916    }
1917    Ok(Some(load_f32_vec(file, name)?))
1918}
1919
1920/// Slice `n` rows starting at `start` out of a quantized matrix without
1921/// dequantizing: every `Quantized` kind stores one interleaved block
1922/// buffer per row (fixed `row_bytes`), so a row range is a contiguous
1923/// byte range. Mapped sources stay zero-copy (sub-range of the same
1924/// mmap); other backings get an owned copy. Returns `None` for non-
1925/// quantized matrices (F32 / MXFP4) -- callers fall back to dequant.
1926pub(crate) fn slice_quantized_rows(
1927    m: &WeightMatrix,
1928    start: usize,
1929    n: usize,
1930) -> Option<WeightMatrix> {
1931    // A folded fused projection splits into folded parts sharing ONE
1932    // fold: the input transform is the same for q, k and v, and
1933    // `apply_gpu_multi` recognises the shared `Arc`.
1934    if let WeightMatrix::Folded { base, fold } = m {
1935        let mut part = slice_quantized_rows(base, start, n)?;
1936        part.fold_hadamard(fold.clone());
1937        return Some(part);
1938    }
1939    let WeightMatrix::Quantized {
1940        data,
1941        rows,
1942        cols,
1943        kind,
1944    } = m
1945    else {
1946        return None;
1947    };
1948    let total = data.len();
1949    if *rows == 0 || total % *rows != 0 || start + n > *rows {
1950        return None;
1951    }
1952    let row_bytes = total / *rows;
1953    let (b0, b1) = (start * row_bytes, (start + n) * row_bytes);
1954    let bytes = match data {
1955        WeightBytes::Mapped { mmap, range } => WeightBytes::Mapped {
1956            mmap: mmap.clone(),
1957            range: range.start + b0..range.start + b1,
1958        },
1959        other => WeightBytes::Owned(other.as_slice()[b0..b1].to_vec()),
1960    };
1961    Some(WeightMatrix::Quantized {
1962        data: bytes,
1963        rows: n,
1964        cols: *cols,
1965        kind: *kind,
1966    })
1967}
1968
1969/// Dense-layer FFN tensors: standard gate/up/down, Phi-3 fused
1970/// `ffn_up` with `2 * ffn_dim` rows and no separate gate, the UNGATED
1971/// two-matrix FFN (`FfnActivation::ReluSqr`), or nothing at all for an
1972/// FFN-free layer (`ffn_dim == 0`).
1973///
1974/// `ffn_dim` is THIS layer's width (`ModelConfig::layer_shape`), which
1975/// is the model's for every architecture but the per-layer ones.
1976fn load_dense_expert(
1977    file: &impl TensorSource,
1978    layer: usize,
1979    config: &ModelConfig,
1980    ffn_dim: usize,
1981) -> Result<ExpertWeights, LoadError> {
1982    if ffn_dim == 0 {
1983        return Ok(crate::layer_shapes::absent_ffn(config.hidden_dim));
1984    }
1985    let gate_name = format!("blk.{layer}.ffn_gate.weight");
1986    let up_name = format!("blk.{layer}.ffn_up.weight");
1987    let down_name = format!("blk.{layer}.ffn_down.weight");
1988    if config.ffn_is_ungated() {
1989        // `arcee.cpp:39-40` and `apertus.cpp:45-46` create `ffn_up` and
1990        // `ffn_down` and no gate; a file carrying one describes a graph
1991        // this architecture does not compute, and would otherwise be
1992        // left as an unread tensor with a less specific message.
1993        if file.find_tensor(&gate_name).is_some() {
1994            return Err(LoadError::UnsupportedFeature(
1995                config.name.to_string(),
1996                format!(
1997                    "{gate_name} is present but this architecture's FFN is ungated \
1998                     ({:?}: LLM_FFN_RELU_SQR under LLM_FFN_SEQ with a null gate, \
1999                     arcee.cpp:123-128, or ggml_xielu over ffn_up alone, apertus.cpp:129-142)",
2000                    config.ffn_activation
2001                ),
2002            ));
2003        }
2004        let up = load_weight_matrix(file, &up_name)?;
2005        if up.rows() != ffn_dim {
2006            return Err(LoadError::UnsupportedFeature(
2007                config.name.to_string(),
2008                format!(
2009                    "{up_name} has {} rows; the ungated FFN expects feed_forward_length = \
2010                     {ffn_dim}",
2011                    up.rows()
2012                ),
2013            ));
2014        }
2015        // The alias: the same tensor read again. A zero-copy view of the
2016        // same bytes for a quantized mmapped file; an owned widening for
2017        // an F32/F16 one. See `FfnActivation::ReluSqr` for why the pair
2018        // is aliased rather than the struct given an `Option`.
2019        return Ok(ExpertWeights {
2020            gate: load_weight_matrix(file, &up_name)?,
2021            up,
2022            down: load_weight_matrix(file, &down_name)?,
2023        });
2024    }
2025    if file.find_tensor(&gate_name).is_some() {
2026        return Ok(ExpertWeights {
2027            gate: load_weight_matrix(file, &gate_name)?,
2028            up: load_weight_matrix(file, &up_name)?,
2029            down: load_weight_matrix(file, &down_name)?,
2030        });
2031    }
2032    // Phi-3 fused SwiGLU: up is [hidden, 2*ff], first half gate, second up.
2033    let fused = load_weight_matrix(file, &up_name)?;
2034    let ff = ffn_dim;
2035    if fused.rows() != 2 * ff {
2036        return Err(LoadError::UnsupportedFeature(
2037            config.name.to_string(),
2038            format!(
2039                "{up_name} has {} rows without a companion ffn_gate; \
2040                 expected fused SwiGLU with 2*ffn_dim = {} rows",
2041                fused.rows(),
2042                2 * ff
2043            ),
2044        ));
2045    }
2046    let cols = fused.cols();
2047    // Quantized fused gate+up: split by rows, no dequant (Metal-capable).
2048    if let (Some(gate), Some(up)) = (
2049        slice_quantized_rows(&fused, 0, ff),
2050        slice_quantized_rows(&fused, ff, ff),
2051    ) {
2052        return Ok(ExpertWeights {
2053            gate,
2054            up,
2055            down: load_weight_matrix(file, &down_name)?,
2056        });
2057    }
2058    let mut full = Vec::with_capacity(fused.rows() * cols);
2059    for r in 0..fused.rows() {
2060        full.extend_from_slice(&fused.dequant_row(r));
2061    }
2062    let gate = WeightMatrix::F32(Tensor::new(full[..ff * cols].to_vec(), vec![ff, cols]));
2063    let up = WeightMatrix::F32(Tensor::new(full[ff * cols..].to_vec(), vec![ff, cols]));
2064    Ok(ExpertWeights {
2065        gate,
2066        up,
2067        down: load_weight_matrix(file, &down_name)?,
2068    })
2069}
2070
2071/// Widen a raw plain-float tensor (`F32` / `F16` / `BF16`) to `f32`.
2072///
2073/// The three unquantized element types are handled identically at every
2074/// call site (eager widening to an owned buffer -- none of them has a
2075/// block structure a fused dot kernel could exploit), and each of the
2076/// seven GGUF loaders used to inline the same two-way match. F16 had no
2077/// arm in any of them, which made every `*-f16.gguf` a hard
2078/// `UnsupportedDtype` even though the type was parsed and sized.
2079pub(crate) fn widen_plain_float(
2080    dtype: GgmlType,
2081    raw: &[u8],
2082    name: &str,
2083) -> Result<Vec<f32>, LoadError> {
2084    match dtype {
2085        GgmlType::F32 => {
2086            let mut out = Vec::with_capacity(raw.len() / 4);
2087            for chunk in raw.as_chunks::<4>().0 {
2088                out.push(f32::from_le_bytes(*chunk));
2089            }
2090            Ok(out)
2091        }
2092        GgmlType::F16 => frink_quant::dequant_f16(raw)
2093            .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::F16)),
2094        GgmlType::BF16 => frink_quant::dequant_bf16(raw)
2095            .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::BF16)),
2096        // MXFP4 is accepted as a weight matrix and as an MoE expert
2097        // tensor, and `WeightMatrix::dequant` already calls this
2098        // dequantizer, so refusing it here made a 1-D MXFP4 norm or
2099        // bias a hard load error on a checkpoint whose 2-D tensors of
2100        // the same type load fine. That contradicted this function's
2101        // own contract, which is to widen whatever the loaders accept.
2102        GgmlType::MXFP4 => frink_quant::dequant_mxfp4_gguf(raw)
2103            .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::MXFP4)),
2104        other => Err(LoadError::UnsupportedDtype(name.to_string(), other)),
2105    }
2106}
2107
2108pub(crate) fn load_f32_vec(file: &impl TensorSource, name: &str) -> Result<Vec<f32>, LoadError> {
2109    let info = find_info(file, name)?;
2110    let raw = file.tensor_bytes(name)?;
2111    match info.dtype {
2112        // MXFP4 rides with the plain floats because `widen_plain_float`
2113        // is where its arm already lives -- routing it here rather than
2114        // giving this table its own `dequant_mxfp4_gguf` call keeps ONE
2115        // MXFP4 arm in this file instead of two that can drift.
2116        //
2117        // It has to be in *both* tables' reach, and it was in neither's:
2118        // `load_weight_matrix` accepts MXFP4 as a 2-D weight and
2119        // `load_moe_expert_matrices` accepts it as an expert tensor, so
2120        // a checkpoint whose norms happen to be MXFP4 failed here with
2121        // `UnsupportedDtype` while its far larger tensors of the exact
2122        // same dtype loaded fine.
2123        GgmlType::F32 | GgmlType::F16 | GgmlType::BF16 | GgmlType::MXFP4 => {
2124            widen_plain_float(info.dtype, raw, name)
2125        }
2126        GgmlType::Q8_0 => frink_quant::dequant_q8_0(raw)
2127            .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::Q8_0)),
2128        GgmlType::Q4_0 => frink_quant::dequant_q4_0(raw)
2129            .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::Q4_0)),
2130        GgmlType::Q4K => frink_quant::dequant_q4_k(raw)
2131            .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::Q4K)),
2132        GgmlType::Q5K => frink_quant::dequant_q5_k(raw)
2133            .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::Q5K)),
2134        GgmlType::Q6K => frink_quant::dequant_q6_k(raw)
2135            .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::Q6K)),
2136        GgmlType::Q2K => frink_quant::dequant_q2_k(raw)
2137            .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::Q2K)),
2138        GgmlType::Q3K => frink_quant::dequant_q3_k(raw)
2139            .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::Q3K)),
2140        GgmlType::Q4_1 => frink_quant::dequant_q4_1(raw)
2141            .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::Q4_1)),
2142        GgmlType::Q5_0 => frink_quant::dequant_q5_0(raw)
2143            .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::Q5_0)),
2144        GgmlType::Q5_1 => frink_quant::dequant_q5_1(raw)
2145            .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::Q5_1)),
2146        GgmlType::Q8_1 => frink_quant::dequant_q8_1(raw)
2147            .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::Q8_1)),
2148        GgmlType::IQ4NL => frink_quant::dequant_iq4_nl(raw)
2149            .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::IQ4NL)),
2150        GgmlType::IQ4XS => frink_quant::dequant_iq4_xs(raw)
2151            .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::IQ4XS)),
2152        // The codebook-grid tiers. Rare on the 1-D tensors this
2153        // function widens (norms and biases are almost always F32),
2154        // but a dtype frink can decode should never be rejected here
2155        // just because the *other* dispatch table below knows it --
2156        // that split is how a supported format turns into a load
2157        // failure on the one checkpoint that uses it.
2158        GgmlType::IQ1S => frink_quant::dequant_iq1_s(raw)
2159            .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::IQ1S)),
2160        GgmlType::IQ1M => frink_quant::dequant_iq1_m(raw)
2161            .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::IQ1M)),
2162        GgmlType::IQ2XXS => frink_quant::dequant_iq2_xxs(raw)
2163            .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::IQ2XXS)),
2164        GgmlType::IQ2XS => frink_quant::dequant_iq2_xs(raw)
2165            .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::IQ2XS)),
2166        GgmlType::IQ2S => frink_quant::dequant_iq2_s(raw)
2167            .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::IQ2S)),
2168        GgmlType::IQ3XXS => frink_quant::dequant_iq3_xxs(raw)
2169            .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::IQ3XXS)),
2170        GgmlType::IQ3S => frink_quant::dequant_iq3_s(raw)
2171            .map_err(|_| LoadError::UnsupportedDtype(name.to_string(), GgmlType::IQ3S)),
2172        other => Err(LoadError::UnsupportedDtype(name.to_string(), other)),
2173    }
2174}
2175
2176/// Loads a 2D weight matrix, keeping Q8_0/Q4_0 tensors quantized (raw
2177/// bytes copied out, never dequantized) and only expanding truly F32
2178/// tensors. This is the memory- and bandwidth-saving path: for a
2179/// multi-billion-parameter checkpoint the difference between this and
2180/// "dequant everything on load" is the difference between fitting in
2181/// RAM and not.
2182pub(crate) fn load_weight_matrix(
2183    file: &impl TensorSource,
2184    name: &str,
2185) -> Result<WeightMatrix, LoadError> {
2186    let mut m = load_weight_matrix_unfolded(file, name)?;
2187    // A PrismML checkpoint folds a Hadamard rotation into the listed
2188    // weights (`crate::hadamard_fold`); the matrix carries the
2189    // activation-side transform so every `apply` undoes it.
2190    if let Some(fold) = crate::hadamard_fold::fold_for(file, name, m.cols())? {
2191        m.fold_hadamard(fold);
2192    }
2193    Ok(m)
2194}
2195
2196fn load_weight_matrix_unfolded(
2197    file: &impl TensorSource,
2198    name: &str,
2199) -> Result<WeightMatrix, LoadError> {
2200    let info = find_info(file, name)?;
2201    // GGUF's on-disk `ne[]` shape array is fastest-varying-dimension-first
2202    // (ggml convention), i.e. `[in_features, out_features]` for a 2D
2203    // weight matrix -- the *reverse* of the row-major `[rows, cols]` =
2204    // `[out_features, in_features]` order `WeightMatrix`/`matmul_f32`
2205    // need. Reversed here once so every consumer below gets the correct
2206    // orientation. Before this reversal existed, every 2D tensor in an
2207    // externally-produced GGUF file was silently loaded transposed -- a
2208    // real bug found by running a real downloaded checkpoint
2209    // (TinyLlama-1.1B-Chat, e.g. `attn_k.weight`'s real raw shape is
2210    // `[2048, 256]` = `[hidden_dim, kv_dim]` = `[in, out]`) -- found
2211    // as a real transposition bug affecting every externally-produced
2212    // GGUF file, caught by serving a real downloaded checkpoint.
2213    let shape: Vec<usize> = info.shape.iter().rev().map(|&d| d as usize).collect();
2214    // A ggml tensor's `ne[]` is always four long and trailing 1s are
2215    // implicit, so a GGUF writer is free to store a `[in, 1]` matrix
2216    // with `n_dims = 1`. llama.cpp reads it back as a matrix anyway --
2217    // `check_tensor_dims` compares each requested dimension against
2218    // `cur->ne[i]` and requires 1 for the dimensions the file does not
2219    // carry -- so a single-output projection is a 2-D weight there and
2220    // must be one here. Refusing it instead made a real checkpoint
2221    // unloadable: `cross-encoder/ms-marco-MiniLM-L6-v2` writes
2222    // `cls.output.weight` as `[384]`, i.e. the 1x384 relevance head
2223    // that `/v1/rerank` exists to run, and the whole route died at load
2224    // with "expected 2D".
2225    let (rows, cols) = match shape.as_slice() {
2226        [r, c] => (*r, *c),
2227        [c] => (1, *c),
2228        other => {
2229            return Err(LoadError::UnsupportedDtype(
2230                format!("{name} (expected 2D, got shape {other:?})"),
2231                info.dtype,
2232            ))
2233        }
2234    };
2235    // `shape` is what the file said; `[rows, cols]` is what the matrix
2236    // is. They differ exactly in the 1-D case above, and the `Tensor`
2237    // must carry the matrix shape or `apply` reads it as a vector.
2238    let shape = vec![rows, cols];
2239
2240    match info.dtype {
2241        // BF16 has no block/scale structure to keep quantized-in-place
2242        // the way Q4_0/Q8_0/K-quants do -- there's no fused dot kernel
2243        // that would make sense for a plain narrowed float, so it's
2244        // eagerly widened to an owned f32 Tensor exactly like F32
2245        // tensors already are.
2246        GgmlType::F32 | GgmlType::F16 | GgmlType::BF16 => {
2247            let data = load_f32_vec(file, name)?;
2248            Ok(WeightMatrix::F32(Tensor::new(data, shape)))
2249        }
2250        other => match quant_kind_for(other) {
2251            Some(kind) => {
2252                let (mmap, range) = file.tensor_mapped_range(name)?;
2253                #[cfg(feature = "metal")]
2254                frink_metal::gpu::register_weight_mmap(Arc::clone(&mmap));
2255                Ok(WeightMatrix::Quantized {
2256                    data: WeightBytes::Mapped { mmap, range },
2257                    rows,
2258                    cols,
2259                    kind,
2260                })
2261            }
2262            None => Err(LoadError::UnsupportedDtype(name.to_string(), other)),
2263        },
2264    }
2265}
2266
2267/// Splits a packed 3D MoE expert tensor `blk.N.ffn_{gate,up,down}_exps.weight`
2268/// (shape `[n_experts, out_dim, in_dim]`) into per-expert `WeightMatrix`es,
2269/// slicing raw bytes directly (quantized tensors stay quantized; block
2270/// boundaries never cross expert boundaries since `in_dim` is a whole
2271/// number of quantization blocks). Matches llama.cpp/ik_llama.cpp layout
2272/// confirmed on real OLMoE and Qwen2-MoE GGUF checkpoints.
2273pub(crate) fn split_expert_tensor(
2274    file: &impl TensorSource,
2275    name: &str,
2276    n_experts: usize,
2277) -> Result<Vec<WeightMatrix>, LoadError> {
2278    let info = find_info(file, name)?;
2279    // Real raw shape is `[in_dim, out_dim, n_experts]` (ggml's
2280    // fastest-first `ne[]` order -- see `load_weight_matrix`'s doc
2281    // comment for the confirmed 2D case this generalizes from). `n_experts`
2282    // is the slowest-varying (last, i.e. outermost/most-major) dimension,
2283    // so each expert's `out_dim*in_dim` block is contiguous with experts
2284    // back-to-back in the mmap.
2285    if info.shape.len() != 3 || info.shape[2] as usize != n_experts {
2286        let file_experts = info.shape.last().map(|&d| d as usize).unwrap_or(0);
2287        return Err(LoadError::ExpertCountMismatch(
2288            name.to_string(),
2289            file_experts,
2290            n_experts,
2291        ));
2292    }
2293    let out_dim = info.shape[1] as usize;
2294    let in_dim = info.shape[0] as usize;
2295    let raw = file.tensor_bytes(name)?;
2296
2297    match info.dtype {
2298        GgmlType::F32 | GgmlType::F16 | GgmlType::BF16 => {
2299            let all = crate::loader::widen_plain_float(info.dtype, raw, name)?;
2300            let per_expert = out_dim * in_dim;
2301            Ok((0..n_experts)
2302                .map(|e| {
2303                    WeightMatrix::F32(Tensor::new(
2304                        all[e * per_expert..(e + 1) * per_expert].to_vec(),
2305                        vec![out_dim, in_dim],
2306                    ))
2307                })
2308                .collect())
2309        }
2310        other => match quant_kind_for(other) {
2311            Some(kind) => {
2312                let (mmap, full_range) = file.tensor_mapped_range(name)?;
2313                #[cfg(feature = "metal")]
2314                frink_metal::gpu::register_weight_mmap(Arc::clone(&mmap));
2315                let bytes_per_expert = raw.len() / n_experts;
2316                Ok((0..n_experts)
2317                    .map(|e| WeightMatrix::Quantized {
2318                        data: WeightBytes::Mapped {
2319                            mmap: Arc::clone(&mmap),
2320                            range: (full_range.start + e * bytes_per_expert)
2321                                ..(full_range.start + (e + 1) * bytes_per_expert),
2322                        },
2323                        rows: out_dim,
2324                        cols: in_dim,
2325                        kind,
2326                    })
2327                    .collect())
2328            }
2329            None => Err(LoadError::UnsupportedDtype(name.to_string(), other)),
2330        },
2331    }
2332}
2333
2334/// When every routed expert is mmap-backed with a Metal simdgroup-GEMM
2335/// kind and back-to-back slices, record the combined gate/up/down planes
2336/// for Metal packed MoE. Gate/up/down may differ in kind (e.g. Q4_K /
2337/// Q4_K / Q8_0) but must be uniform across experts per role.
2338#[cfg(feature = "metal")]
2339fn try_build_moe_packed_q4_planes(experts: &[ExpertWeights]) -> Option<MoePackedQ4Planes> {
2340    use frink_core::weight_matrix::{QuantKind, WeightBytes};
2341    use std::sync::Arc;
2342
2343    if experts.is_empty() {
2344        return None;
2345    }
2346
2347    fn mapped_sg(m: &WeightMatrix) -> Option<(WeightBytes, usize, &'static str)> {
2348        match m {
2349            WeightMatrix::Quantized {
2350                data: WeightBytes::Mapped { mmap, range },
2351                rows,
2352                kind,
2353                ..
2354            } => {
2355                let kind_str = match kind {
2356                    QuantKind::Q4_0 => "Q4_0",
2357                    QuantKind::Q5_0 => "Q5_0",
2358                    QuantKind::Q4K => "Q4_K",
2359                    QuantKind::Q5K => "Q5_K",
2360                    QuantKind::Q6K => "Q6_K",
2361                    QuantKind::Q8_0 => "Q8_0",
2362                    QuantKind::IQ4XS => "IQ4_XS",
2363                    _ => return None,
2364                };
2365                let _ = frink_metal::gpu::mul_mm_sg_meta(kind_str)?;
2366                Some((
2367                    WeightBytes::Mapped {
2368                        mmap: Arc::clone(mmap),
2369                        range: range.clone(),
2370                    },
2371                    *rows,
2372                    kind_str,
2373                ))
2374            }
2375            _ => None,
2376        }
2377    }
2378
2379    let (gate0, ffn_rows, gate_kind) = mapped_sg(&experts[0].gate)?;
2380    let (up0, up_rows, up_kind) = mapped_sg(&experts[0].up)?;
2381    let (down0, hidden_rows, down_kind) = mapped_sg(&experts[0].down)?;
2382    if up_rows != ffn_rows {
2383        return None;
2384    }
2385    let WeightBytes::Mapped {
2386        mmap: gate_mmap,
2387        range: gate0_range,
2388    } = &gate0
2389    else {
2390        return None;
2391    };
2392    let WeightBytes::Mapped {
2393        mmap: up_mmap,
2394        range: up0_range,
2395    } = &up0
2396    else {
2397        return None;
2398    };
2399    let WeightBytes::Mapped {
2400        mmap: down_mmap,
2401        range: down0_range,
2402    } = &down0
2403    else {
2404        return None;
2405    };
2406
2407    let gate_stride = gate0_range.len();
2408    let up_stride = up0_range.len();
2409    let down_stride = down0_range.len();
2410    if gate_stride == 0 || up_stride == 0 || down_stride == 0 {
2411        return None;
2412    }
2413
2414    let n = experts.len();
2415    for (i, ex) in experts.iter().enumerate().skip(1) {
2416        let (g, fr, gk) = mapped_sg(&ex.gate)?;
2417        let (u, ur, uk) = mapped_sg(&ex.up)?;
2418        let (d, hr, dk) = mapped_sg(&ex.down)?;
2419        if gk != gate_kind || uk != up_kind || dk != down_kind {
2420            return None;
2421        }
2422        let WeightBytes::Mapped { mmap, range } = &g else {
2423            return None;
2424        };
2425        if fr != ffn_rows {
2426            return None;
2427        }
2428        if !Arc::ptr_eq(mmap, gate_mmap)
2429            || range.len() != gate_stride
2430            || range.start != gate0_range.start + i * gate_stride
2431        {
2432            return None;
2433        }
2434        let WeightBytes::Mapped { mmap, range } = &u else {
2435            return None;
2436        };
2437        if ur != ffn_rows
2438            || !Arc::ptr_eq(mmap, up_mmap)
2439            || range.len() != up_stride
2440            || range.start != up0_range.start + i * up_stride
2441        {
2442            return None;
2443        }
2444        let WeightBytes::Mapped { mmap, range } = &d else {
2445            return None;
2446        };
2447        if hr != hidden_rows
2448            || !Arc::ptr_eq(mmap, down_mmap)
2449            || range.len() != down_stride
2450            || range.start != down0_range.start + i * down_stride
2451        {
2452            return None;
2453        }
2454    }
2455
2456    Some(MoePackedQ4Planes::new(
2457        WeightBytes::Mapped {
2458            mmap: Arc::clone(gate_mmap),
2459            range: gate0_range.start..gate0_range.start + n * gate_stride,
2460        },
2461        WeightBytes::Mapped {
2462            mmap: Arc::clone(up_mmap),
2463            range: up0_range.start..up0_range.start + n * up_stride,
2464        },
2465        WeightBytes::Mapped {
2466            mmap: Arc::clone(down_mmap),
2467            range: down0_range.start..down0_range.start + n * down_stride,
2468        },
2469        gate_stride,
2470        up_stride,
2471        down_stride,
2472        n,
2473        ffn_rows,
2474        hidden_rows,
2475        gate_kind,
2476        up_kind,
2477        down_kind,
2478    ))
2479}
2480
2481/// One matrix's place inside a store-backed expert's combined byte
2482/// buffer (gate bytes, then up, then down, concatenated by
2483/// `GgufExpertSource::read_expert`).
2484#[derive(Debug, Clone, Copy)]
2485pub struct StoredMatrixSpec {
2486    pub offset: usize,
2487    pub len: usize,
2488    pub rows: usize,
2489    pub cols: usize,
2490    pub kind: QuantKind,
2491}
2492
2493/// Byte-range layout of one store-backed routed expert.
2494#[derive(Debug, Clone, Copy)]
2495pub struct StoredExpertLayout {
2496    pub gate: StoredMatrixSpec,
2497    pub up: StoredMatrixSpec,
2498    pub down: StoredMatrixSpec,
2499}
2500
2501impl StoredExpertLayout {
2502    pub fn total_bytes(&self) -> usize {
2503        self.gate.len + self.up.len + self.down.len
2504    }
2505
2506    /// Builds temporary zero-copy `WeightMatrix` views over a leased
2507    /// buffer. Each view's `WeightBytes::Shared` clone of the lease's
2508    /// `Arc` keeps the cache entry pinned for the view's lifetime.
2509    pub fn materialize(&self, lease: &frink_core::expert_store::ExpertLease) -> ExpertWeights {
2510        let mk = |spec: &StoredMatrixSpec| WeightMatrix::Quantized {
2511            data: WeightBytes::Shared {
2512                buf: lease.shared_buf(),
2513                range: spec.offset..spec.offset + spec.len,
2514            },
2515            rows: spec.rows,
2516            cols: spec.cols,
2517            kind: spec.kind,
2518        };
2519        ExpertWeights {
2520            gate: mk(&self.gate),
2521            up: mk(&self.up),
2522            down: mk(&self.down),
2523        }
2524    }
2525}
2526
2527/// [`ExpertSource`] over a (possibly sharded) GGUF checkpoint: each
2528/// expert's gate/up/down byte ranges are read positionally from the
2529/// owning shard file and concatenated, so a store miss touches exactly
2530/// that expert's bytes -- no mmap of the expert region, no shared seek
2531/// cursor.
2532pub struct GgufExpertSource {
2533    files: Vec<std::fs::File>,
2534    /// (layer, expert) -> the three (file index, offset, len) segments
2535    /// in gate/up/down order.
2536    segments: std::collections::HashMap<ExpertKey, [(usize, u64, usize); 3]>,
2537}
2538
2539impl ExpertSource for GgufExpertSource {
2540    fn expert_len(&self, key: ExpertKey) -> Option<usize> {
2541        self.segments
2542            .get(&key)
2543            .map(|segs| segs.iter().map(|&(_, _, len)| len).sum())
2544    }
2545
2546    fn read_expert(&self, key: ExpertKey) -> std::io::Result<Vec<u8>> {
2547        let segs = self
2548            .segments
2549            .get(&key)
2550            .ok_or_else(|| std::io::Error::new(std::io::ErrorKind::NotFound, format!("{key:?}")))?;
2551        let total: usize = segs.iter().map(|&(_, _, len)| len).sum();
2552        let mut buf = vec![0u8; total];
2553        let mut written = 0;
2554        for &(fi, offset, len) in segs {
2555            let dst = &mut buf[written..written + len];
2556            #[cfg(unix)]
2557            {
2558                use std::os::unix::fs::FileExt;
2559                self.files[fi].read_exact_at(dst, offset)?;
2560            }
2561            #[cfg(not(unix))]
2562            {
2563                use std::io::{Read, Seek, SeekFrom};
2564                let mut f = &self.files[fi];
2565                f.seek(SeekFrom::Start(offset))?;
2566                f.read_exact(dst)?;
2567            }
2568            written += len;
2569        }
2570        Ok(buf)
2571    }
2572}
2573
2574/// Collects the per-expert `(file, offset, len)` segments and layout
2575/// for one packed 3D expert tensor -- the store-backed counterpart of
2576/// `split_expert_tensor`, sharing its shape/offset math. Only
2577/// quantized dtypes are supported (an F32/BF16 expert tensor keeps the
2578/// resident path; the store exists for the quantized multi-hundred-GB
2579/// case).
2580/// One packed 3D expert tensor's store-backed description: the owning
2581/// shard index, each expert's `(offset, len)` within that shard file,
2582/// and the matrix spec shared by every expert's slice.
2583struct StoredTensorSpecs {
2584    shard: usize,
2585    per_expert: Vec<(u64, usize)>,
2586    spec: StoredMatrixSpec,
2587}
2588
2589fn stored_expert_specs(
2590    file: &ShardedGguf,
2591    name: &str,
2592    n_experts: usize,
2593) -> Result<Option<StoredTensorSpecs>, LoadError> {
2594    let info = find_info(file, name)?;
2595    if info.shape.len() != 3 || info.shape[2] as usize != n_experts {
2596        let file_experts = info.shape.last().map(|&d| d as usize).unwrap_or(0);
2597        return Err(LoadError::ExpertCountMismatch(
2598            name.to_string(),
2599            file_experts,
2600            n_experts,
2601        ));
2602    }
2603    let out_dim = info.shape[1] as usize;
2604    let in_dim = info.shape[0] as usize;
2605    let Some(kind) = quant_kind_for(info.dtype) else {
2606        return Ok(None); // F32/BF16 (or unsupported): resident fallback
2607    };
2608    let shard = file
2609        .tensor_shard_index(name)
2610        .expect("find_info succeeded, shard index must exist");
2611    // The mmap range of a tensor within a GgufFile IS its byte offset
2612    // range within that shard file (the mmap covers the whole file).
2613    let (_, full_range) = file.tensor_mapped_range(name)?;
2614    let total_len = full_range.end - full_range.start;
2615    let bytes_per_expert = total_len / n_experts;
2616    let per_expert: Vec<(u64, usize)> = (0..n_experts)
2617        .map(|e| {
2618            (
2619                (full_range.start + e * bytes_per_expert) as u64,
2620                bytes_per_expert,
2621            )
2622        })
2623        .collect();
2624    let spec = StoredMatrixSpec {
2625        offset: 0, // caller assigns the position within the combined buffer
2626        len: bytes_per_expert,
2627        rows: out_dim,
2628        cols: in_dim,
2629        kind,
2630    };
2631    Ok(Some(StoredTensorSpecs {
2632        shard,
2633        per_expert,
2634        spec,
2635    }))
2636}
2637
2638impl Decoder {
2639    /// Loads real weights from `path` for the given `config`. `config`
2640    /// supplies the architecture shape (layer count, head counts, MoE
2641    /// topology); tensor names are resolved against it using the
2642    /// llama.cpp naming convention described in the module docs.
2643    ///
2644    /// A `config.moe.n_experts <= 1` model is treated as dense: expert
2645    /// weights are read from the plain `blk.N.ffn_{gate,up,down}.weight`
2646    /// tensor names rather than the packed 3D `_exps` variant.
2647    pub fn from_gguf(
2648        path: impl AsRef<std::path::Path>,
2649        config: ModelConfig,
2650    ) -> Result<Self, LoadError> {
2651        Self::from_gguf_with_expert_cache(path, config, None)
2652    }
2653
2654    /// Like `from_gguf`, but with `expert_cache_bytes: Some(budget)`
2655    /// routed experts are NOT loaded resident: each layer holds only
2656    /// byte-range layouts, and expert bytes are read on demand through
2657    /// one bounded, lease-protected `ExpertStore` shared by every
2658    /// layer (a single global byte budget; see
2659    /// `frink_core::expert_store`). Dense layers, shared experts,
2660    /// attention, embeddings, and the output head stay resident/mapped
2661    /// exactly as before -- only routed experts stream. Layers whose
2662    /// expert tensors are F32/BF16 fall back to resident loading (the
2663    /// store exists for the quantized case). Output is bit-identical
2664    /// to the resident path -- same bytes, same kernels -- pinned by
2665    /// the roundtrip suite's equivalence test.
2666    pub fn from_gguf_with_expert_cache(
2667        path: impl AsRef<std::path::Path>,
2668        mut config: ModelConfig,
2669        expert_cache_bytes: Option<u64>,
2670    ) -> Result<Self, LoadError> {
2671        let path = path.as_ref();
2672        let file = ShardedGguf::open(path)?;
2673
2674        // gpt-oss carries four per-layer tensors the generic GQA layer
2675        // structs have no home for (its fifth, the attention sinks, is
2676        // `AttnWeights::sinks` and loads by tensor presence). That is
2677        // decided by the architecture string, so resolve it once here.
2678        // See `crate::decoder::GptOssWeights`.
2679        //
2680        // This used to be ONE flag with the norm-slot fact below,
2681        // `arch == "gpt-oss"`, standing for two unrelated facts.
2682        // Splitting them is what let `seed_oss` -- which shares the norm
2683        // slot and has none of the extra tensors -- be admitted without
2684        // also being handed attention sinks.
2685        let arch = file
2686            .metadata_str("general.architecture")
2687            .unwrap_or_default()
2688            .to_string();
2689        let is_gpt_oss = arch == "gpt-oss";
2690        // A `<projection>.scale` companion is a multiply llama.cpp
2691        // applies and frink does not; refused by name here, before
2692        // the unread-tensor gate can be talked past
2693        // (`crate::weight_scales`).
2694        crate::weight_scales::refuse_weight_scale_tensors(
2695            &arch,
2696            file.tensors().map(|(_, t)| t.name.as_str()),
2697        )?;
2698        // Which tensor each of the five norm sites is stored under and
2699        // which FUNCTION norms it, resolved ONCE. Four shapes reach this
2700        // loader -- the plain pre-norm layer, the post-norm-only
2701        // topology (`olmo2`, `exaone4`), the non-parametric LayerNorm
2702        // (`olmo`) and the weighted LayerNorm (`dbrx`) -- and three
2703        // architectures keep a norm under a name another architecture
2704        // uses for a different site (`gpt-oss` / `seed_oss`, `dbrx`,
2705        // `grok`). `crate::norm_sites` is the one table for all of it;
2706        // this loader used to restate the decision at every site.
2707        let norm_sites = crate::norm_sites::NormSites::with_function(&arch, config.norm_function);
2708        let mut gpt_oss_layers: Vec<crate::decoder::GptOssLayer> = Vec::new();
2709
2710        // One store for the whole model (keys are (layer, expert)),
2711        // built up-front with every stored expert's segments; created
2712        // only when the cache is enabled AND some layer can use it.
2713        let mut store_segments: std::collections::HashMap<ExpertKey, [(usize, u64, usize); 3]> =
2714            std::collections::HashMap::new();
2715        let mut stored_layouts: Vec<Option<Vec<StoredExpertLayout>>> = Vec::new();
2716
2717        // Loaded like any other weight matrix: a quantized embedding
2718        // table stays quantized (zero-copy mmap) and token lookup
2719        // dequantizes one row via `WeightMatrix::dequant_row`, instead
2720        // of the whole vocabulary tensor being widened to f32 up front.
2721        let embedding = load_weight_matrix(&file, "token_embd.weight")?;
2722        // The learned position table (`crate::position_embd`), one row
2723        // per trained position, for the graphs that add one.
2724        let position_embd = crate::position_embd::load_position_embd(
2725            &file,
2726            &arch,
2727            config.hidden_dim,
2728            metadata_u64_any(&file, &[format!("{arch}.context_length")]).map(|v| v as usize),
2729        )?;
2730
2731        // PHYSICAL layers: the blocks the file holds tensors for. A
2732        // looped model (`crate::layer_loops`) has more logical layers
2733        // than this, and they run these same weights.
2734        let n_physical = config
2735            .layer_loops
2736            .map_or(config.n_layers, |loops| loops.physical_layers());
2737        let mut layers = Vec::with_capacity(n_physical);
2738        let mut refined_qk_norm = config.qk_norm_style;
2739        for l in 0..n_physical {
2740            // THIS layer's head counts and FFN width. Uniform for every
2741            // architecture but the per-layer ones (`crate::layer_shapes`),
2742            // and the loader reads the shape rather than the scalars so
2743            // that a deci / openelm layer is sized by its own header.
2744            let shape = config.layer_shape(l);
2745            // Whether THIS layer's FFN reads the layer input rather
2746            // than the post-attention residual, and under which norm
2747            // (`crate::parallel_residual`).
2748            let parallel = crate::parallel_residual::layer_parallel_norm(&file, &arch, l);
2749            // THIS layer's norm slots: the architecture's row, with
2750            // Falcon-40B's `attn_norm_2` crossing the two pre-norm names
2751            // on the layers that carry it (`crate::norm_sites`).
2752            let layer_sites = norm_sites.for_layer(&arch, &file, l);
2753            // BitNet's two inner norms, REQUIRED when the architecture
2754            // has them and untouched otherwise (`crate::sub_norms`).
2755            let sub_norms = crate::sub_norms::load_sub_norms(
2756                &file,
2757                &arch,
2758                config.block_sub_norms,
2759                l,
2760                config.hidden_dim,
2761                shape.ffn_dim,
2762            )?;
2763            let attn = match shape.attention {
2764                crate::layer_shapes::AttnShape::Gqa { n_heads, .. } => {
2765                    // Q/K/V and their biases come out of ONE decision about
2766                    // which spelling this layer uses -- see `qkv_fused`. They
2767                    // used to be resolved independently, and a checkpoint that
2768                    // fused both (ChatGLM, Qwen-1) had its bias dropped.
2769                    let crate::qkv_fused::QkvProjections {
2770                        q: q_proj,
2771                        k: k_proj,
2772                        v: v_proj,
2773                        q_bias,
2774                        k_bias,
2775                        v_bias,
2776                    } = crate::qkv_fused::load_fused_or_split_qkv(&file, l, &config)?;
2777                    let q_norm =
2778                        load_f32_vec_optional(&file, &format!("blk.{l}.attn_q_norm.weight"))?;
2779                    let k_norm =
2780                        load_f32_vec_optional(&file, &format!("blk.{l}.attn_k_norm.weight"))?;
2781                    // The per-head LAYERNORM (`crate::qk_layer_norm`), whose
2782                    // weight is `n_heads * head_dim` long and would pass
2783                    // the length rule below as `WholeVector`.
2784                    if let Some(reason) = crate::qk_layer_norm::per_head_layer_norm_refusal(
2785                        &arch,
2786                        l,
2787                        q_norm.is_some() || k_norm.is_some(),
2788                    ) {
2789                        return Err(LoadError::UnsupportedFeature(
2790                            config.name.to_string(),
2791                            reason,
2792                        ));
2793                    }
2794                    // Refine WholeVector vs PerHead from the first observed norm length.
2795                    // The per-head SCALAR gain is decided by architecture first
2796                    // (`capability::PER_HEAD_SCALAR_QK_GAIN`): its length is
2797                    // `n_heads`, which a length test alone could confuse with
2798                    // `head_dim`.
2799                    if let Some(ref w) = q_norm {
2800                        if crate::capability::uses_per_head_scalar_qk_gain(&arch) {
2801                            if w.len() != n_heads {
2802                                return Err(LoadError::UnsupportedFeature(
2803                                    config.name.to_string(),
2804                                    format!(
2805                                        "blk.{l}.attn_q_norm.weight length {} is not one gain per \
2806                                         head (n_heads={n_heads}; talkie.cpp:26 creates it {{1, \
2807                                         n_head}})",
2808                                        w.len()
2809                                    ),
2810                                ));
2811                            }
2812                            refined_qk_norm = crate::capability::QkNormStyle::PerHeadScalar;
2813                        } else if crate::capability::uses_per_head_distinct_qk_norm(&arch) {
2814                            // `plamo2.cpp:92-93`: `{head_dim, n_head}`, one
2815                            // row per head, RMS per head. The same length as
2816                            // a whole-vector weight, so the architecture
2817                            // decides (`capability::PER_HEAD_DISTINCT_QK_NORM`).
2818                            if w.len() != n_heads * config.head_dim {
2819                                return Err(LoadError::UnsupportedFeature(
2820                                    config.name.to_string(),
2821                                    format!(
2822                                        "blk.{l}.attn_q_norm.weight length {} is not one row per \
2823                                         head (n_heads={n_heads} x head_dim={}; plamo2.cpp:92 \
2824                                         creates it {{head_dim, n_head}})",
2825                                        w.len(),
2826                                        config.head_dim
2827                                    ),
2828                                ));
2829                            }
2830                            refined_qk_norm = crate::capability::QkNormStyle::PerHeadDistinct;
2831                        } else if w.len() == config.head_dim {
2832                            refined_qk_norm = crate::capability::QkNormStyle::PerHead;
2833                        } else if w.len() == n_heads * config.head_dim {
2834                            refined_qk_norm = crate::capability::QkNormStyle::WholeVector;
2835                        } else {
2836                            return Err(LoadError::UnsupportedFeature(
2837                                config.name.to_string(),
2838                                format!(
2839                                    "blk.{l}.attn_q_norm.weight length {} matches neither \
2840                                     head_dim={} nor n_heads*head_dim={}",
2841                                    w.len(),
2842                                    config.head_dim,
2843                                    n_heads * config.head_dim
2844                                ),
2845                            ));
2846                        }
2847                    }
2848                    let attn = AttnWeights {
2849                        q_proj,
2850                        k_proj,
2851                        v_proj,
2852                        o_proj: load_weight_matrix(&file, &format!("blk.{l}.attn_output.weight"))?,
2853                        // Which tensor, which function, and whether there is a
2854                        // norm here at all: all three answered by the table.
2855                        norm_weight: layer_sites.load_pre_norm(layer_sites.attn, &file, Some(l))?,
2856                        q_norm,
2857                        k_norm,
2858                        // Qwen2/Qwen2-MoE-family real QKV bias (`attn_{q,k,v}.bias`,
2859                        // real config `qkv_bias`, `o_proj` has none) -- see
2860                        // `AttnWeights::q_bias`'s doc comment. Resolved above,
2861                        // alongside the projections they belong to, because a
2862                        // file that fuses the weight fuses the bias too.
2863                        q_bias,
2864                        k_bias,
2865                        v_bias,
2866                        post_attn_norm: crate::norm_sites::NormSites::load_post_norm(
2867                            norm_sites.post_attn,
2868                            &file,
2869                            l,
2870                        )?,
2871                        post_ffn_norm: crate::norm_sites::NormSites::load_post_norm(
2872                            norm_sites.post_ffn,
2873                            &file,
2874                            l,
2875                        )?,
2876                        // The architecture table decides whether there is
2877                        // a gate and how it is applied; the tensor decides
2878                        // its width. See `crate::attn_gate`.
2879                        output_gate: crate::attn_gate::AttnGate::load(
2880                            &file,
2881                            &arch,
2882                            l,
2883                            n_heads,
2884                            config.head_dim,
2885                            config.hidden_dim,
2886                        )?,
2887                        // The TENSOR decides. Four llama.cpp graphs pass it
2888                        // into the one `build_attn_mha`; on the generic
2889                        // path a file that has it gets the sink term and
2890                        // a file that does not gets none, whatever the
2891                        // architecture string. gpt-oss's requirement is
2892                        // checked where its side table loads.
2893                        sinks: load_attn_sinks(&file, l, n_heads)?,
2894                        attn_sub_norm: sub_norms.as_ref().map(|n| n.attn.clone()),
2895                        o_scale: crate::weight_scales::load_projection_gain(
2896                            &file,
2897                            &arch,
2898                            l,
2899                            "attn_output",
2900                        )?,
2901                        o_bias: crate::proj_bias::load_attn_out_bias(
2902                            &file,
2903                            &arch,
2904                            l,
2905                            config.hidden_dim,
2906                        )?,
2907                        shortconv: None,
2908                        // falcon-h1.cpp:55-71: the Mamba-2 block beside
2909                        // attention on every layer (`crate::mamba2::
2910                        // PARALLEL_WITH_ATTENTION`).
2911                        ssm: if config.parallel_ssm {
2912                            Some(crate::ssm_block::SsmBlock::Mamba2(
2913                                crate::mamba2::Mamba2::load(&file, &arch, l, config.hidden_dim)?,
2914                            ))
2915                        } else {
2916                            None
2917                        },
2918                        q_gate_interleaved: crate::attn_gate::q_gate_interleaved(&arch),
2919                    };
2920                    crate::layer_shapes::check_gqa_projection_widths(
2921                        l,
2922                        shape.attention,
2923                        config.head_dim,
2924                        config.v_head_dim(),
2925                        config.hidden_dim,
2926                        &attn,
2927                    )?;
2928                    attn
2929                }
2930                other => crate::layer_shapes::load_non_gqa_attention(
2931                    other,
2932                    &file,
2933                    &arch,
2934                    l,
2935                    &norm_sites,
2936                    &config,
2937                )?,
2938            };
2939
2940            // Leading dense layers (see ModelConfig::layer_is_dense's
2941            // doc comment) load from the plain dense tensor names
2942            // regardless of this model's global MoE topology, matching
2943            // the DeepSeek-2/3-family convention found in
2944            // ik_llama.cpp's source. A model with n_experts<=1
2945            // globally (the dense test fixture) is dense on every
2946            // layer either way.
2947            // A layer with NO FFN at all (`ffn_dim 0`: deci's, Nemotron-H's
2948            // block-only layers) takes the dense arm, whose loader answers
2949            // `absent_ffn` for that width, whatever the model's MoE says.
2950            let is_dense_layer = config.layer_is_dense(l)
2951                || config.moe.n_experts <= 1
2952                || shape.ffn_dim == 0
2953                || crate::moe_interleave::dense_by_router_absence(&arch, &file, l);
2954            // The ungated experts (`nemotron-h.cpp:82-86,209-215`: a null
2955            // gate into `build_moe_ffn`, `LLM_FFN_RELU_SQR`) are spelled
2956            // the way the dense ungated FFN is (`load_dense_expert`): the
2957            // gate ALIASED to `up`, so `relu(up)^2` runs through the gated
2958            // body with no branch. A file that carries a gate anyway is
2959            // refused, as the dense loader refuses one.
2960            let routed_gate_name = if config.ffn_is_ungated() && !is_dense_layer {
2961                if file
2962                    .find_tensor(&format!("blk.{l}.ffn_gate_exps.weight"))
2963                    .is_some()
2964                {
2965                    return Err(LoadError::UnsupportedFeature(
2966                        arch.clone(),
2967                        format!(
2968                            "blk.{l}.ffn_gate_exps.weight is present but this architecture's \
2969                             experts are ungated ({:?}: a null gate into build_moe_ffn, \
2970                             nemotron-h.cpp:212)",
2971                            config.ffn_activation
2972                        ),
2973                    ));
2974                }
2975                format!("blk.{l}.ffn_up_exps.weight")
2976            } else {
2977                format!("blk.{l}.ffn_gate_exps.weight")
2978            };
2979            // The inner FFN norm has a site in the dense body only
2980            // (`build_ffn` with a NULL down, `bitnet.cpp:127-141`);
2981            // `build_moe_ffn` has none, so a routed layer that carried
2982            // one would have nowhere to apply it.
2983            if sub_norms.is_some() && !is_dense_layer {
2984                return Err(LoadError::UnsupportedFeature(
2985                    arch.clone(),
2986                    format!(
2987                        "blk.{l}.ffn_sub_norm on a MoE layer: llama.cpp applies the inner FFN \
2988                         norm in the dense `build_ffn` body only (bitnet.cpp:127-141), and no \
2989                         routed-expert graph has that site"
2990                    ),
2991                ));
2992            }
2993            let n_experts = if is_dense_layer {
2994                1
2995            } else {
2996                config.moe.n_experts
2997            };
2998            let experts: ExpertBacking = if is_dense_layer {
2999                ExpertBacking::Resident(vec![load_dense_expert(&file, l, &config, shape.ffn_dim)?])
3000            } else {
3001                // Try store-backed layouts first when the cache is
3002                // enabled; fall back to resident when any of the three
3003                // tensors isn't a supported quantized dtype.
3004                let stored = if expert_cache_bytes.is_some() {
3005                    let g = stored_expert_specs(&file, &routed_gate_name, n_experts)?;
3006                    let u = stored_expert_specs(
3007                        &file,
3008                        &format!("blk.{l}.ffn_up_exps.weight"),
3009                        n_experts,
3010                    )?;
3011                    let d = stored_expert_specs(
3012                        &file,
3013                        &format!("blk.{l}.ffn_down_exps.weight"),
3014                        n_experts,
3015                    )?;
3016                    match (g, u, d) {
3017                        (Some(gt), Some(ut), Some(dt)) => {
3018                            let mut layouts = Vec::with_capacity(n_experts);
3019                            for e in 0..n_experts {
3020                                let key = ExpertKey {
3021                                    layer: l as u32,
3022                                    expert: e as u32,
3023                                };
3024                                store_segments.insert(
3025                                    key,
3026                                    [
3027                                        (gt.shard, gt.per_expert[e].0, gt.per_expert[e].1),
3028                                        (ut.shard, ut.per_expert[e].0, ut.per_expert[e].1),
3029                                        (dt.shard, dt.per_expert[e].0, dt.per_expert[e].1),
3030                                    ],
3031                                );
3032                                let mut gate = gt.spec;
3033                                let mut up = ut.spec;
3034                                let mut down = dt.spec;
3035                                gate.offset = 0;
3036                                up.offset = gate.len;
3037                                down.offset = gate.len + up.len;
3038                                layouts.push(StoredExpertLayout { gate, up, down });
3039                            }
3040                            Some(layouts)
3041                        }
3042                        _ => None,
3043                    }
3044                } else {
3045                    None
3046                };
3047                match stored {
3048                    Some(layouts) => {
3049                        // Placeholder; the shared store is attached in a
3050                        // second pass below once every layer's segments
3051                        // are collected.
3052                        stored_layouts.push(Some(layouts));
3053                        ExpertBacking::Resident(Vec::new())
3054                    }
3055                    None => {
3056                        let gates = split_expert_tensor(&file, &routed_gate_name, n_experts)?;
3057                        let ups = split_expert_tensor(
3058                            &file,
3059                            &format!("blk.{l}.ffn_up_exps.weight"),
3060                            n_experts,
3061                        )?;
3062                        let downs = split_expert_tensor(
3063                            &file,
3064                            &format!("blk.{l}.ffn_down_exps.weight"),
3065                            n_experts,
3066                        )?;
3067                        ExpertBacking::Resident(
3068                            gates
3069                                .into_iter()
3070                                .zip(ups)
3071                                .zip(downs)
3072                                .map(|((gate, up), down)| ExpertWeights { gate, up, down })
3073                                .collect(),
3074                        )
3075                    }
3076                }
3077            };
3078            if stored_layouts.len() < layers.len() + 1 {
3079                stored_layouts.push(None);
3080            }
3081
3082            let mut shared_experts: Vec<ExpertWeights> =
3083                if config.moe.n_shared_experts > 0 && !is_dense_layer {
3084                    // The shared expert takes the architecture's dense
3085                    // activation, so an ungated one aliases its gate as
3086                    // `load_dense_expert` does (`nemotron-h.cpp:222-227`).
3087                    let shexp_gate = if config.ffn_is_ungated() {
3088                        if file
3089                            .find_tensor(&format!("blk.{l}.ffn_gate_shexp.weight"))
3090                            .is_some()
3091                        {
3092                            return Err(LoadError::UnsupportedFeature(
3093                                arch.clone(),
3094                                format!(
3095                                    "blk.{l}.ffn_gate_shexp.weight is present but this \
3096                                     architecture's shared expert is ungated"
3097                                ),
3098                            ));
3099                        }
3100                        format!("blk.{l}.ffn_up_shexp.weight")
3101                    } else {
3102                        format!("blk.{l}.ffn_gate_shexp.weight")
3103                    };
3104                    vec![ExpertWeights {
3105                        gate: load_weight_matrix(&file, &shexp_gate)?,
3106                        up: load_weight_matrix(&file, &format!("blk.{l}.ffn_up_shexp.weight"))?,
3107                        down: load_weight_matrix(&file, &format!("blk.{l}.ffn_down_shexp.weight"))?,
3108                    }]
3109                } else {
3110                    Vec::new()
3111                };
3112            // A dense FFN SUMMED with the experts (Grok-2, Arctic) is the
3113            // shared-expert slot under the dense names, plus the row's
3114            // scale on the sum (`crate::parallel_dense_ffn`). Decided per
3115            // layer: Grok-1's layers have no triple and take neither.
3116            // ...and the same scale under the `_shexp` names
3117            // (`SHARED_EXPERT_SUM_SCALE`, cohere2moe's `* 0.5`), on a
3118            // layer that loaded a shared expert above.
3119            let parallel_sum_scale = if is_dense_layer {
3120                None
3121            } else {
3122                match crate::parallel_dense_ffn::parallel_dense_for_layer(&arch, &file, l)? {
3123                    Some(row) => {
3124                        shared_experts.push(ExpertWeights {
3125                            gate: load_weight_matrix(&file, &format!("blk.{l}.ffn_gate.weight"))?,
3126                            up: load_weight_matrix(&file, &format!("blk.{l}.ffn_up.weight"))?,
3127                            down: load_weight_matrix(&file, &format!("blk.{l}.ffn_down.weight"))?,
3128                        });
3129                        row.sum_scale
3130                    }
3131                    None => crate::parallel_dense_ffn::shared_expert_sum_scale(
3132                        &arch,
3133                        !shared_experts.is_empty(),
3134                    ),
3135                }
3136            };
3137            // Arctic's second per-layer norm, the routed branch's operand
3138            // (`crate::router_input::RouterInput::NormedLayerInput`):
3139            // REQUIRED on its routed layers, unread everywhere else.
3140            let exps_norm = if config.router_input.needs_exps_norm() && !is_dense_layer {
3141                Some(load_f32_vec(
3142                    &file,
3143                    &format!("blk.{l}.ffn_norm_exps.weight"),
3144                )?)
3145            } else {
3146                None
3147            };
3148
3149            let router = if !is_dense_layer {
3150                load_weight_matrix(&file, &format!("blk.{l}.ffn_gate_inp.weight"))?
3151            } else {
3152                // dense layer: no real router; a zero [1, hidden] matrix
3153                // always selects the single expert deterministically.
3154                WeightMatrix::F32(Tensor::zeros(vec![1, config.hidden_dim]))
3155            };
3156
3157            let n_for_counts = match &experts {
3158                ExpertBacking::Resident(v) if v.is_empty() => n_experts,
3159                other => other.n_experts(),
3160            };
3161            let activation_counts = (0..n_for_counts)
3162                .map(|_| std::sync::atomic::AtomicU64::new(0))
3163                .collect();
3164            // Qwen2-MoE-specific real tensor (`blk.N.ffn_gate_inp_shexp.weight`,
3165            // real on-disk shape `[hidden_dim]`, confirmed against
3166            // llama.cpp's real `qwen2moe.cpp`) -- see
3167            // `MoeWeights::shared_expert_gate`'s doc comment. Presence
3168            // of the tensor itself is the real signal (not an
3169            // architecture-name list): every other supported
3170            // architecture's checkpoints simply don't carry this
3171            // tensor, so this naturally stays `None` there.
3172            let shared_expert_gate = if is_dense_layer {
3173                None
3174            } else {
3175                load_f32_vec_optional(&file, &format!("blk.{l}.ffn_gate_inp_shexp.weight"))?
3176            };
3177            #[cfg(feature = "metal")]
3178            let packed_q4 = match &experts {
3179                ExpertBacking::Resident(v) if !v.is_empty() => try_build_moe_packed_q4_planes(v),
3180                _ => None,
3181            };
3182            // DeepSeek-V3's aux-loss-free selection bias. The on-disk
3183            // name carries no `ffn_` prefix -- llama.cpp's
3184            // `LLM_TENSOR_FFN_EXP_PROBS_B` maps to `blk.%d.exp_probs_b`
3185            // (`llama-arch.cpp:416`, `gguf-py/gguf/constants.py:1240`).
3186            // Optional: only the DeepSeek-V3-lineage MoE recipes carry
3187            // it, and this same generic loader serves OLMoE / Qwen2-MoE /
3188            // Mixtral, which do not.
3189            let exp_probs_bias = if is_dense_layer {
3190                None
3191            } else {
3192                load_f32_vec_optional(&file, &format!("blk.{l}.exp_probs_b.bias"))?
3193            };
3194            if let Some(bias) = &exp_probs_bias {
3195                if bias.len() != config.moe.n_experts {
3196                    return Err(LoadError::UnsupportedFeature(
3197                        arch.clone(),
3198                        format!(
3199                            "blk.{l}.exp_probs_b.bias has {} entries but the model has {} experts",
3200                            bias.len(),
3201                            config.moe.n_experts
3202                        ),
3203                    ));
3204                }
3205                // Grouped selection masks the *biased* scores before the
3206                // global top-k (`build_moe_ffn`, the `n_expert_groups > 1`
3207                // block). frink's `route_top_k_grouped` takes a fixed
3208                // count from every group instead, which is a different
3209                // algorithm, so combining the two here would be a guess.
3210                // Refuse rather than route wrongly.
3211                if config.moe.expert_group_count.is_some() {
3212                    return Err(LoadError::UnsupportedFeature(
3213                        arch.clone(),
3214                        format!(
3215                            "blk.{l}.exp_probs_b.bias together with expert groups \
3216                             ({:?}): llama.cpp masks the biased scores per group \
3217                             before a global top-k, which is not the per-group \
3218                             top-k frink implements",
3219                            config.moe.expert_group_count
3220                        ),
3221                    ));
3222                }
3223            }
3224            let moe = MoeWeights {
3225                router,
3226                experts,
3227                shared_experts,
3228                shared_expert_gate,
3229                exp_probs_bias,
3230                exps_norm,
3231                parallel_sum_scale,
3232                // The dense FFN's biases (`crate::proj_bias`), on a dense
3233                // layer; a routed layer's experts carry none on the
3234                // generic path (gpt-oss's are its side table's).
3235                dense_bias: if is_dense_layer && shape.ffn_dim > 0 {
3236                    let bias = crate::proj_bias::load_dense_ffn_bias(
3237                        &file,
3238                        &arch,
3239                        l,
3240                        config.hidden_dim,
3241                        shape.ffn_dim,
3242                        config.ffn_is_ungated(),
3243                    )?;
3244                    if bias.is_some() && sub_norms.is_some() {
3245                        return Err(LoadError::UnsupportedFeature(
3246                            arch.clone(),
3247                            format!(
3248                                "layer {l} has both an inner FFN norm and FFN biases; no llama.cpp \
3249                                 graph has both and the dense body has one arm for each"
3250                            ),
3251                        ));
3252                    }
3253                    bias
3254                } else {
3255                    None
3256                },
3257                ffn_sub_norm: sub_norms.map(|n| n.ffn),
3258                down_scale: {
3259                    let gain =
3260                        crate::weight_scales::load_projection_gain(&file, &arch, l, "ffn_down")?;
3261                    if gain.is_some() && !is_dense_layer {
3262                        return Err(LoadError::UnsupportedFeature(
3263                            arch.clone(),
3264                            format!(
3265                                "blk.{l}.ffn_down.scale on a MoE layer: the routed experts' \
3266                                 scales are `ffn_down_exps.scale`, one per expert, which is \
3267                                 not applied here"
3268                            ),
3269                        ));
3270                    }
3271                    gain
3272                },
3273                // The same table as the attention slot, so the two
3274                // pre-norms cannot disagree about the function, and the
3275                // pre-FFN tensor's NAME comes from the same row that
3276                // decided the post-attention slot must not read it. An
3277                // FFN-free layer (`deci.cpp:52-54`) has no such tensor.
3278                // A parallel layer with ONE shared norm has no pre-FFN
3279                // tensor and no pre-FFN norm: the FFN reads the vector
3280                // attention read (`crate::parallel_residual`).
3281                norm_weight: if shape.ffn_dim == 0
3282                    || parallel == Some(crate::parallel_residual::ParallelNorm::SharedNorm)
3283                {
3284                    NormOp::None
3285                } else {
3286                    layer_sites.load_pre_norm(layer_sites.ffn, &file, Some(l))?
3287                },
3288                parallel,
3289                activation_counts,
3290                #[cfg(feature = "metal")]
3291                packed_q4,
3292            };
3293
3294            if is_gpt_oss {
3295                gpt_oss_layers.push(load_gpt_oss_layer(&file, l, &config, attn.sinks.is_some())?);
3296            }
3297
3298            // Talkie's per-layer skip scalar (`crate::skip_stream`);
3299            // REQUIRED there, untouched everywhere else.
3300            let out_scale =
3301                crate::skip_stream::load_out_scale(&file, &arch, config.skip_stream, l)?;
3302            layers.push(LayerWeights {
3303                attn,
3304                moe,
3305                out_scale,
3306            });
3307        }
3308
3309        // `olmo.cpp:15-36` creates no `output_norm` at all and
3310        // `:128-130` norms the final hidden state with a null weight, so
3311        // asking for the tensor would refuse every real OLMo-1 file;
3312        // the table's function decides whether the read happens.
3313        let final_norm = norm_sites.load_pre_norm(norm_sites.output, &file, None)?;
3314        // `hrm-text.cpp:46` creates `hrm_z_l_init` REQUIRED, and only
3315        // that graph does (`crate::hrm`): the learned LOW stream, one
3316        // `[n_embd]` row broadcast over the tokens at `:182`.
3317        let hrm_z_l_init = match config.layer_loops {
3318            Some(crate::layer_loops::LayerLoops::Hrm { .. }) => {
3319                Some(load_f32_vec(&file, "hrm.z_l_init")?)
3320            }
3321            _ => None,
3322        };
3323        // The embedding norm (`norm_sites::EMBEDDING_NORM_ARCHITECTURES`),
3324        // `NormOp::None` where the site is absent.
3325        // Two answers, one field: a STORED embedding norm (`bloom`) or
3326        // a weightless one (`muse-glimmer.cpp:69`), and the tables that
3327        // decide them are disjoint by construction
3328        // (`norm_sites::WEIGHTLESS_EMBEDDING_NORM`).
3329        let embedding_norm = if crate::norm_sites::weightless_embedding_norm(&arch) {
3330            crate::norm::NormOp::RmsNoParams
3331        } else {
3332            norm_sites.load_pre_norm(norm_sites.embedding, &file, None)?
3333        };
3334        // Many small Llama/Gemma-family GGUFs tie the lm-head to
3335        // `token_embd.weight` and omit `output.weight` (llama.cpp
3336        // `llama_model_loader` falls back the same way). Prefer the
3337        // explicit head when present.
3338        let output_head = match load_weight_matrix(&file, "output.weight") {
3339            Ok(w) => w,
3340            Err(_) => load_weight_matrix(&file, "token_embd.weight")?,
3341        };
3342        // `output.bias` for the graphs that create it (`crate::proj_bias`).
3343        let output_bias = crate::proj_bias::load_output_bias(&file, &arch, output_head.rows())?;
3344
3345        // Second pass: attach the one shared store to every
3346        // store-backed layer. Opening the shard files fresh (plain
3347        // `File` handles for positional reads, not mmaps) keeps the
3348        // stored experts' bytes out of the process's mapped footprint
3349        // entirely.
3350        if !store_segments.is_empty() {
3351            let budget = expert_cache_bytes
3352                .expect("store_segments only populated when a cache budget is set")
3353                as usize;
3354            let files: Result<Vec<std::fs::File>, std::io::Error> =
3355                file.shard_paths().iter().map(std::fs::File::open).collect();
3356            let files = files.map_err(GgufError::from)?;
3357            let store = std::sync::Arc::new(ExpertStore::new(
3358                GgufExpertSource {
3359                    files,
3360                    segments: store_segments,
3361                },
3362                budget,
3363            ));
3364            for (l, layer) in layers.iter_mut().enumerate() {
3365                if let Some(layouts) = stored_layouts.get_mut(l).and_then(Option::take) {
3366                    layer.moe.experts = ExpertBacking::Stored {
3367                        store: std::sync::Arc::clone(&store),
3368                        layouts,
3369                        layer: l as u32,
3370                    };
3371                }
3372            }
3373        }
3374
3375        config.qk_norm_style = refined_qk_norm;
3376
3377        let family = crate::capability::resolve_profile(
3378            file.metadata_str("general.architecture").unwrap_or("llama"),
3379        )
3380        .map(|p| p.family)
3381        .unwrap_or(crate::capability::DecoderFamily::StandardGqa);
3382        let memory_kind = crate::capability::resolve_profile(
3383            file.metadata_str("general.architecture").unwrap_or("llama"),
3384        )
3385        .map(|p| p.memory)
3386        .unwrap_or(crate::capability::MemoryKind::KvGqa);
3387        let execution_plan = crate::execution_plan::ExecutionPlan::from_config(
3388            &config,
3389            family,
3390            memory_kind,
3391            crate::execution_plan::ExecutionPlan::probe_metal_caps(),
3392        );
3393
3394        let alibi_slopes = crate::decoder::config_alibi_slopes(&config);
3395        let decoder = Decoder {
3396            config,
3397            embedding,
3398            position_embd,
3399            embedding_norm,
3400            // `hrm-text.cpp:46` creates it REQUIRED, and only that
3401            // graph does (`crate::hrm`); the loader reads it for the
3402            // architecture whose schedule needs it and for no other.
3403            hrm_z_l_init,
3404            alibi_slopes,
3405            layers,
3406            final_norm,
3407            output_head,
3408            output_bias,
3409            gpu_vram_budget_bytes: None,
3410            gpt_oss: if is_gpt_oss {
3411                Some(crate::decoder::GptOssWeights {
3412                    layers: gpt_oss_layers,
3413                })
3414            } else {
3415                None
3416            },
3417            qk_norm_after_rope: QK_NORM_AFTER_ROPE_ARCHITECTURES.contains(&arch.as_str()),
3418            #[cfg(feature = "metal")]
3419            metal_attn_kv: std::sync::Mutex::new(None),
3420            execution_plan,
3421            kv_window: crate::decoder::KvWindowPolicy::from_env(),
3422            plan_cache: std::sync::Mutex::new(std::collections::HashMap::new()),
3423            lora_adapters: Vec::new(),
3424        };
3425        // Resolve every kernel the model will need while we still have a
3426        // load-time error path to report it on, then seal: from here a
3427        // lookup that misses is an unpredicted slow path and says so.
3428        decoder.probe_kernels();
3429        frink_core::kernel_registry::seal_or_error()
3430            .map_err(|e| LoadError::StrictKernels(e.to_string()))?;
3431        // `ModelConfig` is parsed from a *different* handle on the same
3432        // file (the CLI opens its own `GgufFile`, then hands the config
3433        // here), so the model-level tensors it consumed were recorded on
3434        // that handle, not this one. Replay them before the gate, or
3435        // every Llama-3.x checkpoint reads as carrying an unread
3436        // `rope_freqs.weight` it in fact uses on every RoPE call.
3437        for name in crate::config::MODEL_LEVEL_TENSORS_READ_BY_CONFIG {
3438            file.note_consumed(name);
3439        }
3440        // The NextN/MTP blocks llama.cpp creates `TENSOR_SKIP` and never
3441        // runs (`crate::mtp_blocks`): deliberately unread, and said so,
3442        // rather than left for the gate below to report as a term the
3443        // graph is missing. The range is the config's, so the layer
3444        // loop above and this mark cannot disagree about where the
3445        // trunk ends.
3446        let skipped = crate::mtp_blocks::note_mtp_blocks_skipped(
3447            &file,
3448            &crate::mtp_blocks::TrunkLayers {
3449                block_count: n_physical + decoder.config.n_mtp_blocks,
3450                n_layers: n_physical,
3451                n_mtp_blocks: decoder.config.n_mtp_blocks,
3452            },
3453        );
3454        if skipped > 0 {
3455            eprintln!(
3456                "frink: skipping {} NextN/MTP block(s) after layer {} ({skipped} tensors), as \
3457                 llama.cpp does",
3458                decoder.config.n_mtp_blocks,
3459                n_physical - 1
3460            );
3461        }
3462        // Slots llama.cpp creates and never reads (`crate::unread_tensors`):
3463        // ignored as upstream ignores them, and said so.
3464        let ignored = crate::unread_tensors::note_unread_layer_tensors(&file, &arch, n_physical);
3465        if !ignored.is_empty() {
3466            eprintln!(
3467                "frink: ignoring {} tensor(s) llama.cpp creates and never reads for `{}` \
3468                 (first: {}), as llama.cpp does",
3469                ignored.len(),
3470                arch,
3471                ignored[0]
3472            );
3473        }
3474        assert_every_tensor_consumed(&file)?;
3475        Ok(decoder)
3476    }
3477}
3478
3479/// Tensor-name prefixes a text-generation load legitimately never
3480/// reads. Everything here is consumed by a *different* code path, not by
3481/// nothing: multimodal projector planes belong to `mmproj`, and the
3482/// per-shard split bookkeeping is metadata, not weights.
3483const IGNORED_TENSOR_PREFIXES: &[&str] = &["mm.", "v.", "mmproj.", "resampler.", "audio."];
3484
3485/// Fails the load when the checkpoint carries tensors this build never
3486/// looked at.
3487///
3488/// A tensor nobody reads is not a harmless extra: it is a term of the
3489/// real graph that ours is missing. gpt-oss ships `blk.N.attn_sinks`
3490/// and frink has no attention-sink code anywhere, so the file loads,
3491/// runs at full speed, and emits a different distribution than the model
3492/// it claims to be; the newer MoE recipes ship `ffn_exp_probs_b` the
3493/// same way. Both are silent today, and both are exactly what the
3494/// architecture registry cannot catch, because the architecture *string*
3495/// is one frink does support -- it is the checkpoint that carries more
3496/// than the registry entry promises.
3497///
3498/// This is deliberately the last check in the load: by here every loader
3499/// arm has had its chance to ask for what it needs, so what is left over
3500/// is what nothing in this build knows about.
3501///
3502/// `FRINK_ALLOW_UNKNOWN_TENSORS=1` downgrades it to a warning, for the
3503/// case where a human has decided the missing term does not matter (a
3504/// bias tensor of zeros, an auxiliary head that never runs). The default
3505/// is refusal: a wrong answer is worse than no answer.
3506pub fn assert_every_tensor_consumed(file: &ShardedGguf) -> Result<(), LoadError> {
3507    let mut left: Vec<String> = file
3508        .unconsumed_tensors()
3509        .into_iter()
3510        .filter(|n| !IGNORED_TENSOR_PREFIXES.iter().any(|p| n.starts_with(p)))
3511        .collect();
3512    if left.is_empty() {
3513        return Ok(());
3514    }
3515    left.sort();
3516    let shown = left.iter().take(8).cloned().collect::<Vec<_>>().join(", ");
3517    let listing = if left.len() > 8 {
3518        format!("{shown}, … (+{} more)", left.len() - 8)
3519    } else {
3520        shown
3521    };
3522    if matches!(
3523        std::env::var("FRINK_ALLOW_UNKNOWN_TENSORS").ok().as_deref(),
3524        Some("1") | Some("true") | Some("on")
3525    ) {
3526        eprintln!(
3527            "frink: WARNING -- {} tensor(s) in this checkpoint are never read \
3528             ({listing}); output may be wrong (FRINK_ALLOW_UNKNOWN_TENSORS=1)",
3529            left.len()
3530        );
3531        return Ok(());
3532    }
3533    Err(LoadError::UnconsumedTensors(left.len(), listing))
3534}
3535
3536#[cfg(test)]
3537mod tests {
3538
3539    /// A quantized 1-D tensor loads through the shared helper.
3540    ///
3541    /// This used to be six copies of `load_f32_vec`, and they had
3542    /// drifted badly: this one decoded twenty dtypes while the five
3543    /// architecture loaders decoded three (F32/F16/BF16). A quantizer
3544    /// that emits a Q8_0 norm or bias -- ordinary for aggressive
3545    /// quants -- loaded on the generic path and was rejected with
3546    /// `UnsupportedDtype` on GLM-5.2, Kimi, DeepSeek-MLA, Gemma-4 and
3547    /// the hybrid stack.
3548    ///
3549    /// This file's own comment predicted exactly that, about the same
3550    /// split one level down: "a dtype frink can decode should never be
3551    /// rejected here just because the *other* dispatch table below
3552    /// knows it -- that split is how a supported format turns into a
3553    /// load failure on the one checkpoint that uses it."
3554    #[test]
3555    fn a_quantized_one_dimensional_tensor_widens_through_the_shared_helper() {
3556        let values: Vec<f32> = (0..64).map(|i| (i as f32 - 32.0) * 0.25).collect();
3557        let quantized = frink_quant::quantize_q8_0(&values);
3558
3559        struct OneTensor {
3560            info: TensorInfo,
3561            bytes: Vec<u8>,
3562        }
3563        impl TensorSource for OneTensor {
3564            fn metadata(&self, _key: &str) -> Option<&frink_gguf::GgufValue> {
3565                None
3566            }
3567            fn find_tensor(&self, name: &str) -> Option<&TensorInfo> {
3568                (name == self.info.name).then_some(&self.info)
3569            }
3570            fn tensor_bytes(&self, _name: &str) -> Result<&[u8], GgufError> {
3571                Ok(&self.bytes)
3572            }
3573            fn tensor_mapped_range(
3574                &self,
3575                name: &str,
3576            ) -> Result<
3577                (
3578                    std::sync::Arc<frink_gguf::MmapHandle>,
3579                    std::ops::Range<usize>,
3580                ),
3581                GgufError,
3582            > {
3583                // Never reached: `load_f32_vec` widens from bytes.
3584                Err(GgufError::TensorNotFound(name.to_string()))
3585            }
3586        }
3587
3588        let source = OneTensor {
3589            info: TensorInfo {
3590                name: "blk.0.attn_norm.weight".to_string(),
3591                shape: vec![64],
3592                dtype: GgmlType::Q8_0,
3593                offset: 0,
3594            },
3595            bytes: quantized,
3596        };
3597
3598        let widened = load_f32_vec(&source, "blk.0.attn_norm.weight")
3599            .expect("a Q8_0 norm must load, not report an unsupported dtype");
3600        assert_eq!(widened.len(), values.len());
3601        for (got, want) in widened.iter().zip(values.iter()) {
3602            assert!(
3603                (got - want).abs() < 0.05,
3604                "q8_0 round trip: got {got}, want {want}"
3605            );
3606        }
3607    }
3608    use super::*;
3609    use byteorder::{LittleEndian, WriteBytesExt};
3610    use std::io::Write;
3611
3612    fn write_string(buf: &mut Vec<u8>, s: &str) {
3613        buf.write_u64::<LittleEndian>(s.len() as u64).unwrap();
3614        buf.write_all(s.as_bytes()).unwrap();
3615    }
3616
3617    fn write_kv_str(buf: &mut Vec<u8>, key: &str, val: &str) {
3618        write_string(buf, key);
3619        buf.write_u32::<LittleEndian>(8).unwrap(); // type = string
3620        write_string(buf, val);
3621    }
3622
3623    /// A minimal, tensor-free GGUF byte buffer declaring only
3624    /// `general.architecture` (no `{arch}.block_count` or any other
3625    /// hparam key) -- the shape a stripped-down or malformed file might
3626    /// take, and the exact case `ModelConfig::from_gguf` must reject
3627    /// loudly rather than silently default around.
3628    fn build_arch_only_gguf(arch: &str) -> Vec<u8> {
3629        let mut buf = Vec::new();
3630        buf.write_u32::<LittleEndian>(frink_gguf::GGUF_MAGIC)
3631            .unwrap();
3632        buf.write_u32::<LittleEndian>(3).unwrap(); // version
3633        buf.write_u64::<LittleEndian>(0).unwrap(); // tensor_count
3634        buf.write_u64::<LittleEndian>(1).unwrap(); // kv_count
3635        write_kv_str(&mut buf, "general.architecture", arch);
3636        buf
3637    }
3638
3639    #[test]
3640    fn model_config_from_gguf_fails_loudly_when_required_hparams_are_missing() {
3641        let tmp =
3642            std::env::temp_dir().join(format!("frink_test_arch_only_{}.gguf", std::process::id()));
3643        // Use a registered architecture so the failure is MissingHparam,
3644        // not UnsupportedArchitecture.
3645        std::fs::write(&tmp, build_arch_only_gguf("llama")).unwrap();
3646        let file = frink_gguf::GgufFile::open(&tmp).expect("minimal header must still parse");
3647        std::fs::remove_file(&tmp).ok();
3648
3649        match ModelConfig::from_gguf(&file) {
3650            Err(LoadError::MissingHparam(key)) => {
3651                assert_eq!(key, "llama.block_count");
3652            }
3653            other => panic!(
3654                "expected LoadError::MissingHparam for a file with no hparam keys, got {other:?}"
3655            ),
3656        }
3657    }
3658
3659    #[test]
3660    fn model_config_from_gguf_fails_closed_on_unknown_architecture() {
3661        let tmp = std::env::temp_dir().join(format!(
3662            "frink_test_unknown_arch_{}.gguf",
3663            std::process::id()
3664        ));
3665        std::fs::write(&tmp, build_arch_only_gguf("bogus-arch-with-no-hparams")).unwrap();
3666        let file = frink_gguf::GgufFile::open(&tmp).expect("minimal header must still parse");
3667        std::fs::remove_file(&tmp).ok();
3668
3669        match ModelConfig::from_gguf(&file) {
3670            Err(LoadError::UnsupportedArchitecture(arch)) => {
3671                assert_eq!(arch, "bogus-arch-with-no-hparams");
3672            }
3673            other => panic!(
3674                "expected LoadError::UnsupportedArchitecture for an unregistered arch, got {other:?}"
3675            ),
3676        }
3677    }
3678
3679    fn write_kv_f32(buf: &mut Vec<u8>, key: &str, val: f32) {
3680        write_string(buf, key);
3681        buf.write_u32::<LittleEndian>(6).unwrap(); // type = float32
3682        buf.write_f32::<LittleEndian>(val).unwrap();
3683    }
3684
3685    /// `arch` plus one f32 hparam, so a metadata-only feature gate can be
3686    /// exercised without building a whole checkpoint.
3687    fn build_arch_plus_f32_gguf(arch: &str, key: &str, val: f32) -> Vec<u8> {
3688        let mut buf = Vec::new();
3689        buf.write_u32::<LittleEndian>(frink_gguf::GGUF_MAGIC)
3690            .unwrap();
3691        buf.write_u32::<LittleEndian>(3).unwrap(); // version
3692        buf.write_u64::<LittleEndian>(0).unwrap(); // tensor_count
3693        buf.write_u64::<LittleEndian>(2).unwrap(); // kv_count
3694        write_kv_str(&mut buf, "general.architecture", arch);
3695        write_kv_f32(&mut buf, key, val);
3696        buf
3697    }
3698
3699    fn config_error_for(arch: &str, key: &str, val: f32, tag: &str) -> LoadError {
3700        let tmp = std::env::temp_dir().join(format!("frink_test_scale_{tag}.gguf"));
3701        std::fs::write(&tmp, build_arch_plus_f32_gguf(arch, key, val)).unwrap();
3702        let file = frink_gguf::GgufFile::open(&tmp).expect("minimal header must still parse");
3703        std::fs::remove_file(&tmp).ok();
3704        ModelConfig::from_gguf(&file).expect_err("must not succeed")
3705    }
3706
3707    /// Granite / MiniCPM / Command-R multipliers are hparams, not
3708    /// tensors, so `assert_every_tensor_consumed` cannot see them: a
3709    /// checkpoint declaring one loads, runs at full speed, and computes
3710    /// a differently-scaled graph than it was trained as. An
3711    /// architecture whose reference graph does not apply one must refuse
3712    /// it by name.
3713    ///
3714    /// Driven on `llama` rather than on `granite`, and that swap is the
3715    /// point: `granite` APPLIES all four now
3716    /// (`crate::scalar_multipliers`), so leaving the case here would
3717    /// have turned this test into a test of nothing the day the feature
3718    /// landed. llama.cpp's llama graph reads none of the four keys, so a
3719    /// `llama` checkpoint declaring one is exactly the silent divergence
3720    /// the gate exists for.
3721    #[test]
3722    fn a_declared_multiplier_this_decoder_does_not_apply_is_refused_by_name() {
3723        for (key, val) in [
3724            ("llama.logit_scale", 6.0f32),
3725            ("llama.residual_scale", 0.22),
3726            ("llama.embedding_scale", 12.0),
3727            ("llama.attention.scale", 0.015_625),
3728        ] {
3729            let tag = key.replace('.', "_");
3730            match config_error_for("llama", key, val, &tag) {
3731                LoadError::UnsupportedFeature(arch, msg) => {
3732                    assert_eq!(arch, "llama");
3733                    assert!(msg.contains(key), "error must name the key: {msg}");
3734                }
3735                other => panic!("expected UnsupportedFeature for {key}, got {other:?}"),
3736            }
3737        }
3738    }
3739
3740    /// The complement, and the half that would otherwise have gone
3741    /// missing: `granite` must NOT be refused for the keys its graph
3742    /// applies.
3743    ///
3744    /// The refusal list and the implementation are two views of ONE
3745    /// table (`scalar_multipliers::multiplier_support`), so this test
3746    /// and the one above cannot both pass while they disagree -- which
3747    /// is the whole value of deriving the list rather than restating it.
3748    #[test]
3749    fn granite_is_not_refused_for_the_multipliers_it_applies() {
3750        for (key, val) in [
3751            ("granite.logit_scale", 8.0f32),
3752            ("granite.residual_scale", 0.22),
3753            ("granite.embedding_scale", 12.0),
3754            ("granite.attention.scale", 0.015_625),
3755        ] {
3756            let tag = format!("granite_ok_{}", key.replace('.', "_"));
3757            // The file carries no `block_count`, so the load still fails
3758            // -- but on the *missing hparam*, having passed this gate.
3759            match config_error_for("granite", key, val, &tag) {
3760                LoadError::MissingHparam(k) => assert_eq!(k, "granite.block_count"),
3761                other => panic!("{key}={val} must pass the scaling gate, got {other:?}"),
3762            }
3763        }
3764    }
3765
3766    /// The gate must not fire on a multiplier that is a no-op. A file
3767    /// writing `residual_scale = 1.0` describes the graph frink already
3768    /// computes, and refusing it would be a false alarm. llama.cpp's
3769    /// `f_attention_scale` uses `0.0` rather than `1.0` as its "unset"
3770    /// sentinel, so the two are checked against their own no-op values.
3771    #[test]
3772    fn a_multiplier_that_is_a_no_op_is_not_refused() {
3773        for (key, val) in [
3774            ("llama.logit_scale", 1.0f32),
3775            ("llama.residual_scale", 1.0),
3776            ("llama.embedding_scale", 1.0),
3777            ("llama.attention.scale", 0.0),
3778        ] {
3779            let tag = format!("noop_{}", key.replace('.', "_"));
3780            // The file carries no `block_count`, so the load still fails
3781            // -- but on the *missing hparam*, having passed this gate.
3782            match config_error_for("llama", key, val, &tag) {
3783                LoadError::MissingHparam(k) => assert_eq!(k, "llama.block_count"),
3784                other => panic!("no-op {key}={val} must pass the scaling gate, got {other:?}"),
3785            }
3786        }
3787    }
3788
3789    /// One GGUF metadata value, in the three types these header-only
3790    /// fixtures need.
3791    enum Kv<'a> {
3792        Str(&'a str),
3793        U32(u32),
3794        F32(f32),
3795        /// A uint32 ARRAY. Only one gate needs it -- the sliding-window
3796        /// pattern, which llama.cpp reads with `ml.get_key_or_arr` --
3797        /// and without it that gate could only be tested through a
3798        /// value of some other type, which is not the case it exists
3799        /// for.
3800        Arr32(&'a [u32]),
3801    }
3802
3803    /// A tensor-free GGUF carrying exactly `kvs` -- enough for
3804    /// `ModelConfig::from_gguf` to run without a single weight on disk.
3805    fn build_metadata_gguf(kvs: &[(&str, Kv)]) -> Vec<u8> {
3806        let mut buf = Vec::new();
3807        buf.write_u32::<LittleEndian>(frink_gguf::GGUF_MAGIC)
3808            .unwrap();
3809        buf.write_u32::<LittleEndian>(3).unwrap(); // version
3810        buf.write_u64::<LittleEndian>(0).unwrap(); // tensor_count
3811        buf.write_u64::<LittleEndian>(kvs.len() as u64).unwrap();
3812        for (k, v) in kvs {
3813            match v {
3814                Kv::Str(s) => write_kv_str(&mut buf, k, s),
3815                Kv::U32(n) => {
3816                    write_string(&mut buf, k);
3817                    buf.write_u32::<LittleEndian>(4).unwrap(); // type = uint32
3818                    buf.write_u32::<LittleEndian>(*n).unwrap();
3819                }
3820                Kv::F32(f) => write_kv_f32(&mut buf, k, *f),
3821                Kv::Arr32(values) => {
3822                    write_string(&mut buf, k);
3823                    buf.write_u32::<LittleEndian>(9).unwrap(); // type = array
3824                    buf.write_u32::<LittleEndian>(4).unwrap(); // element type = uint32
3825                    buf.write_u64::<LittleEndian>(values.len() as u64).unwrap();
3826                    for v in *values {
3827                        buf.write_u32::<LittleEndian>(*v).unwrap();
3828                    }
3829                }
3830            }
3831        }
3832        buf
3833    }
3834
3835    fn open_metadata_gguf(tag: &str, kvs: &[(&str, Kv)]) -> frink_gguf::GgufFile {
3836        let tmp = std::env::temp_dir().join(format!("frink_test_meta_{tag}.gguf"));
3837        std::fs::write(&tmp, build_metadata_gguf(kvs)).unwrap();
3838        let file = frink_gguf::GgufFile::open(&tmp).expect("header-only file must parse");
3839        std::fs::remove_file(&tmp).ok();
3840        file
3841    }
3842
3843    /// A minimal `llama` hparam set (64-wide single head, base 10000)
3844    /// plus whatever RoPE-scaling keys a test wants to add.
3845    fn llama_config_with(tag: &str, extra: &[(&str, Kv)]) -> ModelConfig {
3846        let mut kvs: Vec<(&str, Kv)> = vec![
3847            ("general.architecture", Kv::Str("llama")),
3848            ("llama.block_count", Kv::U32(1)),
3849            ("llama.embedding_length", Kv::U32(64)),
3850            ("llama.attention.head_count", Kv::U32(1)),
3851            ("llama.attention.head_count_kv", Kv::U32(1)),
3852            ("llama.attention.key_length", Kv::U32(64)),
3853            ("llama.rope.freq_base", Kv::F32(10_000.0)),
3854        ];
3855        for (k, v) in extra {
3856            kvs.push((
3857                k,
3858                match v {
3859                    Kv::Str(s) => Kv::Str(s),
3860                    Kv::U32(n) => Kv::U32(*n),
3861                    Kv::F32(f) => Kv::F32(*f),
3862                    Kv::Arr32(a) => Kv::Arr32(a),
3863                },
3864            ));
3865        }
3866        ModelConfig::from_gguf(&open_metadata_gguf(tag, &kvs)).expect("fixture must load")
3867    }
3868
3869    /// Builds a config for an arbitrary architecture tag, returning the
3870    /// error rather than unwrapping it.
3871    /// llama.cpp chooses the FFN gate activation PER ARCHITECTURE;
3872    /// frink chose it per family. Those are different partitions, and
3873    /// `grok` is where they disagree: `src/models/grok.cpp:165` passes
3874    /// `LLM_FFN_GELU` to `build_moe_ffn`, while `grok` is
3875    /// `DecoderFamily::StandardGqa` and so was handed SwiGLU -- a
3876    /// different FFN on every layer.
3877    ///
3878    /// It was pinned here while `grok` still refused, because the
3879    /// failure mode is that auditing it later makes it silently wrong,
3880    /// and an audit is exactly when nobody thinks to re-check the
3881    /// activation. `grok` is audited now (tests/grok_graphs.rs), and the
3882    /// fixture's GELU experts are what that suite compares.
3883    #[test]
3884    fn the_ffn_activation_follows_the_architecture_not_the_family() {
3885        use crate::capability::uses_geglu;
3886        use crate::config::FfnActivation;
3887
3888        assert!(uses_geglu("grok"), "grok's MoE FFN gate is GELU upstream");
3889        // Same family, SiLU upstream (`src/models/dbrx.cpp:122`), so the
3890        // family rule alone cannot be what selects grok.
3891        assert!(!uses_geglu("dbrx"));
3892        assert!(!uses_geglu("llama"));
3893
3894        // The Gemma lineage keeps its GELU through the FAMILY rule, so
3895        // the new per-architecture arm must not have displaced it.
3896        // gemma2/gemma3 only: `gemma` v1 is unaudited and refuses, so
3897        // it cannot be loaded to check its activation.
3898        for gemma in ["gemma2", "gemma3"] {
3899            assert!(
3900                !uses_geglu(gemma),
3901                "{gemma} is GELU via GemmaFamily; listing it here too \
3902                 would hide a later regression in the family rule"
3903            );
3904            assert_eq!(
3905                config_for_arch(gemma).expect("gemma loads").ffn_activation,
3906                FfnActivation::Gelu,
3907                "{gemma}"
3908            );
3909        }
3910
3911        // And a plain SwiGLU architecture stays SwiGLU.
3912        assert_eq!(
3913            config_for_arch("llama")
3914                .expect("llama loads")
3915                .ffn_activation,
3916            FfnActivation::Swiglu
3917        );
3918
3919        // The ungated ReLU-squared row, and the four that share its FFN
3920        // and refuse for something else (`capability::uses_relu_sqr`).
3921        assert_eq!(
3922            config_for_arch("arcee")
3923                .expect("arcee loads")
3924                .ffn_activation,
3925            FfnActivation::ReluSqr
3926        );
3927        for shared in ["plm", "nemotron", "jais2", "nemotron_h"] {
3928            assert!(crate::capability::uses_relu_sqr(shared), "{shared}");
3929        }
3930        assert!(!crate::capability::uses_relu_sqr("llama"));
3931    }
3932
3933    /// A per-layer array whose entries differ, on an architecture whose
3934    /// llama.cpp graph reads layer 0, is refused naming the table; the
3935    /// same arrays with equal entries are the uniform model, for any
3936    /// architecture, because a converter may spell a scalar as a list.
3937    ///
3938    /// Reachability, not only `LayerShapes::resolve`'s own unit test:
3939    /// this goes through `from_gguf` on a header-only file, which is
3940    /// where `openelm` used to die on `MissingHparam` for a key its
3941    /// file carried.
3942    #[test]
3943    fn a_varying_per_layer_array_is_refused_on_a_layer_zero_architecture_and_equal_ones_are_uniform(
3944    ) {
3945        let kvs = [
3946            ("general.architecture", Kv::Str("llama")),
3947            ("llama.block_count", Kv::U32(2)),
3948            ("llama.embedding_length", Kv::U32(64)),
3949            ("llama.attention.head_count", Kv::Arr32(&[2, 2])),
3950            ("llama.attention.head_count_kv", Kv::Arr32(&[2, 1])),
3951            ("llama.attention.key_length", Kv::U32(32)),
3952            ("llama.rope.freq_base", Kv::F32(10_000.0)),
3953        ];
3954        let err = ModelConfig::from_gguf(&open_metadata_gguf("layer_shapes_vary", &kvs))
3955            .expect_err("llama takes layer 0 upstream");
3956        let msg = err.to_string();
3957        assert!(msg.contains("PER_LAYER_SHAPE_ARCHS"), "{msg}");
3958        assert!(msg.contains("LLAMA_LOAD_LOCALS"), "{msg}");
3959
3960        let kvs = [
3961            ("general.architecture", Kv::Str("llama")),
3962            ("llama.block_count", Kv::U32(2)),
3963            ("llama.embedding_length", Kv::U32(64)),
3964            ("llama.attention.head_count", Kv::Arr32(&[2, 2])),
3965            ("llama.attention.head_count_kv", Kv::Arr32(&[1, 1])),
3966            ("llama.attention.key_length", Kv::U32(32)),
3967            ("llama.rope.freq_base", Kv::F32(10_000.0)),
3968        ];
3969        let cfg = ModelConfig::from_gguf(&open_metadata_gguf("layer_shapes_equal", &kvs))
3970            .expect("equal arrays are the uniform model");
3971        assert!(cfg.layer_shapes.is_uniform());
3972        assert_eq!((cfg.n_heads, cfg.n_kv_heads), (2, 1));
3973
3974        // An array of the wrong length is refused as llama.cpp refuses
3975        // it (`key has wrong array length`).
3976        let kvs = [
3977            ("general.architecture", Kv::Str("llama")),
3978            ("llama.block_count", Kv::U32(2)),
3979            ("llama.embedding_length", Kv::U32(64)),
3980            ("llama.attention.head_count", Kv::Arr32(&[2, 2, 2])),
3981            ("llama.attention.key_length", Kv::U32(32)),
3982            ("llama.rope.freq_base", Kv::F32(10_000.0)),
3983        ];
3984        let err = ModelConfig::from_gguf(&open_metadata_gguf("layer_shapes_len", &kvs))
3985            .expect_err("three entries for two layers");
3986        assert!(err.to_string().contains("wrong array length"), "{err}");
3987    }
3988
3989    /// The no-renormalise list is keyed on what llama.cpp's GRAPH does,
3990    /// not on what a GGUF says, because for these architectures the
3991    /// GGUF says nothing.
3992    ///
3993    /// `expert_weights_norm` is only written by converters that set it.
3994    /// `deepseek.cpp:145` passes `norm_w=false`, and
3995    /// `conversion/deepseek.py`'s `DeepseekModel` never writes the key
3996    /// -- only `DeepseekV2Model` does. So a real `deepseek` checkpoint
3997    /// carries no key at all and frink fell through to its default,
3998    /// renormalising the selected experts' softmax weights where
3999    /// llama.cpp leaves them alone.
4000    ///
4001    /// The same mistake made OLMoE emit garbage, which is why that list
4002    /// exists. This pins the membership so a later edit cannot quietly
4003    /// drop a name back into the renormalising default.
4004    #[test]
4005    fn the_architectures_llama_cpp_does_not_renormalise_are_pinned() {
4006        for arch in ["deepseek", "olmoe", "qwen2moe"] {
4007            assert!(
4008                NO_TOPK_RENORMALIZE_ARCHITECTURES.contains(&arch),
4009                "{arch} passes norm_w=false in llama.cpp and must not be renormalised"
4010            );
4011        }
4012        // `deepseek2` is a DIFFERENT architecture whose converter DOES
4013        // write the key, so it must not be on this list -- it gets its
4014        // answer from the file.
4015        assert!(!NO_TOPK_RENORMALIZE_ARCHITECTURES.contains(&"deepseek2"));
4016        assert!(!NO_TOPK_RENORMALIZE_ARCHITECTURES.contains(&"qwen3moe"));
4017    }
4018
4019    /// Every architecture llama.cpp defaults to SIGMOID gating must be
4020    /// on the list, because for these the GGUF carries no key to say so.
4021    ///
4022    /// Each of these reads `LLM_KV_EXPERT_GATING_FUNC` as optional and
4023    /// then sets SIGMOID when it is absent, so a converted checkpoint
4024    /// has nothing in it that would correct frink's softmax default.
4025    /// Same shape as the `deepseek` top-k renormalisation bug, and as
4026    /// `phi3`'s sliding window: the file is silent and the architecture
4027    /// decides.
4028    /// The literal table and its name list are two spellings of one
4029    /// fact; this is what keeps them one.
4030    #[test]
4031    fn the_gating_literal_names_are_the_gating_literal_table() {
4032        let from_table: Vec<&str> = GATING_LITERAL_ARCHITECTURES
4033            .iter()
4034            .map(|(n, _)| *n)
4035            .collect();
4036        assert_eq!(from_table, GATING_LITERAL_NAMES);
4037        // `mimo2.cpp:227` passes the SIGMOID literal, so the key is
4038        // never read there; a hand-written SOFTMAX key must not turn it.
4039        assert!(matches!(
4040            GATING_LITERAL_ARCHITECTURES
4041                .iter()
4042                .find(|(n, _)| *n == "mimo2")
4043                .map(|(_, g)| *g),
4044            Some(GatingFunction::Sigmoid)
4045        ));
4046    }
4047
4048    #[test]
4049    fn the_architectures_llama_cpp_defaults_to_sigmoid_gating_are_pinned() {
4050        for arch in ["afmoe", "deepseek2", "glm4moe", "laguna", "step35"] {
4051            assert!(
4052                SIGMOID_GATING_ARCHITECTURES.contains(&arch),
4053                "{arch} sets SIGMOID when the gating key is absent"
4054            );
4055        }
4056        // Architectures that HARDCODE softmax must stay off it, or the
4057        // fix becomes the opposite bug: `ernie4-5-moe.cpp:90` and
4058        // `qwen3moe` both gate with softmax unconditionally.
4059        for softmax in ["ernie4_5-moe", "qwen3moe", "olmoe", "llama"] {
4060            assert!(
4061                !SIGMOID_GATING_ARCHITECTURES.contains(&softmax),
4062                "{softmax} does not default to sigmoid"
4063            );
4064        }
4065    }
4066
4067    /// Every name in every architecture-keyed behaviour table is a name
4068    /// the catalog actually resolves, on the generic-GQA path.
4069    ///
4070    /// These five tables are the repo's dominant bug shape in its purest
4071    /// form: five lists of strings that have to agree with a sixth
4072    /// structure (`capability::architecture_catalog`) about what an
4073    /// architecture is called, with nothing checking it. A typo, a
4074    /// hyphen where the GGUF has an underscore, or a name that later
4075    /// moves to a dedicated stack all produce the same thing -- an entry
4076    /// that reads as coverage and can never fire. This repo has shipped
4077    /// exactly that once already, in `unsupported_feature_keys`, keyed
4078    /// on a GGUF spelling no converter writes.
4079    ///
4080    /// The generic-path check is the second half and the sharper one: a
4081    /// behaviour flag on an architecture that is `DedicatedOnly` or
4082    /// `Deferred` never reaches this loader, so it is dead text.
4083    ///
4084    /// Sabotage to confirm: add `"seedoss"` to any list below.
4085    #[test]
4086    fn every_architecture_keyed_behaviour_table_names_a_real_generic_row() {
4087        let tables: &[(&str, &[&str])] = &[
4088            ("SIGMOID_GATING_ARCHITECTURES", SIGMOID_GATING_ARCHITECTURES),
4089            ("GATING_LITERAL_ARCHITECTURES", GATING_LITERAL_NAMES),
4090            ("EXPERT_WEIGHTS_SCALE_READERS", EXPERT_WEIGHTS_SCALE_READERS),
4091            ("EXPERT_WEIGHTS_NORM_READERS", EXPERT_WEIGHTS_NORM_READERS),
4092            (
4093                "NO_TOPK_RENORMALIZE_ARCHITECTURES",
4094                NO_TOPK_RENORMALIZE_ARCHITECTURES,
4095            ),
4096            (
4097                "PRE_FFN_NORM_IS_POST_ATTENTION_NORM",
4098                crate::norm_sites::PRE_FFN_NORM_IS_POST_ATTENTION_NORM,
4099            ),
4100            (
4101                "PRE_FFN_NORM_IS_ATTN_OUTPUT_NORM",
4102                crate::norm_sites::PRE_FFN_NORM_IS_ATTN_OUTPUT_NORM,
4103            ),
4104            (
4105                "POST_NORMS_UNDER_GROK_NAMES",
4106                crate::norm_sites::POST_NORMS_UNDER_GROK_NAMES,
4107            ),
4108            (
4109                "ATTN_NORM_2_FEEDS_ATTENTION",
4110                crate::norm_sites::ATTN_NORM_2_FEEDS_ATTENTION,
4111            ),
4112            ("LEADING_DENSE_KEY_IS_INERT", LEADING_DENSE_KEY_IS_INERT),
4113            (
4114                "QK_NORM_AFTER_ROPE_ARCHITECTURES",
4115                QK_NORM_AFTER_ROPE_ARCHITECTURES,
4116            ),
4117        ];
4118        for (table, names) in tables {
4119            for arch in *names {
4120                let profile = crate::capability::resolve_profile(arch).unwrap_or_else(|| {
4121                    panic!("{table} names `{arch}`, which the catalog does not have")
4122                });
4123                if matches!(profile.path, crate::capability::ArchPath::GenericGqa { .. }) {
4124                    continue;
4125                }
4126                // Not a generic row, so the entry cannot fire HERE.
4127                // That is allowed only when something else is named as
4128                // applying the behaviour instead. An unexplained dead
4129                // entry still fails, which is the whole point.
4130                let owner = DEDICATED_OWNS_ITS_BEHAVIOUR
4131                    .iter()
4132                    .find(|(name, _)| name == arch)
4133                    .map(|(_, owner)| *owner);
4134                assert!(
4135                    owner.is_some(),
4136                    "{table} names `{arch}`, which resolves to {:?} and never reaches this \
4137                     loader, so the entry cannot fire. Either drop it, or add it to \
4138                     DEDICATED_OWNS_ITS_BEHAVIOUR naming what applies the behaviour instead",
4139                    profile.path
4140                );
4141            }
4142        }
4143    }
4144
4145    /// The three tables that describe how a layer is BUILT, rather than
4146    /// how it is routed, only carry architectures that are audited.
4147    ///
4148    /// The distinction matters and is not pedantry. A routing default
4149    /// (`SIGMOID_GATING_ARCHITECTURES`, `NO_TOPK_RENORMALIZE_ARCHITECTURES`)
4150    /// is allowed to name an architecture that still refuses: it is
4151    /// written down ahead of time so a later admission inherits the
4152    /// right answer, and the tables say so. But the three below change
4153    /// which TENSOR a layer reads and in what order -- and each was
4154    /// added for exactly one architecture, whose fixture is the only
4155    /// thing proving the change is right. A fourth name appearing here
4156    /// without evidence would be a claim about a graph nobody read,
4157    /// carried by a list whose doc comment cites two.
4158    #[test]
4159    fn the_layer_shape_tables_only_name_audited_architectures() {
4160        for (table, names) in [
4161            (
4162                "PRE_FFN_NORM_IS_POST_ATTENTION_NORM",
4163                crate::norm_sites::PRE_FFN_NORM_IS_POST_ATTENTION_NORM,
4164            ),
4165            (
4166                "PRE_FFN_NORM_IS_ATTN_OUTPUT_NORM",
4167                crate::norm_sites::PRE_FFN_NORM_IS_ATTN_OUTPUT_NORM,
4168            ),
4169            (
4170                "POST_NORMS_UNDER_GROK_NAMES",
4171                crate::norm_sites::POST_NORMS_UNDER_GROK_NAMES,
4172            ),
4173            (
4174                "ATTN_NORM_2_FEEDS_ATTENTION",
4175                crate::norm_sites::ATTN_NORM_2_FEEDS_ATTENTION,
4176            ),
4177            ("LEADING_DENSE_KEY_IS_INERT", LEADING_DENSE_KEY_IS_INERT),
4178            (
4179                "QK_NORM_AFTER_ROPE_ARCHITECTURES",
4180                QK_NORM_AFTER_ROPE_ARCHITECTURES,
4181            ),
4182        ] {
4183            for arch in names {
4184                assert!(
4185                    crate::capability::is_audited_generic(arch),
4186                    "{table} names `{arch}`, which is not in AUDITED_GENERIC_GQA. Either it \
4187                     has a fixture proving the change is right -- audit it -- or the entry \
4188                     is a guess about a graph"
4189                );
4190            }
4191        }
4192    }
4193
4194    fn config_for_arch(arch: &'static str) -> Result<ModelConfig, LoadError> {
4195        // The per-arch hyperparameter keys are looked up by the arch's
4196        // own prefix, so they have to be built for the arch under test.
4197        let keys: Vec<String> = [
4198            "block_count",
4199            "embedding_length",
4200            "attention.head_count",
4201            "attention.head_count_kv",
4202            "attention.key_length",
4203        ]
4204        .iter()
4205        .map(|k| format!("{arch}.{k}"))
4206        .collect();
4207        let theta = format!("{arch}.rope.freq_base");
4208        let kvs: Vec<(&str, Kv)> = vec![
4209            ("general.architecture", Kv::Str(arch)),
4210            (keys[0].as_str(), Kv::U32(1)),
4211            (keys[1].as_str(), Kv::U32(64)),
4212            (keys[2].as_str(), Kv::U32(1)),
4213            (keys[3].as_str(), Kv::U32(1)),
4214            (keys[4].as_str(), Kv::U32(64)),
4215            (theta.as_str(), Kv::F32(10_000.0)),
4216        ];
4217        ModelConfig::from_gguf(&open_metadata_gguf(arch, &kvs))
4218    }
4219
4220    /// The generic path is OPT-IN, and this is what proves it.
4221    ///
4222    /// An architecture nobody has checked used to FALL ONTO generic GQA
4223    /// and run. Five did exactly that and computed the wrong thing for
4224    /// the life of the project. The refusal exists; nothing tested it,
4225    /// so a reordering or an unevidenced addition to
4226    /// `AUDITED_GENERIC_GQA` would have gone unnoticed.
4227    #[test]
4228    fn an_unaudited_generic_architecture_refuses_rather_than_guessing() {
4229        // `grovemoe` is on the generic path and is not in the audited
4230        // list. It is the sixth name to hold this slot: `starcoder` was
4231        // first, until an audit found it REQUIRES a fused
4232        // `attn_qkv.bias` and a learned `position_embd` the generic
4233        // decoder has no slot for, so it refuses for a stronger reason;
4234        // then `xverse`, until it was admitted with a libllama-golden
4235        // fixture (`tests/fixture_away_graphs.rs`); then `nanbeige`,
4236        // until the layer loop became `crate::layer_loops`; then
4237        // `talkie`, until `crate::skip_stream`; then `arctic`, until
4238        // `crate::parallel_dense_ffn`. `grovemoe` runs a SECOND expert
4239        // bank (`src/models/grovemoe.cpp:57-59,137-164`) whose upstream
4240        // graph diverges from the reference, and its blocker is
4241        // invisible in metadata, so nothing but this gate stops it.
4242        assert!(
4243            !crate::capability::is_audited_generic("grovemoe"),
4244            "this test needs an arch that is generic AND unaudited"
4245        );
4246        match config_for_arch("grovemoe") {
4247            Err(LoadError::UnauditedArchitecture(name, ..)) => assert_eq!(name, "grovemoe"),
4248            other => panic!("expected an unaudited refusal, got {other:?}"),
4249        }
4250    }
4251
4252    /// An architecture with evidence still loads, or the inversion would
4253    /// have turned every model off.
4254    #[test]
4255    fn an_audited_architecture_still_loads() {
4256        assert!(crate::capability::is_audited_generic("llama"));
4257        assert!(config_for_arch("llama").is_ok());
4258    }
4259
4260    /// A NAMED problem must outrank "unaudited".
4261    ///
4262    /// `grovemoe` is unaudited AND names its second expert bank; a
4263    /// `llama4` file declaring a window of zero names the branch
4264    /// libllama aborts on (`crate::chunked_swa`), and that is what its
4265    /// refusal should say. Reporting "unaudited" instead would be true
4266    /// and far less useful, and it is the ordering the loader's own
4267    /// comment claims. Nothing checked that claim. (`gpt2`, `bloom` and
4268    /// then a plain `llama4` were the example until each was served.)
4269    #[test]
4270    fn a_named_refusal_outranks_the_unaudited_one() {
4271        let kvs: Vec<(&str, Kv)> = vec![
4272            ("general.architecture", Kv::Str("llama4")),
4273            ("llama4.block_count", Kv::U32(1)),
4274            ("llama4.embedding_length", Kv::U32(64)),
4275            ("llama4.attention.head_count", Kv::U32(1)),
4276            ("llama4.attention.head_count_kv", Kv::U32(1)),
4277            ("llama4.attention.key_length", Kv::U32(64)),
4278            ("llama4.rope.freq_base", Kv::F32(10_000.0)),
4279            ("llama4.expert_count", Kv::U32(16)),
4280            ("llama4.interleave_moe_layer_step", Kv::U32(1)),
4281            ("llama4.attention.sliding_window", Kv::U32(0)),
4282        ];
4283        let err = ModelConfig::from_gguf(&open_metadata_gguf("llama4", &kvs))
4284            .expect_err("a zero window must refuse");
4285        assert!(
4286            !matches!(err, LoadError::UnauditedArchitecture(..)),
4287            "llama4 should report its own reason, not that nobody audited it: {err:?}"
4288        );
4289        assert!(err.to_string().contains("llama-graph.cpp:159"), "{err}");
4290    }
4291
4292    /// A checkpoint that declares YaRN gets the per-band divisors the
4293    /// reference's `"yarn"` arm implies, folded into `rope_freqs` so the
4294    /// existing RoPE kernels apply them. Expected values are hand-derived
4295    /// from `_find_correction_dim` for this fixture (rotary width 64,
4296    /// base 10000, original context 131072): `low = 22`, `high = 35`.
4297    ///
4298    /// Before this, frink read neither `rope.scaling.type` nor
4299    /// `rope.scaling.factor`, so this file roped exactly like an
4300    /// unscaled one -- correct near position 0, progressively wrong
4301    /// further in.
4302    #[test]
4303    fn a_gguf_declaring_yarn_gets_its_rope_frequencies_rewritten() {
4304        let cfg = llama_config_with(
4305            "yarn",
4306            &[
4307                ("llama.rope.scaling.type", Kv::Str("yarn")),
4308                ("llama.rope.scaling.factor", Kv::F32(8.0)),
4309                (
4310                    "llama.rope.scaling.original_context_length",
4311                    Kv::U32(131_072),
4312                ),
4313            ],
4314        );
4315        let factors = cfg
4316            .rope_freqs
4317            .expect("a YaRN checkpoint must carry rewritten per-band frequencies")
4318            .full;
4319        assert_eq!(factors.len(), 32, "one divisor per rotation band");
4320        assert!(
4321            (factors[0] - 1.0).abs() < 1e-6,
4322            "the fastest band is left extrapolated, got {}",
4323            factors[0]
4324        );
4325        let ramp = (31.0 - 22.0) / (35.0 - 22.0);
4326        let want = 1.0 / (ramp / 8.0 + (1.0 - ramp));
4327        assert!(
4328            (factors[31] - want).abs() < 1e-4,
4329            "slowest band: got {}, reference {want}",
4330            factors[31]
4331        );
4332    }
4333
4334    /// The rewrite must not fire on a file that did not ask for it. A
4335    /// scaling type frink does not implement (`linear`, `longrope`) is
4336    /// left exactly as it was rather than being roped as YaRN, which
4337    /// would be a new kind of wrong rather than the current known one.
4338    /// `rope.scaling.type = "linear"` must actually scale.
4339    ///
4340    /// Rotating position `p/s` is the same as rotating `p` with every
4341    /// band's frequency divided by `s`, and `rope_freqs` is exactly a
4342    /// per-band frequency divisor, so a uniform vector of `s` expresses
4343    /// linear scaling with no new code on the RoPE paths.
4344    ///
4345    /// Before this, the scaling type was compared against "yarn" and
4346    /// anything else returned None, so such a file loaded and roped at
4347    /// unscaled positions: a different model, no error.
4348    #[test]
4349    fn linear_scaling_is_applied_as_a_uniform_frequency_divisor() {
4350        let cfg = llama_config_with(
4351            "linear",
4352            &[
4353                ("llama.rope.scaling.type", Kv::Str("linear")),
4354                ("llama.rope.scaling.factor", Kv::F32(4.0)),
4355            ],
4356        );
4357        let freqs = &cfg
4358            .rope_freqs
4359            .as_ref()
4360            .expect("linear scaling must produce frequency factors")
4361            .full;
4362        assert_eq!(freqs.len(), cfg.head_dim / 2, "one factor per rotated pair");
4363        assert!(
4364            freqs.iter().all(|f| (*f - 4.0).abs() < 1e-6),
4365            "linear scaling is uniform across bands, unlike YaRN: got {freqs:?}"
4366        );
4367    }
4368
4369    /// A factor that corrects nothing is not a correction.
4370    #[test]
4371    fn a_linear_factor_of_one_is_treated_as_absent() {
4372        assert!(llama_config_with(
4373            "linear_one",
4374            &[
4375                ("llama.rope.scaling.type", Kv::Str("linear")),
4376                ("llama.rope.scaling.factor", Kv::F32(1.0)),
4377            ],
4378        )
4379        .rope_freqs
4380        .is_none());
4381    }
4382
4383    #[test]
4384    fn a_gguf_without_yarn_scaling_keeps_its_rope_frequencies_untouched() {
4385        assert!(llama_config_with("noscale", &[]).rope_freqs.is_none());
4386        // Linear scaling is NOT "no scaling". It used to land here,
4387        // asserted as `is_none()`, on the reasoning that leaving
4388        // positions alone beat roping them wrong in a new way. Both are
4389        // wrong output: llama.cpp divides the positions by the factor.
4390        // See `linear_scaling_is_applied_as_a_uniform_frequency_divisor`.
4391        // YaRN with a no-op factor is not a correction either.
4392        assert!(llama_config_with(
4393            "yarn_factor_one",
4394            &[
4395                ("llama.rope.scaling.type", Kv::Str("yarn")),
4396                ("llama.rope.scaling.factor", Kv::F32(1.0)),
4397                (
4398                    "llama.rope.scaling.original_context_length",
4399                    Kv::U32(131_072),
4400                ),
4401            ],
4402        )
4403        .rope_freqs
4404        .is_none());
4405    }
4406
4407    /// The correction range is measured against the context the
4408    /// checkpoint was *trained* at, so a file that declares YaRN without
4409    /// `rope.scaling.original_context_length` leaves the rotation alone
4410    /// rather than inventing a trained length (`context_length` on such
4411    /// a file is the *extended* one, which would put the ramp in the
4412    /// wrong place at every band).
4413    #[test]
4414    fn yarn_without_an_original_context_length_is_not_guessed_at() {
4415        let cfg = llama_config_with(
4416            "yarn_noctx",
4417            &[
4418                ("llama.rope.scaling.type", Kv::Str("yarn")),
4419                ("llama.rope.scaling.factor", Kv::F32(8.0)),
4420            ],
4421        );
4422        assert!(cfg.rope_freqs.is_none());
4423    }
4424
4425    /// `general.sampling.*` is the checkpoint's own recommendation, and
4426    /// only the keys the file carries become one: a file naming just
4427    /// `top_k` must leave temperature and top_p to the server's
4428    /// defaults.
4429    #[test]
4430    fn gguf_sampling_metadata_is_read_as_the_checkpoints_recommendation() {
4431        use crate::sampling::RecommendedSampling;
4432        let full = RecommendedSampling::from_gguf(&open_metadata_gguf(
4433            "sampling_full",
4434            &[
4435                ("general.architecture", Kv::Str("llama")),
4436                ("general.sampling.temp", Kv::F32(1.0)),
4437                ("general.sampling.top_k", Kv::U32(20)),
4438                ("general.sampling.top_p", Kv::F32(0.95)),
4439            ],
4440        ));
4441        assert_eq!(
4442            full,
4443            RecommendedSampling {
4444                temperature: Some(1.0),
4445                top_p: Some(0.95),
4446                top_k: Some(20),
4447            }
4448        );
4449
4450        let partial = RecommendedSampling::from_gguf(&open_metadata_gguf(
4451            "sampling_partial",
4452            &[
4453                ("general.architecture", Kv::Str("llama")),
4454                ("general.sampling.top_k", Kv::U32(40)),
4455            ],
4456        ));
4457        assert_eq!(partial.top_k, Some(40));
4458        assert_eq!(partial.temperature, None);
4459        assert_eq!(partial.top_p, None);
4460    }
4461
4462    /// A converter that wrote `temp = 1` stores a GGUF integer, not a
4463    /// float. Dropping it would serve a checkpoint that asked for
4464    /// temperature 1.0 at the framework's greedy default -- the
4465    /// repetition-loop failure the recommendation exists to prevent.
4466    #[test]
4467    fn an_integer_valued_sampling_temp_is_still_a_recommendation() {
4468        let recommended = crate::sampling::RecommendedSampling::from_gguf(&open_metadata_gguf(
4469            "sampling_int_temp",
4470            &[
4471                ("general.architecture", Kv::Str("llama")),
4472                ("general.sampling.temp", Kv::U32(1)),
4473            ],
4474        ));
4475        assert_eq!(recommended.temperature, Some(1.0));
4476    }
4477
4478    /// The overwhelming majority of checkpoints recommend nothing, and
4479    /// those must keep frink's existing defaults exactly.
4480    #[test]
4481    fn a_gguf_without_sampling_metadata_recommends_nothing() {
4482        let recommended = crate::sampling::RecommendedSampling::from_gguf(&open_metadata_gguf(
4483            "sampling_absent",
4484            &[("general.architecture", Kv::Str("llama"))],
4485        ));
4486        assert!(recommended.is_empty());
4487    }
4488
4489    #[test]
4490    fn model_config_from_gguf_rejects_dedicated_architectures() {
4491        let tmp = std::env::temp_dir().join(format!(
4492            "frink_test_dedicated_arch_{}.gguf",
4493            std::process::id()
4494        ));
4495        std::fs::write(&tmp, build_arch_only_gguf("deepseek4")).unwrap();
4496        let file = frink_gguf::GgufFile::open(&tmp).expect("minimal header must still parse");
4497        std::fs::remove_file(&tmp).ok();
4498
4499        match ModelConfig::from_gguf(&file) {
4500            Err(LoadError::DedicatedArchitectureRequired(arch, _)) => {
4501                assert_eq!(arch, "deepseek4");
4502            }
4503            other => panic!(
4504                "expected LoadError::DedicatedArchitectureRequired for deepseek4, got {other:?}"
4505            ),
4506        }
4507    }
4508
4509    /// The same Q5_K block bytes cross-validated against an independent
4510    /// Python reference in `frink-quant`'s own tests, reused here for
4511    /// the same full-path proof as the Q6_K test below.
4512    #[rustfmt::skip]
4513    const Q5_K_TEST_BLOCK: [u8; 176] = [
4514        0x66, 0x2a, 0x66, 0x2a, 0x01, 0x01, 0x01, 0x01, 0x4f, 0x4b, 0x10, 0x12, 0x41, 0xe2, 0xc1,
4515        0xb1, 0x72, 0x2f, 0x20, 0x07, 0x31, 0x0c, 0x38, 0xb3, 0x9c, 0xb8, 0xad, 0x2f, 0x9a, 0xea,
4516        0x17, 0xd0, 0xee, 0x93, 0x9e, 0x3e, 0x74, 0xbb, 0x28, 0x18, 0x39, 0x25, 0xb6, 0x09, 0x18,
4517        0x29, 0x1c, 0x1d, 0x29, 0x41, 0x40, 0x0a, 0x74, 0x7d, 0xfd, 0x21, 0xdd, 0x6d, 0x45, 0x73,
4518        0x0e, 0x1e, 0xc0, 0x4a, 0xfc, 0xf3, 0x8e, 0x24, 0x6b, 0x34, 0x7d, 0xbe, 0x94, 0xde, 0x59,
4519        0x7a, 0x35, 0x30, 0x36, 0x0a, 0xf9, 0x4a, 0x9b, 0xa2, 0x26, 0x21, 0xa2, 0xfa, 0xdf, 0x4b,
4520        0x29, 0x64, 0x6f, 0xbb, 0xca, 0x0f, 0x3c, 0xda, 0x20, 0xf4, 0x93, 0x86, 0xab, 0x6e, 0xb9,
4521        0xe5, 0xd5, 0xa0, 0x82, 0xd6, 0x41, 0xff, 0x12, 0xbc, 0x34, 0xbb, 0xab, 0xb8, 0x20, 0x2f,
4522        0xbb, 0x5f, 0x0c, 0x10, 0xcf, 0x49, 0xc5, 0x86, 0x5c, 0xdf, 0xff, 0x78, 0x44, 0x26, 0x3b,
4523        0xc2, 0x23, 0x3d, 0x2b, 0xe9, 0x00, 0x12, 0xf8, 0xea, 0xe2, 0x9e, 0x5e, 0x50, 0x20, 0x9f,
4524        0x9d, 0x8d, 0x7d, 0x7f, 0xcc, 0x1d, 0x0e, 0x13, 0xf8, 0xc2, 0xf1, 0x3d, 0x08, 0x2f, 0x23,
4525        0x13, 0xac, 0x0d, 0xa7, 0xe7, 0x20, 0xa3, 0x90, 0xb7, 0xc8, 0x28,
4526    ];
4527
4528    fn build_single_q5_k_tensor_gguf() -> Vec<u8> {
4529        let mut buf = Vec::new();
4530        buf.write_u32::<LittleEndian>(frink_gguf::GGUF_MAGIC)
4531            .unwrap();
4532        buf.write_u32::<LittleEndian>(3).unwrap(); // version
4533        buf.write_u64::<LittleEndian>(1).unwrap(); // tensor_count
4534        buf.write_u64::<LittleEndian>(1).unwrap(); // kv_count
4535
4536        write_kv_str(&mut buf, "general.architecture", "frink-q5k-test");
4537
4538        write_string(&mut buf, "test.weight");
4539        buf.write_u32::<LittleEndian>(2).unwrap(); // n_dims
4540                                                   // Real GGUF ne[] order is fastest-varying-first, i.e. [cols,
4541                                                   // rows] -- reversed from the semantic [rows, cols] this tensor
4542                                                   // represents (1 row, 256 cols / 1 Q5_K block).
4543        buf.write_u64::<LittleEndian>(256).unwrap(); // cols (1 Q5_K block)
4544        buf.write_u64::<LittleEndian>(1).unwrap(); // rows
4545        buf.write_u32::<LittleEndian>(13).unwrap(); // dtype tag: Q5_K
4546        buf.write_u64::<LittleEndian>(0).unwrap(); // offset
4547
4548        while buf.len() % 32 != 0 {
4549            buf.push(0);
4550        }
4551        buf.extend_from_slice(&Q5_K_TEST_BLOCK);
4552        buf
4553    }
4554
4555    /// How far a fused dot may sit from an exact dequantized dot.
4556    ///
4557    /// Two regimes, and one fixed number cannot describe both. With
4558    /// `FRINK_CPU_INT_DOT` off the activation stays f32 and only
4559    /// rounding separates the two. With it on, the activation is
4560    /// quantized to int8 at `d = amax / 127`, which is the flag both
4561    /// binaries turn on by default and the reason the Q5_K and Q6_K
4562    /// cases failed against a flat `1e-2`.
4563    ///
4564    /// The bound grows with the L2 norm of the row, NOT the L1. Each
4565    /// element carries an independent rounding of up to `d/2`, so the
4566    /// dot's error is a sum of independent terms whose standard
4567    /// deviation is `d/sqrt(12) * ||w||_2`. Bounding by the worst case
4568    /// `d/2 * ||w||_1` instead assumes every rounding aligns with its
4569    /// weight's sign, which on this fixture gives 0.347 against a dot
4570    /// of 2.77: 12% of the value, loose enough that injecting a 5%
4571    /// error still passed. Measured here, the real error is 1.8 sigma,
4572    /// so four sigma keeps better than 2x headroom while still failing
4573    /// that 5% injection.
4574    fn fused_dot_tolerance(weights: &[f32], x: &[f32], exact_bound: f32) -> f32 {
4575        if !frink_core::weight_matrix::cpu_int_dot_for(
4576            frink_core::weight_matrix::IntDotShape::Matvec,
4577        ) {
4578            return exact_bound;
4579        }
4580        let amax = x.iter().fold(0.0f32, |a, v| a.max(v.abs()));
4581        let l2 = weights.iter().map(|w| w * w).sum::<f32>().sqrt();
4582        4.0 * (amax / 127.0) / 12f32.sqrt() * l2 + exact_bound
4583    }
4584
4585    #[test]
4586    fn load_weight_matrix_handles_a_real_on_disk_q5_k_tensor_end_to_end() {
4587        let tmp =
4588            std::env::temp_dir().join(format!("frink_test_q5k_tensor_{}.gguf", std::process::id()));
4589        std::fs::write(&tmp, build_single_q5_k_tensor_gguf()).unwrap();
4590        let file = frink_gguf::GgufFile::open(&tmp).expect("real Q5_K GGUF file must parse");
4591        std::fs::remove_file(&tmp).ok();
4592
4593        let matrix = load_weight_matrix(&file, "test.weight").expect("Q5_K tensor must load");
4594        assert_eq!(matrix.rows(), 1);
4595        assert_eq!(matrix.cols(), 256);
4596        match &matrix {
4597            WeightMatrix::Quantized { kind, data, .. } => {
4598                assert_eq!(*kind, QuantKind::Q5K);
4599                assert!(
4600                    data.is_mapped(),
4601                    "Q5_K tensors should take the zero-copy mmap path, same as Q8_0/Q4_0"
4602                );
4603            }
4604            _ => panic!("expected a Quantized matrix for a Q5_K tensor"),
4605        }
4606
4607        let expected = frink_quant::dequant_q5_k(&Q5_K_TEST_BLOCK).unwrap();
4608        let x: Vec<f32> = (0..256).map(|i| ((i as f32) * 0.013).sin()).collect();
4609        let expected_dot: f32 = expected.iter().zip(x.iter()).map(|(a, b)| a * b).sum();
4610
4611        let got = matrix.apply(&x);
4612        assert_eq!(got.len(), 1);
4613        assert!(
4614            (got[0] - expected_dot).abs() < fused_dot_tolerance(&expected, &x, 1e-2),
4615            "end-to-end loaded+applied Q5_K matrix diverged from direct dequant: got={} expected={}",
4616            got[0],
4617            expected_dot
4618        );
4619    }
4620
4621    /// The same Q6_K block bytes cross-validated against an independent
4622    /// Python reference in `frink-quant`'s own tests; reused here to
4623    /// prove the *full*
4624    /// path -- real on-disk GGUF bytes, parsed by `frink-gguf`, read
4625    /// through `GgufFile::tensor_mapped_range`, dispatched by
4626    /// `WeightMatrix::apply` to `frink_quant::dot_q6_k_f32` -- produces
4627    /// the same result as directly dequantizing those bytes, not just
4628    /// that the isolated kernel is correct in unit-test isolation.
4629    #[rustfmt::skip]
4630    const Q6_K_TEST_BLOCK: [u8; 210] = [
4631        0xe0, 0xa5, 0x40, 0x5c, 0x8d, 0x3a, 0x0a, 0x26, 0xfb, 0x4b, 0x6e, 0x9a, 0xdf, 0x3e, 0xa3,
4632        0xc4, 0xf8, 0x2b, 0x1d, 0x95, 0x76, 0x7d, 0x3b, 0xcd, 0xfd, 0xef, 0xc2, 0x0b, 0x07, 0x63,
4633        0x29, 0xfb, 0x81, 0x57, 0xbe, 0xbe, 0x06, 0xf7, 0x3a, 0x92, 0xc4, 0x43, 0xff, 0xad, 0xac,
4634        0x7e, 0x0f, 0x00, 0x2a, 0x4f, 0xf0, 0xf8, 0xa9, 0xfa, 0x3c, 0x90, 0x6d, 0x73, 0x2d, 0x5a,
4635        0xe6, 0xc6, 0x46, 0xf2, 0x0d, 0x55, 0x4c, 0x25, 0x38, 0x71, 0x2b, 0x35, 0x38, 0x82, 0x16,
4636        0x37, 0x5f, 0x32, 0x61, 0x02, 0xdd, 0x2f, 0x6f, 0x7b, 0x1f, 0xb4, 0x1a, 0x1b, 0x3e, 0x4f,
4637        0x11, 0xa3, 0x17, 0x40, 0x5a, 0x5f, 0x76, 0xcd, 0x19, 0x27, 0x9b, 0xc7, 0xc8, 0xf7, 0xf7,
4638        0xee, 0xf4, 0x86, 0xd9, 0xfd, 0xa7, 0xfe, 0x9e, 0xac, 0x70, 0x53, 0x5b, 0x76, 0xfb, 0x39,
4639        0xf8, 0x4b, 0x98, 0xfe, 0xd0, 0x06, 0x21, 0x4c, 0x4d, 0xbe, 0x10, 0x2b, 0x06, 0x65, 0xc9,
4640        0x5e, 0xf9, 0x95, 0x72, 0xae, 0x99, 0xd9, 0x7e, 0x15, 0xbd, 0x5e, 0x6d, 0xe8, 0x25, 0x8a,
4641        0xd5, 0x99, 0xc6, 0x6b, 0x69, 0xc7, 0x84, 0xc6, 0xa4, 0xf7, 0xb9, 0x6d, 0x68, 0x45, 0x0e,
4642        0x65, 0x69, 0xeb, 0xe6, 0xeb, 0xe9, 0x28, 0xa6, 0xb9, 0x96, 0xf2, 0xe8, 0xa7, 0x9b, 0x6e,
4643        0x79, 0x8a, 0x68, 0x65, 0x59, 0x98, 0x8b, 0x44, 0x41, 0x98, 0x9a, 0x56, 0x01, 0x01, 0x01,
4644        0x02, 0x01, 0x01, 0x01, 0x01, 0x02, 0x01, 0x02, 0x02, 0x01, 0x01, 0x01, 0x02, 0x1f, 0x25,
4645    ];
4646
4647    fn build_single_q6_k_tensor_gguf() -> Vec<u8> {
4648        let mut buf = Vec::new();
4649        buf.write_u32::<LittleEndian>(frink_gguf::GGUF_MAGIC)
4650            .unwrap();
4651        buf.write_u32::<LittleEndian>(3).unwrap(); // version
4652        buf.write_u64::<LittleEndian>(1).unwrap(); // tensor_count
4653        buf.write_u64::<LittleEndian>(1).unwrap(); // kv_count
4654
4655        write_kv_str(&mut buf, "general.architecture", "frink-q6k-test");
4656
4657        write_string(&mut buf, "test.weight");
4658        buf.write_u32::<LittleEndian>(2).unwrap(); // n_dims
4659                                                   // Real GGUF ne[] order is fastest-varying-first, i.e. [cols, rows].
4660        buf.write_u64::<LittleEndian>(256).unwrap(); // cols (1 Q6_K block)
4661        buf.write_u64::<LittleEndian>(1).unwrap(); // rows
4662        buf.write_u32::<LittleEndian>(14).unwrap(); // dtype tag: Q6_K
4663        buf.write_u64::<LittleEndian>(0).unwrap(); // offset
4664
4665        while buf.len() % 32 != 0 {
4666            buf.push(0);
4667        }
4668        buf.extend_from_slice(&Q6_K_TEST_BLOCK);
4669        buf
4670    }
4671
4672    #[test]
4673    fn load_weight_matrix_handles_a_real_on_disk_q6_k_tensor_end_to_end() {
4674        let tmp =
4675            std::env::temp_dir().join(format!("frink_test_q6k_tensor_{}.gguf", std::process::id()));
4676        std::fs::write(&tmp, build_single_q6_k_tensor_gguf()).unwrap();
4677        let file = frink_gguf::GgufFile::open(&tmp).expect("real Q6_K GGUF file must parse");
4678        std::fs::remove_file(&tmp).ok();
4679
4680        let matrix = load_weight_matrix(&file, "test.weight").expect("Q6_K tensor must load");
4681        assert_eq!(matrix.rows(), 1);
4682        assert_eq!(matrix.cols(), 256);
4683        match &matrix {
4684            WeightMatrix::Quantized { kind, data, .. } => {
4685                assert_eq!(*kind, QuantKind::Q6K);
4686                assert!(
4687                    data.is_mapped(),
4688                    "Q6_K tensors should take the zero-copy mmap path, same as Q8_0/Q4_0"
4689                );
4690            }
4691            _ => panic!("expected a Quantized matrix for a Q6_K tensor"),
4692        }
4693
4694        let expected = frink_quant::dequant_q6_k(&Q6_K_TEST_BLOCK).unwrap();
4695        let x: Vec<f32> = (0..256).map(|i| ((i as f32) * 0.013).sin()).collect();
4696        let expected_dot: f32 = expected.iter().zip(x.iter()).map(|(a, b)| a * b).sum();
4697
4698        let got = matrix.apply(&x);
4699        assert_eq!(got.len(), 1);
4700        assert!(
4701            (got[0] - expected_dot).abs() < fused_dot_tolerance(&expected, &x, 1e-2),
4702            "end-to-end loaded+applied Q6_K matrix diverged from direct dequant: got={} expected={}",
4703            got[0],
4704            expected_dot
4705        );
4706    }
4707
4708    fn build_single_bf16_tensor_gguf(rows: u64, cols: u64, values: &[f32]) -> Vec<u8> {
4709        let mut buf = Vec::new();
4710        buf.write_u32::<LittleEndian>(frink_gguf::GGUF_MAGIC)
4711            .unwrap();
4712        buf.write_u32::<LittleEndian>(3).unwrap(); // version
4713        buf.write_u64::<LittleEndian>(1).unwrap(); // tensor_count
4714        buf.write_u64::<LittleEndian>(1).unwrap(); // kv_count
4715
4716        write_kv_str(&mut buf, "general.architecture", "frink-bf16-test");
4717
4718        write_string(&mut buf, "test.weight");
4719        buf.write_u32::<LittleEndian>(2).unwrap(); // n_dims
4720                                                   // Real GGUF ne[] order is fastest-varying-first, i.e. [cols, rows].
4721        buf.write_u64::<LittleEndian>(cols).unwrap();
4722        buf.write_u64::<LittleEndian>(rows).unwrap();
4723        buf.write_u32::<LittleEndian>(30).unwrap(); // dtype tag: BF16
4724        buf.write_u64::<LittleEndian>(0).unwrap(); // offset
4725
4726        while buf.len() % 32 != 0 {
4727            buf.push(0);
4728        }
4729        for &v in values {
4730            // Real bf16 truncation (round-toward-zero, matching a real
4731            // writer closely enough for round-trip test purposes): top
4732            // 16 bits of the f32 bit pattern.
4733            let bf16_bits = (v.to_bits() >> 16) as u16;
4734            buf.extend_from_slice(&bf16_bits.to_le_bytes());
4735        }
4736        buf
4737    }
4738
4739    #[test]
4740    fn load_weight_matrix_handles_a_real_on_disk_bf16_tensor_end_to_end() {
4741        // Values with zero low-mantissa bits, so f32->bf16 truncation
4742        // is lossless and this is an exact-equality check.
4743        let values: Vec<f32> = vec![1.0, -2.5, 0.0, 4.0, -8.0, 16.0];
4744        let tmp = std::env::temp_dir().join(format!(
4745            "frink_test_bf16_tensor_{}.gguf",
4746            std::process::id()
4747        ));
4748        std::fs::write(&tmp, build_single_bf16_tensor_gguf(2, 3, &values)).unwrap();
4749        let file = frink_gguf::GgufFile::open(&tmp).expect("real BF16 GGUF file must parse");
4750        std::fs::remove_file(&tmp).ok();
4751
4752        let matrix = load_weight_matrix(&file, "test.weight").expect("BF16 tensor must load");
4753        assert_eq!(matrix.rows(), 2);
4754        assert_eq!(matrix.cols(), 3);
4755        match &matrix {
4756            WeightMatrix::F32(tensor) => {
4757                assert_eq!(tensor.data, values, "BF16 must widen to f32 exactly");
4758            }
4759            _ => panic!("expected an F32 matrix for a BF16 tensor (no fused dot kernel for it)"),
4760        }
4761    }
4762
4763    fn build_single_f16_tensor_gguf(rows: u64, cols: u64, values: &[f32]) -> Vec<u8> {
4764        let mut buf = Vec::new();
4765        buf.write_u32::<LittleEndian>(frink_gguf::GGUF_MAGIC)
4766            .unwrap();
4767        buf.write_u32::<LittleEndian>(3).unwrap(); // version
4768        buf.write_u64::<LittleEndian>(1).unwrap(); // tensor_count
4769        buf.write_u64::<LittleEndian>(1).unwrap(); // kv_count
4770
4771        write_kv_str(&mut buf, "general.architecture", "frink-f16-test");
4772
4773        write_string(&mut buf, "test.weight");
4774        buf.write_u32::<LittleEndian>(2).unwrap(); // n_dims
4775        buf.write_u64::<LittleEndian>(cols).unwrap();
4776        buf.write_u64::<LittleEndian>(rows).unwrap();
4777        buf.write_u32::<LittleEndian>(1).unwrap(); // dtype tag: F16
4778        buf.write_u64::<LittleEndian>(0).unwrap(); // offset
4779
4780        while buf.len() % 32 != 0 {
4781            buf.push(0);
4782        }
4783        for &v in values {
4784            buf.extend_from_slice(&half::f16::from_f32(v).to_le_bytes());
4785        }
4786        buf
4787    }
4788
4789    /// `GgmlType::F16` was parsed and sized but had no dequant arm in any
4790    /// of the seven loaders, so every `*-f16.gguf` was a hard
4791    /// `UnsupportedDtype`. Values are exactly representable in f16, so
4792    /// this is an exact-equality check.
4793    #[test]
4794    fn load_weight_matrix_handles_a_real_on_disk_f16_tensor_end_to_end() {
4795        let values: Vec<f32> = vec![1.0, -2.5, 0.0, 4.0, -8.0, 16.0];
4796        let tmp =
4797            std::env::temp_dir().join(format!("frink_test_f16_tensor_{}.gguf", std::process::id()));
4798        std::fs::write(&tmp, build_single_f16_tensor_gguf(2, 3, &values)).unwrap();
4799        let file = frink_gguf::GgufFile::open(&tmp).expect("real F16 GGUF file must parse");
4800        std::fs::remove_file(&tmp).ok();
4801
4802        let matrix = load_weight_matrix(&file, "test.weight").expect("F16 tensor must load");
4803        assert_eq!(matrix.rows(), 2);
4804        assert_eq!(matrix.cols(), 3);
4805        match &matrix {
4806            WeightMatrix::F32(tensor) => {
4807                assert_eq!(tensor.data, values, "F16 must widen to f32 exactly");
4808            }
4809            _ => panic!("expected an F32 matrix for an F16 tensor (no fused dot kernel for it)"),
4810        }
4811
4812        // The same tensor read as a plain vector (norm weights, biases and
4813        // the router all take this path, not `load_weight_matrix`).
4814        let tmp =
4815            std::env::temp_dir().join(format!("frink_test_f16_vec_{}.gguf", std::process::id()));
4816        std::fs::write(&tmp, build_single_f16_tensor_gguf(2, 3, &values)).unwrap();
4817        let file = frink_gguf::GgufFile::open(&tmp).expect("real F16 GGUF file must parse");
4818        std::fs::remove_file(&tmp).ok();
4819        assert_eq!(load_f32_vec(&file, "test.weight").unwrap(), values);
4820    }
4821
4822    fn build_single_q5_1_tensor_gguf() -> Vec<u8> {
4823        let mut buf = Vec::new();
4824        buf.write_u32::<LittleEndian>(frink_gguf::GGUF_MAGIC)
4825            .unwrap();
4826        buf.write_u32::<LittleEndian>(3).unwrap(); // version
4827        buf.write_u64::<LittleEndian>(1).unwrap(); // tensor_count
4828        buf.write_u64::<LittleEndian>(1).unwrap(); // kv_count
4829
4830        write_kv_str(&mut buf, "general.architecture", "frink-q5-1-test");
4831
4832        write_string(&mut buf, "test.weight");
4833        buf.write_u32::<LittleEndian>(2).unwrap(); // n_dims
4834                                                   // Real GGUF ne[] order is fastest-varying-first, i.e. [cols, rows].
4835        buf.write_u64::<LittleEndian>(32).unwrap(); // cols (1 Q5_1 block)
4836        buf.write_u64::<LittleEndian>(1).unwrap(); // rows
4837        buf.write_u32::<LittleEndian>(7).unwrap(); // dtype tag: Q5_1
4838        buf.write_u64::<LittleEndian>(0).unwrap(); // offset
4839
4840        while buf.len() % 32 != 0 {
4841            buf.push(0);
4842        }
4843        // d=0.25 (f16 0x3400), m=1.5 (f16 0x3E00) -- both exact in f16,
4844        // hand-verified bit patterns to avoid pulling in the `half`
4845        // crate just for two test constants. qh varied, qs a real
4846        // (non-degenerate) pattern.
4847        buf.extend_from_slice(&0x3400u16.to_le_bytes());
4848        buf.extend_from_slice(&0x3E00u16.to_le_bytes());
4849        buf.extend_from_slice(&[0x9au8, 0x3c, 0xf0, 0x0f]);
4850        buf.extend_from_slice(&(0..16u8).map(|i| i | ((15 - i) << 4)).collect::<Vec<u8>>());
4851        buf
4852    }
4853
4854    #[test]
4855    fn load_weight_matrix_handles_a_real_on_disk_q5_1_tensor_end_to_end() {
4856        let tmp = std::env::temp_dir().join(format!(
4857            "frink_test_q5_1_tensor_{}.gguf",
4858            std::process::id()
4859        ));
4860        std::fs::write(&tmp, build_single_q5_1_tensor_gguf()).unwrap();
4861        let file = frink_gguf::GgufFile::open(&tmp).expect("real Q5_1 GGUF file must parse");
4862        std::fs::remove_file(&tmp).ok();
4863
4864        let matrix = load_weight_matrix(&file, "test.weight").expect("Q5_1 tensor must load");
4865        assert_eq!(matrix.rows(), 1);
4866        assert_eq!(matrix.cols(), 32);
4867        let raw = file.tensor_bytes("test.weight").unwrap();
4868        let expected = frink_quant::dequant_q5_1(raw).unwrap();
4869        match &matrix {
4870            WeightMatrix::Quantized { kind, data, .. } => {
4871                assert_eq!(*kind, QuantKind::Q5_1);
4872                assert!(data.is_mapped());
4873            }
4874            _ => panic!("expected a Quantized matrix for a Q5_1 tensor"),
4875        }
4876
4877        let x: Vec<f32> = (0..32).map(|i| ((i as f32) * 0.017).cos()).collect();
4878        let expected_dot: f32 = expected.iter().zip(x.iter()).map(|(a, b)| a * b).sum();
4879        let got = matrix.apply(&x);
4880        assert_eq!(got.len(), 1);
4881        assert!(
4882            (got[0] - expected_dot).abs() < 1e-2,
4883            "end-to-end loaded+applied Q5_1 matrix diverged from direct dequant: got={} expected={}",
4884            got[0],
4885            expected_dot
4886        );
4887    }
4888
4889    // Same bytes as frink-quant's own Q3_K_TEST_BLOCK (Python-cross-
4890    // validated there); duplicated here to build a real on-disk GGUF
4891    // file, matching this file's existing per-format test convention
4892    // (see Q6_K_TEST_BLOCK above).
4893    const Q3_K_TEST_BLOCK: [u8; 110] = [
4894        0x56, 0xf2, 0xb4, 0x2b, 0xd5, 0x6f, 0x51, 0x71, 0x3c, 0x0a, 0xb9, 0x1d, 0xd0, 0xb9, 0x3b,
4895        0xb3, 0x0f, 0xff, 0x8c, 0xb2, 0x83, 0x3a, 0x3d, 0x24, 0xb1, 0x12, 0x56, 0xe3, 0x23, 0x54,
4896        0xf2, 0xfa, 0x7f, 0xdf, 0x31, 0xe1, 0x18, 0x26, 0x6e, 0xcd, 0x5b, 0x38, 0xee, 0xbd, 0x9f,
4897        0x8c, 0x57, 0x47, 0x0b, 0x11, 0xcb, 0xfb, 0xb4, 0x83, 0xa0, 0x4e, 0x0b, 0xd4, 0xa7, 0x85,
4898        0xe0, 0x60, 0xf3, 0xb3, 0xe3, 0x95, 0x43, 0xc6, 0x05, 0x05, 0x77, 0x53, 0xed, 0x23, 0xcc,
4899        0x6a, 0x0e, 0x89, 0xa1, 0x79, 0x85, 0xf6, 0x6e, 0x5a, 0x23, 0x63, 0xbe, 0x53, 0xfa, 0xa2,
4900        0x2b, 0xe9, 0xcd, 0xce, 0xf8, 0x3d, 0x6f, 0xd0, 0x42, 0x6e, 0x3b, 0x7f, 0x23, 0x26, 0xd3,
4901        0xb9, 0x18, 0xbf, 0xa4, 0x34,
4902    ];
4903
4904    fn build_single_q3_k_tensor_gguf() -> Vec<u8> {
4905        let mut buf = Vec::new();
4906        buf.write_u32::<LittleEndian>(frink_gguf::GGUF_MAGIC)
4907            .unwrap();
4908        buf.write_u32::<LittleEndian>(3).unwrap(); // version
4909        buf.write_u64::<LittleEndian>(1).unwrap(); // tensor_count
4910        buf.write_u64::<LittleEndian>(1).unwrap(); // kv_count
4911
4912        write_kv_str(&mut buf, "general.architecture", "frink-q3k-test");
4913
4914        write_string(&mut buf, "test.weight");
4915        buf.write_u32::<LittleEndian>(2).unwrap(); // n_dims
4916                                                   // Real GGUF ne[] order is fastest-varying-first, i.e. [cols, rows].
4917        buf.write_u64::<LittleEndian>(256).unwrap(); // cols (1 Q3_K block)
4918        buf.write_u64::<LittleEndian>(1).unwrap(); // rows
4919        buf.write_u32::<LittleEndian>(11).unwrap(); // dtype tag: Q3_K
4920        buf.write_u64::<LittleEndian>(0).unwrap(); // offset
4921
4922        while buf.len() % 32 != 0 {
4923            buf.push(0);
4924        }
4925        buf.extend_from_slice(&Q3_K_TEST_BLOCK);
4926        buf
4927    }
4928
4929    #[test]
4930    fn load_weight_matrix_handles_a_real_on_disk_q3_k_tensor_end_to_end() {
4931        let tmp =
4932            std::env::temp_dir().join(format!("frink_test_q3k_tensor_{}.gguf", std::process::id()));
4933        std::fs::write(&tmp, build_single_q3_k_tensor_gguf()).unwrap();
4934        let file = frink_gguf::GgufFile::open(&tmp).expect("real Q3_K GGUF file must parse");
4935        std::fs::remove_file(&tmp).ok();
4936
4937        let matrix = load_weight_matrix(&file, "test.weight").expect("Q3_K tensor must load");
4938        assert_eq!(matrix.rows(), 1);
4939        assert_eq!(matrix.cols(), 256);
4940        match &matrix {
4941            WeightMatrix::Quantized { kind, data, .. } => {
4942                assert_eq!(*kind, QuantKind::Q3K);
4943                assert!(data.is_mapped());
4944            }
4945            _ => panic!("expected a Quantized matrix for a Q3_K tensor"),
4946        }
4947
4948        let expected = frink_quant::dequant_q3_k(&Q3_K_TEST_BLOCK).unwrap();
4949        let x: Vec<f32> = (0..256).map(|i| ((i as f32) * 0.013).sin()).collect();
4950        let expected_dot: f32 = expected.iter().zip(x.iter()).map(|(a, b)| a * b).sum();
4951
4952        let got = matrix.apply(&x);
4953        assert_eq!(got.len(), 1);
4954        assert!(
4955            (got[0] - expected_dot).abs() < fused_dot_tolerance(&expected, &x, 1e-1),
4956            "end-to-end loaded+applied Q3_K matrix diverged from direct dequant: got={} expected={}",
4957            got[0],
4958            expected_dot
4959        );
4960    }
4961
4962    // Same bytes as frink-quant's own IQ4_XS_TEST_BLOCK (Python-cross-
4963    // validated there); duplicated here to build a real on-disk GGUF
4964    // file, matching this file's existing per-format test convention.
4965    const IQ4_XS_TEST_BLOCK: [u8; 136] = [
4966        0x5c, 0x33, 0xb4, 0x39, 0xd1, 0x64, 0x97, 0x82, 0xcb, 0xbd, 0x88, 0x95, 0xf3, 0x60, 0x2a,
4967        0xb5, 0xe7, 0x24, 0xd3, 0xee, 0xfe, 0x71, 0x13, 0xbe, 0x70, 0x84, 0x48, 0x79, 0x7b, 0x3e,
4968        0xf0, 0x55, 0xdc, 0xb2, 0xb2, 0xde, 0x32, 0xa1, 0x5b, 0x02, 0x01, 0xdc, 0x2a, 0xbb, 0xf7,
4969        0x0b, 0x8a, 0x88, 0xdd, 0x0b, 0x02, 0x7e, 0x5e, 0x76, 0x87, 0x30, 0x1e, 0x1c, 0xcf, 0x48,
4970        0xd7, 0x61, 0xf3, 0x51, 0x52, 0x17, 0x98, 0x0a, 0x87, 0xcf, 0x02, 0x91, 0xc8, 0xee, 0xc0,
4971        0x91, 0x69, 0x2a, 0x4f, 0x64, 0x68, 0xa7, 0xb2, 0xe6, 0x98, 0x21, 0x81, 0x75, 0x53, 0x2a,
4972        0x8d, 0x12, 0xae, 0xe0, 0xea, 0x0c, 0x75, 0xff, 0x22, 0x5e, 0x25, 0x19, 0xda, 0x2e, 0x51,
4973        0x4e, 0x81, 0xdc, 0x0e, 0x78, 0x86, 0xd7, 0x58, 0xb5, 0xb7, 0xf6, 0x45, 0xa9, 0x0a, 0x83,
4974        0xfd, 0x2a, 0x12, 0x7d, 0xf0, 0x12, 0x97, 0xe2, 0xfe, 0xf4, 0xd0, 0xa2, 0x11, 0x14, 0x78,
4975        0xdb,
4976    ];
4977
4978    fn build_single_iq4_xs_tensor_gguf() -> Vec<u8> {
4979        let mut buf = Vec::new();
4980        buf.write_u32::<LittleEndian>(frink_gguf::GGUF_MAGIC)
4981            .unwrap();
4982        buf.write_u32::<LittleEndian>(3).unwrap(); // version
4983        buf.write_u64::<LittleEndian>(1).unwrap(); // tensor_count
4984        buf.write_u64::<LittleEndian>(1).unwrap(); // kv_count
4985
4986        write_kv_str(&mut buf, "general.architecture", "frink-iq4xs-test");
4987
4988        write_string(&mut buf, "test.weight");
4989        buf.write_u32::<LittleEndian>(2).unwrap(); // n_dims
4990                                                   // Real GGUF ne[] order is fastest-varying-first, i.e. [cols, rows].
4991        buf.write_u64::<LittleEndian>(256).unwrap(); // cols (1 IQ4_XS block)
4992        buf.write_u64::<LittleEndian>(1).unwrap(); // rows
4993        buf.write_u32::<LittleEndian>(23).unwrap(); // dtype tag: IQ4_XS
4994        buf.write_u64::<LittleEndian>(0).unwrap(); // offset
4995
4996        while buf.len() % 32 != 0 {
4997            buf.push(0);
4998        }
4999        buf.extend_from_slice(&IQ4_XS_TEST_BLOCK);
5000        buf
5001    }
5002
5003    #[test]
5004    fn load_weight_matrix_handles_a_real_on_disk_iq4_xs_tensor_end_to_end() {
5005        let tmp = std::env::temp_dir().join(format!(
5006            "frink_test_iq4xs_tensor_{}.gguf",
5007            std::process::id()
5008        ));
5009        std::fs::write(&tmp, build_single_iq4_xs_tensor_gguf()).unwrap();
5010        let file = frink_gguf::GgufFile::open(&tmp).expect("real IQ4_XS GGUF file must parse");
5011        std::fs::remove_file(&tmp).ok();
5012
5013        let matrix = load_weight_matrix(&file, "test.weight").expect("IQ4_XS tensor must load");
5014        assert_eq!(matrix.rows(), 1);
5015        assert_eq!(matrix.cols(), 256);
5016        match &matrix {
5017            WeightMatrix::Quantized { kind, data, .. } => {
5018                assert_eq!(*kind, QuantKind::IQ4XS);
5019                assert!(data.is_mapped());
5020            }
5021            _ => panic!("expected a Quantized matrix for an IQ4_XS tensor"),
5022        }
5023
5024        let expected = frink_quant::dequant_iq4_xs(&IQ4_XS_TEST_BLOCK).unwrap();
5025        let x: Vec<f32> = (0..256).map(|i| ((i as f32) * 0.013).sin()).collect();
5026        let expected_dot: f32 = expected.iter().zip(x.iter()).map(|(a, b)| a * b).sum();
5027
5028        let got = matrix.apply(&x);
5029        assert_eq!(got.len(), 1);
5030        assert!(
5031            (got[0] - expected_dot).abs() < 1e-1,
5032            "end-to-end loaded+applied IQ4_XS matrix diverged from direct dequant: got={} expected={}",
5033            got[0],
5034            expected_dot
5035        );
5036    }
5037
5038    // Same bytes as frink-quant's own IQ low-bit test blocks
5039    // (Python-cross-validated there against the real compiled ggml
5040    // implementation), duplicated as literals for the same reason as
5041    // IQ4_XS_TEST_BLOCK above.
5042    const IQ1_S_TEST_BLOCK: [u8; 50] = [
5043        0x0a, 0x2f, 0xfa, 0x06, 0x1e, 0x37, 0x6f, 0xe3, 0x62, 0xd0, 0xb6, 0xa4, 0x25, 0xae, 0x76,
5044        0x14, 0x72, 0x5b, 0xfa, 0x05, 0xd1, 0xf1, 0x2a, 0x4c, 0xad, 0x29, 0xae, 0xf4, 0xcf, 0x0c,
5045        0x96, 0x51, 0x58, 0x03, 0x6d, 0xd3, 0x10, 0x92, 0x70, 0xff, 0x61, 0x58, 0xc8, 0x30, 0x25,
5046        0x64, 0x49, 0x85, 0xc0, 0x24,
5047    ];
5048    const IQ2_XXS_TEST_BLOCK: [u8; 66] = [
5049        0x29, 0x30, 0xd9, 0x33, 0x95, 0x4c, 0x08, 0x1e, 0xad, 0x79, 0x49, 0xf2, 0x8d, 0x5f, 0x93,
5050        0xea, 0x78, 0x18, 0x98, 0xb9, 0x94, 0x14, 0xad, 0xce, 0xca, 0x1d, 0xab, 0x81, 0x53, 0x4a,
5051        0x68, 0xd0, 0x59, 0x96, 0x36, 0x5d, 0xbe, 0x20, 0xc4, 0xff, 0xe4, 0x2c, 0xcd, 0x2f, 0x4f,
5052        0x4f, 0x67, 0x53, 0xc6, 0xd5, 0xa2, 0xfb, 0xc7, 0xf3, 0xe2, 0x6b, 0xf1, 0x99, 0x23, 0x1e,
5053        0x2d, 0x5e, 0x8c, 0x78, 0xc2, 0x31,
5054    ];
5055    const IQ3_XXS_TEST_BLOCK: [u8; 98] = [
5056        0x71, 0x31, 0x16, 0x0a, 0x79, 0x04, 0x5d, 0x87, 0xae, 0x2a, 0x4a, 0x43, 0xfd, 0x02, 0xba,
5057        0x6c, 0x10, 0x42, 0x80, 0xe5, 0x1d, 0x08, 0x22, 0xcb, 0x21, 0x54, 0xf9, 0xaa, 0x8e, 0xc2,
5058        0xf2, 0x34, 0x66, 0x1e, 0x2a, 0xef, 0x19, 0xae, 0x48, 0x47, 0x29, 0xa0, 0x72, 0xd1, 0x31,
5059        0xc0, 0x65, 0x49, 0xde, 0x79, 0x32, 0xe6, 0x4d, 0xb6, 0x55, 0x3f, 0x4d, 0xf1, 0x18, 0xbb,
5060        0x18, 0x59, 0x4c, 0x31, 0xa3, 0xb2, 0x34, 0xdd, 0xf6, 0x4a, 0x91, 0x51, 0x3f, 0x3e, 0x40,
5061        0x69, 0xad, 0xbf, 0x1a, 0xd0, 0x05, 0xfb, 0xbe, 0x8b, 0x0b, 0xdd, 0xdf, 0x7d, 0x94, 0x74,
5062        0x92, 0x3e, 0xff, 0x04, 0x2a, 0xc4, 0xea, 0xc9,
5063    ];
5064
5065    #[rustfmt::skip]
5066    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];
5067
5068    /// A live ggml type this build has no kernel for must be REFUSED BY
5069    /// NAME at execution, having been sized correctly at parse.
5070    ///
5071    /// Before `TQ2_0` was recognized, tag 35 was `Other(35)`, which had
5072    /// no block layout: the tensor's size was unknown, so `tensor_bytes`
5073    /// could not even hand back the row, and the error named a number.
5074    /// Now the file parses, the tensor measures 66 bytes per 256
5075    /// elements, and the stop happens where it belongs -- at the point
5076    /// something wants to multiply by it -- naming `TQ2_0`.
5077    #[test]
5078    fn a_recognized_but_unimplemented_ggml_type_refuses_by_name_after_sizing_correctly() {
5079        // 256 elements of TQ2_0 = one 66-byte block.
5080        let block = pseudo_iq_block(66, 0x0720_5eed);
5081        let tmp =
5082            std::env::temp_dir().join(format!("frink_test_tq2_0_{}.gguf", std::process::id()));
5083        std::fs::write(
5084            &tmp,
5085            build_single_iq_lowbit_tensor_gguf("tq2test", 35, 256, &block),
5086        )
5087        .unwrap();
5088        let file = frink_gguf::GgufFile::open(&tmp).expect("a TQ2_0 file must still parse");
5089        std::fs::remove_file(&tmp).ok();
5090
5091        // Sized, not zero: the size estimate is right even though the
5092        // kernel is missing.
5093        let info = file.find_tensor("test.weight").expect("tensor present");
5094        assert_eq!(info.dtype, GgmlType::TQ2_0);
5095        assert_eq!(info.byte_len(), Some(66));
5096        assert_eq!(
5097            file.tensor_bytes("test.weight").map(<[u8]>::len).ok(),
5098            Some(66)
5099        );
5100
5101        match load_weight_matrix(&file, "test.weight") {
5102            Err(LoadError::UnsupportedDtype(name, GgmlType::TQ2_0)) => {
5103                assert_eq!(name, "test.weight");
5104            }
5105            Err(other) => panic!("TQ2_0 must be refused by name, got {other:?}"),
5106            Ok(_) => panic!("TQ2_0 must be refused, not loaded as some other kind"),
5107        }
5108    }
5109
5110    /// An MXFP4 norm/bias must widen, not be refused.
5111    ///
5112    /// `load_weight_matrix` accepts MXFP4 as a 2-D weight and
5113    /// `load_moe_expert_matrices` accepts it as an expert tensor, and
5114    /// `WeightMatrix::dequant` calls `dequant_mxfp4_gguf` on both. One
5115    /// missing arm in `widen_plain_float` made the *1-D* tensors of the
5116    /// exact same dtype a hard `UnsupportedDtype` -- the split that
5117    /// turns a supported format into a load failure on the one
5118    /// checkpoint that uses it.
5119    #[test]
5120    fn an_mxfp4_one_dimensional_tensor_widens_instead_of_being_refused() {
5121        let expected = frink_quant::dequant_mxfp4_gguf(&MXFP4_GGUF_TEST_BLOCKS)
5122            .expect("the fixture blocks must dequantize");
5123        let cols = expected.len();
5124        let tmp =
5125            std::env::temp_dir().join(format!("frink_test_mxfp4_norm_{}.gguf", std::process::id()));
5126        std::fs::write(
5127            &tmp,
5128            build_single_iq_lowbit_tensor_gguf(
5129                "mxfp4norm",
5130                39,
5131                cols as u64,
5132                &MXFP4_GGUF_TEST_BLOCKS,
5133            ),
5134        )
5135        .unwrap();
5136        let file = frink_gguf::GgufFile::open(&tmp).expect("file must parse");
5137        std::fs::remove_file(&tmp).ok();
5138
5139        let got = load_f32_vec(&file, "test.weight")
5140            .expect("an MXFP4 norm must load, not report an unsupported dtype");
5141        assert_eq!(got, expected);
5142
5143        // Same arm, reached directly: `widen_plain_float` is the shared
5144        // helper the six architecture loaders call, so its table is the
5145        // one that has to know MXFP4.
5146        let direct = widen_plain_float(GgmlType::MXFP4, &MXFP4_GGUF_TEST_BLOCKS, "test.weight")
5147            .expect("widen_plain_float must widen MXFP4");
5148        assert_eq!(direct, expected);
5149
5150        // And the refusal still works for a dtype that genuinely has no
5151        // widening path, so this test cannot pass by making everything
5152        // succeed.
5153        match widen_plain_float(GgmlType::TQ2_0, &MXFP4_GGUF_TEST_BLOCKS, "test.weight") {
5154            Err(LoadError::UnsupportedDtype(name, GgmlType::TQ2_0)) => {
5155                assert_eq!(name, "test.weight");
5156            }
5157            other => panic!("TQ2_0 must be refused by name, got {other:?}"),
5158        }
5159    }
5160
5161    fn build_single_iq_lowbit_tensor_gguf(
5162        arch: &str,
5163        tag: u32,
5164        cols: u64,
5165        block: &[u8],
5166    ) -> Vec<u8> {
5167        let mut buf = Vec::new();
5168        buf.write_u32::<LittleEndian>(frink_gguf::GGUF_MAGIC)
5169            .unwrap();
5170        buf.write_u32::<LittleEndian>(3).unwrap(); // version
5171        buf.write_u64::<LittleEndian>(1).unwrap(); // tensor_count
5172        buf.write_u64::<LittleEndian>(1).unwrap(); // kv_count
5173        write_kv_str(&mut buf, "general.architecture", arch);
5174        write_string(&mut buf, "test.weight");
5175        buf.write_u32::<LittleEndian>(2).unwrap(); // n_dims
5176        buf.write_u64::<LittleEndian>(cols).unwrap();
5177        buf.write_u64::<LittleEndian>(1).unwrap(); // rows
5178        buf.write_u32::<LittleEndian>(tag).unwrap();
5179        buf.write_u64::<LittleEndian>(0).unwrap(); // offset
5180        while buf.len() % 32 != 0 {
5181            buf.push(0);
5182        }
5183        buf.extend_from_slice(block);
5184        buf
5185    }
5186
5187    /// A structurally valid block of `len` bytes for any of the
5188    /// codebook-grid formats: every bit pattern is a legal code in all
5189    /// of them (the grid indices are bounded by their own bit widths),
5190    /// so a deterministic byte fill is a real block, not a fixture that
5191    /// happens to avoid the interesting paths. Only the f16 scale needs
5192    /// pinning, and only so the comparison below can't be NaN-vs-NaN.
5193    fn pseudo_iq_block(len: usize, seed: u32) -> Vec<u8> {
5194        let mut s = seed;
5195        let mut out = Vec::with_capacity(len);
5196        for _ in 0..len {
5197            s ^= s << 13;
5198            s ^= s >> 17;
5199            s ^= s << 5;
5200            out.push((s >> 24) as u8);
5201        }
5202        out
5203    }
5204
5205    /// End-to-end load+apply for the codebook-grid low-bit formats the
5206    /// published Dynamic GGUFs are built from: a real on-disk tensor of
5207    /// each type must load zero-copy as the right `QuantKind` and
5208    /// produce the same matvec result as dequantizing the block
5209    /// directly. That is the property this test exists for -- the
5210    /// *values* are pinned against real ggml in `frink-quant`; what
5211    /// can only break here is the tag -> kind -> block-stride chain,
5212    /// and a wrong stride silently reads the neighbouring row.
5213    /// Dtype tags (19/29/16/17/22/18/21/39) verified against ggml.h's
5214    /// enum ggml_type.
5215    #[test]
5216    fn load_weight_matrix_handles_real_on_disk_iq_lowbit_tensors_end_to_end() {
5217        type DequantFn = fn(&[u8]) -> Result<Vec<f32>, frink_quant::QuantError>;
5218        // IQ1_M carries no f16 scale field; its scale is reassembled
5219        // from the four scale words' top nibbles, and the top nibble of
5220        // the last one supplies the f16 sign + high exponent bits.
5221        // Pinning it to 0x2 keeps the exponent out of the all-ones
5222        // NaN/Inf pattern whatever the rest of the fill does. The other
5223        // three do carry a leading f16 `d`, pinned for the same reason.
5224        let mut iq1m = pseudo_iq_block(frink_quant::IQ1_M_BLOCK_BYTES, 0x2907_31A0);
5225        iq1m[55] = (iq1m[55] & 0x0F) | 0x20;
5226        let mut iq2xs = pseudo_iq_block(frink_quant::IQ2_XS_BLOCK_BYTES, 0x2107_31A1);
5227        let mut iq2s = pseudo_iq_block(frink_quant::IQ2_S_BLOCK_BYTES, 0x2207_31A2);
5228        let mut iq3s = pseudo_iq_block(frink_quant::IQ3_S_BLOCK_BYTES, 0x2307_31A3);
5229        for blk in [&mut iq2xs, &mut iq2s, &mut iq3s] {
5230            blk[0..2].copy_from_slice(&half::f16::from_f32(0.115).to_le_bytes());
5231        }
5232        let cases: [(&str, u32, &[u8], QuantKind, DequantFn); 8] = [
5233            (
5234                "iq1s",
5235                19,
5236                &IQ1_S_TEST_BLOCK,
5237                QuantKind::IQ1S,
5238                frink_quant::dequant_iq1_s,
5239            ),
5240            (
5241                "iq1m",
5242                29,
5243                &iq1m,
5244                QuantKind::IQ1M,
5245                frink_quant::dequant_iq1_m,
5246            ),
5247            (
5248                "iq2xxs",
5249                16,
5250                &IQ2_XXS_TEST_BLOCK,
5251                QuantKind::IQ2XXS,
5252                frink_quant::dequant_iq2_xxs,
5253            ),
5254            (
5255                "iq2xs",
5256                17,
5257                &iq2xs,
5258                QuantKind::IQ2XS,
5259                frink_quant::dequant_iq2_xs,
5260            ),
5261            (
5262                "iq2s",
5263                22,
5264                &iq2s,
5265                QuantKind::IQ2S,
5266                frink_quant::dequant_iq2_s,
5267            ),
5268            (
5269                "iq3xxs",
5270                18,
5271                &IQ3_XXS_TEST_BLOCK,
5272                QuantKind::IQ3XXS,
5273                frink_quant::dequant_iq3_xxs,
5274            ),
5275            (
5276                "iq3s",
5277                21,
5278                &iq3s,
5279                QuantKind::IQ3S,
5280                frink_quant::dequant_iq3_s,
5281            ),
5282            (
5283                "mxfp4_gguf",
5284                39,
5285                &MXFP4_GGUF_TEST_BLOCKS,
5286                QuantKind::Mxfp4Gguf,
5287                frink_quant::dequant_mxfp4_gguf,
5288            ),
5289        ];
5290        for (name, tag, block, kind, dequant) in cases {
5291            let expected = dequant(block).unwrap();
5292            let cols = expected.len();
5293            let tmp = std::env::temp_dir().join(format!("frink_test_{name}_tensor.gguf"));
5294            std::fs::write(
5295                &tmp,
5296                build_single_iq_lowbit_tensor_gguf(name, tag, cols as u64, block),
5297            )
5298            .unwrap();
5299            let file = frink_gguf::GgufFile::open(&tmp).expect("file must parse");
5300            std::fs::remove_file(&tmp).ok();
5301
5302            let matrix =
5303                load_weight_matrix(&file, "test.weight").expect("low-bit tensor must load");
5304            assert_eq!((matrix.rows(), matrix.cols()), (1, cols), "{name}");
5305            match &matrix {
5306                WeightMatrix::Quantized { kind: k, data, .. } => {
5307                    assert_eq!(*k, kind, "{name}");
5308                    assert!(data.is_mapped(), "{name} must load zero-copy");
5309                }
5310                _ => panic!("expected a Quantized matrix for {name}"),
5311            }
5312
5313            let x: Vec<f32> = (0..cols).map(|i| ((i as f32) * 0.013).sin()).collect();
5314            let expected_dot: f32 = expected.iter().zip(x.iter()).map(|(a, b)| a * b).sum();
5315            let got = matrix.apply(&x);
5316            assert!(
5317                (got[0] - expected_dot).abs() < 1e-1,
5318                "{name}: loaded+applied diverged from direct dequant: got={} expected={}",
5319                got[0],
5320                expected_dot
5321            );
5322        }
5323    }
5324
5325    #[test]
5326    fn qwen2moe_disables_topk_renorm() {
5327        assert!(
5328            NO_TOPK_RENORMALIZE_ARCHITECTURES.contains(&"qwen2moe"),
5329            "qwen2moe must have norm_topk_prob=false (llama.cpp build_moe_ffn norm_w=false)"
5330        );
5331    }
5332
5333    /// The `LLAMA_ROPE_TYPE_NONE` group used to be refused by name here;
5334    /// every row is served now, positioned the way its graph positions
5335    /// (`crate::position_embd`, `crate::alibi`), and what this pins is
5336    /// that not one of them reaches a rotation: the rule is
5337    /// `RopeLayers::Never` for each, at any depth it takes.
5338    #[test]
5339    fn an_architecture_with_no_rope_rotates_nothing() {
5340        for (arch, n_layers) in [
5341            ("gpt2", 12),
5342            ("mpt", 32),
5343            ("refact", 32),
5344            ("bloom", 30),
5345            ("jais", 40),
5346            ("baichuan", 40),
5347        ] {
5348            assert_eq!(
5349                crate::rope_layers::rope_layers(arch, n_layers, false, 0),
5350                crate::rope_layers::RopeLayers::Never,
5351                "{arch} positions without RoPE and must rotate nothing"
5352            );
5353            assert!(crate::capability::is_audited_generic(arch), "{arch}");
5354        }
5355    }
5356
5357    /// A per-layer sliding-window ARRAY on an architecture whose graph
5358    /// reads the key as a scalar is IGNORED and the seeded period
5359    /// stands, exactly as llama.cpp does; a scalar still overrides the
5360    /// period.
5361    ///
5362    /// Three generations of this gate. `capability::
5363    /// unsupported_feature_keys` refused the key outright with the
5364    /// reason "not implemented in the generic decoder", which was
5365    /// false. Then the loader refused the ARRAY form for every
5366    /// architecture, on the reasoning that honouring it as a period was
5367    /// impossible and ignoring it would substitute the seed for the
5368    /// file's layout -- which is TRUE and is ALSO what llama.cpp does:
5369    /// `get_key_or_arr(kid, swa_period, false)` returns false on an
5370    /// array (`llama-model-loader.cpp:502-507`) and `plamo3.cpp:9-11`
5371    /// keeps its 8. Every real EXAONE-4 32B, EXAONE-MoE and Olmo-3
5372    /// export carries the array (`conversion/exaone.py:84`,
5373    /// `olmo.py:59-66`) and was refused over a value upstream never
5374    /// reads. `crate::swa_layers` carries which graphs read which form;
5375    /// the array-HONOURED mode has its own fixture in
5376    /// `tests/window_array_graphs.rs`.
5377    #[test]
5378    fn an_array_valued_sliding_window_pattern_is_ignored_where_llama_cpp_ignores_it() {
5379        // Disagrees with plamo3's seeded last-dense 8 on layers 0..3,
5380        // so honouring it would be visible.
5381        let pattern: [u32; 4] = [0, 0, 0, 0];
5382        let kvs: Vec<(&str, Kv)> = vec![
5383            ("general.architecture", Kv::Str("plamo3")),
5384            ("plamo3.block_count", Kv::U32(4)),
5385            ("plamo3.embedding_length", Kv::U32(64)),
5386            ("plamo3.attention.head_count", Kv::U32(1)),
5387            ("plamo3.attention.head_count_kv", Kv::U32(1)),
5388            ("plamo3.attention.key_length", Kv::U32(64)),
5389            ("plamo3.rope.freq_base", Kv::F32(10_000.0)),
5390            ("plamo3.attention.sliding_window", Kv::U32(3)),
5391            (
5392                "plamo3.attention.sliding_window_pattern",
5393                Kv::Arr32(&pattern),
5394            ),
5395        ];
5396        let file = open_metadata_gguf("swa_pattern_array", &kvs);
5397        let config = ModelConfig::from_gguf(&file).expect("the array is not a refusal");
5398        assert_eq!(
5399            config.swa_layers,
5400            crate::swa_layers::SwaLayers::period(8, false),
5401            "plamo3.cpp:9-11 seeds 8 and the scalar overload ignores an array"
5402        );
5403        assert_eq!(config.layer_sliding_window(0), Some(3));
5404        assert_eq!(config.layer_sliding_window(3), Some(3));
5405
5406        // And the scalar spelling of the same key overrides the seed.
5407        let mut scalar = kvs;
5408        scalar.pop();
5409        scalar.push(("plamo3.attention.sliding_window_pattern", Kv::U32(2)));
5410        let file = open_metadata_gguf("swa_pattern_scalar", &scalar);
5411        let config = ModelConfig::from_gguf(&file).expect("a scalar period must load");
5412        assert_eq!(
5413            config.swa_layers,
5414            crate::swa_layers::SwaLayers::period(2, false)
5415        );
5416        assert_eq!(config.layer_sliding_window(0), Some(3));
5417        assert_eq!(config.layer_sliding_window(1), None);
5418    }
5419
5420    /// Baichuan is one `general.architecture` string covering two
5421    /// positional schemes, and llama.cpp picks between them on the layer
5422    /// count alone (`src/models/baichuan.cpp:11-14`, with its own "TODO:
5423    /// become GGUF KV parameter"). The 13B used to be refused HERE; it is
5424    /// served now, and what this pins is that the two schemes are still
5425    /// told apart by the count, on both tables that must agree about it
5426    /// (`crate::alibi`, `crate::rope_layers`).
5427    #[test]
5428    fn baichuan_13b_positions_by_alibi_and_the_7b_rotates() {
5429        assert_eq!(
5430            crate::alibi::max_alibi_bias("baichuan", 40, None),
5431            Some(8.0)
5432        );
5433        assert_eq!(
5434            crate::rope_layers::rope_layers("baichuan", 40, false, 0),
5435            crate::rope_layers::RopeLayers::Never
5436        );
5437        assert_eq!(crate::alibi::max_alibi_bias("baichuan", 32, None), None);
5438        assert_eq!(
5439            crate::rope_layers::rope_layers("baichuan", 32, false, 0),
5440            crate::rope_layers::RopeLayers::All
5441        );
5442        // Both sizes pass the header stage and fail on the next missing
5443        // hparam, which is what proves neither is gated here any more.
5444        for (name, n) in [("baichuan13b", 40u32), ("baichuan7b", 32)] {
5445            let file = open_metadata_gguf(
5446                name,
5447                &[
5448                    ("general.architecture", Kv::Str("baichuan")),
5449                    ("baichuan.block_count", Kv::U32(n)),
5450                ],
5451            );
5452            match ModelConfig::from_gguf(&file) {
5453                Err(LoadError::MissingHparam(key)) => assert_eq!(key, "baichuan.embedding_length"),
5454                other => panic!("{name} must pass the header stage, got {other:?}"),
5455            }
5456        }
5457    }
5458
5459    /// EXAONE-4 is ONE architecture string over TWO graphs, and
5460    /// llama.cpp picks between them off the LAYER COUNT with no GGUF key
5461    /// involved. It used to be refused for it; both sizes run now, and
5462    /// this is the test that says they run DIFFERENTLY.
5463    ///
5464    /// `exaone4.cpp:4-9` wraps the entire SWA setup in
5465    /// `if (hparams.n_layer() == 64)`, and :116 then gates rotation on
5466    /// it -- `use_rope = is_swa(il) || swa_type == NONE`. So:
5467    ///
5468    /// * 64 layers: a window, `set_swa_pattern(4)` last-dense, and the
5469    ///   FULL-ATTENTION layer of every period gets no rotation at all.
5470    /// * 30 layers: no window whatever the file declares, and every
5471    ///   layer rotates.
5472    ///
5473    /// Both halves are here because the gate is a layer-count EQUALITY.
5474    /// A one-sided version would pass while windowing the 1.2B off a key
5475    /// llama.cpp never reaches, which is the divergence
5476    /// `capability::swa_disabled_by_arch` was extended to stop -- and it
5477    /// would then rope three layers in four of the 1.2B not at all.
5478    #[test]
5479    fn the_two_exaone4_sizes_get_different_windows_and_different_rotation() {
5480        // `Kv` is not `Clone`, so the shared header is a builder rather
5481        // than a value; both sizes must read from one list or the test
5482        // compares two transcriptions.
5483        let base = |n_layers: u32| -> Vec<(&str, Kv)> {
5484            vec![
5485                ("general.architecture", Kv::Str("exaone4")),
5486                ("exaone4.block_count", Kv::U32(n_layers)),
5487                ("exaone4.embedding_length", Kv::U32(32)),
5488                ("exaone4.attention.head_count", Kv::U32(4)),
5489                ("exaone4.attention.head_count_kv", Kv::U32(2)),
5490                ("exaone4.attention.key_length", Kv::U32(8)),
5491                ("exaone4.attention.value_length", Kv::U32(8)),
5492                ("exaone4.rope.freq_base", Kv::F32(10_000.0)),
5493                // The SAME declared window for both sizes: that is the
5494                // whole point. Only the layer count may change the
5495                // answer.
5496                ("exaone4.attention.sliding_window", Kv::U32(4096)),
5497            ]
5498        };
5499
5500        let file = open_metadata_gguf("exaone4_32b", &base(64));
5501        let cfg = ModelConfig::from_gguf(&file).expect("EXAONE-4 32B loads");
5502        assert_eq!(cfg.sliding_window, Some(4096));
5503        assert_eq!(
5504            cfg.swa_layers,
5505            crate::swa_layers::SwaLayers::period(4, false),
5506            "exaone4.cpp:7-9, and set_swa_pattern's default phase"
5507        );
5508        for il in 0..64 {
5509            assert_eq!(
5510                cfg.layer_rotates(il),
5511                il % 4 != 3,
5512                "layer {il} of EXAONE-4 32B: only the sliding layers rotate"
5513            );
5514        }
5515
5516        let file = open_metadata_gguf("exaone4_1_2b", &base(30));
5517        let cfg = ModelConfig::from_gguf(&file).expect("EXAONE-4 1.2B loads");
5518        assert_eq!(
5519            cfg.sliding_window, None,
5520            "exaone4.cpp:4 never reaches set_swa_pattern below 64 layers, \
5521             so the declared window is dead metadata"
5522        );
5523        for il in 0..30 {
5524            assert!(cfg.layer_rotates(il), "layer {il} of EXAONE-4 1.2B");
5525        }
5526    }
5527
5528    /// NextN/MTP blocks are inside `block_count` and llama.cpp skips
5529    /// them (`n_layer = n_layer_all - n_layer_nextn`, llama-hparams.cpp
5530    /// :280-282). For a graph that reads the key (`exaone-moe.cpp:23`)
5531    /// the trunk is what loads; the key is written as `0` by
5532    /// `conversion/exaone.py:146` for every EXAONE-MoE export without an
5533    /// MTP head, so zero must be the whole file. A nonzero value on a
5534    /// graph that does NOT read the key stays refused
5535    /// (`mtp_blocks::tests`).
5536    ///
5537    /// The second half pins the ORDER of two reads in `exaone4.cpp`:
5538    /// `:4` tests `n_layer() == 64` before `:18` reads the key, so it
5539    /// sees `block_count`. A 64-trunk file with one MTP block appended
5540    /// is 65 there and gets NO window in llama.cpp; frink feeds
5541    /// `block_count` to the same gate and gets the same answer.
5542    #[test]
5543    fn nextn_predict_layers_subtracts_the_trunk_for_a_reader_and_zero_is_the_whole_file() {
5544        let base = |nextn: u32| -> Vec<(&str, Kv)> {
5545            vec![
5546                ("general.architecture", Kv::Str("exaone-moe")),
5547                ("exaone-moe.block_count", Kv::U32(5)),
5548                ("exaone-moe.nextn_predict_layers", Kv::U32(nextn)),
5549                ("exaone-moe.embedding_length", Kv::U32(32)),
5550                ("exaone-moe.attention.head_count", Kv::U32(4)),
5551                ("exaone-moe.attention.head_count_kv", Kv::U32(2)),
5552                ("exaone-moe.attention.key_length", Kv::U32(8)),
5553                ("exaone-moe.attention.value_length", Kv::U32(8)),
5554                ("exaone-moe.rope.freq_base", Kv::F32(10_000.0)),
5555                ("exaone-moe.attention.sliding_window", Kv::U32(128)),
5556                ("exaone-moe.expert_count", Kv::U32(4)),
5557                ("exaone-moe.expert_used_count", Kv::U32(2)),
5558                ("exaone-moe.expert_gating_func", Kv::U32(2)),
5559            ]
5560        };
5561        let cfg = ModelConfig::from_gguf(&open_metadata_gguf("exaone_moe_mtp", &base(1)))
5562            .expect("a reader with an MTP block loads its trunk");
5563        assert_eq!((cfg.n_layers, cfg.n_mtp_blocks), (4, 1));
5564        let cfg = ModelConfig::from_gguf(&open_metadata_gguf("exaone_moe_no_mtp", &base(0)))
5565            .expect("zero is the whole file");
5566        assert_eq!((cfg.n_layers, cfg.n_mtp_blocks), (5, 0));
5567
5568        // `exaone4.cpp:4` before `:18`: 64 trunk layers plus one MTP
5569        // block is NOT the 32B to llama.cpp.
5570        let exaone4 = |block_count: u32, nextn: u32| -> Vec<(&str, Kv)> {
5571            vec![
5572                ("general.architecture", Kv::Str("exaone4")),
5573                ("exaone4.block_count", Kv::U32(block_count)),
5574                ("exaone4.nextn_predict_layers", Kv::U32(nextn)),
5575                ("exaone4.embedding_length", Kv::U32(32)),
5576                ("exaone4.attention.head_count", Kv::U32(4)),
5577                ("exaone4.attention.head_count_kv", Kv::U32(2)),
5578                ("exaone4.attention.key_length", Kv::U32(8)),
5579                ("exaone4.attention.value_length", Kv::U32(8)),
5580                ("exaone4.rope.freq_base", Kv::F32(10_000.0)),
5581                ("exaone4.attention.sliding_window", Kv::U32(4096)),
5582            ]
5583        };
5584        let with_mtp =
5585            ModelConfig::from_gguf(&open_metadata_gguf("exaone4_65", &exaone4(65, 1))).unwrap();
5586        assert_eq!((with_mtp.n_layers, with_mtp.n_mtp_blocks), (64, 1));
5587        assert_eq!(
5588            with_mtp.sliding_window, None,
5589            "exaone4.cpp:4 sees n_layer_all = 65 and never reaches set_swa_pattern"
5590        );
5591        let without =
5592            ModelConfig::from_gguf(&open_metadata_gguf("exaone4_64", &exaone4(64, 0))).unwrap();
5593        assert_eq!(
5594            without.sliding_window,
5595            Some(4096),
5596            "the same trunk without the block is the 32B"
5597        );
5598    }
5599
5600    /// `expert_used_count` is scalar-or-array upstream, and the array
5601    /// spelling used to fall through to a DEFAULT of 2 here.
5602    ///
5603    /// `llama-model.cpp:1266` reads the key with `get_key_or_arr` in
5604    /// the common loader -- every architecture, `n_layer_all` entries
5605    /// -- and `conversion/nemotron.py:574` writes a list for
5606    /// Nemotron-H Puzzle, whose architecture (`nemotron_h`) frink
5607    /// serves. Before this test, `metadata_u64` answered `None` for an
5608    /// array value, the `unwrap_or_else` below it pushed a best-effort
5609    /// note, and the model routed top-2 on every layer whatever the
5610    /// file declared: the silent-wrong class, not a refusal.
5611    ///
5612    /// Both arms are pinned, because a reader that honoured the
5613    /// uniform case and silently averaged the varying one would pass
5614    /// half of this.
5615    #[test]
5616    fn a_per_layer_expert_used_count_is_honoured_when_uniform_and_refused_when_not() {
5617        fn file(used: Kv<'_>) -> Vec<(&'static str, Kv<'_>)> {
5618            vec![
5619                ("general.architecture", Kv::Str("llama")),
5620                ("llama.block_count", Kv::U32(2)),
5621                ("llama.embedding_length", Kv::U32(32)),
5622                ("llama.attention.head_count", Kv::U32(4)),
5623                ("llama.attention.head_count_kv", Kv::U32(2)),
5624                ("llama.attention.key_length", Kv::U32(8)),
5625                ("llama.attention.value_length", Kv::U32(8)),
5626                ("llama.rope.freq_base", Kv::F32(10_000.0)),
5627                ("llama.expert_count", Kv::U32(8)),
5628                ("llama.expert_used_count", used),
5629            ]
5630        }
5631        let scalar = ModelConfig::from_gguf(&open_metadata_gguf(
5632            "experts_used_scalar",
5633            &file(Kv::U32(3)),
5634        ))
5635        .expect("the scalar spelling loads");
5636        assert_eq!(scalar.moe.n_experts_active, 3);
5637
5638        let uniform = ModelConfig::from_gguf(&open_metadata_gguf(
5639            "experts_used_uniform",
5640            &file(Kv::Arr32(&[3, 3])),
5641        ))
5642        .expect("a uniform array is that one value");
5643        assert_eq!(
5644            uniform.moe.n_experts_active, 3,
5645            "an array of one repeated value is the scalar, not the default of 2"
5646        );
5647
5648        let err = ModelConfig::from_gguf(&open_metadata_gguf(
5649            "experts_used_varying",
5650            &file(Kv::Arr32(&[3, 5])),
5651        ))
5652        .expect_err("a varying array has no single top-k and must stop");
5653        let msg = format!("{err}");
5654        assert!(
5655            msg.contains("expert_used_count") && msg.contains("PER-LAYER"),
5656            "the refusal must name the key and what is wrong with it: {msg}"
5657        );
5658    }
5659
5660    /// An `olmo2` file carrying BOTH a sliding window and a RoPE
5661    /// scaling ropes its two kinds of layer differently, and frink
5662    /// carries one scaling for the whole model.
5663    ///
5664    /// `olmo2.cpp:120-134` runs the sliding layers with the scaling
5665    /// switched off -- `freq_scale = 1`, `ext_factor = 0`,
5666    /// `attn_factor = 1`, and the comment above it says so in as many
5667    /// words -- while :136-146 gives the full-attention layers the
5668    /// model's own. Rotating half the layers at a magnitude the
5669    /// checkpoint never trained at is the ALiBi class of divergence and
5670    /// runs fluently.
5671    ///
5672    /// Both negative halves are here because the gate is a CONJUNCTION
5673    /// and a gate that fires on either half alone would refuse every
5674    /// OLMo-2 checkpoint ever published.
5675    #[test]
5676    fn olmo2_is_refused_only_when_it_has_a_window_and_a_rope_scaling_together() {
5677        // `Kv` is not `Clone`, so the shared header is a builder
5678        // rather than a value -- which also keeps the three cases
5679        // reading from one list instead of three transcriptions.
5680        let base = || -> Vec<(&str, Kv)> {
5681            vec![
5682                ("general.architecture", Kv::Str("olmo2")),
5683                ("olmo2.block_count", Kv::U32(2)),
5684                ("olmo2.embedding_length", Kv::U32(24)),
5685                ("olmo2.attention.head_count", Kv::U32(4)),
5686                ("olmo2.attention.head_count_kv", Kv::U32(2)),
5687                ("olmo2.attention.key_length", Kv::U32(6)),
5688                ("olmo2.attention.value_length", Kv::U32(6)),
5689                ("olmo2.rope.freq_base", Kv::F32(10_000.0)),
5690            ]
5691        };
5692
5693        let mut both = base();
5694        both.push(("olmo2.attention.sliding_window", Kv::U32(3)));
5695        both.push(("olmo2.rope.scaling.type", Kv::Str("yarn")));
5696        both.push(("olmo2.rope.scaling.factor", Kv::F32(4.0)));
5697        let file = open_metadata_gguf("olmo2_swa_yarn", &both);
5698        match ModelConfig::from_gguf(&file) {
5699            Err(LoadError::UnsupportedFeature(arch, msg)) => {
5700                assert_eq!(arch, "olmo2");
5701                assert!(msg.contains("sliding window"), "{msg}");
5702                assert!(msg.contains("yarn"), "{msg}");
5703            }
5704            other => panic!("olmo2 with a window AND yarn must refuse, got {other:?}"),
5705        }
5706
5707        // A window with no scaling: both of llama.cpp's RoPE branches
5708        // reduce to the same plain rotation, and the difference is
5709        // masking alone, which frink implements.
5710        let mut window_only = base();
5711        window_only.push(("olmo2.attention.sliding_window", Kv::U32(3)));
5712        let file = open_metadata_gguf("olmo2_swa_only", &window_only);
5713        let config = ModelConfig::from_gguf(&file).expect("a window with no scaling must load");
5714        assert_eq!(config.sliding_window, Some(3));
5715        // olmo2.cpp:9-11: the period defaults to 4 and `set_swa_pattern`
5716        // leaves `dense_first` false.
5717        assert_eq!(
5718            config.swa_layers,
5719            crate::swa_layers::SwaLayers::period(4, false)
5720        );
5721
5722        // Scaling with no window: one RoPE for the whole model, which is
5723        // what frink carries.
5724        let mut scaling_only = base();
5725        scaling_only.push(("olmo2.rope.scaling.type", Kv::Str("yarn")));
5726        scaling_only.push(("olmo2.rope.scaling.factor", Kv::F32(4.0)));
5727        let file = open_metadata_gguf("olmo2_yarn_only", &scaling_only);
5728        let config = ModelConfig::from_gguf(&file).expect("scaling with no window must load");
5729        assert_eq!(config.sliding_window, None);
5730    }
5731
5732    /// The hyper-parameters a real Gemma-3 GGUF header carries for one
5733    /// size. `block_count` is the field llama.cpp's `LLM_TYPE_27B`
5734    /// switch reads (`gemma3.cpp:20-28`), so it is never a free
5735    /// parameter here.
5736    ///
5737    /// `linear_factor` adds the pair `conversion/base.py:1222-1230`
5738    /// writes from `rope_parameters["full_attention"]` -- and only from
5739    /// there: its own comment is "TODO: Handle sliding_attention
5740    /// similarly when models start implementing it", so the sliding
5741    /// layers get no scaling key at all.
5742    fn gemma3_config(
5743        tag: &str,
5744        n_layers: u32,
5745        hidden_dim: u32,
5746        n_heads: u32,
5747        head_dim: u32,
5748        linear_factor: Option<f32>,
5749    ) -> ModelConfig {
5750        let mut kvs: Vec<(&str, Kv)> = vec![
5751            ("general.architecture", Kv::Str("gemma3")),
5752            ("gemma3.block_count", Kv::U32(n_layers)),
5753            ("gemma3.embedding_length", Kv::U32(hidden_dim)),
5754            ("gemma3.attention.head_count", Kv::U32(n_heads)),
5755            ("gemma3.attention.head_count_kv", Kv::U32(n_heads)),
5756            ("gemma3.attention.key_length", Kv::U32(head_dim)),
5757            ("gemma3.attention.value_length", Kv::U32(head_dim)),
5758            // Global layers rotate at 1e6; the sliding ones fall back to
5759            // llama.cpp's `rope_freq_base_train_swa` default of 10000,
5760            // because `gemma3.cpp:11` reads only the BASE key.
5761            ("gemma3.rope.freq_base", Kv::F32(1_000_000.0)),
5762            ("gemma3.attention.sliding_window", Kv::U32(1024)),
5763            ("gemma3.attention.sliding_window_pattern", Kv::U32(6)),
5764        ];
5765        if let Some(factor) = linear_factor {
5766            kvs.push(("gemma3.rope.scaling.type", Kv::Str("linear")));
5767            kvs.push(("gemma3.rope.scaling.factor", Kv::F32(factor)));
5768        }
5769        ModelConfig::from_gguf(&open_metadata_gguf(tag, &kvs)).expect("gemma3 fixture must load")
5770    }
5771
5772    /// The 27B attention scale reaches `ModelConfig`, and no other
5773    /// Gemma-3 size acquires one.
5774    ///
5775    /// `capability::attention_scale_override` is where the arithmetic is
5776    /// checked; this pins that the LOADER calls it with this file's own
5777    /// numbers. That step is the one that shipped broken: the function
5778    /// did not exist and `attention_scale` was the literal `None`, under
5779    /// a comment naming the exception. A helper nobody calls looks
5780    /// exactly like a fix.
5781    #[test]
5782    fn a_gemma3_27b_header_sets_the_attention_scale_and_no_smaller_size_does() {
5783        // Gemma-3-27B: 62 layers, n_embd 5376, 32 heads, head_dim 128.
5784        let big = gemma3_config("g3_27b_scale", 62, 5376, 32, 128, Some(8.0));
5785        let want = 1.0f32 / (5376.0f32 / 32.0).sqrt();
5786        let got = big
5787            .attention_scale
5788            .expect("Gemma-3-27B is llama.cpp's LLM_TYPE_27B");
5789        assert!(
5790            (got - want).abs() < 1e-7,
5791            "want 1/sqrt(168) = {want}, got {got}"
5792        );
5793        // The bug's magnitude: scores were sqrt(168/128) = 1.146x too
5794        // large without this.
5795        let kernel = 1.0f32 / 128.0f32.sqrt();
5796        assert!((kernel / got - (168.0f32 / 128.0).sqrt()).abs() < 1e-5);
5797
5798        // Gemma-3-1B and -4B take llama.cpp's other branch, which is the
5799        // scale the attention kernels already apply. A `Some` here would
5800        // double-scale them.
5801        for (tag, n_layers, hidden, heads) in
5802            [("g3_1b_scale", 26, 1152, 4), ("g3_4b_scale", 34, 2560, 8)]
5803        {
5804            let cfg = gemma3_config(tag, n_layers, hidden, heads, 256, None);
5805            assert_eq!(
5806                cfg.attention_scale, None,
5807                "{tag} must keep the kernels' own 1/sqrt(head_dim)"
5808            );
5809        }
5810    }
5811
5812    /// Gemma-3's declared linear scaling reaches the FULL-ATTENTION
5813    /// layers only, and the sliding ones rope unscaled.
5814    ///
5815    /// `gemma3.cpp:11` reads `LLM_KV_ROPE_FREQ_BASE_SWA` and nothing
5816    /// else, so `rope_freq_scale_train_swa` keeps its `1.0f` default
5817    /// (`src/llama-hparams.h:129`) while `get_rope_freq_scale`
5818    /// (`llama-model.cpp:2033-2035`) hands the trained scale to the full
5819    /// layers. The converter agrees: `conversion/base.py:1222-1230`
5820    /// takes the factor from `rope_parameters["full_attention"]` and
5821    /// writes nothing for the sliding half.
5822    ///
5823    /// frink folded the factor into ONE global `rope_freqs` vector, so
5824    /// Gemma-3-4B/12B/27B rotated five layers in six at `p/8`.
5825    #[test]
5826    fn gemma3_linear_scaling_reaches_the_full_layers_and_not_the_sliding_ones() {
5827        // Gemma-3-4B: 34 layers, head_dim 256, `rope_scaling: linear 8`.
5828        let cfg = gemma3_config("g3_4b_rope", 34, 2560, 8, 256, Some(8.0));
5829        let freqs = cfg
5830            .rope_freqs
5831            .as_ref()
5832            .expect("declared linear scaling must produce per-band divisors");
5833        assert!(
5834            freqs.full.iter().all(|f| (*f - 8.0).abs() < 1e-6),
5835            "full-attention layers divide every band by the trained factor: {:?}",
5836            freqs.full
5837        );
5838        let swa = freqs
5839            .swa
5840            .as_ref()
5841            .expect("gemma3 does not assign rope_freq_scale_train_swa, so 1.0 applies");
5842        assert!(
5843            swa.iter().all(|f| (*f - 1.0).abs() < 1e-6),
5844            "sliding layers rope at the raw position: {swa:?}"
5845        );
5846        assert_eq!(swa.len(), freqs.full.len(), "one divisor per rotated pair");
5847
5848        // Period 6, last-dense (`capability::default_swa_layout`), so
5849        // layer 5 is the full-attention one and 0..=4 slide. The phase
5850        // matters: getting it wrong swaps which five-sixths are wrong.
5851        assert!(cfg.layer_sliding_window(0).is_some());
5852        assert!(cfg.layer_sliding_window(5).is_none());
5853        assert_eq!(
5854            cfg.layer_rope(0),
5855            Some(crate::config::LayerRopeParams {
5856                theta: 10_000.0,
5857                freq_factors: Some(&[1.0f32; 128][..]),
5858                rot_dim: None,
5859            })
5860        );
5861        assert_eq!(
5862            cfg.layer_rope(5),
5863            Some(crate::config::LayerRopeParams {
5864                theta: 1_000_000.0,
5865                freq_factors: Some(&[8.0f32; 128][..]),
5866                rot_dim: None,
5867            })
5868        );
5869        assert!(
5870            cfg.rope_freqs_vary_by_layer(),
5871            "the fused Metal stacks take one divisor slice for a whole run \
5872             and so must refuse this model"
5873        );
5874
5875        // Gemma-3-1B declares no scaling at all -- the audited fixture,
5876        // and the reason this was invisible. Nothing to split, so no
5877        // per-layer set and no Metal refusal.
5878        let plain = gemma3_config("g3_1b_rope", 26, 1152, 4, 256, None);
5879        assert!(plain.rope_freqs.is_none());
5880        assert!(!plain.rope_freqs_vary_by_layer());
5881    }
5882
5883    /// Gemma-2 is the counter-case, and it is why the SWA scale needs
5884    /// its own table rather than reusing `swa_rope_base_follows_model`.
5885    ///
5886    /// `gemma2.cpp:10-11` assigns BOTH `rope_freq_base_train_swa` and
5887    /// `rope_freq_scale_train_swa` from the model's trained values, so
5888    /// its sliding layers keep the declared scaling. Splitting them here
5889    /// would be the same bug pointed the other way.
5890    #[test]
5891    fn gemma2_sliding_layers_inherit_the_trained_rope_scale() {
5892        let cfg = ModelConfig::from_gguf(&open_metadata_gguf(
5893            "g2_rope",
5894            &[
5895                ("general.architecture", Kv::Str("gemma2")),
5896                ("gemma2.block_count", Kv::U32(26)),
5897                ("gemma2.embedding_length", Kv::U32(2304)),
5898                ("gemma2.attention.head_count", Kv::U32(8)),
5899                ("gemma2.attention.head_count_kv", Kv::U32(4)),
5900                ("gemma2.attention.key_length", Kv::U32(256)),
5901                ("gemma2.attention.value_length", Kv::U32(256)),
5902                ("gemma2.rope.freq_base", Kv::F32(10_000.0)),
5903                ("gemma2.attention.sliding_window", Kv::U32(4096)),
5904                ("gemma2.rope.scaling.type", Kv::Str("linear")),
5905                ("gemma2.rope.scaling.factor", Kv::F32(8.0)),
5906            ],
5907        ))
5908        .expect("gemma2 fixture must load");
5909
5910        let freqs = cfg.rope_freqs.as_ref().expect("linear scaling declared");
5911        assert_eq!(
5912            freqs.swa, None,
5913            "gemma2.cpp:11 assigns rope_freq_scale_train_swa from the trained scale"
5914        );
5915        assert!(!cfg.rope_freqs_vary_by_layer());
5916        // Period 2, last-dense: layer 0 slides, layer 1 does not, and
5917        // both get the same divisors.
5918        assert!(cfg.layer_sliding_window(0).is_some());
5919        assert!(cfg.layer_sliding_window(1).is_none());
5920        assert_eq!(cfg.layer_rope(0), cfg.layer_rope(1));
5921
5922        // The two tables really are different: this is the pair that
5923        // must not be collapsed into one.
5924        assert!(crate::capability::swa_rope_scale_follows_model("gemma2"));
5925        assert!(!crate::capability::swa_rope_scale_follows_model("gemma3"));
5926        for arch in ["olmo2", "laguna"] {
5927            assert!(
5928                crate::capability::swa_rope_base_follows_model(arch),
5929                "{arch} seeds the SWA base from the model"
5930            );
5931            assert!(
5932                !crate::capability::swa_rope_scale_follows_model(arch),
5933                "{arch} pins the SWA scale to 1.0 (olmo2.cpp:14, laguna.cpp:48)"
5934            );
5935        }
5936    }
5937}