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        // Canonicalised: an architecture that llama.cpp computes
2686        // another one's graph for reads that one's tables, so an alias
2687        // needs one entry rather than a row in each of a dozen
2688        // per-architecture lists (`capability::canonical_architecture`).
2689        let arch = crate::capability::canonical_architecture(
2690            file.metadata_str("general.architecture")
2691                .unwrap_or_default(),
2692        )
2693        .to_string();
2694        let is_gpt_oss = arch == "gpt-oss";
2695        // A `<projection>.scale` companion is a multiply llama.cpp
2696        // applies and frink does not; refused by name here, before
2697        // the unread-tensor gate can be talked past
2698        // (`crate::weight_scales`).
2699        crate::weight_scales::refuse_weight_scale_tensors(
2700            &arch,
2701            file.tensors().map(|(_, t)| t.name.as_str()),
2702        )?;
2703        // Which tensor each of the five norm sites is stored under and
2704        // which FUNCTION norms it, resolved ONCE. Four shapes reach this
2705        // loader -- the plain pre-norm layer, the post-norm-only
2706        // topology (`olmo2`, `exaone4`), the non-parametric LayerNorm
2707        // (`olmo`) and the weighted LayerNorm (`dbrx`) -- and three
2708        // architectures keep a norm under a name another architecture
2709        // uses for a different site (`gpt-oss` / `seed_oss`, `dbrx`,
2710        // `grok`). `crate::norm_sites` is the one table for all of it;
2711        // this loader used to restate the decision at every site.
2712        let norm_sites = crate::norm_sites::NormSites::with_function(&arch, config.norm_function);
2713        let mut gpt_oss_layers: Vec<crate::decoder::GptOssLayer> = Vec::new();
2714
2715        // One store for the whole model (keys are (layer, expert)),
2716        // built up-front with every stored expert's segments; created
2717        // only when the cache is enabled AND some layer can use it.
2718        let mut store_segments: std::collections::HashMap<ExpertKey, [(usize, u64, usize); 3]> =
2719            std::collections::HashMap::new();
2720        let mut stored_layouts: Vec<Option<Vec<StoredExpertLayout>>> = Vec::new();
2721
2722        // Loaded like any other weight matrix: a quantized embedding
2723        // table stays quantized (zero-copy mmap) and token lookup
2724        // dequantizes one row via `WeightMatrix::dequant_row`, instead
2725        // of the whole vocabulary tensor being widened to f32 up front.
2726        let embedding = load_weight_matrix(&file, "token_embd.weight")?;
2727        // The learned position table (`crate::position_embd`), one row
2728        // per trained position, for the graphs that add one.
2729        let position_embd = crate::position_embd::load_position_embd(
2730            &file,
2731            &arch,
2732            config.hidden_dim,
2733            metadata_u64_any(&file, &[format!("{arch}.context_length")]).map(|v| v as usize),
2734        )?;
2735
2736        // PHYSICAL layers: the blocks the file holds tensors for. A
2737        // looped model (`crate::layer_loops`) has more logical layers
2738        // than this, and they run these same weights.
2739        let n_physical = config
2740            .layer_loops
2741            .map_or(config.n_layers, |loops| loops.physical_layers());
2742        let mut layers = Vec::with_capacity(n_physical);
2743        let mut refined_qk_norm = config.qk_norm_style;
2744        for l in 0..n_physical {
2745            // THIS layer's head counts and FFN width. Uniform for every
2746            // architecture but the per-layer ones (`crate::layer_shapes`),
2747            // and the loader reads the shape rather than the scalars so
2748            // that a deci / openelm layer is sized by its own header.
2749            let shape = config.layer_shape(l);
2750            // Whether THIS layer's FFN reads the layer input rather
2751            // than the post-attention residual, and under which norm
2752            // (`crate::parallel_residual`).
2753            let parallel = crate::parallel_residual::layer_parallel_norm(&file, &arch, l);
2754            // THIS layer's norm slots: the architecture's row, with
2755            // Falcon-40B's `attn_norm_2` crossing the two pre-norm names
2756            // on the layers that carry it (`crate::norm_sites`).
2757            let layer_sites = norm_sites.for_layer(&arch, &file, l);
2758            // BitNet's two inner norms, REQUIRED when the architecture
2759            // has them and untouched otherwise (`crate::sub_norms`).
2760            let sub_norms = crate::sub_norms::load_sub_norms(
2761                &file,
2762                &arch,
2763                config.block_sub_norms,
2764                l,
2765                config.hidden_dim,
2766                shape.ffn_dim,
2767            )?;
2768            let attn = match shape.attention {
2769                crate::layer_shapes::AttnShape::Gqa { n_heads, .. } => {
2770                    // Q/K/V and their biases come out of ONE decision about
2771                    // which spelling this layer uses -- see `qkv_fused`. They
2772                    // used to be resolved independently, and a checkpoint that
2773                    // fused both (ChatGLM, Qwen-1) had its bias dropped.
2774                    let crate::qkv_fused::QkvProjections {
2775                        q: q_proj,
2776                        k: k_proj,
2777                        v: v_proj,
2778                        q_bias,
2779                        k_bias,
2780                        v_bias,
2781                    } = crate::qkv_fused::load_fused_or_split_qkv(&file, l, &config)?;
2782                    let q_norm =
2783                        load_f32_vec_optional(&file, &format!("blk.{l}.attn_q_norm.weight"))?;
2784                    let k_norm =
2785                        load_f32_vec_optional(&file, &format!("blk.{l}.attn_k_norm.weight"))?;
2786                    // The per-head LAYERNORM (`crate::qk_layer_norm`), whose
2787                    // weight is `n_heads * head_dim` long and would pass
2788                    // the length rule below as `WholeVector`.
2789                    if let Some(reason) = crate::qk_layer_norm::per_head_layer_norm_refusal(
2790                        &arch,
2791                        l,
2792                        q_norm.is_some() || k_norm.is_some(),
2793                    ) {
2794                        return Err(LoadError::UnsupportedFeature(
2795                            config.name.to_string(),
2796                            reason,
2797                        ));
2798                    }
2799                    // Refine WholeVector vs PerHead from the first observed norm length.
2800                    // The per-head SCALAR gain is decided by architecture first
2801                    // (`capability::PER_HEAD_SCALAR_QK_GAIN`): its length is
2802                    // `n_heads`, which a length test alone could confuse with
2803                    // `head_dim`.
2804                    if let Some(ref w) = q_norm {
2805                        if crate::capability::uses_per_head_scalar_qk_gain(&arch) {
2806                            if w.len() != n_heads {
2807                                return Err(LoadError::UnsupportedFeature(
2808                                    config.name.to_string(),
2809                                    format!(
2810                                        "blk.{l}.attn_q_norm.weight length {} is not one gain per \
2811                                         head (n_heads={n_heads}; talkie.cpp:26 creates it {{1, \
2812                                         n_head}})",
2813                                        w.len()
2814                                    ),
2815                                ));
2816                            }
2817                            refined_qk_norm = crate::capability::QkNormStyle::PerHeadScalar;
2818                        } else if crate::capability::uses_per_head_distinct_qk_norm(&arch) {
2819                            // `plamo2.cpp:92-93`: `{head_dim, n_head}`, one
2820                            // row per head, RMS per head. The same length as
2821                            // a whole-vector weight, so the architecture
2822                            // decides (`capability::PER_HEAD_DISTINCT_QK_NORM`).
2823                            if w.len() != n_heads * config.head_dim {
2824                                return Err(LoadError::UnsupportedFeature(
2825                                    config.name.to_string(),
2826                                    format!(
2827                                        "blk.{l}.attn_q_norm.weight length {} is not one row per \
2828                                         head (n_heads={n_heads} x head_dim={}; plamo2.cpp:92 \
2829                                         creates it {{head_dim, n_head}})",
2830                                        w.len(),
2831                                        config.head_dim
2832                                    ),
2833                                ));
2834                            }
2835                            refined_qk_norm = crate::capability::QkNormStyle::PerHeadDistinct;
2836                        } else if w.len() == config.head_dim {
2837                            refined_qk_norm = crate::capability::QkNormStyle::PerHead;
2838                        } else if w.len() == n_heads * config.head_dim {
2839                            refined_qk_norm = crate::capability::QkNormStyle::WholeVector;
2840                        } else {
2841                            return Err(LoadError::UnsupportedFeature(
2842                                config.name.to_string(),
2843                                format!(
2844                                    "blk.{l}.attn_q_norm.weight length {} matches neither \
2845                                     head_dim={} nor n_heads*head_dim={}",
2846                                    w.len(),
2847                                    config.head_dim,
2848                                    n_heads * config.head_dim
2849                                ),
2850                            ));
2851                        }
2852                    }
2853                    let attn = AttnWeights {
2854                        q_proj,
2855                        k_proj,
2856                        v_proj,
2857                        o_proj: load_weight_matrix(&file, &format!("blk.{l}.attn_output.weight"))?,
2858                        // Which tensor, which function, and whether there is a
2859                        // norm here at all: all three answered by the table.
2860                        norm_weight: layer_sites.load_pre_norm(layer_sites.attn, &file, Some(l))?,
2861                        q_norm,
2862                        k_norm,
2863                        // Qwen2/Qwen2-MoE-family real QKV bias (`attn_{q,k,v}.bias`,
2864                        // real config `qkv_bias`, `o_proj` has none) -- see
2865                        // `AttnWeights::q_bias`'s doc comment. Resolved above,
2866                        // alongside the projections they belong to, because a
2867                        // file that fuses the weight fuses the bias too.
2868                        q_bias,
2869                        k_bias,
2870                        v_bias,
2871                        post_attn_norm: crate::norm_sites::NormSites::load_post_norm(
2872                            norm_sites.post_attn,
2873                            &file,
2874                            l,
2875                        )?,
2876                        post_ffn_norm: crate::norm_sites::NormSites::load_post_norm(
2877                            norm_sites.post_ffn,
2878                            &file,
2879                            l,
2880                        )?,
2881                        // The architecture table decides whether there is
2882                        // a gate and how it is applied; the tensor decides
2883                        // its width. See `crate::attn_gate`.
2884                        output_gate: crate::attn_gate::AttnGate::load(
2885                            &file,
2886                            &arch,
2887                            l,
2888                            n_heads,
2889                            config.head_dim,
2890                            config.hidden_dim,
2891                        )?,
2892                        // The TENSOR decides. Four llama.cpp graphs pass it
2893                        // into the one `build_attn_mha`; on the generic
2894                        // path a file that has it gets the sink term and
2895                        // a file that does not gets none, whatever the
2896                        // architecture string. gpt-oss's requirement is
2897                        // checked where its side table loads.
2898                        sinks: load_attn_sinks(&file, l, n_heads)?,
2899                        attn_sub_norm: sub_norms.as_ref().map(|n| n.attn.clone()),
2900                        o_scale: crate::weight_scales::load_projection_gain(
2901                            &file,
2902                            &arch,
2903                            l,
2904                            "attn_output",
2905                        )?,
2906                        o_bias: crate::proj_bias::load_attn_out_bias(
2907                            &file,
2908                            &arch,
2909                            l,
2910                            config.hidden_dim,
2911                        )?,
2912                        shortconv: None,
2913                        // falcon-h1.cpp:55-71: the Mamba-2 block beside
2914                        // attention on every layer (`crate::mamba2::
2915                        // PARALLEL_WITH_ATTENTION`).
2916                        ssm: if config.parallel_ssm {
2917                            Some(crate::ssm_block::SsmBlock::Mamba2(
2918                                crate::mamba2::Mamba2::load(&file, &arch, l, config.hidden_dim)?,
2919                            ))
2920                        } else {
2921                            None
2922                        },
2923                        q_gate_interleaved: crate::attn_gate::q_gate_interleaved(&arch),
2924                    };
2925                    crate::layer_shapes::check_gqa_projection_widths(
2926                        l,
2927                        shape.attention,
2928                        config.head_dim,
2929                        config.v_head_dim(),
2930                        config.hidden_dim,
2931                        &attn,
2932                    )?;
2933                    attn
2934                }
2935                other => crate::layer_shapes::load_non_gqa_attention(
2936                    other,
2937                    &file,
2938                    &arch,
2939                    l,
2940                    &norm_sites,
2941                    &config,
2942                )?,
2943            };
2944
2945            // Leading dense layers (see ModelConfig::layer_is_dense's
2946            // doc comment) load from the plain dense tensor names
2947            // regardless of this model's global MoE topology, matching
2948            // the DeepSeek-2/3-family convention found in
2949            // ik_llama.cpp's source. A model with n_experts<=1
2950            // globally (the dense test fixture) is dense on every
2951            // layer either way.
2952            // A layer with NO FFN at all (`ffn_dim 0`: deci's, Nemotron-H's
2953            // block-only layers) takes the dense arm, whose loader answers
2954            // `absent_ffn` for that width, whatever the model's MoE says.
2955            let is_dense_layer = config.layer_is_dense(l)
2956                || config.moe.n_experts <= 1
2957                || shape.ffn_dim == 0
2958                || crate::moe_interleave::dense_by_router_absence(&arch, &file, l);
2959            // The ungated experts (`nemotron-h.cpp:82-86,209-215`: a null
2960            // gate into `build_moe_ffn`, `LLM_FFN_RELU_SQR`) are spelled
2961            // the way the dense ungated FFN is (`load_dense_expert`): the
2962            // gate ALIASED to `up`, so `relu(up)^2` runs through the gated
2963            // body with no branch. A file that carries a gate anyway is
2964            // refused, as the dense loader refuses one.
2965            let routed_gate_name = if config.ffn_is_ungated() && !is_dense_layer {
2966                if file
2967                    .find_tensor(&format!("blk.{l}.ffn_gate_exps.weight"))
2968                    .is_some()
2969                {
2970                    return Err(LoadError::UnsupportedFeature(
2971                        arch.clone(),
2972                        format!(
2973                            "blk.{l}.ffn_gate_exps.weight is present but this architecture's \
2974                             experts are ungated ({:?}: a null gate into build_moe_ffn, \
2975                             nemotron-h.cpp:212)",
2976                            config.ffn_activation
2977                        ),
2978                    ));
2979                }
2980                format!("blk.{l}.ffn_up_exps.weight")
2981            } else {
2982                format!("blk.{l}.ffn_gate_exps.weight")
2983            };
2984            // The inner FFN norm has a site in the dense body only
2985            // (`build_ffn` with a NULL down, `bitnet.cpp:127-141`);
2986            // `build_moe_ffn` has none, so a routed layer that carried
2987            // one would have nowhere to apply it.
2988            if sub_norms.is_some() && !is_dense_layer {
2989                return Err(LoadError::UnsupportedFeature(
2990                    arch.clone(),
2991                    format!(
2992                        "blk.{l}.ffn_sub_norm on a MoE layer: llama.cpp applies the inner FFN \
2993                         norm in the dense `build_ffn` body only (bitnet.cpp:127-141), and no \
2994                         routed-expert graph has that site"
2995                    ),
2996                ));
2997            }
2998            let n_experts = if is_dense_layer {
2999                1
3000            } else {
3001                config.moe.n_experts
3002            };
3003            let experts: ExpertBacking = if is_dense_layer {
3004                ExpertBacking::Resident(vec![load_dense_expert(&file, l, &config, shape.ffn_dim)?])
3005            } else {
3006                // Try store-backed layouts first when the cache is
3007                // enabled; fall back to resident when any of the three
3008                // tensors isn't a supported quantized dtype.
3009                let stored = if expert_cache_bytes.is_some() {
3010                    let g = stored_expert_specs(&file, &routed_gate_name, n_experts)?;
3011                    let u = stored_expert_specs(
3012                        &file,
3013                        &format!("blk.{l}.ffn_up_exps.weight"),
3014                        n_experts,
3015                    )?;
3016                    let d = stored_expert_specs(
3017                        &file,
3018                        &format!("blk.{l}.ffn_down_exps.weight"),
3019                        n_experts,
3020                    )?;
3021                    match (g, u, d) {
3022                        (Some(gt), Some(ut), Some(dt)) => {
3023                            let mut layouts = Vec::with_capacity(n_experts);
3024                            for e in 0..n_experts {
3025                                let key = ExpertKey {
3026                                    layer: l as u32,
3027                                    expert: e as u32,
3028                                };
3029                                store_segments.insert(
3030                                    key,
3031                                    [
3032                                        (gt.shard, gt.per_expert[e].0, gt.per_expert[e].1),
3033                                        (ut.shard, ut.per_expert[e].0, ut.per_expert[e].1),
3034                                        (dt.shard, dt.per_expert[e].0, dt.per_expert[e].1),
3035                                    ],
3036                                );
3037                                let mut gate = gt.spec;
3038                                let mut up = ut.spec;
3039                                let mut down = dt.spec;
3040                                gate.offset = 0;
3041                                up.offset = gate.len;
3042                                down.offset = gate.len + up.len;
3043                                layouts.push(StoredExpertLayout { gate, up, down });
3044                            }
3045                            Some(layouts)
3046                        }
3047                        _ => None,
3048                    }
3049                } else {
3050                    None
3051                };
3052                match stored {
3053                    Some(layouts) => {
3054                        // Placeholder; the shared store is attached in a
3055                        // second pass below once every layer's segments
3056                        // are collected.
3057                        stored_layouts.push(Some(layouts));
3058                        ExpertBacking::Resident(Vec::new())
3059                    }
3060                    None => {
3061                        let gates = split_expert_tensor(&file, &routed_gate_name, n_experts)?;
3062                        let ups = split_expert_tensor(
3063                            &file,
3064                            &format!("blk.{l}.ffn_up_exps.weight"),
3065                            n_experts,
3066                        )?;
3067                        let downs = split_expert_tensor(
3068                            &file,
3069                            &format!("blk.{l}.ffn_down_exps.weight"),
3070                            n_experts,
3071                        )?;
3072                        ExpertBacking::Resident(
3073                            gates
3074                                .into_iter()
3075                                .zip(ups)
3076                                .zip(downs)
3077                                .map(|((gate, up), down)| ExpertWeights { gate, up, down })
3078                                .collect(),
3079                        )
3080                    }
3081                }
3082            };
3083            if stored_layouts.len() < layers.len() + 1 {
3084                stored_layouts.push(None);
3085            }
3086
3087            let mut shared_experts: Vec<ExpertWeights> =
3088                if config.moe.n_shared_experts > 0 && !is_dense_layer {
3089                    // The shared expert takes the architecture's dense
3090                    // activation, so an ungated one aliases its gate as
3091                    // `load_dense_expert` does (`nemotron-h.cpp:222-227`).
3092                    let shexp_gate = if config.ffn_is_ungated() {
3093                        if file
3094                            .find_tensor(&format!("blk.{l}.ffn_gate_shexp.weight"))
3095                            .is_some()
3096                        {
3097                            return Err(LoadError::UnsupportedFeature(
3098                                arch.clone(),
3099                                format!(
3100                                    "blk.{l}.ffn_gate_shexp.weight is present but this \
3101                                     architecture's shared expert is ungated"
3102                                ),
3103                            ));
3104                        }
3105                        format!("blk.{l}.ffn_up_shexp.weight")
3106                    } else {
3107                        format!("blk.{l}.ffn_gate_shexp.weight")
3108                    };
3109                    vec![ExpertWeights {
3110                        gate: load_weight_matrix(&file, &shexp_gate)?,
3111                        up: load_weight_matrix(&file, &format!("blk.{l}.ffn_up_shexp.weight"))?,
3112                        down: load_weight_matrix(&file, &format!("blk.{l}.ffn_down_shexp.weight"))?,
3113                    }]
3114                } else {
3115                    Vec::new()
3116                };
3117            // A dense FFN SUMMED with the experts (Grok-2, Arctic) is the
3118            // shared-expert slot under the dense names, plus the row's
3119            // scale on the sum (`crate::parallel_dense_ffn`). Decided per
3120            // layer: Grok-1's layers have no triple and take neither.
3121            // ...and the same scale under the `_shexp` names
3122            // (`SHARED_EXPERT_SUM_SCALE`, cohere2moe's `* 0.5`), on a
3123            // layer that loaded a shared expert above.
3124            let parallel_sum_scale = if is_dense_layer {
3125                None
3126            } else {
3127                match crate::parallel_dense_ffn::parallel_dense_for_layer(&arch, &file, l)? {
3128                    Some(row) => {
3129                        shared_experts.push(ExpertWeights {
3130                            gate: load_weight_matrix(&file, &format!("blk.{l}.ffn_gate.weight"))?,
3131                            up: load_weight_matrix(&file, &format!("blk.{l}.ffn_up.weight"))?,
3132                            down: load_weight_matrix(&file, &format!("blk.{l}.ffn_down.weight"))?,
3133                        });
3134                        row.sum_scale
3135                    }
3136                    None => crate::parallel_dense_ffn::shared_expert_sum_scale(
3137                        &arch,
3138                        !shared_experts.is_empty(),
3139                    ),
3140                }
3141            };
3142            // Arctic's second per-layer norm, the routed branch's operand
3143            // (`crate::router_input::RouterInput::NormedLayerInput`):
3144            // REQUIRED on its routed layers, unread everywhere else.
3145            let exps_norm = if config.router_input.needs_exps_norm() && !is_dense_layer {
3146                Some(load_f32_vec(
3147                    &file,
3148                    &format!("blk.{l}.ffn_norm_exps.weight"),
3149                )?)
3150            } else {
3151                None
3152            };
3153
3154            let router = if !is_dense_layer {
3155                load_weight_matrix(&file, &format!("blk.{l}.ffn_gate_inp.weight"))?
3156            } else {
3157                // dense layer: no real router; a zero [1, hidden] matrix
3158                // always selects the single expert deterministically.
3159                WeightMatrix::F32(Tensor::zeros(vec![1, config.hidden_dim]))
3160            };
3161
3162            let n_for_counts = match &experts {
3163                ExpertBacking::Resident(v) if v.is_empty() => n_experts,
3164                other => other.n_experts(),
3165            };
3166            let activation_counts = (0..n_for_counts)
3167                .map(|_| std::sync::atomic::AtomicU64::new(0))
3168                .collect();
3169            // Qwen2-MoE-specific real tensor (`blk.N.ffn_gate_inp_shexp.weight`,
3170            // real on-disk shape `[hidden_dim]`, confirmed against
3171            // llama.cpp's real `qwen2moe.cpp`) -- see
3172            // `MoeWeights::shared_expert_gate`'s doc comment. Presence
3173            // of the tensor itself is the real signal (not an
3174            // architecture-name list): every other supported
3175            // architecture's checkpoints simply don't carry this
3176            // tensor, so this naturally stays `None` there.
3177            let shared_expert_gate = if is_dense_layer {
3178                None
3179            } else {
3180                load_f32_vec_optional(&file, &format!("blk.{l}.ffn_gate_inp_shexp.weight"))?
3181            };
3182            #[cfg(feature = "metal")]
3183            let packed_q4 = match &experts {
3184                ExpertBacking::Resident(v) if !v.is_empty() => try_build_moe_packed_q4_planes(v),
3185                _ => None,
3186            };
3187            // DeepSeek-V3's aux-loss-free selection bias. The on-disk
3188            // name carries no `ffn_` prefix -- llama.cpp's
3189            // `LLM_TENSOR_FFN_EXP_PROBS_B` maps to `blk.%d.exp_probs_b`
3190            // (`llama-arch.cpp:416`, `gguf-py/gguf/constants.py:1240`).
3191            // Optional: only the DeepSeek-V3-lineage MoE recipes carry
3192            // it, and this same generic loader serves OLMoE / Qwen2-MoE /
3193            // Mixtral, which do not.
3194            let exp_probs_bias = if is_dense_layer {
3195                None
3196            } else {
3197                load_f32_vec_optional(&file, &format!("blk.{l}.exp_probs_b.bias"))?
3198            };
3199            if let Some(bias) = &exp_probs_bias {
3200                if bias.len() != config.moe.n_experts {
3201                    return Err(LoadError::UnsupportedFeature(
3202                        arch.clone(),
3203                        format!(
3204                            "blk.{l}.exp_probs_b.bias has {} entries but the model has {} experts",
3205                            bias.len(),
3206                            config.moe.n_experts
3207                        ),
3208                    ));
3209                }
3210                // Grouped selection masks the *biased* scores before the
3211                // global top-k (`build_moe_ffn`, the `n_expert_groups > 1`
3212                // block). frink's `route_top_k_grouped` takes a fixed
3213                // count from every group instead, which is a different
3214                // algorithm, so combining the two here would be a guess.
3215                // Refuse rather than route wrongly.
3216                if config.moe.expert_group_count.is_some() {
3217                    return Err(LoadError::UnsupportedFeature(
3218                        arch.clone(),
3219                        format!(
3220                            "blk.{l}.exp_probs_b.bias together with expert groups \
3221                             ({:?}): llama.cpp masks the biased scores per group \
3222                             before a global top-k, which is not the per-group \
3223                             top-k frink implements",
3224                            config.moe.expert_group_count
3225                        ),
3226                    ));
3227                }
3228            }
3229            let moe = MoeWeights {
3230                router,
3231                experts,
3232                shared_experts,
3233                shared_expert_gate,
3234                exp_probs_bias,
3235                exps_norm,
3236                parallel_sum_scale,
3237                // The dense FFN's biases (`crate::proj_bias`), on a dense
3238                // layer; a routed layer's experts carry none on the
3239                // generic path (gpt-oss's are its side table's).
3240                dense_bias: if is_dense_layer && shape.ffn_dim > 0 {
3241                    let bias = crate::proj_bias::load_dense_ffn_bias(
3242                        &file,
3243                        &arch,
3244                        l,
3245                        config.hidden_dim,
3246                        shape.ffn_dim,
3247                        config.ffn_is_ungated(),
3248                    )?;
3249                    if bias.is_some() && sub_norms.is_some() {
3250                        return Err(LoadError::UnsupportedFeature(
3251                            arch.clone(),
3252                            format!(
3253                                "layer {l} has both an inner FFN norm and FFN biases; no llama.cpp \
3254                                 graph has both and the dense body has one arm for each"
3255                            ),
3256                        ));
3257                    }
3258                    bias
3259                } else {
3260                    None
3261                },
3262                ffn_sub_norm: sub_norms.map(|n| n.ffn),
3263                down_scale: {
3264                    let gain =
3265                        crate::weight_scales::load_projection_gain(&file, &arch, l, "ffn_down")?;
3266                    if gain.is_some() && !is_dense_layer {
3267                        return Err(LoadError::UnsupportedFeature(
3268                            arch.clone(),
3269                            format!(
3270                                "blk.{l}.ffn_down.scale on a MoE layer: the routed experts' \
3271                                 scales are `ffn_down_exps.scale`, one per expert, which is \
3272                                 not applied here"
3273                            ),
3274                        ));
3275                    }
3276                    gain
3277                },
3278                // The same table as the attention slot, so the two
3279                // pre-norms cannot disagree about the function, and the
3280                // pre-FFN tensor's NAME comes from the same row that
3281                // decided the post-attention slot must not read it. An
3282                // FFN-free layer (`deci.cpp:52-54`) has no such tensor.
3283                // A parallel layer with ONE shared norm has no pre-FFN
3284                // tensor and no pre-FFN norm: the FFN reads the vector
3285                // attention read (`crate::parallel_residual`).
3286                norm_weight: if shape.ffn_dim == 0
3287                    || parallel == Some(crate::parallel_residual::ParallelNorm::SharedNorm)
3288                {
3289                    NormOp::None
3290                } else {
3291                    layer_sites.load_pre_norm(layer_sites.ffn, &file, Some(l))?
3292                },
3293                parallel,
3294                activation_counts,
3295                #[cfg(feature = "metal")]
3296                packed_q4,
3297            };
3298
3299            if is_gpt_oss {
3300                gpt_oss_layers.push(load_gpt_oss_layer(&file, l, &config, attn.sinks.is_some())?);
3301            }
3302
3303            // Talkie's per-layer skip scalar (`crate::skip_stream`);
3304            // REQUIRED there, untouched everywhere else.
3305            let out_scale =
3306                crate::skip_stream::load_out_scale(&file, &arch, config.skip_stream, l)?;
3307            layers.push(LayerWeights {
3308                attn,
3309                moe,
3310                out_scale,
3311            });
3312        }
3313
3314        // `olmo.cpp:15-36` creates no `output_norm` at all and
3315        // `:128-130` norms the final hidden state with a null weight, so
3316        // asking for the tensor would refuse every real OLMo-1 file;
3317        // the table's function decides whether the read happens.
3318        let final_norm = norm_sites.load_pre_norm(norm_sites.output, &file, None)?;
3319        // `hrm-text.cpp:46` creates `hrm_z_l_init` REQUIRED, and only
3320        // that graph does (`crate::hrm`): the learned LOW stream, one
3321        // `[n_embd]` row broadcast over the tokens at `:182`.
3322        let hrm_z_l_init = match config.layer_loops {
3323            Some(crate::layer_loops::LayerLoops::Hrm { .. }) => {
3324                Some(load_f32_vec(&file, "hrm.z_l_init")?)
3325            }
3326            _ => None,
3327        };
3328        // The embedding norm (`norm_sites::EMBEDDING_NORM_ARCHITECTURES`),
3329        // `NormOp::None` where the site is absent.
3330        // Two answers, one field: a STORED embedding norm (`bloom`) or
3331        // a weightless one (`muse-glimmer.cpp:69`), and the tables that
3332        // decide them are disjoint by construction
3333        // (`norm_sites::WEIGHTLESS_EMBEDDING_NORM`).
3334        let embedding_norm = if crate::norm_sites::weightless_embedding_norm(&arch) {
3335            crate::norm::NormOp::RmsNoParams
3336        } else {
3337            norm_sites.load_pre_norm(norm_sites.embedding, &file, None)?
3338        };
3339        // Many small Llama/Gemma-family GGUFs tie the lm-head to
3340        // `token_embd.weight` and omit `output.weight` (llama.cpp
3341        // `llama_model_loader` falls back the same way). Prefer the
3342        // explicit head when present.
3343        let output_head = match load_weight_matrix(&file, "output.weight") {
3344            Ok(w) => w,
3345            Err(_) => load_weight_matrix(&file, "token_embd.weight")?,
3346        };
3347        // `output.bias` for the graphs that create it (`crate::proj_bias`).
3348        let output_bias = crate::proj_bias::load_output_bias(&file, &arch, output_head.rows())?;
3349
3350        // Second pass: attach the one shared store to every
3351        // store-backed layer. Opening the shard files fresh (plain
3352        // `File` handles for positional reads, not mmaps) keeps the
3353        // stored experts' bytes out of the process's mapped footprint
3354        // entirely.
3355        if !store_segments.is_empty() {
3356            let budget = expert_cache_bytes
3357                .expect("store_segments only populated when a cache budget is set")
3358                as usize;
3359            let files: Result<Vec<std::fs::File>, std::io::Error> =
3360                file.shard_paths().iter().map(std::fs::File::open).collect();
3361            let files = files.map_err(GgufError::from)?;
3362            let store = std::sync::Arc::new(ExpertStore::new(
3363                GgufExpertSource {
3364                    files,
3365                    segments: store_segments,
3366                },
3367                budget,
3368            ));
3369            for (l, layer) in layers.iter_mut().enumerate() {
3370                if let Some(layouts) = stored_layouts.get_mut(l).and_then(Option::take) {
3371                    layer.moe.experts = ExpertBacking::Stored {
3372                        store: std::sync::Arc::clone(&store),
3373                        layouts,
3374                        layer: l as u32,
3375                    };
3376                }
3377            }
3378        }
3379
3380        config.qk_norm_style = refined_qk_norm;
3381
3382        let family = crate::capability::resolve_profile(
3383            file.metadata_str("general.architecture").unwrap_or("llama"),
3384        )
3385        .map(|p| p.family)
3386        .unwrap_or(crate::capability::DecoderFamily::StandardGqa);
3387        let memory_kind = crate::capability::resolve_profile(
3388            file.metadata_str("general.architecture").unwrap_or("llama"),
3389        )
3390        .map(|p| p.memory)
3391        .unwrap_or(crate::capability::MemoryKind::KvGqa);
3392        let execution_plan = crate::execution_plan::ExecutionPlan::from_config(
3393            &config,
3394            family,
3395            memory_kind,
3396            crate::execution_plan::ExecutionPlan::probe_metal_caps(),
3397        );
3398
3399        let alibi_slopes = crate::decoder::config_alibi_slopes(&config);
3400        let decoder = Decoder {
3401            config,
3402            embedding,
3403            position_embd,
3404            embedding_norm,
3405            // `hrm-text.cpp:46` creates it REQUIRED, and only that
3406            // graph does (`crate::hrm`); the loader reads it for the
3407            // architecture whose schedule needs it and for no other.
3408            hrm_z_l_init,
3409            alibi_slopes,
3410            layers,
3411            final_norm,
3412            output_head,
3413            output_bias,
3414            gpu_vram_budget_bytes: None,
3415            gpt_oss: if is_gpt_oss {
3416                Some(crate::decoder::GptOssWeights {
3417                    layers: gpt_oss_layers,
3418                })
3419            } else {
3420                None
3421            },
3422            qk_norm_after_rope: QK_NORM_AFTER_ROPE_ARCHITECTURES.contains(&arch.as_str()),
3423            #[cfg(feature = "metal")]
3424            metal_attn_kv: std::sync::Mutex::new(None),
3425            execution_plan,
3426            kv_window: crate::decoder::KvWindowPolicy::from_env(),
3427            plan_cache: std::sync::Mutex::new(std::collections::HashMap::new()),
3428            lora_adapters: Vec::new(),
3429        };
3430        // Resolve every kernel the model will need while we still have a
3431        // load-time error path to report it on, then seal: from here a
3432        // lookup that misses is an unpredicted slow path and says so.
3433        decoder.probe_kernels();
3434        frink_core::kernel_registry::seal_or_error()
3435            .map_err(|e| LoadError::StrictKernels(e.to_string()))?;
3436        // `ModelConfig` is parsed from a *different* handle on the same
3437        // file (the CLI opens its own `GgufFile`, then hands the config
3438        // here), so the model-level tensors it consumed were recorded on
3439        // that handle, not this one. Replay them before the gate, or
3440        // every Llama-3.x checkpoint reads as carrying an unread
3441        // `rope_freqs.weight` it in fact uses on every RoPE call.
3442        for name in crate::config::MODEL_LEVEL_TENSORS_READ_BY_CONFIG {
3443            file.note_consumed(name);
3444        }
3445        // The NextN/MTP blocks llama.cpp creates `TENSOR_SKIP` and never
3446        // runs (`crate::mtp_blocks`): deliberately unread, and said so,
3447        // rather than left for the gate below to report as a term the
3448        // graph is missing. The range is the config's, so the layer
3449        // loop above and this mark cannot disagree about where the
3450        // trunk ends.
3451        let skipped = crate::mtp_blocks::note_mtp_blocks_skipped(
3452            &file,
3453            &crate::mtp_blocks::TrunkLayers {
3454                block_count: n_physical + decoder.config.n_mtp_blocks,
3455                n_layers: n_physical,
3456                n_mtp_blocks: decoder.config.n_mtp_blocks,
3457            },
3458        );
3459        if skipped > 0 {
3460            eprintln!(
3461                "frink: skipping {} NextN/MTP block(s) after layer {} ({skipped} tensors), as \
3462                 llama.cpp does",
3463                decoder.config.n_mtp_blocks,
3464                n_physical - 1
3465            );
3466        }
3467        // Slots llama.cpp creates and never reads (`crate::unread_tensors`):
3468        // ignored as upstream ignores them, and said so.
3469        let ignored = crate::unread_tensors::note_unread_layer_tensors(&file, &arch, n_physical);
3470        if !ignored.is_empty() {
3471            eprintln!(
3472                "frink: ignoring {} tensor(s) llama.cpp creates and never reads for `{}` \
3473                 (first: {}), as llama.cpp does",
3474                ignored.len(),
3475                arch,
3476                ignored[0]
3477            );
3478        }
3479        assert_every_tensor_consumed(&file)?;
3480        Ok(decoder)
3481    }
3482}
3483
3484/// Tensor-name prefixes a text-generation load legitimately never
3485/// reads. Everything here is consumed by a *different* code path, not by
3486/// nothing: multimodal projector planes belong to `mmproj`, and the
3487/// per-shard split bookkeeping is metadata, not weights.
3488const IGNORED_TENSOR_PREFIXES: &[&str] = &["mm.", "v.", "mmproj.", "resampler.", "audio."];
3489
3490/// Fails the load when the checkpoint carries tensors this build never
3491/// looked at.
3492///
3493/// A tensor nobody reads is not a harmless extra: it is a term of the
3494/// real graph that ours is missing. gpt-oss ships `blk.N.attn_sinks`
3495/// and frink has no attention-sink code anywhere, so the file loads,
3496/// runs at full speed, and emits a different distribution than the model
3497/// it claims to be; the newer MoE recipes ship `ffn_exp_probs_b` the
3498/// same way. Both are silent today, and both are exactly what the
3499/// architecture registry cannot catch, because the architecture *string*
3500/// is one frink does support -- it is the checkpoint that carries more
3501/// than the registry entry promises.
3502///
3503/// This is deliberately the last check in the load: by here every loader
3504/// arm has had its chance to ask for what it needs, so what is left over
3505/// is what nothing in this build knows about.
3506///
3507/// `FRINK_ALLOW_UNKNOWN_TENSORS=1` downgrades it to a warning, for the
3508/// case where a human has decided the missing term does not matter (a
3509/// bias tensor of zeros, an auxiliary head that never runs). The default
3510/// is refusal: a wrong answer is worse than no answer.
3511pub fn assert_every_tensor_consumed(file: &ShardedGguf) -> Result<(), LoadError> {
3512    let mut left: Vec<String> = file
3513        .unconsumed_tensors()
3514        .into_iter()
3515        .filter(|n| !IGNORED_TENSOR_PREFIXES.iter().any(|p| n.starts_with(p)))
3516        .collect();
3517    if left.is_empty() {
3518        return Ok(());
3519    }
3520    left.sort();
3521    let shown = left.iter().take(8).cloned().collect::<Vec<_>>().join(", ");
3522    let listing = if left.len() > 8 {
3523        format!("{shown}, … (+{} more)", left.len() - 8)
3524    } else {
3525        shown
3526    };
3527    if matches!(
3528        std::env::var("FRINK_ALLOW_UNKNOWN_TENSORS").ok().as_deref(),
3529        Some("1") | Some("true") | Some("on")
3530    ) {
3531        eprintln!(
3532            "frink: WARNING -- {} tensor(s) in this checkpoint are never read \
3533             ({listing}); output may be wrong (FRINK_ALLOW_UNKNOWN_TENSORS=1)",
3534            left.len()
3535        );
3536        return Ok(());
3537    }
3538    Err(LoadError::UnconsumedTensors(left.len(), listing))
3539}
3540
3541#[cfg(test)]
3542mod tests {
3543
3544    /// A quantized 1-D tensor loads through the shared helper.
3545    ///
3546    /// This used to be six copies of `load_f32_vec`, and they had
3547    /// drifted badly: this one decoded twenty dtypes while the five
3548    /// architecture loaders decoded three (F32/F16/BF16). A quantizer
3549    /// that emits a Q8_0 norm or bias -- ordinary for aggressive
3550    /// quants -- loaded on the generic path and was rejected with
3551    /// `UnsupportedDtype` on GLM-5.2, Kimi, DeepSeek-MLA, Gemma-4 and
3552    /// the hybrid stack.
3553    ///
3554    /// This file's own comment predicted exactly that, about the same
3555    /// split one level down: "a dtype frink can decode should never be
3556    /// rejected here just because the *other* dispatch table below
3557    /// knows it -- that split is how a supported format turns into a
3558    /// load failure on the one checkpoint that uses it."
3559    #[test]
3560    fn a_quantized_one_dimensional_tensor_widens_through_the_shared_helper() {
3561        let values: Vec<f32> = (0..64).map(|i| (i as f32 - 32.0) * 0.25).collect();
3562        let quantized = frink_quant::quantize_q8_0(&values);
3563
3564        struct OneTensor {
3565            info: TensorInfo,
3566            bytes: Vec<u8>,
3567        }
3568        impl TensorSource for OneTensor {
3569            fn metadata(&self, _key: &str) -> Option<&frink_gguf::GgufValue> {
3570                None
3571            }
3572            fn find_tensor(&self, name: &str) -> Option<&TensorInfo> {
3573                (name == self.info.name).then_some(&self.info)
3574            }
3575            fn tensor_bytes(&self, _name: &str) -> Result<&[u8], GgufError> {
3576                Ok(&self.bytes)
3577            }
3578            fn tensor_mapped_range(
3579                &self,
3580                name: &str,
3581            ) -> Result<
3582                (
3583                    std::sync::Arc<frink_gguf::MmapHandle>,
3584                    std::ops::Range<usize>,
3585                ),
3586                GgufError,
3587            > {
3588                // Never reached: `load_f32_vec` widens from bytes.
3589                Err(GgufError::TensorNotFound(name.to_string()))
3590            }
3591        }
3592
3593        let source = OneTensor {
3594            info: TensorInfo {
3595                name: "blk.0.attn_norm.weight".to_string(),
3596                shape: vec![64],
3597                dtype: GgmlType::Q8_0,
3598                offset: 0,
3599            },
3600            bytes: quantized,
3601        };
3602
3603        let widened = load_f32_vec(&source, "blk.0.attn_norm.weight")
3604            .expect("a Q8_0 norm must load, not report an unsupported dtype");
3605        assert_eq!(widened.len(), values.len());
3606        for (got, want) in widened.iter().zip(values.iter()) {
3607            assert!(
3608                (got - want).abs() < 0.05,
3609                "q8_0 round trip: got {got}, want {want}"
3610            );
3611        }
3612    }
3613    use super::*;
3614    use byteorder::{LittleEndian, WriteBytesExt};
3615    use std::io::Write;
3616
3617    fn write_string(buf: &mut Vec<u8>, s: &str) {
3618        buf.write_u64::<LittleEndian>(s.len() as u64).unwrap();
3619        buf.write_all(s.as_bytes()).unwrap();
3620    }
3621
3622    fn write_kv_str(buf: &mut Vec<u8>, key: &str, val: &str) {
3623        write_string(buf, key);
3624        buf.write_u32::<LittleEndian>(8).unwrap(); // type = string
3625        write_string(buf, val);
3626    }
3627
3628    /// A minimal, tensor-free GGUF byte buffer declaring only
3629    /// `general.architecture` (no `{arch}.block_count` or any other
3630    /// hparam key) -- the shape a stripped-down or malformed file might
3631    /// take, and the exact case `ModelConfig::from_gguf` must reject
3632    /// loudly rather than silently default around.
3633    fn build_arch_only_gguf(arch: &str) -> Vec<u8> {
3634        let mut buf = Vec::new();
3635        buf.write_u32::<LittleEndian>(frink_gguf::GGUF_MAGIC)
3636            .unwrap();
3637        buf.write_u32::<LittleEndian>(3).unwrap(); // version
3638        buf.write_u64::<LittleEndian>(0).unwrap(); // tensor_count
3639        buf.write_u64::<LittleEndian>(1).unwrap(); // kv_count
3640        write_kv_str(&mut buf, "general.architecture", arch);
3641        buf
3642    }
3643
3644    #[test]
3645    fn model_config_from_gguf_fails_loudly_when_required_hparams_are_missing() {
3646        let tmp =
3647            std::env::temp_dir().join(format!("frink_test_arch_only_{}.gguf", std::process::id()));
3648        // Use a registered architecture so the failure is MissingHparam,
3649        // not UnsupportedArchitecture.
3650        std::fs::write(&tmp, build_arch_only_gguf("llama")).unwrap();
3651        let file = frink_gguf::GgufFile::open(&tmp).expect("minimal header must still parse");
3652        std::fs::remove_file(&tmp).ok();
3653
3654        match ModelConfig::from_gguf(&file) {
3655            Err(LoadError::MissingHparam(key)) => {
3656                assert_eq!(key, "llama.block_count");
3657            }
3658            other => panic!(
3659                "expected LoadError::MissingHparam for a file with no hparam keys, got {other:?}"
3660            ),
3661        }
3662    }
3663
3664    #[test]
3665    fn model_config_from_gguf_fails_closed_on_unknown_architecture() {
3666        let tmp = std::env::temp_dir().join(format!(
3667            "frink_test_unknown_arch_{}.gguf",
3668            std::process::id()
3669        ));
3670        std::fs::write(&tmp, build_arch_only_gguf("bogus-arch-with-no-hparams")).unwrap();
3671        let file = frink_gguf::GgufFile::open(&tmp).expect("minimal header must still parse");
3672        std::fs::remove_file(&tmp).ok();
3673
3674        match ModelConfig::from_gguf(&file) {
3675            Err(LoadError::UnsupportedArchitecture(arch)) => {
3676                assert_eq!(arch, "bogus-arch-with-no-hparams");
3677            }
3678            other => panic!(
3679                "expected LoadError::UnsupportedArchitecture for an unregistered arch, got {other:?}"
3680            ),
3681        }
3682    }
3683
3684    fn write_kv_f32(buf: &mut Vec<u8>, key: &str, val: f32) {
3685        write_string(buf, key);
3686        buf.write_u32::<LittleEndian>(6).unwrap(); // type = float32
3687        buf.write_f32::<LittleEndian>(val).unwrap();
3688    }
3689
3690    /// `arch` plus one f32 hparam, so a metadata-only feature gate can be
3691    /// exercised without building a whole checkpoint.
3692    fn build_arch_plus_f32_gguf(arch: &str, key: &str, val: f32) -> Vec<u8> {
3693        let mut buf = Vec::new();
3694        buf.write_u32::<LittleEndian>(frink_gguf::GGUF_MAGIC)
3695            .unwrap();
3696        buf.write_u32::<LittleEndian>(3).unwrap(); // version
3697        buf.write_u64::<LittleEndian>(0).unwrap(); // tensor_count
3698        buf.write_u64::<LittleEndian>(2).unwrap(); // kv_count
3699        write_kv_str(&mut buf, "general.architecture", arch);
3700        write_kv_f32(&mut buf, key, val);
3701        buf
3702    }
3703
3704    fn config_error_for(arch: &str, key: &str, val: f32, tag: &str) -> LoadError {
3705        let tmp = std::env::temp_dir().join(format!("frink_test_scale_{tag}.gguf"));
3706        std::fs::write(&tmp, build_arch_plus_f32_gguf(arch, key, val)).unwrap();
3707        let file = frink_gguf::GgufFile::open(&tmp).expect("minimal header must still parse");
3708        std::fs::remove_file(&tmp).ok();
3709        ModelConfig::from_gguf(&file).expect_err("must not succeed")
3710    }
3711
3712    /// Granite / MiniCPM / Command-R multipliers are hparams, not
3713    /// tensors, so `assert_every_tensor_consumed` cannot see them: a
3714    /// checkpoint declaring one loads, runs at full speed, and computes
3715    /// a differently-scaled graph than it was trained as. An
3716    /// architecture whose reference graph does not apply one must refuse
3717    /// it by name.
3718    ///
3719    /// Driven on `llama` rather than on `granite`, and that swap is the
3720    /// point: `granite` APPLIES all four now
3721    /// (`crate::scalar_multipliers`), so leaving the case here would
3722    /// have turned this test into a test of nothing the day the feature
3723    /// landed. llama.cpp's llama graph reads none of the four keys, so a
3724    /// `llama` checkpoint declaring one is exactly the silent divergence
3725    /// the gate exists for.
3726    #[test]
3727    fn a_declared_multiplier_this_decoder_does_not_apply_is_refused_by_name() {
3728        for (key, val) in [
3729            ("llama.logit_scale", 6.0f32),
3730            ("llama.residual_scale", 0.22),
3731            ("llama.embedding_scale", 12.0),
3732            ("llama.attention.scale", 0.015_625),
3733        ] {
3734            let tag = key.replace('.', "_");
3735            match config_error_for("llama", key, val, &tag) {
3736                LoadError::UnsupportedFeature(arch, msg) => {
3737                    assert_eq!(arch, "llama");
3738                    assert!(msg.contains(key), "error must name the key: {msg}");
3739                }
3740                other => panic!("expected UnsupportedFeature for {key}, got {other:?}"),
3741            }
3742        }
3743    }
3744
3745    /// The complement, and the half that would otherwise have gone
3746    /// missing: `granite` must NOT be refused for the keys its graph
3747    /// applies.
3748    ///
3749    /// The refusal list and the implementation are two views of ONE
3750    /// table (`scalar_multipliers::multiplier_support`), so this test
3751    /// and the one above cannot both pass while they disagree -- which
3752    /// is the whole value of deriving the list rather than restating it.
3753    #[test]
3754    fn granite_is_not_refused_for_the_multipliers_it_applies() {
3755        for (key, val) in [
3756            ("granite.logit_scale", 8.0f32),
3757            ("granite.residual_scale", 0.22),
3758            ("granite.embedding_scale", 12.0),
3759            ("granite.attention.scale", 0.015_625),
3760        ] {
3761            let tag = format!("granite_ok_{}", key.replace('.', "_"));
3762            // The file carries no `block_count`, so the load still fails
3763            // -- but on the *missing hparam*, having passed this gate.
3764            match config_error_for("granite", key, val, &tag) {
3765                LoadError::MissingHparam(k) => assert_eq!(k, "granite.block_count"),
3766                other => panic!("{key}={val} must pass the scaling gate, got {other:?}"),
3767            }
3768        }
3769    }
3770
3771    /// The gate must not fire on a multiplier that is a no-op. A file
3772    /// writing `residual_scale = 1.0` describes the graph frink already
3773    /// computes, and refusing it would be a false alarm. llama.cpp's
3774    /// `f_attention_scale` uses `0.0` rather than `1.0` as its "unset"
3775    /// sentinel, so the two are checked against their own no-op values.
3776    #[test]
3777    fn a_multiplier_that_is_a_no_op_is_not_refused() {
3778        for (key, val) in [
3779            ("llama.logit_scale", 1.0f32),
3780            ("llama.residual_scale", 1.0),
3781            ("llama.embedding_scale", 1.0),
3782            ("llama.attention.scale", 0.0),
3783        ] {
3784            let tag = format!("noop_{}", key.replace('.', "_"));
3785            // The file carries no `block_count`, so the load still fails
3786            // -- but on the *missing hparam*, having passed this gate.
3787            match config_error_for("llama", key, val, &tag) {
3788                LoadError::MissingHparam(k) => assert_eq!(k, "llama.block_count"),
3789                other => panic!("no-op {key}={val} must pass the scaling gate, got {other:?}"),
3790            }
3791        }
3792    }
3793
3794    /// One GGUF metadata value, in the three types these header-only
3795    /// fixtures need.
3796    enum Kv<'a> {
3797        Str(&'a str),
3798        U32(u32),
3799        F32(f32),
3800        /// A uint32 ARRAY. Only one gate needs it -- the sliding-window
3801        /// pattern, which llama.cpp reads with `ml.get_key_or_arr` --
3802        /// and without it that gate could only be tested through a
3803        /// value of some other type, which is not the case it exists
3804        /// for.
3805        Arr32(&'a [u32]),
3806    }
3807
3808    /// A tensor-free GGUF carrying exactly `kvs` -- enough for
3809    /// `ModelConfig::from_gguf` to run without a single weight on disk.
3810    fn build_metadata_gguf(kvs: &[(&str, Kv)]) -> Vec<u8> {
3811        let mut buf = Vec::new();
3812        buf.write_u32::<LittleEndian>(frink_gguf::GGUF_MAGIC)
3813            .unwrap();
3814        buf.write_u32::<LittleEndian>(3).unwrap(); // version
3815        buf.write_u64::<LittleEndian>(0).unwrap(); // tensor_count
3816        buf.write_u64::<LittleEndian>(kvs.len() as u64).unwrap();
3817        for (k, v) in kvs {
3818            match v {
3819                Kv::Str(s) => write_kv_str(&mut buf, k, s),
3820                Kv::U32(n) => {
3821                    write_string(&mut buf, k);
3822                    buf.write_u32::<LittleEndian>(4).unwrap(); // type = uint32
3823                    buf.write_u32::<LittleEndian>(*n).unwrap();
3824                }
3825                Kv::F32(f) => write_kv_f32(&mut buf, k, *f),
3826                Kv::Arr32(values) => {
3827                    write_string(&mut buf, k);
3828                    buf.write_u32::<LittleEndian>(9).unwrap(); // type = array
3829                    buf.write_u32::<LittleEndian>(4).unwrap(); // element type = uint32
3830                    buf.write_u64::<LittleEndian>(values.len() as u64).unwrap();
3831                    for v in *values {
3832                        buf.write_u32::<LittleEndian>(*v).unwrap();
3833                    }
3834                }
3835            }
3836        }
3837        buf
3838    }
3839
3840    fn open_metadata_gguf(tag: &str, kvs: &[(&str, Kv)]) -> frink_gguf::GgufFile {
3841        let tmp = std::env::temp_dir().join(format!("frink_test_meta_{tag}.gguf"));
3842        std::fs::write(&tmp, build_metadata_gguf(kvs)).unwrap();
3843        let file = frink_gguf::GgufFile::open(&tmp).expect("header-only file must parse");
3844        std::fs::remove_file(&tmp).ok();
3845        file
3846    }
3847
3848    /// A minimal `llama` hparam set (64-wide single head, base 10000)
3849    /// plus whatever RoPE-scaling keys a test wants to add.
3850    fn llama_config_with(tag: &str, extra: &[(&str, Kv)]) -> ModelConfig {
3851        let mut kvs: Vec<(&str, Kv)> = vec![
3852            ("general.architecture", Kv::Str("llama")),
3853            ("llama.block_count", Kv::U32(1)),
3854            ("llama.embedding_length", Kv::U32(64)),
3855            ("llama.attention.head_count", Kv::U32(1)),
3856            ("llama.attention.head_count_kv", Kv::U32(1)),
3857            ("llama.attention.key_length", Kv::U32(64)),
3858            ("llama.rope.freq_base", Kv::F32(10_000.0)),
3859        ];
3860        for (k, v) in extra {
3861            kvs.push((
3862                k,
3863                match v {
3864                    Kv::Str(s) => Kv::Str(s),
3865                    Kv::U32(n) => Kv::U32(*n),
3866                    Kv::F32(f) => Kv::F32(*f),
3867                    Kv::Arr32(a) => Kv::Arr32(a),
3868                },
3869            ));
3870        }
3871        ModelConfig::from_gguf(&open_metadata_gguf(tag, &kvs)).expect("fixture must load")
3872    }
3873
3874    /// Builds a config for an arbitrary architecture tag, returning the
3875    /// error rather than unwrapping it.
3876    /// llama.cpp chooses the FFN gate activation PER ARCHITECTURE;
3877    /// frink chose it per family. Those are different partitions, and
3878    /// `grok` is where they disagree: `src/models/grok.cpp:165` passes
3879    /// `LLM_FFN_GELU` to `build_moe_ffn`, while `grok` is
3880    /// `DecoderFamily::StandardGqa` and so was handed SwiGLU -- a
3881    /// different FFN on every layer.
3882    ///
3883    /// It was pinned here while `grok` still refused, because the
3884    /// failure mode is that auditing it later makes it silently wrong,
3885    /// and an audit is exactly when nobody thinks to re-check the
3886    /// activation. `grok` is audited now (tests/grok_graphs.rs), and the
3887    /// fixture's GELU experts are what that suite compares.
3888    #[test]
3889    fn the_ffn_activation_follows_the_architecture_not_the_family() {
3890        use crate::capability::uses_geglu;
3891        use crate::config::FfnActivation;
3892
3893        assert!(uses_geglu("grok"), "grok's MoE FFN gate is GELU upstream");
3894        // Same family, SiLU upstream (`src/models/dbrx.cpp:122`), so the
3895        // family rule alone cannot be what selects grok.
3896        assert!(!uses_geglu("dbrx"));
3897        assert!(!uses_geglu("llama"));
3898
3899        // The Gemma lineage keeps its GELU through the FAMILY rule, so
3900        // the new per-architecture arm must not have displaced it.
3901        // gemma2/gemma3 only: `gemma` v1 is unaudited and refuses, so
3902        // it cannot be loaded to check its activation.
3903        for gemma in ["gemma2", "gemma3"] {
3904            assert!(
3905                !uses_geglu(gemma),
3906                "{gemma} is GELU via GemmaFamily; listing it here too \
3907                 would hide a later regression in the family rule"
3908            );
3909            assert_eq!(
3910                config_for_arch(gemma).expect("gemma loads").ffn_activation,
3911                FfnActivation::Gelu,
3912                "{gemma}"
3913            );
3914        }
3915
3916        // And a plain SwiGLU architecture stays SwiGLU.
3917        assert_eq!(
3918            config_for_arch("llama")
3919                .expect("llama loads")
3920                .ffn_activation,
3921            FfnActivation::Swiglu
3922        );
3923
3924        // The ungated ReLU-squared row, and the four that share its FFN
3925        // and refuse for something else (`capability::uses_relu_sqr`).
3926        assert_eq!(
3927            config_for_arch("arcee")
3928                .expect("arcee loads")
3929                .ffn_activation,
3930            FfnActivation::ReluSqr
3931        );
3932        for shared in ["plm", "nemotron", "jais2", "nemotron_h"] {
3933            assert!(crate::capability::uses_relu_sqr(shared), "{shared}");
3934        }
3935        assert!(!crate::capability::uses_relu_sqr("llama"));
3936    }
3937
3938    /// A per-layer array whose entries differ, on an architecture whose
3939    /// llama.cpp graph reads layer 0, is refused naming the table; the
3940    /// same arrays with equal entries are the uniform model, for any
3941    /// architecture, because a converter may spell a scalar as a list.
3942    ///
3943    /// Reachability, not only `LayerShapes::resolve`'s own unit test:
3944    /// this goes through `from_gguf` on a header-only file, which is
3945    /// where `openelm` used to die on `MissingHparam` for a key its
3946    /// file carried.
3947    #[test]
3948    fn a_varying_per_layer_array_is_refused_on_a_layer_zero_architecture_and_equal_ones_are_uniform(
3949    ) {
3950        let kvs = [
3951            ("general.architecture", Kv::Str("llama")),
3952            ("llama.block_count", Kv::U32(2)),
3953            ("llama.embedding_length", Kv::U32(64)),
3954            ("llama.attention.head_count", Kv::Arr32(&[2, 2])),
3955            ("llama.attention.head_count_kv", Kv::Arr32(&[2, 1])),
3956            ("llama.attention.key_length", Kv::U32(32)),
3957            ("llama.rope.freq_base", Kv::F32(10_000.0)),
3958        ];
3959        let err = ModelConfig::from_gguf(&open_metadata_gguf("layer_shapes_vary", &kvs))
3960            .expect_err("llama takes layer 0 upstream");
3961        let msg = err.to_string();
3962        assert!(msg.contains("PER_LAYER_SHAPE_ARCHS"), "{msg}");
3963        assert!(msg.contains("LLAMA_LOAD_LOCALS"), "{msg}");
3964
3965        let kvs = [
3966            ("general.architecture", Kv::Str("llama")),
3967            ("llama.block_count", Kv::U32(2)),
3968            ("llama.embedding_length", Kv::U32(64)),
3969            ("llama.attention.head_count", Kv::Arr32(&[2, 2])),
3970            ("llama.attention.head_count_kv", Kv::Arr32(&[1, 1])),
3971            ("llama.attention.key_length", Kv::U32(32)),
3972            ("llama.rope.freq_base", Kv::F32(10_000.0)),
3973        ];
3974        let cfg = ModelConfig::from_gguf(&open_metadata_gguf("layer_shapes_equal", &kvs))
3975            .expect("equal arrays are the uniform model");
3976        assert!(cfg.layer_shapes.is_uniform());
3977        assert_eq!((cfg.n_heads, cfg.n_kv_heads), (2, 1));
3978
3979        // An array of the wrong length is refused as llama.cpp refuses
3980        // it (`key has wrong array length`).
3981        let kvs = [
3982            ("general.architecture", Kv::Str("llama")),
3983            ("llama.block_count", Kv::U32(2)),
3984            ("llama.embedding_length", Kv::U32(64)),
3985            ("llama.attention.head_count", Kv::Arr32(&[2, 2, 2])),
3986            ("llama.attention.key_length", Kv::U32(32)),
3987            ("llama.rope.freq_base", Kv::F32(10_000.0)),
3988        ];
3989        let err = ModelConfig::from_gguf(&open_metadata_gguf("layer_shapes_len", &kvs))
3990            .expect_err("three entries for two layers");
3991        assert!(err.to_string().contains("wrong array length"), "{err}");
3992    }
3993
3994    /// The no-renormalise list is keyed on what llama.cpp's GRAPH does,
3995    /// not on what a GGUF says, because for these architectures the
3996    /// GGUF says nothing.
3997    ///
3998    /// `expert_weights_norm` is only written by converters that set it.
3999    /// `deepseek.cpp:145` passes `norm_w=false`, and
4000    /// `conversion/deepseek.py`'s `DeepseekModel` never writes the key
4001    /// -- only `DeepseekV2Model` does. So a real `deepseek` checkpoint
4002    /// carries no key at all and frink fell through to its default,
4003    /// renormalising the selected experts' softmax weights where
4004    /// llama.cpp leaves them alone.
4005    ///
4006    /// The same mistake made OLMoE emit garbage, which is why that list
4007    /// exists. This pins the membership so a later edit cannot quietly
4008    /// drop a name back into the renormalising default.
4009    #[test]
4010    fn the_architectures_llama_cpp_does_not_renormalise_are_pinned() {
4011        for arch in ["deepseek", "olmoe", "qwen2moe"] {
4012            assert!(
4013                NO_TOPK_RENORMALIZE_ARCHITECTURES.contains(&arch),
4014                "{arch} passes norm_w=false in llama.cpp and must not be renormalised"
4015            );
4016        }
4017        // `deepseek2` is a DIFFERENT architecture whose converter DOES
4018        // write the key, so it must not be on this list -- it gets its
4019        // answer from the file.
4020        assert!(!NO_TOPK_RENORMALIZE_ARCHITECTURES.contains(&"deepseek2"));
4021        assert!(!NO_TOPK_RENORMALIZE_ARCHITECTURES.contains(&"qwen3moe"));
4022    }
4023
4024    /// Every architecture llama.cpp defaults to SIGMOID gating must be
4025    /// on the list, because for these the GGUF carries no key to say so.
4026    ///
4027    /// Each of these reads `LLM_KV_EXPERT_GATING_FUNC` as optional and
4028    /// then sets SIGMOID when it is absent, so a converted checkpoint
4029    /// has nothing in it that would correct frink's softmax default.
4030    /// Same shape as the `deepseek` top-k renormalisation bug, and as
4031    /// `phi3`'s sliding window: the file is silent and the architecture
4032    /// decides.
4033    /// The literal table and its name list are two spellings of one
4034    /// fact; this is what keeps them one.
4035    #[test]
4036    fn the_gating_literal_names_are_the_gating_literal_table() {
4037        let from_table: Vec<&str> = GATING_LITERAL_ARCHITECTURES
4038            .iter()
4039            .map(|(n, _)| *n)
4040            .collect();
4041        assert_eq!(from_table, GATING_LITERAL_NAMES);
4042        // `mimo2.cpp:227` passes the SIGMOID literal, so the key is
4043        // never read there; a hand-written SOFTMAX key must not turn it.
4044        assert!(matches!(
4045            GATING_LITERAL_ARCHITECTURES
4046                .iter()
4047                .find(|(n, _)| *n == "mimo2")
4048                .map(|(_, g)| *g),
4049            Some(GatingFunction::Sigmoid)
4050        ));
4051    }
4052
4053    #[test]
4054    fn the_architectures_llama_cpp_defaults_to_sigmoid_gating_are_pinned() {
4055        for arch in ["afmoe", "deepseek2", "glm4moe", "laguna", "step35"] {
4056            assert!(
4057                SIGMOID_GATING_ARCHITECTURES.contains(&arch),
4058                "{arch} sets SIGMOID when the gating key is absent"
4059            );
4060        }
4061        // Architectures that HARDCODE softmax must stay off it, or the
4062        // fix becomes the opposite bug: `ernie4-5-moe.cpp:90` and
4063        // `qwen3moe` both gate with softmax unconditionally.
4064        for softmax in ["ernie4_5-moe", "qwen3moe", "olmoe", "llama"] {
4065            assert!(
4066                !SIGMOID_GATING_ARCHITECTURES.contains(&softmax),
4067                "{softmax} does not default to sigmoid"
4068            );
4069        }
4070    }
4071
4072    /// Every name in every architecture-keyed behaviour table is a name
4073    /// the catalog actually resolves, on the generic-GQA path.
4074    ///
4075    /// These five tables are the repo's dominant bug shape in its purest
4076    /// form: five lists of strings that have to agree with a sixth
4077    /// structure (`capability::architecture_catalog`) about what an
4078    /// architecture is called, with nothing checking it. A typo, a
4079    /// hyphen where the GGUF has an underscore, or a name that later
4080    /// moves to a dedicated stack all produce the same thing -- an entry
4081    /// that reads as coverage and can never fire. This repo has shipped
4082    /// exactly that once already, in `unsupported_feature_keys`, keyed
4083    /// on a GGUF spelling no converter writes.
4084    ///
4085    /// The generic-path check is the second half and the sharper one: a
4086    /// behaviour flag on an architecture that is `DedicatedOnly` or
4087    /// `Deferred` never reaches this loader, so it is dead text.
4088    ///
4089    /// Sabotage to confirm: add `"seedoss"` to any list below.
4090    #[test]
4091    fn every_architecture_keyed_behaviour_table_names_a_real_generic_row() {
4092        let tables: &[(&str, &[&str])] = &[
4093            ("SIGMOID_GATING_ARCHITECTURES", SIGMOID_GATING_ARCHITECTURES),
4094            ("GATING_LITERAL_ARCHITECTURES", GATING_LITERAL_NAMES),
4095            ("EXPERT_WEIGHTS_SCALE_READERS", EXPERT_WEIGHTS_SCALE_READERS),
4096            ("EXPERT_WEIGHTS_NORM_READERS", EXPERT_WEIGHTS_NORM_READERS),
4097            (
4098                "NO_TOPK_RENORMALIZE_ARCHITECTURES",
4099                NO_TOPK_RENORMALIZE_ARCHITECTURES,
4100            ),
4101            (
4102                "PRE_FFN_NORM_IS_POST_ATTENTION_NORM",
4103                crate::norm_sites::PRE_FFN_NORM_IS_POST_ATTENTION_NORM,
4104            ),
4105            (
4106                "PRE_FFN_NORM_IS_ATTN_OUTPUT_NORM",
4107                crate::norm_sites::PRE_FFN_NORM_IS_ATTN_OUTPUT_NORM,
4108            ),
4109            (
4110                "POST_NORMS_UNDER_GROK_NAMES",
4111                crate::norm_sites::POST_NORMS_UNDER_GROK_NAMES,
4112            ),
4113            (
4114                "ATTN_NORM_2_FEEDS_ATTENTION",
4115                crate::norm_sites::ATTN_NORM_2_FEEDS_ATTENTION,
4116            ),
4117            ("LEADING_DENSE_KEY_IS_INERT", LEADING_DENSE_KEY_IS_INERT),
4118            (
4119                "QK_NORM_AFTER_ROPE_ARCHITECTURES",
4120                QK_NORM_AFTER_ROPE_ARCHITECTURES,
4121            ),
4122        ];
4123        for (table, names) in tables {
4124            for arch in *names {
4125                let profile = crate::capability::resolve_profile(arch).unwrap_or_else(|| {
4126                    panic!("{table} names `{arch}`, which the catalog does not have")
4127                });
4128                if matches!(profile.path, crate::capability::ArchPath::GenericGqa { .. }) {
4129                    continue;
4130                }
4131                // Not a generic row, so the entry cannot fire HERE.
4132                // That is allowed only when something else is named as
4133                // applying the behaviour instead. An unexplained dead
4134                // entry still fails, which is the whole point.
4135                let owner = DEDICATED_OWNS_ITS_BEHAVIOUR
4136                    .iter()
4137                    .find(|(name, _)| name == arch)
4138                    .map(|(_, owner)| *owner);
4139                assert!(
4140                    owner.is_some(),
4141                    "{table} names `{arch}`, which resolves to {:?} and never reaches this \
4142                     loader, so the entry cannot fire. Either drop it, or add it to \
4143                     DEDICATED_OWNS_ITS_BEHAVIOUR naming what applies the behaviour instead",
4144                    profile.path
4145                );
4146            }
4147        }
4148    }
4149
4150    /// The three tables that describe how a layer is BUILT, rather than
4151    /// how it is routed, only carry architectures that are audited.
4152    ///
4153    /// The distinction matters and is not pedantry. A routing default
4154    /// (`SIGMOID_GATING_ARCHITECTURES`, `NO_TOPK_RENORMALIZE_ARCHITECTURES`)
4155    /// is allowed to name an architecture that still refuses: it is
4156    /// written down ahead of time so a later admission inherits the
4157    /// right answer, and the tables say so. But the three below change
4158    /// which TENSOR a layer reads and in what order -- and each was
4159    /// added for exactly one architecture, whose fixture is the only
4160    /// thing proving the change is right. A fourth name appearing here
4161    /// without evidence would be a claim about a graph nobody read,
4162    /// carried by a list whose doc comment cites two.
4163    #[test]
4164    fn the_layer_shape_tables_only_name_audited_architectures() {
4165        for (table, names) in [
4166            (
4167                "PRE_FFN_NORM_IS_POST_ATTENTION_NORM",
4168                crate::norm_sites::PRE_FFN_NORM_IS_POST_ATTENTION_NORM,
4169            ),
4170            (
4171                "PRE_FFN_NORM_IS_ATTN_OUTPUT_NORM",
4172                crate::norm_sites::PRE_FFN_NORM_IS_ATTN_OUTPUT_NORM,
4173            ),
4174            (
4175                "POST_NORMS_UNDER_GROK_NAMES",
4176                crate::norm_sites::POST_NORMS_UNDER_GROK_NAMES,
4177            ),
4178            (
4179                "ATTN_NORM_2_FEEDS_ATTENTION",
4180                crate::norm_sites::ATTN_NORM_2_FEEDS_ATTENTION,
4181            ),
4182            ("LEADING_DENSE_KEY_IS_INERT", LEADING_DENSE_KEY_IS_INERT),
4183            (
4184                "QK_NORM_AFTER_ROPE_ARCHITECTURES",
4185                QK_NORM_AFTER_ROPE_ARCHITECTURES,
4186            ),
4187        ] {
4188            for arch in names {
4189                assert!(
4190                    crate::capability::is_audited_generic(arch),
4191                    "{table} names `{arch}`, which is not in AUDITED_GENERIC_GQA. Either it \
4192                     has a fixture proving the change is right -- audit it -- or the entry \
4193                     is a guess about a graph"
4194                );
4195            }
4196        }
4197    }
4198
4199    fn config_for_arch(arch: &'static str) -> Result<ModelConfig, LoadError> {
4200        // The per-arch hyperparameter keys are looked up by the arch's
4201        // own prefix, so they have to be built for the arch under test.
4202        let keys: Vec<String> = [
4203            "block_count",
4204            "embedding_length",
4205            "attention.head_count",
4206            "attention.head_count_kv",
4207            "attention.key_length",
4208        ]
4209        .iter()
4210        .map(|k| format!("{arch}.{k}"))
4211        .collect();
4212        let theta = format!("{arch}.rope.freq_base");
4213        let kvs: Vec<(&str, Kv)> = vec![
4214            ("general.architecture", Kv::Str(arch)),
4215            (keys[0].as_str(), Kv::U32(1)),
4216            (keys[1].as_str(), Kv::U32(64)),
4217            (keys[2].as_str(), Kv::U32(1)),
4218            (keys[3].as_str(), Kv::U32(1)),
4219            (keys[4].as_str(), Kv::U32(64)),
4220            (theta.as_str(), Kv::F32(10_000.0)),
4221        ];
4222        ModelConfig::from_gguf(&open_metadata_gguf(arch, &kvs))
4223    }
4224
4225    /// The generic path is OPT-IN, and this is what proves it.
4226    ///
4227    /// An architecture nobody has checked used to FALL ONTO generic GQA
4228    /// and run. Five did exactly that and computed the wrong thing for
4229    /// the life of the project. The refusal exists; nothing tested it,
4230    /// so a reordering or an unevidenced addition to
4231    /// `AUDITED_GENERIC_GQA` would have gone unnoticed.
4232    #[test]
4233    fn an_unaudited_generic_architecture_refuses_rather_than_guessing() {
4234        // `grovemoe` is on the generic path and is not in the audited
4235        // list. It is the sixth name to hold this slot: `starcoder` was
4236        // first, until an audit found it REQUIRES a fused
4237        // `attn_qkv.bias` and a learned `position_embd` the generic
4238        // decoder has no slot for, so it refuses for a stronger reason;
4239        // then `xverse`, until it was admitted with a libllama-golden
4240        // fixture (`tests/fixture_away_graphs.rs`); then `nanbeige`,
4241        // until the layer loop became `crate::layer_loops`; then
4242        // `talkie`, until `crate::skip_stream`; then `arctic`, until
4243        // `crate::parallel_dense_ffn`. `grovemoe` runs a SECOND expert
4244        // bank (`src/models/grovemoe.cpp:57-59,137-164`) whose upstream
4245        // graph diverges from the reference, and its blocker is
4246        // invisible in metadata, so nothing but this gate stops it.
4247        assert!(
4248            !crate::capability::is_audited_generic("grovemoe"),
4249            "this test needs an arch that is generic AND unaudited"
4250        );
4251        match config_for_arch("grovemoe") {
4252            Err(LoadError::UnauditedArchitecture(name, ..)) => assert_eq!(name, "grovemoe"),
4253            other => panic!("expected an unaudited refusal, got {other:?}"),
4254        }
4255    }
4256
4257    /// An architecture with evidence still loads, or the inversion would
4258    /// have turned every model off.
4259    #[test]
4260    fn an_audited_architecture_still_loads() {
4261        assert!(crate::capability::is_audited_generic("llama"));
4262        assert!(config_for_arch("llama").is_ok());
4263    }
4264
4265    /// A NAMED problem must outrank "unaudited".
4266    ///
4267    /// `grovemoe` is unaudited AND names its second expert bank; a
4268    /// `llama4` file declaring a window of zero names the branch
4269    /// libllama aborts on (`crate::chunked_swa`), and that is what its
4270    /// refusal should say. Reporting "unaudited" instead would be true
4271    /// and far less useful, and it is the ordering the loader's own
4272    /// comment claims. Nothing checked that claim. (`gpt2`, `bloom` and
4273    /// then a plain `llama4` were the example until each was served.)
4274    #[test]
4275    fn a_named_refusal_outranks_the_unaudited_one() {
4276        let kvs: Vec<(&str, Kv)> = vec![
4277            ("general.architecture", Kv::Str("llama4")),
4278            ("llama4.block_count", Kv::U32(1)),
4279            ("llama4.embedding_length", Kv::U32(64)),
4280            ("llama4.attention.head_count", Kv::U32(1)),
4281            ("llama4.attention.head_count_kv", Kv::U32(1)),
4282            ("llama4.attention.key_length", Kv::U32(64)),
4283            ("llama4.rope.freq_base", Kv::F32(10_000.0)),
4284            ("llama4.expert_count", Kv::U32(16)),
4285            ("llama4.interleave_moe_layer_step", Kv::U32(1)),
4286            ("llama4.attention.sliding_window", Kv::U32(0)),
4287        ];
4288        let err = ModelConfig::from_gguf(&open_metadata_gguf("llama4", &kvs))
4289            .expect_err("a zero window must refuse");
4290        assert!(
4291            !matches!(err, LoadError::UnauditedArchitecture(..)),
4292            "llama4 should report its own reason, not that nobody audited it: {err:?}"
4293        );
4294        assert!(err.to_string().contains("llama-graph.cpp:159"), "{err}");
4295    }
4296
4297    /// A checkpoint that declares YaRN gets the per-band divisors the
4298    /// reference's `"yarn"` arm implies, folded into `rope_freqs` so the
4299    /// existing RoPE kernels apply them. Expected values are hand-derived
4300    /// from `_find_correction_dim` for this fixture (rotary width 64,
4301    /// base 10000, original context 131072): `low = 22`, `high = 35`.
4302    ///
4303    /// Before this, frink read neither `rope.scaling.type` nor
4304    /// `rope.scaling.factor`, so this file roped exactly like an
4305    /// unscaled one -- correct near position 0, progressively wrong
4306    /// further in.
4307    #[test]
4308    fn a_gguf_declaring_yarn_gets_its_rope_frequencies_rewritten() {
4309        let cfg = llama_config_with(
4310            "yarn",
4311            &[
4312                ("llama.rope.scaling.type", Kv::Str("yarn")),
4313                ("llama.rope.scaling.factor", Kv::F32(8.0)),
4314                (
4315                    "llama.rope.scaling.original_context_length",
4316                    Kv::U32(131_072),
4317                ),
4318            ],
4319        );
4320        let factors = cfg
4321            .rope_freqs
4322            .expect("a YaRN checkpoint must carry rewritten per-band frequencies")
4323            .full;
4324        assert_eq!(factors.len(), 32, "one divisor per rotation band");
4325        assert!(
4326            (factors[0] - 1.0).abs() < 1e-6,
4327            "the fastest band is left extrapolated, got {}",
4328            factors[0]
4329        );
4330        let ramp = (31.0 - 22.0) / (35.0 - 22.0);
4331        let want = 1.0 / (ramp / 8.0 + (1.0 - ramp));
4332        assert!(
4333            (factors[31] - want).abs() < 1e-4,
4334            "slowest band: got {}, reference {want}",
4335            factors[31]
4336        );
4337    }
4338
4339    /// The rewrite must not fire on a file that did not ask for it. A
4340    /// scaling type frink does not implement (`linear`, `longrope`) is
4341    /// left exactly as it was rather than being roped as YaRN, which
4342    /// would be a new kind of wrong rather than the current known one.
4343    /// `rope.scaling.type = "linear"` must actually scale.
4344    ///
4345    /// Rotating position `p/s` is the same as rotating `p` with every
4346    /// band's frequency divided by `s`, and `rope_freqs` is exactly a
4347    /// per-band frequency divisor, so a uniform vector of `s` expresses
4348    /// linear scaling with no new code on the RoPE paths.
4349    ///
4350    /// Before this, the scaling type was compared against "yarn" and
4351    /// anything else returned None, so such a file loaded and roped at
4352    /// unscaled positions: a different model, no error.
4353    #[test]
4354    fn linear_scaling_is_applied_as_a_uniform_frequency_divisor() {
4355        let cfg = llama_config_with(
4356            "linear",
4357            &[
4358                ("llama.rope.scaling.type", Kv::Str("linear")),
4359                ("llama.rope.scaling.factor", Kv::F32(4.0)),
4360            ],
4361        );
4362        let freqs = &cfg
4363            .rope_freqs
4364            .as_ref()
4365            .expect("linear scaling must produce frequency factors")
4366            .full;
4367        assert_eq!(freqs.len(), cfg.head_dim / 2, "one factor per rotated pair");
4368        assert!(
4369            freqs.iter().all(|f| (*f - 4.0).abs() < 1e-6),
4370            "linear scaling is uniform across bands, unlike YaRN: got {freqs:?}"
4371        );
4372    }
4373
4374    /// A factor that corrects nothing is not a correction.
4375    #[test]
4376    fn a_linear_factor_of_one_is_treated_as_absent() {
4377        assert!(llama_config_with(
4378            "linear_one",
4379            &[
4380                ("llama.rope.scaling.type", Kv::Str("linear")),
4381                ("llama.rope.scaling.factor", Kv::F32(1.0)),
4382            ],
4383        )
4384        .rope_freqs
4385        .is_none());
4386    }
4387
4388    #[test]
4389    fn a_gguf_without_yarn_scaling_keeps_its_rope_frequencies_untouched() {
4390        assert!(llama_config_with("noscale", &[]).rope_freqs.is_none());
4391        // Linear scaling is NOT "no scaling". It used to land here,
4392        // asserted as `is_none()`, on the reasoning that leaving
4393        // positions alone beat roping them wrong in a new way. Both are
4394        // wrong output: llama.cpp divides the positions by the factor.
4395        // See `linear_scaling_is_applied_as_a_uniform_frequency_divisor`.
4396        // YaRN with a no-op factor is not a correction either.
4397        assert!(llama_config_with(
4398            "yarn_factor_one",
4399            &[
4400                ("llama.rope.scaling.type", Kv::Str("yarn")),
4401                ("llama.rope.scaling.factor", Kv::F32(1.0)),
4402                (
4403                    "llama.rope.scaling.original_context_length",
4404                    Kv::U32(131_072),
4405                ),
4406            ],
4407        )
4408        .rope_freqs
4409        .is_none());
4410    }
4411
4412    /// The correction range is measured against the context the
4413    /// checkpoint was *trained* at, so a file that declares YaRN without
4414    /// `rope.scaling.original_context_length` leaves the rotation alone
4415    /// rather than inventing a trained length (`context_length` on such
4416    /// a file is the *extended* one, which would put the ramp in the
4417    /// wrong place at every band).
4418    #[test]
4419    fn yarn_without_an_original_context_length_is_not_guessed_at() {
4420        let cfg = llama_config_with(
4421            "yarn_noctx",
4422            &[
4423                ("llama.rope.scaling.type", Kv::Str("yarn")),
4424                ("llama.rope.scaling.factor", Kv::F32(8.0)),
4425            ],
4426        );
4427        assert!(cfg.rope_freqs.is_none());
4428    }
4429
4430    /// `general.sampling.*` is the checkpoint's own recommendation, and
4431    /// only the keys the file carries become one: a file naming just
4432    /// `top_k` must leave temperature and top_p to the server's
4433    /// defaults.
4434    #[test]
4435    fn gguf_sampling_metadata_is_read_as_the_checkpoints_recommendation() {
4436        use crate::sampling::RecommendedSampling;
4437        let full = RecommendedSampling::from_gguf(&open_metadata_gguf(
4438            "sampling_full",
4439            &[
4440                ("general.architecture", Kv::Str("llama")),
4441                ("general.sampling.temp", Kv::F32(1.0)),
4442                ("general.sampling.top_k", Kv::U32(20)),
4443                ("general.sampling.top_p", Kv::F32(0.95)),
4444            ],
4445        ));
4446        assert_eq!(
4447            full,
4448            RecommendedSampling {
4449                temperature: Some(1.0),
4450                top_p: Some(0.95),
4451                top_k: Some(20),
4452            }
4453        );
4454
4455        let partial = RecommendedSampling::from_gguf(&open_metadata_gguf(
4456            "sampling_partial",
4457            &[
4458                ("general.architecture", Kv::Str("llama")),
4459                ("general.sampling.top_k", Kv::U32(40)),
4460            ],
4461        ));
4462        assert_eq!(partial.top_k, Some(40));
4463        assert_eq!(partial.temperature, None);
4464        assert_eq!(partial.top_p, None);
4465    }
4466
4467    /// A converter that wrote `temp = 1` stores a GGUF integer, not a
4468    /// float. Dropping it would serve a checkpoint that asked for
4469    /// temperature 1.0 at the framework's greedy default -- the
4470    /// repetition-loop failure the recommendation exists to prevent.
4471    #[test]
4472    fn an_integer_valued_sampling_temp_is_still_a_recommendation() {
4473        let recommended = crate::sampling::RecommendedSampling::from_gguf(&open_metadata_gguf(
4474            "sampling_int_temp",
4475            &[
4476                ("general.architecture", Kv::Str("llama")),
4477                ("general.sampling.temp", Kv::U32(1)),
4478            ],
4479        ));
4480        assert_eq!(recommended.temperature, Some(1.0));
4481    }
4482
4483    /// The overwhelming majority of checkpoints recommend nothing, and
4484    /// those must keep frink's existing defaults exactly.
4485    #[test]
4486    fn a_gguf_without_sampling_metadata_recommends_nothing() {
4487        let recommended = crate::sampling::RecommendedSampling::from_gguf(&open_metadata_gguf(
4488            "sampling_absent",
4489            &[("general.architecture", Kv::Str("llama"))],
4490        ));
4491        assert!(recommended.is_empty());
4492    }
4493
4494    #[test]
4495    fn model_config_from_gguf_rejects_dedicated_architectures() {
4496        let tmp = std::env::temp_dir().join(format!(
4497            "frink_test_dedicated_arch_{}.gguf",
4498            std::process::id()
4499        ));
4500        std::fs::write(&tmp, build_arch_only_gguf("deepseek4")).unwrap();
4501        let file = frink_gguf::GgufFile::open(&tmp).expect("minimal header must still parse");
4502        std::fs::remove_file(&tmp).ok();
4503
4504        match ModelConfig::from_gguf(&file) {
4505            Err(LoadError::DedicatedArchitectureRequired(arch, _)) => {
4506                assert_eq!(arch, "deepseek4");
4507            }
4508            other => panic!(
4509                "expected LoadError::DedicatedArchitectureRequired for deepseek4, got {other:?}"
4510            ),
4511        }
4512    }
4513
4514    /// The same Q5_K block bytes cross-validated against an independent
4515    /// Python reference in `frink-quant`'s own tests, reused here for
4516    /// the same full-path proof as the Q6_K test below.
4517    #[rustfmt::skip]
4518    const Q5_K_TEST_BLOCK: [u8; 176] = [
4519        0x66, 0x2a, 0x66, 0x2a, 0x01, 0x01, 0x01, 0x01, 0x4f, 0x4b, 0x10, 0x12, 0x41, 0xe2, 0xc1,
4520        0xb1, 0x72, 0x2f, 0x20, 0x07, 0x31, 0x0c, 0x38, 0xb3, 0x9c, 0xb8, 0xad, 0x2f, 0x9a, 0xea,
4521        0x17, 0xd0, 0xee, 0x93, 0x9e, 0x3e, 0x74, 0xbb, 0x28, 0x18, 0x39, 0x25, 0xb6, 0x09, 0x18,
4522        0x29, 0x1c, 0x1d, 0x29, 0x41, 0x40, 0x0a, 0x74, 0x7d, 0xfd, 0x21, 0xdd, 0x6d, 0x45, 0x73,
4523        0x0e, 0x1e, 0xc0, 0x4a, 0xfc, 0xf3, 0x8e, 0x24, 0x6b, 0x34, 0x7d, 0xbe, 0x94, 0xde, 0x59,
4524        0x7a, 0x35, 0x30, 0x36, 0x0a, 0xf9, 0x4a, 0x9b, 0xa2, 0x26, 0x21, 0xa2, 0xfa, 0xdf, 0x4b,
4525        0x29, 0x64, 0x6f, 0xbb, 0xca, 0x0f, 0x3c, 0xda, 0x20, 0xf4, 0x93, 0x86, 0xab, 0x6e, 0xb9,
4526        0xe5, 0xd5, 0xa0, 0x82, 0xd6, 0x41, 0xff, 0x12, 0xbc, 0x34, 0xbb, 0xab, 0xb8, 0x20, 0x2f,
4527        0xbb, 0x5f, 0x0c, 0x10, 0xcf, 0x49, 0xc5, 0x86, 0x5c, 0xdf, 0xff, 0x78, 0x44, 0x26, 0x3b,
4528        0xc2, 0x23, 0x3d, 0x2b, 0xe9, 0x00, 0x12, 0xf8, 0xea, 0xe2, 0x9e, 0x5e, 0x50, 0x20, 0x9f,
4529        0x9d, 0x8d, 0x7d, 0x7f, 0xcc, 0x1d, 0x0e, 0x13, 0xf8, 0xc2, 0xf1, 0x3d, 0x08, 0x2f, 0x23,
4530        0x13, 0xac, 0x0d, 0xa7, 0xe7, 0x20, 0xa3, 0x90, 0xb7, 0xc8, 0x28,
4531    ];
4532
4533    fn build_single_q5_k_tensor_gguf() -> Vec<u8> {
4534        let mut buf = Vec::new();
4535        buf.write_u32::<LittleEndian>(frink_gguf::GGUF_MAGIC)
4536            .unwrap();
4537        buf.write_u32::<LittleEndian>(3).unwrap(); // version
4538        buf.write_u64::<LittleEndian>(1).unwrap(); // tensor_count
4539        buf.write_u64::<LittleEndian>(1).unwrap(); // kv_count
4540
4541        write_kv_str(&mut buf, "general.architecture", "frink-q5k-test");
4542
4543        write_string(&mut buf, "test.weight");
4544        buf.write_u32::<LittleEndian>(2).unwrap(); // n_dims
4545                                                   // Real GGUF ne[] order is fastest-varying-first, i.e. [cols,
4546                                                   // rows] -- reversed from the semantic [rows, cols] this tensor
4547                                                   // represents (1 row, 256 cols / 1 Q5_K block).
4548        buf.write_u64::<LittleEndian>(256).unwrap(); // cols (1 Q5_K block)
4549        buf.write_u64::<LittleEndian>(1).unwrap(); // rows
4550        buf.write_u32::<LittleEndian>(13).unwrap(); // dtype tag: Q5_K
4551        buf.write_u64::<LittleEndian>(0).unwrap(); // offset
4552
4553        while buf.len() % 32 != 0 {
4554            buf.push(0);
4555        }
4556        buf.extend_from_slice(&Q5_K_TEST_BLOCK);
4557        buf
4558    }
4559
4560    /// How far a fused dot may sit from an exact dequantized dot.
4561    ///
4562    /// Two regimes, and one fixed number cannot describe both. With
4563    /// `FRINK_CPU_INT_DOT` off the activation stays f32 and only
4564    /// rounding separates the two. With it on, the activation is
4565    /// quantized to int8 at `d = amax / 127`, which is the flag both
4566    /// binaries turn on by default and the reason the Q5_K and Q6_K
4567    /// cases failed against a flat `1e-2`.
4568    ///
4569    /// The bound grows with the L2 norm of the row, NOT the L1. Each
4570    /// element carries an independent rounding of up to `d/2`, so the
4571    /// dot's error is a sum of independent terms whose standard
4572    /// deviation is `d/sqrt(12) * ||w||_2`. Bounding by the worst case
4573    /// `d/2 * ||w||_1` instead assumes every rounding aligns with its
4574    /// weight's sign, which on this fixture gives 0.347 against a dot
4575    /// of 2.77: 12% of the value, loose enough that injecting a 5%
4576    /// error still passed. Measured here, the real error is 1.8 sigma,
4577    /// so four sigma keeps better than 2x headroom while still failing
4578    /// that 5% injection.
4579    fn fused_dot_tolerance(weights: &[f32], x: &[f32], exact_bound: f32) -> f32 {
4580        if !frink_core::weight_matrix::cpu_int_dot_for(
4581            frink_core::weight_matrix::IntDotShape::Matvec,
4582        ) {
4583            return exact_bound;
4584        }
4585        let amax = x.iter().fold(0.0f32, |a, v| a.max(v.abs()));
4586        let l2 = weights.iter().map(|w| w * w).sum::<f32>().sqrt();
4587        4.0 * (amax / 127.0) / 12f32.sqrt() * l2 + exact_bound
4588    }
4589
4590    #[test]
4591    fn load_weight_matrix_handles_a_real_on_disk_q5_k_tensor_end_to_end() {
4592        let tmp =
4593            std::env::temp_dir().join(format!("frink_test_q5k_tensor_{}.gguf", std::process::id()));
4594        std::fs::write(&tmp, build_single_q5_k_tensor_gguf()).unwrap();
4595        let file = frink_gguf::GgufFile::open(&tmp).expect("real Q5_K GGUF file must parse");
4596        std::fs::remove_file(&tmp).ok();
4597
4598        let matrix = load_weight_matrix(&file, "test.weight").expect("Q5_K tensor must load");
4599        assert_eq!(matrix.rows(), 1);
4600        assert_eq!(matrix.cols(), 256);
4601        match &matrix {
4602            WeightMatrix::Quantized { kind, data, .. } => {
4603                assert_eq!(*kind, QuantKind::Q5K);
4604                assert!(
4605                    data.is_mapped(),
4606                    "Q5_K tensors should take the zero-copy mmap path, same as Q8_0/Q4_0"
4607                );
4608            }
4609            _ => panic!("expected a Quantized matrix for a Q5_K tensor"),
4610        }
4611
4612        let expected = frink_quant::dequant_q5_k(&Q5_K_TEST_BLOCK).unwrap();
4613        let x: Vec<f32> = (0..256).map(|i| ((i as f32) * 0.013).sin()).collect();
4614        let expected_dot: f32 = expected.iter().zip(x.iter()).map(|(a, b)| a * b).sum();
4615
4616        let got = matrix.apply(&x);
4617        assert_eq!(got.len(), 1);
4618        assert!(
4619            (got[0] - expected_dot).abs() < fused_dot_tolerance(&expected, &x, 1e-2),
4620            "end-to-end loaded+applied Q5_K matrix diverged from direct dequant: got={} expected={}",
4621            got[0],
4622            expected_dot
4623        );
4624    }
4625
4626    /// The same Q6_K block bytes cross-validated against an independent
4627    /// Python reference in `frink-quant`'s own tests; reused here to
4628    /// prove the *full*
4629    /// path -- real on-disk GGUF bytes, parsed by `frink-gguf`, read
4630    /// through `GgufFile::tensor_mapped_range`, dispatched by
4631    /// `WeightMatrix::apply` to `frink_quant::dot_q6_k_f32` -- produces
4632    /// the same result as directly dequantizing those bytes, not just
4633    /// that the isolated kernel is correct in unit-test isolation.
4634    #[rustfmt::skip]
4635    const Q6_K_TEST_BLOCK: [u8; 210] = [
4636        0xe0, 0xa5, 0x40, 0x5c, 0x8d, 0x3a, 0x0a, 0x26, 0xfb, 0x4b, 0x6e, 0x9a, 0xdf, 0x3e, 0xa3,
4637        0xc4, 0xf8, 0x2b, 0x1d, 0x95, 0x76, 0x7d, 0x3b, 0xcd, 0xfd, 0xef, 0xc2, 0x0b, 0x07, 0x63,
4638        0x29, 0xfb, 0x81, 0x57, 0xbe, 0xbe, 0x06, 0xf7, 0x3a, 0x92, 0xc4, 0x43, 0xff, 0xad, 0xac,
4639        0x7e, 0x0f, 0x00, 0x2a, 0x4f, 0xf0, 0xf8, 0xa9, 0xfa, 0x3c, 0x90, 0x6d, 0x73, 0x2d, 0x5a,
4640        0xe6, 0xc6, 0x46, 0xf2, 0x0d, 0x55, 0x4c, 0x25, 0x38, 0x71, 0x2b, 0x35, 0x38, 0x82, 0x16,
4641        0x37, 0x5f, 0x32, 0x61, 0x02, 0xdd, 0x2f, 0x6f, 0x7b, 0x1f, 0xb4, 0x1a, 0x1b, 0x3e, 0x4f,
4642        0x11, 0xa3, 0x17, 0x40, 0x5a, 0x5f, 0x76, 0xcd, 0x19, 0x27, 0x9b, 0xc7, 0xc8, 0xf7, 0xf7,
4643        0xee, 0xf4, 0x86, 0xd9, 0xfd, 0xa7, 0xfe, 0x9e, 0xac, 0x70, 0x53, 0x5b, 0x76, 0xfb, 0x39,
4644        0xf8, 0x4b, 0x98, 0xfe, 0xd0, 0x06, 0x21, 0x4c, 0x4d, 0xbe, 0x10, 0x2b, 0x06, 0x65, 0xc9,
4645        0x5e, 0xf9, 0x95, 0x72, 0xae, 0x99, 0xd9, 0x7e, 0x15, 0xbd, 0x5e, 0x6d, 0xe8, 0x25, 0x8a,
4646        0xd5, 0x99, 0xc6, 0x6b, 0x69, 0xc7, 0x84, 0xc6, 0xa4, 0xf7, 0xb9, 0x6d, 0x68, 0x45, 0x0e,
4647        0x65, 0x69, 0xeb, 0xe6, 0xeb, 0xe9, 0x28, 0xa6, 0xb9, 0x96, 0xf2, 0xe8, 0xa7, 0x9b, 0x6e,
4648        0x79, 0x8a, 0x68, 0x65, 0x59, 0x98, 0x8b, 0x44, 0x41, 0x98, 0x9a, 0x56, 0x01, 0x01, 0x01,
4649        0x02, 0x01, 0x01, 0x01, 0x01, 0x02, 0x01, 0x02, 0x02, 0x01, 0x01, 0x01, 0x02, 0x1f, 0x25,
4650    ];
4651
4652    fn build_single_q6_k_tensor_gguf() -> Vec<u8> {
4653        let mut buf = Vec::new();
4654        buf.write_u32::<LittleEndian>(frink_gguf::GGUF_MAGIC)
4655            .unwrap();
4656        buf.write_u32::<LittleEndian>(3).unwrap(); // version
4657        buf.write_u64::<LittleEndian>(1).unwrap(); // tensor_count
4658        buf.write_u64::<LittleEndian>(1).unwrap(); // kv_count
4659
4660        write_kv_str(&mut buf, "general.architecture", "frink-q6k-test");
4661
4662        write_string(&mut buf, "test.weight");
4663        buf.write_u32::<LittleEndian>(2).unwrap(); // n_dims
4664                                                   // Real GGUF ne[] order is fastest-varying-first, i.e. [cols, rows].
4665        buf.write_u64::<LittleEndian>(256).unwrap(); // cols (1 Q6_K block)
4666        buf.write_u64::<LittleEndian>(1).unwrap(); // rows
4667        buf.write_u32::<LittleEndian>(14).unwrap(); // dtype tag: Q6_K
4668        buf.write_u64::<LittleEndian>(0).unwrap(); // offset
4669
4670        while buf.len() % 32 != 0 {
4671            buf.push(0);
4672        }
4673        buf.extend_from_slice(&Q6_K_TEST_BLOCK);
4674        buf
4675    }
4676
4677    #[test]
4678    fn load_weight_matrix_handles_a_real_on_disk_q6_k_tensor_end_to_end() {
4679        let tmp =
4680            std::env::temp_dir().join(format!("frink_test_q6k_tensor_{}.gguf", std::process::id()));
4681        std::fs::write(&tmp, build_single_q6_k_tensor_gguf()).unwrap();
4682        let file = frink_gguf::GgufFile::open(&tmp).expect("real Q6_K GGUF file must parse");
4683        std::fs::remove_file(&tmp).ok();
4684
4685        let matrix = load_weight_matrix(&file, "test.weight").expect("Q6_K tensor must load");
4686        assert_eq!(matrix.rows(), 1);
4687        assert_eq!(matrix.cols(), 256);
4688        match &matrix {
4689            WeightMatrix::Quantized { kind, data, .. } => {
4690                assert_eq!(*kind, QuantKind::Q6K);
4691                assert!(
4692                    data.is_mapped(),
4693                    "Q6_K tensors should take the zero-copy mmap path, same as Q8_0/Q4_0"
4694                );
4695            }
4696            _ => panic!("expected a Quantized matrix for a Q6_K tensor"),
4697        }
4698
4699        let expected = frink_quant::dequant_q6_k(&Q6_K_TEST_BLOCK).unwrap();
4700        let x: Vec<f32> = (0..256).map(|i| ((i as f32) * 0.013).sin()).collect();
4701        let expected_dot: f32 = expected.iter().zip(x.iter()).map(|(a, b)| a * b).sum();
4702
4703        let got = matrix.apply(&x);
4704        assert_eq!(got.len(), 1);
4705        assert!(
4706            (got[0] - expected_dot).abs() < fused_dot_tolerance(&expected, &x, 1e-2),
4707            "end-to-end loaded+applied Q6_K matrix diverged from direct dequant: got={} expected={}",
4708            got[0],
4709            expected_dot
4710        );
4711    }
4712
4713    fn build_single_bf16_tensor_gguf(rows: u64, cols: u64, values: &[f32]) -> Vec<u8> {
4714        let mut buf = Vec::new();
4715        buf.write_u32::<LittleEndian>(frink_gguf::GGUF_MAGIC)
4716            .unwrap();
4717        buf.write_u32::<LittleEndian>(3).unwrap(); // version
4718        buf.write_u64::<LittleEndian>(1).unwrap(); // tensor_count
4719        buf.write_u64::<LittleEndian>(1).unwrap(); // kv_count
4720
4721        write_kv_str(&mut buf, "general.architecture", "frink-bf16-test");
4722
4723        write_string(&mut buf, "test.weight");
4724        buf.write_u32::<LittleEndian>(2).unwrap(); // n_dims
4725                                                   // Real GGUF ne[] order is fastest-varying-first, i.e. [cols, rows].
4726        buf.write_u64::<LittleEndian>(cols).unwrap();
4727        buf.write_u64::<LittleEndian>(rows).unwrap();
4728        buf.write_u32::<LittleEndian>(30).unwrap(); // dtype tag: BF16
4729        buf.write_u64::<LittleEndian>(0).unwrap(); // offset
4730
4731        while buf.len() % 32 != 0 {
4732            buf.push(0);
4733        }
4734        for &v in values {
4735            // Real bf16 truncation (round-toward-zero, matching a real
4736            // writer closely enough for round-trip test purposes): top
4737            // 16 bits of the f32 bit pattern.
4738            let bf16_bits = (v.to_bits() >> 16) as u16;
4739            buf.extend_from_slice(&bf16_bits.to_le_bytes());
4740        }
4741        buf
4742    }
4743
4744    #[test]
4745    fn load_weight_matrix_handles_a_real_on_disk_bf16_tensor_end_to_end() {
4746        // Values with zero low-mantissa bits, so f32->bf16 truncation
4747        // is lossless and this is an exact-equality check.
4748        let values: Vec<f32> = vec![1.0, -2.5, 0.0, 4.0, -8.0, 16.0];
4749        let tmp = std::env::temp_dir().join(format!(
4750            "frink_test_bf16_tensor_{}.gguf",
4751            std::process::id()
4752        ));
4753        std::fs::write(&tmp, build_single_bf16_tensor_gguf(2, 3, &values)).unwrap();
4754        let file = frink_gguf::GgufFile::open(&tmp).expect("real BF16 GGUF file must parse");
4755        std::fs::remove_file(&tmp).ok();
4756
4757        let matrix = load_weight_matrix(&file, "test.weight").expect("BF16 tensor must load");
4758        assert_eq!(matrix.rows(), 2);
4759        assert_eq!(matrix.cols(), 3);
4760        match &matrix {
4761            WeightMatrix::F32(tensor) => {
4762                assert_eq!(tensor.data, values, "BF16 must widen to f32 exactly");
4763            }
4764            _ => panic!("expected an F32 matrix for a BF16 tensor (no fused dot kernel for it)"),
4765        }
4766    }
4767
4768    fn build_single_f16_tensor_gguf(rows: u64, cols: u64, values: &[f32]) -> Vec<u8> {
4769        let mut buf = Vec::new();
4770        buf.write_u32::<LittleEndian>(frink_gguf::GGUF_MAGIC)
4771            .unwrap();
4772        buf.write_u32::<LittleEndian>(3).unwrap(); // version
4773        buf.write_u64::<LittleEndian>(1).unwrap(); // tensor_count
4774        buf.write_u64::<LittleEndian>(1).unwrap(); // kv_count
4775
4776        write_kv_str(&mut buf, "general.architecture", "frink-f16-test");
4777
4778        write_string(&mut buf, "test.weight");
4779        buf.write_u32::<LittleEndian>(2).unwrap(); // n_dims
4780        buf.write_u64::<LittleEndian>(cols).unwrap();
4781        buf.write_u64::<LittleEndian>(rows).unwrap();
4782        buf.write_u32::<LittleEndian>(1).unwrap(); // dtype tag: F16
4783        buf.write_u64::<LittleEndian>(0).unwrap(); // offset
4784
4785        while buf.len() % 32 != 0 {
4786            buf.push(0);
4787        }
4788        for &v in values {
4789            buf.extend_from_slice(&half::f16::from_f32(v).to_le_bytes());
4790        }
4791        buf
4792    }
4793
4794    /// `GgmlType::F16` was parsed and sized but had no dequant arm in any
4795    /// of the seven loaders, so every `*-f16.gguf` was a hard
4796    /// `UnsupportedDtype`. Values are exactly representable in f16, so
4797    /// this is an exact-equality check.
4798    #[test]
4799    fn load_weight_matrix_handles_a_real_on_disk_f16_tensor_end_to_end() {
4800        let values: Vec<f32> = vec![1.0, -2.5, 0.0, 4.0, -8.0, 16.0];
4801        let tmp =
4802            std::env::temp_dir().join(format!("frink_test_f16_tensor_{}.gguf", std::process::id()));
4803        std::fs::write(&tmp, build_single_f16_tensor_gguf(2, 3, &values)).unwrap();
4804        let file = frink_gguf::GgufFile::open(&tmp).expect("real F16 GGUF file must parse");
4805        std::fs::remove_file(&tmp).ok();
4806
4807        let matrix = load_weight_matrix(&file, "test.weight").expect("F16 tensor must load");
4808        assert_eq!(matrix.rows(), 2);
4809        assert_eq!(matrix.cols(), 3);
4810        match &matrix {
4811            WeightMatrix::F32(tensor) => {
4812                assert_eq!(tensor.data, values, "F16 must widen to f32 exactly");
4813            }
4814            _ => panic!("expected an F32 matrix for an F16 tensor (no fused dot kernel for it)"),
4815        }
4816
4817        // The same tensor read as a plain vector (norm weights, biases and
4818        // the router all take this path, not `load_weight_matrix`).
4819        let tmp =
4820            std::env::temp_dir().join(format!("frink_test_f16_vec_{}.gguf", std::process::id()));
4821        std::fs::write(&tmp, build_single_f16_tensor_gguf(2, 3, &values)).unwrap();
4822        let file = frink_gguf::GgufFile::open(&tmp).expect("real F16 GGUF file must parse");
4823        std::fs::remove_file(&tmp).ok();
4824        assert_eq!(load_f32_vec(&file, "test.weight").unwrap(), values);
4825    }
4826
4827    fn build_single_q5_1_tensor_gguf() -> Vec<u8> {
4828        let mut buf = Vec::new();
4829        buf.write_u32::<LittleEndian>(frink_gguf::GGUF_MAGIC)
4830            .unwrap();
4831        buf.write_u32::<LittleEndian>(3).unwrap(); // version
4832        buf.write_u64::<LittleEndian>(1).unwrap(); // tensor_count
4833        buf.write_u64::<LittleEndian>(1).unwrap(); // kv_count
4834
4835        write_kv_str(&mut buf, "general.architecture", "frink-q5-1-test");
4836
4837        write_string(&mut buf, "test.weight");
4838        buf.write_u32::<LittleEndian>(2).unwrap(); // n_dims
4839                                                   // Real GGUF ne[] order is fastest-varying-first, i.e. [cols, rows].
4840        buf.write_u64::<LittleEndian>(32).unwrap(); // cols (1 Q5_1 block)
4841        buf.write_u64::<LittleEndian>(1).unwrap(); // rows
4842        buf.write_u32::<LittleEndian>(7).unwrap(); // dtype tag: Q5_1
4843        buf.write_u64::<LittleEndian>(0).unwrap(); // offset
4844
4845        while buf.len() % 32 != 0 {
4846            buf.push(0);
4847        }
4848        // d=0.25 (f16 0x3400), m=1.5 (f16 0x3E00) -- both exact in f16,
4849        // hand-verified bit patterns to avoid pulling in the `half`
4850        // crate just for two test constants. qh varied, qs a real
4851        // (non-degenerate) pattern.
4852        buf.extend_from_slice(&0x3400u16.to_le_bytes());
4853        buf.extend_from_slice(&0x3E00u16.to_le_bytes());
4854        buf.extend_from_slice(&[0x9au8, 0x3c, 0xf0, 0x0f]);
4855        buf.extend_from_slice(&(0..16u8).map(|i| i | ((15 - i) << 4)).collect::<Vec<u8>>());
4856        buf
4857    }
4858
4859    #[test]
4860    fn load_weight_matrix_handles_a_real_on_disk_q5_1_tensor_end_to_end() {
4861        let tmp = std::env::temp_dir().join(format!(
4862            "frink_test_q5_1_tensor_{}.gguf",
4863            std::process::id()
4864        ));
4865        std::fs::write(&tmp, build_single_q5_1_tensor_gguf()).unwrap();
4866        let file = frink_gguf::GgufFile::open(&tmp).expect("real Q5_1 GGUF file must parse");
4867        std::fs::remove_file(&tmp).ok();
4868
4869        let matrix = load_weight_matrix(&file, "test.weight").expect("Q5_1 tensor must load");
4870        assert_eq!(matrix.rows(), 1);
4871        assert_eq!(matrix.cols(), 32);
4872        let raw = file.tensor_bytes("test.weight").unwrap();
4873        let expected = frink_quant::dequant_q5_1(raw).unwrap();
4874        match &matrix {
4875            WeightMatrix::Quantized { kind, data, .. } => {
4876                assert_eq!(*kind, QuantKind::Q5_1);
4877                assert!(data.is_mapped());
4878            }
4879            _ => panic!("expected a Quantized matrix for a Q5_1 tensor"),
4880        }
4881
4882        let x: Vec<f32> = (0..32).map(|i| ((i as f32) * 0.017).cos()).collect();
4883        let expected_dot: f32 = expected.iter().zip(x.iter()).map(|(a, b)| a * b).sum();
4884        let got = matrix.apply(&x);
4885        assert_eq!(got.len(), 1);
4886        assert!(
4887            (got[0] - expected_dot).abs() < 1e-2,
4888            "end-to-end loaded+applied Q5_1 matrix diverged from direct dequant: got={} expected={}",
4889            got[0],
4890            expected_dot
4891        );
4892    }
4893
4894    // Same bytes as frink-quant's own Q3_K_TEST_BLOCK (Python-cross-
4895    // validated there); duplicated here to build a real on-disk GGUF
4896    // file, matching this file's existing per-format test convention
4897    // (see Q6_K_TEST_BLOCK above).
4898    const Q3_K_TEST_BLOCK: [u8; 110] = [
4899        0x56, 0xf2, 0xb4, 0x2b, 0xd5, 0x6f, 0x51, 0x71, 0x3c, 0x0a, 0xb9, 0x1d, 0xd0, 0xb9, 0x3b,
4900        0xb3, 0x0f, 0xff, 0x8c, 0xb2, 0x83, 0x3a, 0x3d, 0x24, 0xb1, 0x12, 0x56, 0xe3, 0x23, 0x54,
4901        0xf2, 0xfa, 0x7f, 0xdf, 0x31, 0xe1, 0x18, 0x26, 0x6e, 0xcd, 0x5b, 0x38, 0xee, 0xbd, 0x9f,
4902        0x8c, 0x57, 0x47, 0x0b, 0x11, 0xcb, 0xfb, 0xb4, 0x83, 0xa0, 0x4e, 0x0b, 0xd4, 0xa7, 0x85,
4903        0xe0, 0x60, 0xf3, 0xb3, 0xe3, 0x95, 0x43, 0xc6, 0x05, 0x05, 0x77, 0x53, 0xed, 0x23, 0xcc,
4904        0x6a, 0x0e, 0x89, 0xa1, 0x79, 0x85, 0xf6, 0x6e, 0x5a, 0x23, 0x63, 0xbe, 0x53, 0xfa, 0xa2,
4905        0x2b, 0xe9, 0xcd, 0xce, 0xf8, 0x3d, 0x6f, 0xd0, 0x42, 0x6e, 0x3b, 0x7f, 0x23, 0x26, 0xd3,
4906        0xb9, 0x18, 0xbf, 0xa4, 0x34,
4907    ];
4908
4909    fn build_single_q3_k_tensor_gguf() -> Vec<u8> {
4910        let mut buf = Vec::new();
4911        buf.write_u32::<LittleEndian>(frink_gguf::GGUF_MAGIC)
4912            .unwrap();
4913        buf.write_u32::<LittleEndian>(3).unwrap(); // version
4914        buf.write_u64::<LittleEndian>(1).unwrap(); // tensor_count
4915        buf.write_u64::<LittleEndian>(1).unwrap(); // kv_count
4916
4917        write_kv_str(&mut buf, "general.architecture", "frink-q3k-test");
4918
4919        write_string(&mut buf, "test.weight");
4920        buf.write_u32::<LittleEndian>(2).unwrap(); // n_dims
4921                                                   // Real GGUF ne[] order is fastest-varying-first, i.e. [cols, rows].
4922        buf.write_u64::<LittleEndian>(256).unwrap(); // cols (1 Q3_K block)
4923        buf.write_u64::<LittleEndian>(1).unwrap(); // rows
4924        buf.write_u32::<LittleEndian>(11).unwrap(); // dtype tag: Q3_K
4925        buf.write_u64::<LittleEndian>(0).unwrap(); // offset
4926
4927        while buf.len() % 32 != 0 {
4928            buf.push(0);
4929        }
4930        buf.extend_from_slice(&Q3_K_TEST_BLOCK);
4931        buf
4932    }
4933
4934    #[test]
4935    fn load_weight_matrix_handles_a_real_on_disk_q3_k_tensor_end_to_end() {
4936        let tmp =
4937            std::env::temp_dir().join(format!("frink_test_q3k_tensor_{}.gguf", std::process::id()));
4938        std::fs::write(&tmp, build_single_q3_k_tensor_gguf()).unwrap();
4939        let file = frink_gguf::GgufFile::open(&tmp).expect("real Q3_K GGUF file must parse");
4940        std::fs::remove_file(&tmp).ok();
4941
4942        let matrix = load_weight_matrix(&file, "test.weight").expect("Q3_K tensor must load");
4943        assert_eq!(matrix.rows(), 1);
4944        assert_eq!(matrix.cols(), 256);
4945        match &matrix {
4946            WeightMatrix::Quantized { kind, data, .. } => {
4947                assert_eq!(*kind, QuantKind::Q3K);
4948                assert!(data.is_mapped());
4949            }
4950            _ => panic!("expected a Quantized matrix for a Q3_K tensor"),
4951        }
4952
4953        let expected = frink_quant::dequant_q3_k(&Q3_K_TEST_BLOCK).unwrap();
4954        let x: Vec<f32> = (0..256).map(|i| ((i as f32) * 0.013).sin()).collect();
4955        let expected_dot: f32 = expected.iter().zip(x.iter()).map(|(a, b)| a * b).sum();
4956
4957        let got = matrix.apply(&x);
4958        assert_eq!(got.len(), 1);
4959        assert!(
4960            (got[0] - expected_dot).abs() < fused_dot_tolerance(&expected, &x, 1e-1),
4961            "end-to-end loaded+applied Q3_K matrix diverged from direct dequant: got={} expected={}",
4962            got[0],
4963            expected_dot
4964        );
4965    }
4966
4967    // Same bytes as frink-quant's own IQ4_XS_TEST_BLOCK (Python-cross-
4968    // validated there); duplicated here to build a real on-disk GGUF
4969    // file, matching this file's existing per-format test convention.
4970    const IQ4_XS_TEST_BLOCK: [u8; 136] = [
4971        0x5c, 0x33, 0xb4, 0x39, 0xd1, 0x64, 0x97, 0x82, 0xcb, 0xbd, 0x88, 0x95, 0xf3, 0x60, 0x2a,
4972        0xb5, 0xe7, 0x24, 0xd3, 0xee, 0xfe, 0x71, 0x13, 0xbe, 0x70, 0x84, 0x48, 0x79, 0x7b, 0x3e,
4973        0xf0, 0x55, 0xdc, 0xb2, 0xb2, 0xde, 0x32, 0xa1, 0x5b, 0x02, 0x01, 0xdc, 0x2a, 0xbb, 0xf7,
4974        0x0b, 0x8a, 0x88, 0xdd, 0x0b, 0x02, 0x7e, 0x5e, 0x76, 0x87, 0x30, 0x1e, 0x1c, 0xcf, 0x48,
4975        0xd7, 0x61, 0xf3, 0x51, 0x52, 0x17, 0x98, 0x0a, 0x87, 0xcf, 0x02, 0x91, 0xc8, 0xee, 0xc0,
4976        0x91, 0x69, 0x2a, 0x4f, 0x64, 0x68, 0xa7, 0xb2, 0xe6, 0x98, 0x21, 0x81, 0x75, 0x53, 0x2a,
4977        0x8d, 0x12, 0xae, 0xe0, 0xea, 0x0c, 0x75, 0xff, 0x22, 0x5e, 0x25, 0x19, 0xda, 0x2e, 0x51,
4978        0x4e, 0x81, 0xdc, 0x0e, 0x78, 0x86, 0xd7, 0x58, 0xb5, 0xb7, 0xf6, 0x45, 0xa9, 0x0a, 0x83,
4979        0xfd, 0x2a, 0x12, 0x7d, 0xf0, 0x12, 0x97, 0xe2, 0xfe, 0xf4, 0xd0, 0xa2, 0x11, 0x14, 0x78,
4980        0xdb,
4981    ];
4982
4983    fn build_single_iq4_xs_tensor_gguf() -> Vec<u8> {
4984        let mut buf = Vec::new();
4985        buf.write_u32::<LittleEndian>(frink_gguf::GGUF_MAGIC)
4986            .unwrap();
4987        buf.write_u32::<LittleEndian>(3).unwrap(); // version
4988        buf.write_u64::<LittleEndian>(1).unwrap(); // tensor_count
4989        buf.write_u64::<LittleEndian>(1).unwrap(); // kv_count
4990
4991        write_kv_str(&mut buf, "general.architecture", "frink-iq4xs-test");
4992
4993        write_string(&mut buf, "test.weight");
4994        buf.write_u32::<LittleEndian>(2).unwrap(); // n_dims
4995                                                   // Real GGUF ne[] order is fastest-varying-first, i.e. [cols, rows].
4996        buf.write_u64::<LittleEndian>(256).unwrap(); // cols (1 IQ4_XS block)
4997        buf.write_u64::<LittleEndian>(1).unwrap(); // rows
4998        buf.write_u32::<LittleEndian>(23).unwrap(); // dtype tag: IQ4_XS
4999        buf.write_u64::<LittleEndian>(0).unwrap(); // offset
5000
5001        while buf.len() % 32 != 0 {
5002            buf.push(0);
5003        }
5004        buf.extend_from_slice(&IQ4_XS_TEST_BLOCK);
5005        buf
5006    }
5007
5008    #[test]
5009    fn load_weight_matrix_handles_a_real_on_disk_iq4_xs_tensor_end_to_end() {
5010        let tmp = std::env::temp_dir().join(format!(
5011            "frink_test_iq4xs_tensor_{}.gguf",
5012            std::process::id()
5013        ));
5014        std::fs::write(&tmp, build_single_iq4_xs_tensor_gguf()).unwrap();
5015        let file = frink_gguf::GgufFile::open(&tmp).expect("real IQ4_XS GGUF file must parse");
5016        std::fs::remove_file(&tmp).ok();
5017
5018        let matrix = load_weight_matrix(&file, "test.weight").expect("IQ4_XS tensor must load");
5019        assert_eq!(matrix.rows(), 1);
5020        assert_eq!(matrix.cols(), 256);
5021        match &matrix {
5022            WeightMatrix::Quantized { kind, data, .. } => {
5023                assert_eq!(*kind, QuantKind::IQ4XS);
5024                assert!(data.is_mapped());
5025            }
5026            _ => panic!("expected a Quantized matrix for an IQ4_XS tensor"),
5027        }
5028
5029        let expected = frink_quant::dequant_iq4_xs(&IQ4_XS_TEST_BLOCK).unwrap();
5030        let x: Vec<f32> = (0..256).map(|i| ((i as f32) * 0.013).sin()).collect();
5031        let expected_dot: f32 = expected.iter().zip(x.iter()).map(|(a, b)| a * b).sum();
5032
5033        let got = matrix.apply(&x);
5034        assert_eq!(got.len(), 1);
5035        assert!(
5036            (got[0] - expected_dot).abs() < 1e-1,
5037            "end-to-end loaded+applied IQ4_XS matrix diverged from direct dequant: got={} expected={}",
5038            got[0],
5039            expected_dot
5040        );
5041    }
5042
5043    // Same bytes as frink-quant's own IQ low-bit test blocks
5044    // (Python-cross-validated there against the real compiled ggml
5045    // implementation), duplicated as literals for the same reason as
5046    // IQ4_XS_TEST_BLOCK above.
5047    const IQ1_S_TEST_BLOCK: [u8; 50] = [
5048        0x0a, 0x2f, 0xfa, 0x06, 0x1e, 0x37, 0x6f, 0xe3, 0x62, 0xd0, 0xb6, 0xa4, 0x25, 0xae, 0x76,
5049        0x14, 0x72, 0x5b, 0xfa, 0x05, 0xd1, 0xf1, 0x2a, 0x4c, 0xad, 0x29, 0xae, 0xf4, 0xcf, 0x0c,
5050        0x96, 0x51, 0x58, 0x03, 0x6d, 0xd3, 0x10, 0x92, 0x70, 0xff, 0x61, 0x58, 0xc8, 0x30, 0x25,
5051        0x64, 0x49, 0x85, 0xc0, 0x24,
5052    ];
5053    const IQ2_XXS_TEST_BLOCK: [u8; 66] = [
5054        0x29, 0x30, 0xd9, 0x33, 0x95, 0x4c, 0x08, 0x1e, 0xad, 0x79, 0x49, 0xf2, 0x8d, 0x5f, 0x93,
5055        0xea, 0x78, 0x18, 0x98, 0xb9, 0x94, 0x14, 0xad, 0xce, 0xca, 0x1d, 0xab, 0x81, 0x53, 0x4a,
5056        0x68, 0xd0, 0x59, 0x96, 0x36, 0x5d, 0xbe, 0x20, 0xc4, 0xff, 0xe4, 0x2c, 0xcd, 0x2f, 0x4f,
5057        0x4f, 0x67, 0x53, 0xc6, 0xd5, 0xa2, 0xfb, 0xc7, 0xf3, 0xe2, 0x6b, 0xf1, 0x99, 0x23, 0x1e,
5058        0x2d, 0x5e, 0x8c, 0x78, 0xc2, 0x31,
5059    ];
5060    const IQ3_XXS_TEST_BLOCK: [u8; 98] = [
5061        0x71, 0x31, 0x16, 0x0a, 0x79, 0x04, 0x5d, 0x87, 0xae, 0x2a, 0x4a, 0x43, 0xfd, 0x02, 0xba,
5062        0x6c, 0x10, 0x42, 0x80, 0xe5, 0x1d, 0x08, 0x22, 0xcb, 0x21, 0x54, 0xf9, 0xaa, 0x8e, 0xc2,
5063        0xf2, 0x34, 0x66, 0x1e, 0x2a, 0xef, 0x19, 0xae, 0x48, 0x47, 0x29, 0xa0, 0x72, 0xd1, 0x31,
5064        0xc0, 0x65, 0x49, 0xde, 0x79, 0x32, 0xe6, 0x4d, 0xb6, 0x55, 0x3f, 0x4d, 0xf1, 0x18, 0xbb,
5065        0x18, 0x59, 0x4c, 0x31, 0xa3, 0xb2, 0x34, 0xdd, 0xf6, 0x4a, 0x91, 0x51, 0x3f, 0x3e, 0x40,
5066        0x69, 0xad, 0xbf, 0x1a, 0xd0, 0x05, 0xfb, 0xbe, 0x8b, 0x0b, 0xdd, 0xdf, 0x7d, 0x94, 0x74,
5067        0x92, 0x3e, 0xff, 0x04, 0x2a, 0xc4, 0xea, 0xc9,
5068    ];
5069
5070    #[rustfmt::skip]
5071    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];
5072
5073    /// A live ggml type this build has no kernel for must be REFUSED BY
5074    /// NAME at execution, having been sized correctly at parse.
5075    ///
5076    /// Before `TQ2_0` was recognized, tag 35 was `Other(35)`, which had
5077    /// no block layout: the tensor's size was unknown, so `tensor_bytes`
5078    /// could not even hand back the row, and the error named a number.
5079    /// Now the file parses, the tensor measures 66 bytes per 256
5080    /// elements, and the stop happens where it belongs -- at the point
5081    /// something wants to multiply by it -- naming `TQ2_0`.
5082    #[test]
5083    fn a_recognized_but_unimplemented_ggml_type_refuses_by_name_after_sizing_correctly() {
5084        // 256 elements of TQ2_0 = one 66-byte block.
5085        let block = pseudo_iq_block(66, 0x0720_5eed);
5086        let tmp =
5087            std::env::temp_dir().join(format!("frink_test_tq2_0_{}.gguf", std::process::id()));
5088        std::fs::write(
5089            &tmp,
5090            build_single_iq_lowbit_tensor_gguf("tq2test", 35, 256, &block),
5091        )
5092        .unwrap();
5093        let file = frink_gguf::GgufFile::open(&tmp).expect("a TQ2_0 file must still parse");
5094        std::fs::remove_file(&tmp).ok();
5095
5096        // Sized, not zero: the size estimate is right even though the
5097        // kernel is missing.
5098        let info = file.find_tensor("test.weight").expect("tensor present");
5099        assert_eq!(info.dtype, GgmlType::TQ2_0);
5100        assert_eq!(info.byte_len(), Some(66));
5101        assert_eq!(
5102            file.tensor_bytes("test.weight").map(<[u8]>::len).ok(),
5103            Some(66)
5104        );
5105
5106        match load_weight_matrix(&file, "test.weight") {
5107            Err(LoadError::UnsupportedDtype(name, GgmlType::TQ2_0)) => {
5108                assert_eq!(name, "test.weight");
5109            }
5110            Err(other) => panic!("TQ2_0 must be refused by name, got {other:?}"),
5111            Ok(_) => panic!("TQ2_0 must be refused, not loaded as some other kind"),
5112        }
5113    }
5114
5115    /// An MXFP4 norm/bias must widen, not be refused.
5116    ///
5117    /// `load_weight_matrix` accepts MXFP4 as a 2-D weight and
5118    /// `load_moe_expert_matrices` accepts it as an expert tensor, and
5119    /// `WeightMatrix::dequant` calls `dequant_mxfp4_gguf` on both. One
5120    /// missing arm in `widen_plain_float` made the *1-D* tensors of the
5121    /// exact same dtype a hard `UnsupportedDtype` -- the split that
5122    /// turns a supported format into a load failure on the one
5123    /// checkpoint that uses it.
5124    #[test]
5125    fn an_mxfp4_one_dimensional_tensor_widens_instead_of_being_refused() {
5126        let expected = frink_quant::dequant_mxfp4_gguf(&MXFP4_GGUF_TEST_BLOCKS)
5127            .expect("the fixture blocks must dequantize");
5128        let cols = expected.len();
5129        let tmp =
5130            std::env::temp_dir().join(format!("frink_test_mxfp4_norm_{}.gguf", std::process::id()));
5131        std::fs::write(
5132            &tmp,
5133            build_single_iq_lowbit_tensor_gguf(
5134                "mxfp4norm",
5135                39,
5136                cols as u64,
5137                &MXFP4_GGUF_TEST_BLOCKS,
5138            ),
5139        )
5140        .unwrap();
5141        let file = frink_gguf::GgufFile::open(&tmp).expect("file must parse");
5142        std::fs::remove_file(&tmp).ok();
5143
5144        let got = load_f32_vec(&file, "test.weight")
5145            .expect("an MXFP4 norm must load, not report an unsupported dtype");
5146        assert_eq!(got, expected);
5147
5148        // Same arm, reached directly: `widen_plain_float` is the shared
5149        // helper the six architecture loaders call, so its table is the
5150        // one that has to know MXFP4.
5151        let direct = widen_plain_float(GgmlType::MXFP4, &MXFP4_GGUF_TEST_BLOCKS, "test.weight")
5152            .expect("widen_plain_float must widen MXFP4");
5153        assert_eq!(direct, expected);
5154
5155        // And the refusal still works for a dtype that genuinely has no
5156        // widening path, so this test cannot pass by making everything
5157        // succeed.
5158        match widen_plain_float(GgmlType::TQ2_0, &MXFP4_GGUF_TEST_BLOCKS, "test.weight") {
5159            Err(LoadError::UnsupportedDtype(name, GgmlType::TQ2_0)) => {
5160                assert_eq!(name, "test.weight");
5161            }
5162            other => panic!("TQ2_0 must be refused by name, got {other:?}"),
5163        }
5164    }
5165
5166    fn build_single_iq_lowbit_tensor_gguf(
5167        arch: &str,
5168        tag: u32,
5169        cols: u64,
5170        block: &[u8],
5171    ) -> Vec<u8> {
5172        let mut buf = Vec::new();
5173        buf.write_u32::<LittleEndian>(frink_gguf::GGUF_MAGIC)
5174            .unwrap();
5175        buf.write_u32::<LittleEndian>(3).unwrap(); // version
5176        buf.write_u64::<LittleEndian>(1).unwrap(); // tensor_count
5177        buf.write_u64::<LittleEndian>(1).unwrap(); // kv_count
5178        write_kv_str(&mut buf, "general.architecture", arch);
5179        write_string(&mut buf, "test.weight");
5180        buf.write_u32::<LittleEndian>(2).unwrap(); // n_dims
5181        buf.write_u64::<LittleEndian>(cols).unwrap();
5182        buf.write_u64::<LittleEndian>(1).unwrap(); // rows
5183        buf.write_u32::<LittleEndian>(tag).unwrap();
5184        buf.write_u64::<LittleEndian>(0).unwrap(); // offset
5185        while buf.len() % 32 != 0 {
5186            buf.push(0);
5187        }
5188        buf.extend_from_slice(block);
5189        buf
5190    }
5191
5192    /// A structurally valid block of `len` bytes for any of the
5193    /// codebook-grid formats: every bit pattern is a legal code in all
5194    /// of them (the grid indices are bounded by their own bit widths),
5195    /// so a deterministic byte fill is a real block, not a fixture that
5196    /// happens to avoid the interesting paths. Only the f16 scale needs
5197    /// pinning, and only so the comparison below can't be NaN-vs-NaN.
5198    fn pseudo_iq_block(len: usize, seed: u32) -> Vec<u8> {
5199        let mut s = seed;
5200        let mut out = Vec::with_capacity(len);
5201        for _ in 0..len {
5202            s ^= s << 13;
5203            s ^= s >> 17;
5204            s ^= s << 5;
5205            out.push((s >> 24) as u8);
5206        }
5207        out
5208    }
5209
5210    /// End-to-end load+apply for the codebook-grid low-bit formats the
5211    /// published Dynamic GGUFs are built from: a real on-disk tensor of
5212    /// each type must load zero-copy as the right `QuantKind` and
5213    /// produce the same matvec result as dequantizing the block
5214    /// directly. That is the property this test exists for -- the
5215    /// *values* are pinned against real ggml in `frink-quant`; what
5216    /// can only break here is the tag -> kind -> block-stride chain,
5217    /// and a wrong stride silently reads the neighbouring row.
5218    /// Dtype tags (19/29/16/17/22/18/21/39) verified against ggml.h's
5219    /// enum ggml_type.
5220    #[test]
5221    fn load_weight_matrix_handles_real_on_disk_iq_lowbit_tensors_end_to_end() {
5222        type DequantFn = fn(&[u8]) -> Result<Vec<f32>, frink_quant::QuantError>;
5223        // IQ1_M carries no f16 scale field; its scale is reassembled
5224        // from the four scale words' top nibbles, and the top nibble of
5225        // the last one supplies the f16 sign + high exponent bits.
5226        // Pinning it to 0x2 keeps the exponent out of the all-ones
5227        // NaN/Inf pattern whatever the rest of the fill does. The other
5228        // three do carry a leading f16 `d`, pinned for the same reason.
5229        let mut iq1m = pseudo_iq_block(frink_quant::IQ1_M_BLOCK_BYTES, 0x2907_31A0);
5230        iq1m[55] = (iq1m[55] & 0x0F) | 0x20;
5231        let mut iq2xs = pseudo_iq_block(frink_quant::IQ2_XS_BLOCK_BYTES, 0x2107_31A1);
5232        let mut iq2s = pseudo_iq_block(frink_quant::IQ2_S_BLOCK_BYTES, 0x2207_31A2);
5233        let mut iq3s = pseudo_iq_block(frink_quant::IQ3_S_BLOCK_BYTES, 0x2307_31A3);
5234        for blk in [&mut iq2xs, &mut iq2s, &mut iq3s] {
5235            blk[0..2].copy_from_slice(&half::f16::from_f32(0.115).to_le_bytes());
5236        }
5237        let cases: [(&str, u32, &[u8], QuantKind, DequantFn); 8] = [
5238            (
5239                "iq1s",
5240                19,
5241                &IQ1_S_TEST_BLOCK,
5242                QuantKind::IQ1S,
5243                frink_quant::dequant_iq1_s,
5244            ),
5245            (
5246                "iq1m",
5247                29,
5248                &iq1m,
5249                QuantKind::IQ1M,
5250                frink_quant::dequant_iq1_m,
5251            ),
5252            (
5253                "iq2xxs",
5254                16,
5255                &IQ2_XXS_TEST_BLOCK,
5256                QuantKind::IQ2XXS,
5257                frink_quant::dequant_iq2_xxs,
5258            ),
5259            (
5260                "iq2xs",
5261                17,
5262                &iq2xs,
5263                QuantKind::IQ2XS,
5264                frink_quant::dequant_iq2_xs,
5265            ),
5266            (
5267                "iq2s",
5268                22,
5269                &iq2s,
5270                QuantKind::IQ2S,
5271                frink_quant::dequant_iq2_s,
5272            ),
5273            (
5274                "iq3xxs",
5275                18,
5276                &IQ3_XXS_TEST_BLOCK,
5277                QuantKind::IQ3XXS,
5278                frink_quant::dequant_iq3_xxs,
5279            ),
5280            (
5281                "iq3s",
5282                21,
5283                &iq3s,
5284                QuantKind::IQ3S,
5285                frink_quant::dequant_iq3_s,
5286            ),
5287            (
5288                "mxfp4_gguf",
5289                39,
5290                &MXFP4_GGUF_TEST_BLOCKS,
5291                QuantKind::Mxfp4Gguf,
5292                frink_quant::dequant_mxfp4_gguf,
5293            ),
5294        ];
5295        for (name, tag, block, kind, dequant) in cases {
5296            let expected = dequant(block).unwrap();
5297            let cols = expected.len();
5298            let tmp = std::env::temp_dir().join(format!("frink_test_{name}_tensor.gguf"));
5299            std::fs::write(
5300                &tmp,
5301                build_single_iq_lowbit_tensor_gguf(name, tag, cols as u64, block),
5302            )
5303            .unwrap();
5304            let file = frink_gguf::GgufFile::open(&tmp).expect("file must parse");
5305            std::fs::remove_file(&tmp).ok();
5306
5307            let matrix =
5308                load_weight_matrix(&file, "test.weight").expect("low-bit tensor must load");
5309            assert_eq!((matrix.rows(), matrix.cols()), (1, cols), "{name}");
5310            match &matrix {
5311                WeightMatrix::Quantized { kind: k, data, .. } => {
5312                    assert_eq!(*k, kind, "{name}");
5313                    assert!(data.is_mapped(), "{name} must load zero-copy");
5314                }
5315                _ => panic!("expected a Quantized matrix for {name}"),
5316            }
5317
5318            let x: Vec<f32> = (0..cols).map(|i| ((i as f32) * 0.013).sin()).collect();
5319            let expected_dot: f32 = expected.iter().zip(x.iter()).map(|(a, b)| a * b).sum();
5320            let got = matrix.apply(&x);
5321            assert!(
5322                (got[0] - expected_dot).abs() < 1e-1,
5323                "{name}: loaded+applied diverged from direct dequant: got={} expected={}",
5324                got[0],
5325                expected_dot
5326            );
5327        }
5328    }
5329
5330    #[test]
5331    fn qwen2moe_disables_topk_renorm() {
5332        assert!(
5333            NO_TOPK_RENORMALIZE_ARCHITECTURES.contains(&"qwen2moe"),
5334            "qwen2moe must have norm_topk_prob=false (llama.cpp build_moe_ffn norm_w=false)"
5335        );
5336    }
5337
5338    /// The `LLAMA_ROPE_TYPE_NONE` group used to be refused by name here;
5339    /// every row is served now, positioned the way its graph positions
5340    /// (`crate::position_embd`, `crate::alibi`), and what this pins is
5341    /// that not one of them reaches a rotation: the rule is
5342    /// `RopeLayers::Never` for each, at any depth it takes.
5343    #[test]
5344    fn an_architecture_with_no_rope_rotates_nothing() {
5345        for (arch, n_layers) in [
5346            ("gpt2", 12),
5347            ("mpt", 32),
5348            ("refact", 32),
5349            ("bloom", 30),
5350            ("jais", 40),
5351            ("baichuan", 40),
5352        ] {
5353            assert_eq!(
5354                crate::rope_layers::rope_layers(arch, n_layers, false, 0),
5355                crate::rope_layers::RopeLayers::Never,
5356                "{arch} positions without RoPE and must rotate nothing"
5357            );
5358            assert!(crate::capability::is_audited_generic(arch), "{arch}");
5359        }
5360    }
5361
5362    /// A per-layer sliding-window ARRAY on an architecture whose graph
5363    /// reads the key as a scalar is IGNORED and the seeded period
5364    /// stands, exactly as llama.cpp does; a scalar still overrides the
5365    /// period.
5366    ///
5367    /// Three generations of this gate. `capability::
5368    /// unsupported_feature_keys` refused the key outright with the
5369    /// reason "not implemented in the generic decoder", which was
5370    /// false. Then the loader refused the ARRAY form for every
5371    /// architecture, on the reasoning that honouring it as a period was
5372    /// impossible and ignoring it would substitute the seed for the
5373    /// file's layout -- which is TRUE and is ALSO what llama.cpp does:
5374    /// `get_key_or_arr(kid, swa_period, false)` returns false on an
5375    /// array (`llama-model-loader.cpp:502-507`) and `plamo3.cpp:9-11`
5376    /// keeps its 8. Every real EXAONE-4 32B, EXAONE-MoE and Olmo-3
5377    /// export carries the array (`conversion/exaone.py:84`,
5378    /// `olmo.py:59-66`) and was refused over a value upstream never
5379    /// reads. `crate::swa_layers` carries which graphs read which form;
5380    /// the array-HONOURED mode has its own fixture in
5381    /// `tests/window_array_graphs.rs`.
5382    #[test]
5383    fn an_array_valued_sliding_window_pattern_is_ignored_where_llama_cpp_ignores_it() {
5384        // Disagrees with plamo3's seeded last-dense 8 on layers 0..3,
5385        // so honouring it would be visible.
5386        let pattern: [u32; 4] = [0, 0, 0, 0];
5387        let kvs: Vec<(&str, Kv)> = vec![
5388            ("general.architecture", Kv::Str("plamo3")),
5389            ("plamo3.block_count", Kv::U32(4)),
5390            ("plamo3.embedding_length", Kv::U32(64)),
5391            ("plamo3.attention.head_count", Kv::U32(1)),
5392            ("plamo3.attention.head_count_kv", Kv::U32(1)),
5393            ("plamo3.attention.key_length", Kv::U32(64)),
5394            ("plamo3.rope.freq_base", Kv::F32(10_000.0)),
5395            ("plamo3.attention.sliding_window", Kv::U32(3)),
5396            (
5397                "plamo3.attention.sliding_window_pattern",
5398                Kv::Arr32(&pattern),
5399            ),
5400        ];
5401        let file = open_metadata_gguf("swa_pattern_array", &kvs);
5402        let config = ModelConfig::from_gguf(&file).expect("the array is not a refusal");
5403        assert_eq!(
5404            config.swa_layers,
5405            crate::swa_layers::SwaLayers::period(8, false),
5406            "plamo3.cpp:9-11 seeds 8 and the scalar overload ignores an array"
5407        );
5408        assert_eq!(config.layer_sliding_window(0), Some(3));
5409        assert_eq!(config.layer_sliding_window(3), Some(3));
5410
5411        // And the scalar spelling of the same key overrides the seed.
5412        let mut scalar = kvs;
5413        scalar.pop();
5414        scalar.push(("plamo3.attention.sliding_window_pattern", Kv::U32(2)));
5415        let file = open_metadata_gguf("swa_pattern_scalar", &scalar);
5416        let config = ModelConfig::from_gguf(&file).expect("a scalar period must load");
5417        assert_eq!(
5418            config.swa_layers,
5419            crate::swa_layers::SwaLayers::period(2, false)
5420        );
5421        assert_eq!(config.layer_sliding_window(0), Some(3));
5422        assert_eq!(config.layer_sliding_window(1), None);
5423    }
5424
5425    /// Baichuan is one `general.architecture` string covering two
5426    /// positional schemes, and llama.cpp picks between them on the layer
5427    /// count alone (`src/models/baichuan.cpp:11-14`, with its own "TODO:
5428    /// become GGUF KV parameter"). The 13B used to be refused HERE; it is
5429    /// served now, and what this pins is that the two schemes are still
5430    /// told apart by the count, on both tables that must agree about it
5431    /// (`crate::alibi`, `crate::rope_layers`).
5432    #[test]
5433    fn baichuan_13b_positions_by_alibi_and_the_7b_rotates() {
5434        assert_eq!(
5435            crate::alibi::max_alibi_bias("baichuan", 40, None),
5436            Some(8.0)
5437        );
5438        assert_eq!(
5439            crate::rope_layers::rope_layers("baichuan", 40, false, 0),
5440            crate::rope_layers::RopeLayers::Never
5441        );
5442        assert_eq!(crate::alibi::max_alibi_bias("baichuan", 32, None), None);
5443        assert_eq!(
5444            crate::rope_layers::rope_layers("baichuan", 32, false, 0),
5445            crate::rope_layers::RopeLayers::All
5446        );
5447        // Both sizes pass the header stage and fail on the next missing
5448        // hparam, which is what proves neither is gated here any more.
5449        for (name, n) in [("baichuan13b", 40u32), ("baichuan7b", 32)] {
5450            let file = open_metadata_gguf(
5451                name,
5452                &[
5453                    ("general.architecture", Kv::Str("baichuan")),
5454                    ("baichuan.block_count", Kv::U32(n)),
5455                ],
5456            );
5457            match ModelConfig::from_gguf(&file) {
5458                Err(LoadError::MissingHparam(key)) => assert_eq!(key, "baichuan.embedding_length"),
5459                other => panic!("{name} must pass the header stage, got {other:?}"),
5460            }
5461        }
5462    }
5463
5464    /// EXAONE-4 is ONE architecture string over TWO graphs, and
5465    /// llama.cpp picks between them off the LAYER COUNT with no GGUF key
5466    /// involved. It used to be refused for it; both sizes run now, and
5467    /// this is the test that says they run DIFFERENTLY.
5468    ///
5469    /// `exaone4.cpp:4-9` wraps the entire SWA setup in
5470    /// `if (hparams.n_layer() == 64)`, and :116 then gates rotation on
5471    /// it -- `use_rope = is_swa(il) || swa_type == NONE`. So:
5472    ///
5473    /// * 64 layers: a window, `set_swa_pattern(4)` last-dense, and the
5474    ///   FULL-ATTENTION layer of every period gets no rotation at all.
5475    /// * 30 layers: no window whatever the file declares, and every
5476    ///   layer rotates.
5477    ///
5478    /// Both halves are here because the gate is a layer-count EQUALITY.
5479    /// A one-sided version would pass while windowing the 1.2B off a key
5480    /// llama.cpp never reaches, which is the divergence
5481    /// `capability::swa_disabled_by_arch` was extended to stop -- and it
5482    /// would then rope three layers in four of the 1.2B not at all.
5483    #[test]
5484    fn the_two_exaone4_sizes_get_different_windows_and_different_rotation() {
5485        // `Kv` is not `Clone`, so the shared header is a builder rather
5486        // than a value; both sizes must read from one list or the test
5487        // compares two transcriptions.
5488        let base = |n_layers: u32| -> Vec<(&str, Kv)> {
5489            vec![
5490                ("general.architecture", Kv::Str("exaone4")),
5491                ("exaone4.block_count", Kv::U32(n_layers)),
5492                ("exaone4.embedding_length", Kv::U32(32)),
5493                ("exaone4.attention.head_count", Kv::U32(4)),
5494                ("exaone4.attention.head_count_kv", Kv::U32(2)),
5495                ("exaone4.attention.key_length", Kv::U32(8)),
5496                ("exaone4.attention.value_length", Kv::U32(8)),
5497                ("exaone4.rope.freq_base", Kv::F32(10_000.0)),
5498                // The SAME declared window for both sizes: that is the
5499                // whole point. Only the layer count may change the
5500                // answer.
5501                ("exaone4.attention.sliding_window", Kv::U32(4096)),
5502            ]
5503        };
5504
5505        let file = open_metadata_gguf("exaone4_32b", &base(64));
5506        let cfg = ModelConfig::from_gguf(&file).expect("EXAONE-4 32B loads");
5507        assert_eq!(cfg.sliding_window, Some(4096));
5508        assert_eq!(
5509            cfg.swa_layers,
5510            crate::swa_layers::SwaLayers::period(4, false),
5511            "exaone4.cpp:7-9, and set_swa_pattern's default phase"
5512        );
5513        for il in 0..64 {
5514            assert_eq!(
5515                cfg.layer_rotates(il),
5516                il % 4 != 3,
5517                "layer {il} of EXAONE-4 32B: only the sliding layers rotate"
5518            );
5519        }
5520
5521        let file = open_metadata_gguf("exaone4_1_2b", &base(30));
5522        let cfg = ModelConfig::from_gguf(&file).expect("EXAONE-4 1.2B loads");
5523        assert_eq!(
5524            cfg.sliding_window, None,
5525            "exaone4.cpp:4 never reaches set_swa_pattern below 64 layers, \
5526             so the declared window is dead metadata"
5527        );
5528        for il in 0..30 {
5529            assert!(cfg.layer_rotates(il), "layer {il} of EXAONE-4 1.2B");
5530        }
5531    }
5532
5533    /// NextN/MTP blocks are inside `block_count` and llama.cpp skips
5534    /// them (`n_layer = n_layer_all - n_layer_nextn`, llama-hparams.cpp
5535    /// :280-282). For a graph that reads the key (`exaone-moe.cpp:23`)
5536    /// the trunk is what loads; the key is written as `0` by
5537    /// `conversion/exaone.py:146` for every EXAONE-MoE export without an
5538    /// MTP head, so zero must be the whole file. A nonzero value on a
5539    /// graph that does NOT read the key stays refused
5540    /// (`mtp_blocks::tests`).
5541    ///
5542    /// The second half pins the ORDER of two reads in `exaone4.cpp`:
5543    /// `:4` tests `n_layer() == 64` before `:18` reads the key, so it
5544    /// sees `block_count`. A 64-trunk file with one MTP block appended
5545    /// is 65 there and gets NO window in llama.cpp; frink feeds
5546    /// `block_count` to the same gate and gets the same answer.
5547    #[test]
5548    fn nextn_predict_layers_subtracts_the_trunk_for_a_reader_and_zero_is_the_whole_file() {
5549        let base = |nextn: u32| -> Vec<(&str, Kv)> {
5550            vec![
5551                ("general.architecture", Kv::Str("exaone-moe")),
5552                ("exaone-moe.block_count", Kv::U32(5)),
5553                ("exaone-moe.nextn_predict_layers", Kv::U32(nextn)),
5554                ("exaone-moe.embedding_length", Kv::U32(32)),
5555                ("exaone-moe.attention.head_count", Kv::U32(4)),
5556                ("exaone-moe.attention.head_count_kv", Kv::U32(2)),
5557                ("exaone-moe.attention.key_length", Kv::U32(8)),
5558                ("exaone-moe.attention.value_length", Kv::U32(8)),
5559                ("exaone-moe.rope.freq_base", Kv::F32(10_000.0)),
5560                ("exaone-moe.attention.sliding_window", Kv::U32(128)),
5561                ("exaone-moe.expert_count", Kv::U32(4)),
5562                ("exaone-moe.expert_used_count", Kv::U32(2)),
5563                ("exaone-moe.expert_gating_func", Kv::U32(2)),
5564            ]
5565        };
5566        let cfg = ModelConfig::from_gguf(&open_metadata_gguf("exaone_moe_mtp", &base(1)))
5567            .expect("a reader with an MTP block loads its trunk");
5568        assert_eq!((cfg.n_layers, cfg.n_mtp_blocks), (4, 1));
5569        let cfg = ModelConfig::from_gguf(&open_metadata_gguf("exaone_moe_no_mtp", &base(0)))
5570            .expect("zero is the whole file");
5571        assert_eq!((cfg.n_layers, cfg.n_mtp_blocks), (5, 0));
5572
5573        // `exaone4.cpp:4` before `:18`: 64 trunk layers plus one MTP
5574        // block is NOT the 32B to llama.cpp.
5575        let exaone4 = |block_count: u32, nextn: u32| -> Vec<(&str, Kv)> {
5576            vec![
5577                ("general.architecture", Kv::Str("exaone4")),
5578                ("exaone4.block_count", Kv::U32(block_count)),
5579                ("exaone4.nextn_predict_layers", Kv::U32(nextn)),
5580                ("exaone4.embedding_length", Kv::U32(32)),
5581                ("exaone4.attention.head_count", Kv::U32(4)),
5582                ("exaone4.attention.head_count_kv", Kv::U32(2)),
5583                ("exaone4.attention.key_length", Kv::U32(8)),
5584                ("exaone4.attention.value_length", Kv::U32(8)),
5585                ("exaone4.rope.freq_base", Kv::F32(10_000.0)),
5586                ("exaone4.attention.sliding_window", Kv::U32(4096)),
5587            ]
5588        };
5589        let with_mtp =
5590            ModelConfig::from_gguf(&open_metadata_gguf("exaone4_65", &exaone4(65, 1))).unwrap();
5591        assert_eq!((with_mtp.n_layers, with_mtp.n_mtp_blocks), (64, 1));
5592        assert_eq!(
5593            with_mtp.sliding_window, None,
5594            "exaone4.cpp:4 sees n_layer_all = 65 and never reaches set_swa_pattern"
5595        );
5596        let without =
5597            ModelConfig::from_gguf(&open_metadata_gguf("exaone4_64", &exaone4(64, 0))).unwrap();
5598        assert_eq!(
5599            without.sliding_window,
5600            Some(4096),
5601            "the same trunk without the block is the 32B"
5602        );
5603    }
5604
5605    /// `expert_used_count` is scalar-or-array upstream, and the array
5606    /// spelling used to fall through to a DEFAULT of 2 here.
5607    ///
5608    /// `llama-model.cpp:1266` reads the key with `get_key_or_arr` in
5609    /// the common loader -- every architecture, `n_layer_all` entries
5610    /// -- and `conversion/nemotron.py:574` writes a list for
5611    /// Nemotron-H Puzzle, whose architecture (`nemotron_h`) frink
5612    /// serves. Before this test, `metadata_u64` answered `None` for an
5613    /// array value, the `unwrap_or_else` below it pushed a best-effort
5614    /// note, and the model routed top-2 on every layer whatever the
5615    /// file declared: the silent-wrong class, not a refusal.
5616    ///
5617    /// Both arms are pinned, because a reader that honoured the
5618    /// uniform case and silently averaged the varying one would pass
5619    /// half of this.
5620    #[test]
5621    fn a_per_layer_expert_used_count_is_honoured_when_uniform_and_refused_when_not() {
5622        fn file(used: Kv<'_>) -> Vec<(&'static str, Kv<'_>)> {
5623            vec![
5624                ("general.architecture", Kv::Str("llama")),
5625                ("llama.block_count", Kv::U32(2)),
5626                ("llama.embedding_length", Kv::U32(32)),
5627                ("llama.attention.head_count", Kv::U32(4)),
5628                ("llama.attention.head_count_kv", Kv::U32(2)),
5629                ("llama.attention.key_length", Kv::U32(8)),
5630                ("llama.attention.value_length", Kv::U32(8)),
5631                ("llama.rope.freq_base", Kv::F32(10_000.0)),
5632                ("llama.expert_count", Kv::U32(8)),
5633                ("llama.expert_used_count", used),
5634            ]
5635        }
5636        let scalar = ModelConfig::from_gguf(&open_metadata_gguf(
5637            "experts_used_scalar",
5638            &file(Kv::U32(3)),
5639        ))
5640        .expect("the scalar spelling loads");
5641        assert_eq!(scalar.moe.n_experts_active, 3);
5642
5643        let uniform = ModelConfig::from_gguf(&open_metadata_gguf(
5644            "experts_used_uniform",
5645            &file(Kv::Arr32(&[3, 3])),
5646        ))
5647        .expect("a uniform array is that one value");
5648        assert_eq!(
5649            uniform.moe.n_experts_active, 3,
5650            "an array of one repeated value is the scalar, not the default of 2"
5651        );
5652
5653        let err = ModelConfig::from_gguf(&open_metadata_gguf(
5654            "experts_used_varying",
5655            &file(Kv::Arr32(&[3, 5])),
5656        ))
5657        .expect_err("a varying array has no single top-k and must stop");
5658        let msg = format!("{err}");
5659        assert!(
5660            msg.contains("expert_used_count") && msg.contains("PER-LAYER"),
5661            "the refusal must name the key and what is wrong with it: {msg}"
5662        );
5663    }
5664
5665    /// An `olmo2` file carrying BOTH a sliding window and a RoPE
5666    /// scaling ropes its two kinds of layer differently, and frink
5667    /// carries one scaling for the whole model.
5668    ///
5669    /// `olmo2.cpp:120-134` runs the sliding layers with the scaling
5670    /// switched off -- `freq_scale = 1`, `ext_factor = 0`,
5671    /// `attn_factor = 1`, and the comment above it says so in as many
5672    /// words -- while :136-146 gives the full-attention layers the
5673    /// model's own. Rotating half the layers at a magnitude the
5674    /// checkpoint never trained at is the ALiBi class of divergence and
5675    /// runs fluently.
5676    ///
5677    /// Both negative halves are here because the gate is a CONJUNCTION
5678    /// and a gate that fires on either half alone would refuse every
5679    /// OLMo-2 checkpoint ever published.
5680    #[test]
5681    fn olmo2_is_refused_only_when_it_has_a_window_and_a_rope_scaling_together() {
5682        // `Kv` is not `Clone`, so the shared header is a builder
5683        // rather than a value -- which also keeps the three cases
5684        // reading from one list instead of three transcriptions.
5685        let base = || -> Vec<(&str, Kv)> {
5686            vec![
5687                ("general.architecture", Kv::Str("olmo2")),
5688                ("olmo2.block_count", Kv::U32(2)),
5689                ("olmo2.embedding_length", Kv::U32(24)),
5690                ("olmo2.attention.head_count", Kv::U32(4)),
5691                ("olmo2.attention.head_count_kv", Kv::U32(2)),
5692                ("olmo2.attention.key_length", Kv::U32(6)),
5693                ("olmo2.attention.value_length", Kv::U32(6)),
5694                ("olmo2.rope.freq_base", Kv::F32(10_000.0)),
5695            ]
5696        };
5697
5698        let mut both = base();
5699        both.push(("olmo2.attention.sliding_window", Kv::U32(3)));
5700        both.push(("olmo2.rope.scaling.type", Kv::Str("yarn")));
5701        both.push(("olmo2.rope.scaling.factor", Kv::F32(4.0)));
5702        let file = open_metadata_gguf("olmo2_swa_yarn", &both);
5703        match ModelConfig::from_gguf(&file) {
5704            Err(LoadError::UnsupportedFeature(arch, msg)) => {
5705                assert_eq!(arch, "olmo2");
5706                assert!(msg.contains("sliding window"), "{msg}");
5707                assert!(msg.contains("yarn"), "{msg}");
5708            }
5709            other => panic!("olmo2 with a window AND yarn must refuse, got {other:?}"),
5710        }
5711
5712        // A window with no scaling: both of llama.cpp's RoPE branches
5713        // reduce to the same plain rotation, and the difference is
5714        // masking alone, which frink implements.
5715        let mut window_only = base();
5716        window_only.push(("olmo2.attention.sliding_window", Kv::U32(3)));
5717        let file = open_metadata_gguf("olmo2_swa_only", &window_only);
5718        let config = ModelConfig::from_gguf(&file).expect("a window with no scaling must load");
5719        assert_eq!(config.sliding_window, Some(3));
5720        // olmo2.cpp:9-11: the period defaults to 4 and `set_swa_pattern`
5721        // leaves `dense_first` false.
5722        assert_eq!(
5723            config.swa_layers,
5724            crate::swa_layers::SwaLayers::period(4, false)
5725        );
5726
5727        // Scaling with no window: one RoPE for the whole model, which is
5728        // what frink carries.
5729        let mut scaling_only = base();
5730        scaling_only.push(("olmo2.rope.scaling.type", Kv::Str("yarn")));
5731        scaling_only.push(("olmo2.rope.scaling.factor", Kv::F32(4.0)));
5732        let file = open_metadata_gguf("olmo2_yarn_only", &scaling_only);
5733        let config = ModelConfig::from_gguf(&file).expect("scaling with no window must load");
5734        assert_eq!(config.sliding_window, None);
5735    }
5736
5737    /// The hyper-parameters a real Gemma-3 GGUF header carries for one
5738    /// size. `block_count` is the field llama.cpp's `LLM_TYPE_27B`
5739    /// switch reads (`gemma3.cpp:20-28`), so it is never a free
5740    /// parameter here.
5741    ///
5742    /// `linear_factor` adds the pair `conversion/base.py:1222-1230`
5743    /// writes from `rope_parameters["full_attention"]` -- and only from
5744    /// there: its own comment is "TODO: Handle sliding_attention
5745    /// similarly when models start implementing it", so the sliding
5746    /// layers get no scaling key at all.
5747    fn gemma3_config(
5748        tag: &str,
5749        n_layers: u32,
5750        hidden_dim: u32,
5751        n_heads: u32,
5752        head_dim: u32,
5753        linear_factor: Option<f32>,
5754    ) -> ModelConfig {
5755        let mut kvs: Vec<(&str, Kv)> = vec![
5756            ("general.architecture", Kv::Str("gemma3")),
5757            ("gemma3.block_count", Kv::U32(n_layers)),
5758            ("gemma3.embedding_length", Kv::U32(hidden_dim)),
5759            ("gemma3.attention.head_count", Kv::U32(n_heads)),
5760            ("gemma3.attention.head_count_kv", Kv::U32(n_heads)),
5761            ("gemma3.attention.key_length", Kv::U32(head_dim)),
5762            ("gemma3.attention.value_length", Kv::U32(head_dim)),
5763            // Global layers rotate at 1e6; the sliding ones fall back to
5764            // llama.cpp's `rope_freq_base_train_swa` default of 10000,
5765            // because `gemma3.cpp:11` reads only the BASE key.
5766            ("gemma3.rope.freq_base", Kv::F32(1_000_000.0)),
5767            ("gemma3.attention.sliding_window", Kv::U32(1024)),
5768            ("gemma3.attention.sliding_window_pattern", Kv::U32(6)),
5769        ];
5770        if let Some(factor) = linear_factor {
5771            kvs.push(("gemma3.rope.scaling.type", Kv::Str("linear")));
5772            kvs.push(("gemma3.rope.scaling.factor", Kv::F32(factor)));
5773        }
5774        ModelConfig::from_gguf(&open_metadata_gguf(tag, &kvs)).expect("gemma3 fixture must load")
5775    }
5776
5777    /// The 27B attention scale reaches `ModelConfig`, and no other
5778    /// Gemma-3 size acquires one.
5779    ///
5780    /// `capability::attention_scale_override` is where the arithmetic is
5781    /// checked; this pins that the LOADER calls it with this file's own
5782    /// numbers. That step is the one that shipped broken: the function
5783    /// did not exist and `attention_scale` was the literal `None`, under
5784    /// a comment naming the exception. A helper nobody calls looks
5785    /// exactly like a fix.
5786    #[test]
5787    fn a_gemma3_27b_header_sets_the_attention_scale_and_no_smaller_size_does() {
5788        // Gemma-3-27B: 62 layers, n_embd 5376, 32 heads, head_dim 128.
5789        let big = gemma3_config("g3_27b_scale", 62, 5376, 32, 128, Some(8.0));
5790        let want = 1.0f32 / (5376.0f32 / 32.0).sqrt();
5791        let got = big
5792            .attention_scale
5793            .expect("Gemma-3-27B is llama.cpp's LLM_TYPE_27B");
5794        assert!(
5795            (got - want).abs() < 1e-7,
5796            "want 1/sqrt(168) = {want}, got {got}"
5797        );
5798        // The bug's magnitude: scores were sqrt(168/128) = 1.146x too
5799        // large without this.
5800        let kernel = 1.0f32 / 128.0f32.sqrt();
5801        assert!((kernel / got - (168.0f32 / 128.0).sqrt()).abs() < 1e-5);
5802
5803        // Gemma-3-1B and -4B take llama.cpp's other branch, which is the
5804        // scale the attention kernels already apply. A `Some` here would
5805        // double-scale them.
5806        for (tag, n_layers, hidden, heads) in
5807            [("g3_1b_scale", 26, 1152, 4), ("g3_4b_scale", 34, 2560, 8)]
5808        {
5809            let cfg = gemma3_config(tag, n_layers, hidden, heads, 256, None);
5810            assert_eq!(
5811                cfg.attention_scale, None,
5812                "{tag} must keep the kernels' own 1/sqrt(head_dim)"
5813            );
5814        }
5815    }
5816
5817    /// Gemma-3's declared linear scaling reaches the FULL-ATTENTION
5818    /// layers only, and the sliding ones rope unscaled.
5819    ///
5820    /// `gemma3.cpp:11` reads `LLM_KV_ROPE_FREQ_BASE_SWA` and nothing
5821    /// else, so `rope_freq_scale_train_swa` keeps its `1.0f` default
5822    /// (`src/llama-hparams.h:129`) while `get_rope_freq_scale`
5823    /// (`llama-model.cpp:2033-2035`) hands the trained scale to the full
5824    /// layers. The converter agrees: `conversion/base.py:1222-1230`
5825    /// takes the factor from `rope_parameters["full_attention"]` and
5826    /// writes nothing for the sliding half.
5827    ///
5828    /// frink folded the factor into ONE global `rope_freqs` vector, so
5829    /// Gemma-3-4B/12B/27B rotated five layers in six at `p/8`.
5830    #[test]
5831    fn gemma3_linear_scaling_reaches_the_full_layers_and_not_the_sliding_ones() {
5832        // Gemma-3-4B: 34 layers, head_dim 256, `rope_scaling: linear 8`.
5833        let cfg = gemma3_config("g3_4b_rope", 34, 2560, 8, 256, Some(8.0));
5834        let freqs = cfg
5835            .rope_freqs
5836            .as_ref()
5837            .expect("declared linear scaling must produce per-band divisors");
5838        assert!(
5839            freqs.full.iter().all(|f| (*f - 8.0).abs() < 1e-6),
5840            "full-attention layers divide every band by the trained factor: {:?}",
5841            freqs.full
5842        );
5843        let swa = freqs
5844            .swa
5845            .as_ref()
5846            .expect("gemma3 does not assign rope_freq_scale_train_swa, so 1.0 applies");
5847        assert!(
5848            swa.iter().all(|f| (*f - 1.0).abs() < 1e-6),
5849            "sliding layers rope at the raw position: {swa:?}"
5850        );
5851        assert_eq!(swa.len(), freqs.full.len(), "one divisor per rotated pair");
5852
5853        // Period 6, last-dense (`capability::default_swa_layout`), so
5854        // layer 5 is the full-attention one and 0..=4 slide. The phase
5855        // matters: getting it wrong swaps which five-sixths are wrong.
5856        assert!(cfg.layer_sliding_window(0).is_some());
5857        assert!(cfg.layer_sliding_window(5).is_none());
5858        assert_eq!(
5859            cfg.layer_rope(0),
5860            Some(crate::config::LayerRopeParams {
5861                theta: 10_000.0,
5862                freq_factors: Some(&[1.0f32; 128][..]),
5863                rot_dim: None,
5864            })
5865        );
5866        assert_eq!(
5867            cfg.layer_rope(5),
5868            Some(crate::config::LayerRopeParams {
5869                theta: 1_000_000.0,
5870                freq_factors: Some(&[8.0f32; 128][..]),
5871                rot_dim: None,
5872            })
5873        );
5874        assert!(
5875            cfg.rope_freqs_vary_by_layer(),
5876            "the fused Metal stacks take one divisor slice for a whole run \
5877             and so must refuse this model"
5878        );
5879
5880        // Gemma-3-1B declares no scaling at all -- the audited fixture,
5881        // and the reason this was invisible. Nothing to split, so no
5882        // per-layer set and no Metal refusal.
5883        let plain = gemma3_config("g3_1b_rope", 26, 1152, 4, 256, None);
5884        assert!(plain.rope_freqs.is_none());
5885        assert!(!plain.rope_freqs_vary_by_layer());
5886    }
5887
5888    /// Gemma-2 is the counter-case, and it is why the SWA scale needs
5889    /// its own table rather than reusing `swa_rope_base_follows_model`.
5890    ///
5891    /// `gemma2.cpp:10-11` assigns BOTH `rope_freq_base_train_swa` and
5892    /// `rope_freq_scale_train_swa` from the model's trained values, so
5893    /// its sliding layers keep the declared scaling. Splitting them here
5894    /// would be the same bug pointed the other way.
5895    #[test]
5896    fn gemma2_sliding_layers_inherit_the_trained_rope_scale() {
5897        let cfg = ModelConfig::from_gguf(&open_metadata_gguf(
5898            "g2_rope",
5899            &[
5900                ("general.architecture", Kv::Str("gemma2")),
5901                ("gemma2.block_count", Kv::U32(26)),
5902                ("gemma2.embedding_length", Kv::U32(2304)),
5903                ("gemma2.attention.head_count", Kv::U32(8)),
5904                ("gemma2.attention.head_count_kv", Kv::U32(4)),
5905                ("gemma2.attention.key_length", Kv::U32(256)),
5906                ("gemma2.attention.value_length", Kv::U32(256)),
5907                ("gemma2.rope.freq_base", Kv::F32(10_000.0)),
5908                ("gemma2.attention.sliding_window", Kv::U32(4096)),
5909                ("gemma2.rope.scaling.type", Kv::Str("linear")),
5910                ("gemma2.rope.scaling.factor", Kv::F32(8.0)),
5911            ],
5912        ))
5913        .expect("gemma2 fixture must load");
5914
5915        let freqs = cfg.rope_freqs.as_ref().expect("linear scaling declared");
5916        assert_eq!(
5917            freqs.swa, None,
5918            "gemma2.cpp:11 assigns rope_freq_scale_train_swa from the trained scale"
5919        );
5920        assert!(!cfg.rope_freqs_vary_by_layer());
5921        // Period 2, last-dense: layer 0 slides, layer 1 does not, and
5922        // both get the same divisors.
5923        assert!(cfg.layer_sliding_window(0).is_some());
5924        assert!(cfg.layer_sliding_window(1).is_none());
5925        assert_eq!(cfg.layer_rope(0), cfg.layer_rope(1));
5926
5927        // The two tables really are different: this is the pair that
5928        // must not be collapsed into one.
5929        assert!(crate::capability::swa_rope_scale_follows_model("gemma2"));
5930        assert!(!crate::capability::swa_rope_scale_follows_model("gemma3"));
5931        for arch in ["olmo2", "laguna"] {
5932            assert!(
5933                crate::capability::swa_rope_base_follows_model(arch),
5934                "{arch} seeds the SWA base from the model"
5935            );
5936            assert!(
5937                !crate::capability::swa_rope_scale_follows_model(arch),
5938                "{arch} pins the SWA scale to 1.0 (olmo2.cpp:14, laguna.cpp:48)"
5939            );
5940        }
5941    }
5942}