frink_models/capability.rs
1//! Explicit architecture capability registry for the generic GGUF path.
2//!
3//! Mirrors the pinned llama.cpp `llm_arch` / `LLM_ARCH_NAMES` inventory
4//! (`.scratch/llama.cpp/src/llama-arch.{h,cpp}`) with Frink-side
5//! classification into decoder families, memory kinds, and scope.
6//! Unknown strings and detected-but-unimplemented features fail closed
7//! (`LoadError`) instead of silently defaulting into fluent-but-wrong
8//! logits.
9//!
10//! Architecture names are registry keys only. Hot-path kernels never
11//! branch on them; load-time resolution produces an [`ArchProfile`]
12//! whose fields the decoder reads as plain data.
13
14use crate::config::RopeLayout;
15
16/// How far this architecture is in Frink's delivery scope (plan:
17/// text-generation parity; encoder/multimodal/diffusion/audio deferred).
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum ArchScope {
20 /// Autoregressive / encoder-decoder text generation -- in scope.
21 TextGeneration,
22 /// Encoder / embedding / pooling models -- deferred.
23 DeferredEncoderEmbedding,
24 /// Vision / multimodal projector paths -- deferred.
25 DeferredMultimodal,
26 /// Diffusion / masked-LM samplers -- deferred.
27 DeferredDiffusion,
28 /// Audio tokenizers / codecs -- deferred.
29 DeferredAudio,
30 /// Enum present in llama.cpp but not a real serve target here.
31 EnumOnly,
32}
33
34/// Shared execution family (maps many GGUF strings onto one engine path).
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub enum DecoderFamily {
37 /// Standard GQA (+ optional MoE) with whole-vector optional QK-norm.
38 StandardGqa,
39 /// Qwen3-style: explicit head_dim + per-head Q/K RMSNorm before RoPE.
40 Qwen3Family,
41 /// Gemma-family: embedding scale, post-norms, softcap, SWA pattern, GeGLU.
42 GemmaFamily,
43 /// Phi-family: fused QKV and/or fused gate+up SwiGLU.
44 PhiFamily,
45 /// DeepSeek-2 / Mistral4 MLA (not generic GQA).
46 Mla,
47 /// Attn + SSM / delta-net hybrids.
48 Hybrid,
49 /// Pure recurrent (Mamba / RWKV) -- no KV cache.
50 Recurrent,
51 /// T5-style encoder-decoder.
52 EncoderDecoder,
53 /// Dedicated Frink stacks (GLM DSA, DeepSeek V4, Kimi).
54 Dedicated,
55 /// In-repo synthetic fixtures.
56 TestFixture,
57}
58
59/// Memory / KV backend selected once at load (llama.cpp `create_memory`).
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub enum MemoryKind {
62 KvGqa,
63 KvIswa,
64 KvMla,
65 KvDsa,
66 KvDsv4,
67 Recurrent,
68 Hybrid,
69 None,
70}
71
72/// How `attn_q_norm` / `attn_k_norm` weights are applied (when present).
73#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
74pub enum QkNormStyle {
75 /// OLMoE: one RMSNorm over the full Q/K projection width.
76 #[default]
77 WholeVector,
78 /// Qwen3 / Gemma3: RMSNorm per head with weight length `head_dim`.
79 PerHead,
80 /// Talkie (`talkie.cpp:26,82-91`): RMSNorm per head, then ONE
81 /// learned scalar per head for Q (`attn_q_norm` is `{1, n_head}`),
82 /// and the same per-head RMSNorm with NO weight for K (`:90`,
83 /// `build_norm(Kcur, nullptr, ...)`); there is no `attn_k_norm`
84 /// tensor. Decided by architecture (`PER_HEAD_SCALAR_QK_GAIN`), not
85 /// by the weight's length: a file whose `n_head == head_dim` would
86 /// make the length ambiguous. Applied after RoPE, as the graph does.
87 PerHeadScalar,
88 /// PLaMo-2 (`plamo2.cpp:92-93,163,166`): RMSNorm per head with a
89 /// DISTINCT weight per head -- `attn_q_norm` is `{head_dim, n_head}`
90 /// and `attn_k_norm` `{head_dim, n_head_kv}`, and `build_norm` over
91 /// the 3-d `{head_dim, n_head, n_tokens}` view norms each head and
92 /// multiplies by that head's row. The weight is `n_heads * head_dim`
93 /// long, the same length as [`QkNormStyle::WholeVector`]'s, which is
94 /// why it is decided by architecture ([`PER_HEAD_DISTINCT_QK_NORM`])
95 /// and not by the length rule.
96 PerHeadDistinct,
97}
98
99/// Architectures whose Q/K norm is the per-head RMSNorm with one weight
100/// row per head ([`QkNormStyle::PerHeadDistinct`]). Measured over the
101/// 155 graphs: `attn_q_norm` created `{n_embd_head_k, n_head}` in five
102/// (`chameleon`, `command-r`, `stablelm`, which norm with LLM_NORM and
103/// are `crate::qk_layer_norm`'s; `talkie`, whose weight is `{1,
104/// n_head}`; and `plamo2`, the one RMS row).
105pub const PER_HEAD_DISTINCT_QK_NORM: &[&str] = &["plamo2"];
106
107/// See [`PER_HEAD_DISTINCT_QK_NORM`].
108pub fn uses_per_head_distinct_qk_norm(arch: &str) -> bool {
109 PER_HEAD_DISTINCT_QK_NORM.contains(&arch)
110}
111
112/// Architectures whose Q norm weight is one scalar per head and whose K
113/// norm has no weight ([`QkNormStyle::PerHeadScalar`]). Measured:
114/// `attn_q_norm` created `{1, n_head}` in one of 155 graphs,
115/// `talkie.cpp:26`.
116pub const PER_HEAD_SCALAR_QK_GAIN: &[&str] = &["talkie"];
117
118/// See [`PER_HEAD_SCALAR_QK_GAIN`].
119pub fn uses_per_head_scalar_qk_gain(arch: &str) -> bool {
120 PER_HEAD_SCALAR_QK_GAIN.contains(&arch)
121}
122
123/// How much work admitting one UNAUDITED architecture to the generic
124/// path would actually be.
125///
126/// Every architecture on the generic path that is not in
127/// [`AUDITED_GENERIC_GQA`] refuses with
128/// `LoadError::UnauditedArchitecture`, and that message used to say the
129/// same thing for all 47 of them. It hid a real difference:
130/// `bailingmoe2` needs a test fixture and nothing else, `deepseek` needs
131/// one name added to one list, and `olmo2` needs a decoder that can skip
132/// the two pre-norms it does not have. A user reading "nobody has
133/// checked this" cannot tell a one-line fix from a new attention
134/// implementation.
135///
136/// **A verdict here is a reading of BOTH trees, never a guess.** Every
137/// non-[`TriageClass::Unknown`] verdict names the `src/models/*.cpp`
138/// line that decides it and the frink file that would change.
139/// `Unknown` is a legitimate answer and says what would settle it. The
140/// precedent this rule exists for: four architectures in this very file
141/// once refused while naming a blocker that was not the real one --
142/// `glm4moe` was told it lacked an MLA hyper-parameter it must not have,
143/// and `minimax-m2` was blamed on MTP weights no converter can emit.
144#[derive(Debug, Clone, Copy, PartialEq, Eq)]
145pub enum TriageClass {
146 /// Frink already implements everything this architecture needs.
147 /// What is missing is EVIDENCE: a fixture, or a parity run against
148 /// llama.cpp on a real checkpoint.
149 FixtureAway,
150 /// One small, nameable piece is missing: an activation, a norm slot,
151 /// a routing flag, an ordering. Nameable is the bar -- if the blocker
152 /// cannot be written as a sentence naming the thing, it is not this
153 /// class.
154 OneMatchArm,
155 /// A different attention or residual structure: a norm the decoder
156 /// unconditionally applies and this model does not have, a scaled
157 /// residual, ALiBi, MLA, block-sparse, recurrent, hybrid.
158 NewCode,
159 /// Not decidable from reading the two trees. The blocker says what
160 /// would settle it.
161 Unknown,
162}
163
164impl TriageClass {
165 /// Short slug used in the refusal message.
166 pub fn label(self) -> &'static str {
167 match self {
168 TriageClass::FixtureAway => "FIXTURE-AWAY",
169 TriageClass::OneMatchArm => "ONE MATCH ARM",
170 TriageClass::NewCode => "NEW CODE",
171 TriageClass::Unknown => "UNKNOWN",
172 }
173 }
174
175 /// One sentence saying what the class means, so the message stands
176 /// alone without this doc comment.
177 pub fn headline(self) -> &'static str {
178 match self {
179 TriageClass::FixtureAway => {
180 "frink already implements everything this architecture needs; what is \
181 missing is EVIDENCE, not capability"
182 }
183 TriageClass::OneMatchArm => {
184 "one small, named piece is missing -- an activation, a norm slot, a \
185 routing flag or an ordering"
186 }
187 TriageClass::NewCode => {
188 "a different attention or residual structure than the generic decoder \
189 computes; this is not a fixture away"
190 }
191 TriageClass::Unknown => {
192 "reading both trees did not settle this one; the note below says what \
193 would"
194 }
195 }
196 }
197}
198
199/// One architecture's triage verdict, carried on its own catalog row.
200///
201/// Deliberately NOT a second table keyed by architecture name. This repo
202/// has fixed three separate bugs caused by two structures disagreeing
203/// about the same architecture, so the verdict lives on the
204/// [`ArchProfile`] the loader already resolves, and
205/// `every_unaudited_generic_architecture_is_triaged_or_listed_as_pending`
206/// pins that no generic row can exist without one or the other.
207#[derive(Debug, Clone, Copy, PartialEq, Eq)]
208pub struct UnauditedTriage {
209 pub class: TriageClass,
210 /// What is missing, with the llama.cpp `src/models/*.cpp` line that
211 /// decides it and the frink file that would change.
212 pub blocker: &'static str,
213}
214
215/// Unaudited generic-path architectures nobody has read against
216/// llama.cpp's graph yet.
217///
218/// This is a TO-DO, not cover. A name here means the refusal honestly
219/// says "not triaged" rather than inventing a class; a name leaves this
220/// list only by gaining an [`UnauditedTriage`] on its catalog row, and
221/// the two tests below make it impossible for a name to be on both or on
222/// neither.
223pub const TRIAGE_PENDING: &[&str] = &[
224 // Norm-RoPE group.
225 // NEOX-RoPE group.
226];
227
228/// This architecture's triage verdict, or `None` when it has not been
229/// triaged (see [`TRIAGE_PENDING`]) or does not need one.
230pub fn unaudited_triage(arch: &str) -> Option<UnauditedTriage> {
231 resolve_profile(arch).and_then(|p| p.triage)
232}
233
234/// The triage half of the `UnauditedArchitecture` refusal, rendered for
235/// the user.
236///
237/// Appended to the generic "nobody has verified this" sentence so the
238/// message says which of the three classes the architecture is in and
239/// what specifically is missing, rather than the same paragraph for all
240/// 47.
241pub fn unaudited_refusal_detail(arch: &str) -> String {
242 match unaudited_triage(arch) {
243 Some(t) => format!(
244 "TRIAGE ({}): {}. {}.",
245 t.class.label(),
246 t.class.headline(),
247 t.blocker
248 ),
249 None => format!(
250 "TRIAGE: not done for `{arch}` yet -- nobody has read llama.cpp's \
251 src/models/*.cpp for it against the generic decoder, so this refusal names \
252 no blocker and you should not read it as one. Triaging the remaining \
253 architectures is docs/plans/llama-cpp-gap-inventory.md section 8, item 6."
254 ),
255 }
256}
257
258/// Architectures on the shared generic-GQA path that somebody has
259/// actually PROVEN, and the evidence for each.
260///
261/// The generic path is a guess: it assumes an architecture is plain GQA
262/// because nothing said otherwise. That guess has already been wrong
263/// five times. `gpt2`, `mpt`, `refact`, `bloom` and `jais` all sat here
264/// computing ALiBi or learned absolute position embeddings as though
265/// they were NEOX RoPE, and every downstream guard missed them: two
266/// hardcode their ALiBi slope with no GGUF key, one leaves no unread
267/// tensor, and the RoPE pin excluded their group by construction.
268///
269/// So membership here is not "we think this works", it is "there is a
270/// benchmark row, a pinned logit comparison against llama.cpp, or a
271/// fixture". Everything else on the generic path is UNAUDITED and says
272/// so at load time rather than running and hoping.
273///
274/// Adding a name here without evidence defeats the entire point.
275pub const AUDITED_GENERIC_GQA: &[&str] = &[
276 // Bench rows in benchmarks/suite.json, measured against llama.cpp
277 // on the same host and file.
278 "llama", // TinyLlama, Mistral, Mixtral, SmolLM2, Llama-3.x all tag llama
279 "qwen2", // Qwen2.5-0.5B
280 "qwen2moe", // Qwen1.5-MoE-A2.7B
281 "qwen3", // Qwen3-0.6B
282 "olmoe", // OLMoE-1B-7B
283 "gemma2", // Gemma-2-2B
284 "gemma3", // Gemma-3-1B
285 "phi3", // Phi-4-mini tags phi3
286 // Pinned against real libllama logits in tests/.
287 "gpt-oss",
288 "dots1",
289 // tests/qwen3moe_graph.rs: a synthetic 2-layer fixture
290 // (scripts/make_qwen3moe_fixture.py) compared against llama.cpp's
291 // own qwen3moe graph via libllama, on all three forward paths.
292 // Carries per-head QK norm before RoPE, head_dim * n_head != n_embd,
293 // GQA, NEOX RoPE, softmax gating with renormalised top-k, and
294 // n_ff != n_ff_exp.
295 "qwen3moe",
296 // tests/one_match_arm_graphs.rs: five architectures that were
297 // triaged ONE MATCH ARM, each admitted with the same evidence
298 // qwen3moe has -- a synthetic fixture whose golden logits come from
299 // llama.cpp's own graph via libllama, checked on all three forward
300 // paths. The arm each one needed is named beside it; every fixture
301 // is built so that getting that arm wrong moves the logits by orders
302 // of magnitude more than the comparison tolerance.
303 //
304 // `deepseek` (V1, not the MLA deepseek2): top-k weights are NOT
305 // renormalised (deepseek.cpp:145-155 passes norm_w=false and no
306 // converter writes expert_weights_norm), so the fixture carries no
307 // such key and the answer has to come from
308 // NO_TOPK_RENORMALIZE_ARCHITECTURES.
309 "deepseek",
310 // `bailingmoe`: llama.cpp reads leading_dense_block_count and never
311 // branches on it (bailingmoe.cpp:5 vs :39-54). The fixture sets the
312 // key to 1 and ships NO dense FFN on layer 0.
313 "bailingmoe",
314 // `seed_oss`: the pre-FFN norm is stored as post_attention_norm and
315 // there is no ffn_norm (seed-oss.cpp:36-37,113-115) -- gpt-oss's
316 // slot, now a named list rather than an `arch == "gpt-oss"` flag.
317 "seed_oss",
318 // `maincoder` and `hunyuan-moe`: per-head QK norm applied AFTER RoPE
319 // (maincoder.cpp:78-95, hunyuan-moe.cpp:93-118). Both fixtures use
320 // QK-norm weights centred near 1.5 so the ordering is visible.
321 "maincoder",
322 "hunyuan-moe",
323 // `hunyuan-dense`: the same post-RoPE QK-norm order (it has no graph
324 // of its own -- models.h:1830-1834 derives it from
325 // llama_model_hunyuan_vl) PLUS the NTK-alpha RoPE base rescale at
326 // hunyuan-vl.cpp:8-12, which is now `rope_ntk_alpha`. Its fixture
327 // carries `hunyuan-dense.rope.scaling.alpha` explicitly, because the
328 // HUNYUAN_DENSE converter does that arithmetic in Python and writes
329 // the already-scaled base (conversion/hunyuan.py:254-281) -- the
330 // `add_rope_scaling_alpha` at :356 is HunyuanVLTextModel, i.e. the
331 // separate `hunyuan-vl` row. The triage verdict cited that line for
332 // this architecture and was wrong about it.
333 "hunyuan-dense",
334 // `ernie4_5-moe`: the MoE sibling of the audited `ernie4_5`. Its
335 // interleave step is a REFUSAL rather than an implementation, and
336 // that is the finding, not a shortcut: llama.cpp's tensor loader
337 // (ernie4-5.cpp:49) creates expert tensors for every layer past the
338 // leading-dense prefix with NO step in the condition, while its
339 // graph (ernie4-5-moe.cpp:64) takes the dense branch when
340 // `(il + 1) % step != 0`, so a checkpoint whose interleave really
341 // interleaves cannot be loaded by llama.cpp at all -- measured, on a
342 // two-step fixture, as `check_tensor_dims: tensor
343 // 'blk.2.ffn_gate_inp.weight' not found`. Both published ERNIE-4.5
344 // MoE checkpoints carry a step of 1, which is what the golden
345 // fixture pins; `moe_interleave` refuses anything else by name.
346 "ernie4_5-moe",
347 // tests/fixture_away_graphs.rs: architectures that were triaged
348 // FIXTURE-AWAY -- frink already built their graph, and only the
349 // evidence was missing. Same standard as the rows above: a synthetic
350 // fixture from `scripts/make_<arch>_fixture.py` whose golden values
351 // come from llama.cpp's own graph via libllama, compared on prefill,
352 // decode and continuous batching, with a sabotage test per row
353 // proving the fixture can SEE the fact its architecture turns on.
354 //
355 // Each was checked, against the C, on the six things this repo has
356 // lost at least once: RoPE variant, SWA pattern and phase,
357 // `attention_scale`, the two post-norm slots, and QK-norm ordering.
358 //
359 // `internlm2` (internlm2.cpp:3-11,25-33,59-122): plain llama, NORM
360 // RoPE, `1/sqrt(head_dim)` scale, no post-norms, no QK-norm, no SWA.
361 // Its fixture carries the OPTIONAL q/k/v projection biases real
362 // InternLM2 exports ship.
363 "internlm2",
364 // `xverse` (xverse.cpp:3-12,14-35,59-121): the same, with no biases.
365 "xverse",
366 // `gemma` (gemma.cpp:3-11,13-34,41-138): Gemma-1, the oldest row of
367 // the family and the last one that was not evidenced. Its three
368 // Gemma-specific pieces were already implemented for `GemmaFamily`
369 // and the fixture is what proves each of them: the sqrt(n_embd)
370 // embedding scale (:49), GeGLU rather than SwiGLU (:112,
371 // LLM_FFN_GELU) and a `1/sqrt(head_dim)` attention scale that
372 // llama.cpp reaches by scaling Q at :86 and passing kq_scale = 1.0f
373 // at :91, which is what leaving `attention_scale` as None already
374 // produces. Its lm_head is TIED with no fallback (:20), so the
375 // fixture ships no `output.weight` and the embedding scale is not
376 // cancelled downstream. Gemma-1 declares no softcap and no sliding
377 // window, so the Gemma-2/3 machinery must resolve to inert, and
378 // `tests/fixture_away_graphs.rs` asserts that rather than assuming
379 // it.
380 "gemma",
381 // `ernie4_5` DENSE (ernie4-5.cpp:36-69,95-149): NORM RoPE, head_dim
382 // decoupled from n_embd/n_head. `ernie4_5-moe` is a different row
383 // with its own fixture, above.
384 "ernie4_5",
385 // `baichuan` (baichuan.cpp:5-14,17-40,64-137): the 7B ONLY. The 13B
386 // is a different model under the same string and is refused by name
387 // on `block_count == 40` in loader.rs before this list is consulted,
388 // because llama.cpp picks ALiBi-and-no-RoPE off the layer count with
389 // no GGUF key to declare it. The fixture therefore has 32 layers: a
390 // 2-layer one would be LLM_TYPE_UNKNOWN and get no RoPE at all.
391 "baichuan",
392 // `exaone` (exaone.cpp:3-10,12-40,65-121): EXAONE 3.x, NEOX RoPE,
393 // tied lm_head. NOT `exaone4` (no pre-norms) and NOT `exaone-moe`
394 // (no RoPE on the full-attention layers); both stay refusing.
395 "exaone",
396 // `plamo3` (plamo3.cpp:3-60,91-193): the sandwich-norm row, and the
397 // only one here with a sliding window. Its verdict was FIXTURE-AWAY
398 // and was WRONG by one tensor name: plamo3 is the sole architecture
399 // upstream that creates ATTN_POST_NORM / FFN_POST_NORM through the
400 // two-argument `tn` overload (:52,55), so it asks for
401 // `blk.N.post_attention_norm` and `blk.N.post_ffw_norm` with NO
402 // `.weight`, and gguf-py emits exactly those names for it. frink
403 // read only the suffixed spelling; `load_norm_vec_either_spelling`
404 // in loader.rs now reads both, and says why.
405 //
406 // Its SWA is a real pattern with a real phase -- period from
407 // `attention.sliding_window_pattern`, `dense_first = false` from
408 // `set_swa_pattern`'s default -- and the fixture sets a window
409 // narrower than the prompt so the mask actually bites.
410 "plamo3",
411 // tests/granite_family_graphs.rs: the Granite family, which was
412 // triaged NEW CODE on four SCALAR MULTIPLIERS the generic decoder
413 // did not apply -- `logit_scale`, `residual_scale`,
414 // `embedding_scale` and `attention.scale`. They are hparams rather
415 // than tensors, so `assert_every_tensor_consumed` cannot see them
416 // and a Granite checkpoint would otherwise have loaded and answered
417 // at the wrong scale. `crate::scalar_multipliers` implements all
418 // four ONCE, parameterised by architecture, and
419 // `capability::unsupported_scaling_keys` is now DERIVED from that
420 // same table rather than restated beside it.
421 //
422 // `granite` (granite.cpp:5-10,180,225,235-238,288-292) is the dense
423 // row. `granitemoe` has no graph of its own -- `models.h:1583-1591`
424 // is `using graph = llama_model_granite::graph` -- so the two differ
425 // in the FFN and in nothing else, and its fixture carries the MoE
426 // branch, an UNGATED shared expert, and expert tensors sized from
427 // `n_ff` rather than `n_ff_exp`.
428 //
429 // `granite-moe` is a frink-only alias: `llama-arch.cpp:101` spells
430 // the architecture `granitemoe` and no GGUF anywhere says
431 // `granite-moe`, so there is no libllama golden for it and there
432 // never can be. Its evidence is a SECOND fixture, byte-identical
433 // except for the architecture string and key prefixes, asserted
434 // against `granitemoe`'s libllama golden -- which is the only thing
435 // that can keep an alias nothing outside frink would ever exercise
436 // from drifting away from the row it aliases.
437 //
438 // The `rope_finetuned` half of the verdict landed as a REFUSAL
439 // (`crate::rope_finetuned`) and was SERVED on 2026-09-14 as
440 // `RopeLayers::Never` when Granite-4.0 needed it: granite.cpp:33-35
441 // reads `{arch}.rope.scaling.finetuned` as a switch for RoPE itself,
442 // and a file declaring it false runs UNROTATED, which the fixture
443 // that had evidenced the refusal now matches.
444 "granite",
445 "granitemoe",
446 "granite-moe",
447 // `bailingmoe2` (bailingmoe2.cpp:23-87,111-198): Ling-2.0. The one
448 // MoE row in this batch, so the two MoE facts do arise and both are
449 // asserted: SIGMOID gating, read from the file's REQUIRED
450 // `expert_gating_func` (:11) against frink's softmax default, and
451 // `expert_weights_norm` (:10), also read from the file. Its shared
452 // expert is `n_ff_shexp * n_expert_shared` wide (:58), not
453 // `n_ff_shexp`. Per-head QK norm BEFORE RoPE (:123-135), fused
454 // attn_qkv, leading dense layers that llama.cpp really does branch
455 // on (:57) -- unlike `bailingmoe`, which reads the same key and
456 // ignores it.
457 "bailingmoe2",
458 // tests/post_norm_only_graphs.rs: the POST-NORM-ONLY family, two
459 // architectures and ONE implementation (`crate::norm::NormOp`).
460 // Neither has an `attn_norm` or an `ffn_norm` tensor; both read the
461 // raw residual at both sublayers and norm each branch's output
462 // before its residual add. Same evidence standard as the rows
463 // above: a synthetic fixture per row whose golden logits come from
464 // llama.cpp's own graph via libllama, on all three forward paths.
465 //
466 // `olmo2` (olmo2.cpp:45-52,92,160-165,169,177-182): WHOLE-VECTOR
467 // QK-norm -- :45-46 sizes the norms `{n_embd}` and
468 // `{n_head_kv * n_embd_head}` and :106-112 applies them to the 2-D
469 // projections before `ggml_reshape_3d`. An `olmo2` file carrying
470 // BOTH a sliding window and a rope scaling is Olmo-3, ropes its two
471 // kinds of layer differently (:120-146), and is refused by name in
472 // loader.rs.
473 "olmo2",
474 // `exaone4` (exaone4.cpp:60-67,118,152-169): the same graph with
475 // PER-HEAD QK-norm instead -- :61-62 sizes them `{n_embd_head_k}`
476 // and :127-128 applies them to what `build_qkv` already reshaped.
477 // NOT the audited `exaone` row, which is EXAONE 3.x and a plain
478 // pre-norm llama.
479 //
480 // BOTH SIZES run. EXAONE-4 32B (`block_count == 64`) used to be
481 // refused by name: llama.cpp turns SWA on off the layer count
482 // (:4-9) and then ropes only the sliding layers (:116), so its
483 // full-attention layers get no rotation. `crate::rope_layers`
484 // implements that rule and `capability::swa_disabled_by_arch`
485 // carries the layer-count gate it depends on, with a 64-layer
486 // libllama-golden fixture in `tests/no_rope_layer_graphs.rs`.
487 "exaone4",
488 // tests/no_rope_layer_graphs.rs: the PER-LAYER-RoPE group, three
489 // rows on one rule (`crate::rope_layers`). llama.cpp gates rotation
490 // per layer in six architectures and frink had no way to say so,
491 // which cost `smollm3` and `exaone-moe` an outright refusal and
492 // EXAONE-4 32B a refusal by name.
493 //
494 // `exaone-moe` (exaone-moe.cpp:136,155-161): `is_swa(il)` around
495 // both `ggml_rope_ext` calls, with `swa_type` pinned to STANDARD at
496 // :4 -- which is `exaone4.cpp:116` with the second disjunct nailed
497 // false, i.e. the same rule and not a similar one. Its MoE half
498 // (:72-93) is machinery frink already had and the fixture carries
499 // all of it: leading dense, `exp_probs_b`, a shared expert sized by
500 // `expert_shared_feed_forward_length`, sigmoid gating from
501 // metadata, `expert_weights_scale`/`_norm`.
502 "exaone-moe",
503 // `smollm3` (smollm3.cpp:5,69): `(il + 1) % 4 != 0`, nine layers of
504 // a 36-layer SmolLM3-3B unrotated, from a literal with no GGUF key.
505 // The graph is otherwise the plain pre-norm llama one, so this is
506 // the row where the rule is the ONLY thing -- which is why it was
507 // in the "No RoPE at all" refusal group beside the ALiBi
508 // architectures until the rule existed.
509 "smollm3",
510 // tests/one_match_arm_graphs.rs: the FUSED-`attn_qkv.bias` pair.
511 // `create_tensor_qkv` (llama-model.cpp:2886-2900) creates the bias
512 // beside a fused `wqkv`, and `build_qkv` (llama-graph.cpp:1605-1609)
513 // adds it to the fused projection before splitting. frink split the
514 // fused WEIGHT and then looked for the bias only under the split
515 // `attn_q.bias` names, so it was dropped and all three projections
516 // ran unbiased. Both halves now come out of one decision in
517 // `qkv_fused`, sliced by the same spans.
518 //
519 // `chatglm` (chatglm.cpp:25-52,58-161) was the LAST ONE MATCH ARM
520 // row anywhere in this file. It also pins the two things that made
521 // it look fixture-away and are not: PARTIAL RoPE
522 // (conversion/chatglm.py:151 writes `rope_dimension_count` as
523 // `head_dim * 0.5`) and the fused gate+up SwiGLU
524 // (chatglm.cpp:48,128-133), which is phi3's call shape.
525 "chatglm",
526 // `qwen` is Qwen-1 (`QWenLMHeadModel`), not qwen2/qwen3. Its
527 // `attn_qkv.bias` is REQUIRED (qwen.cpp:28, flag `0`), which is
528 // stronger than chatglm's optional one, and it needed a SECOND arm
529 // the chatglm verdict did not name: qwen.cpp:33-35 sizes every FFN
530 // matrix at `n_ff / 2`, because Qwen-1's `intermediate_size` counts
531 // gate and up together. See `FFN_LENGTH_COUNTS_GATE_AND_UP` in
532 // loader.rs.
533 "qwen",
534 // tests/minicpm_graphs.rs: MiniCPM, which was never an unaudited
535 // row -- it was refused BY NAME, because the thing it does is
536 // invisible in the file. `models.h:1594-1601` is
537 // `using graph = llama_model_granite::graph`, so it is the Granite
538 // graph object verbatim; what `minicpm.cpp:5-7` adds is DEFAULTS,
539 // assigning an embedding multiplier of 12.0, a residual multiplier
540 // of `1.4/sqrt(n_layer)` and a logit multiplier of `256/n_embd`
541 // before `:12-14` lets the file override them. A MiniCPM export
542 // carrying none of the three keys is still scaled by all three, so
543 // `unsupported_scaling_keys` -- a key-PRESENCE gate -- can see
544 // nothing to refuse. `scalar_multipliers::MultiplierDefaults` is
545 // that hook, and the fixture that evidences it declares NO key at
546 // all, which is the only fixture shape that can tell the hook from
547 // its absence. A second fixture declares all three and pins that
548 // the file still wins.
549 //
550 // It is Granite's arithmetic minus one column: `minicpm.cpp:3-24`
551 // never reads `{arch}.attention.scale`, so that key stays refused
552 // for this row by the derived list.
553 "minicpm",
554 // tests/olmo_graphs.rs: OLMo-1, the THIRD norm shape and the reason
555 // `crate::norm::NormOp` has three variants rather than two. It is
556 // pre-norm like `llama` -- `olmo.cpp:65-67` before attention,
557 // :104-106 before the FFN -- so it is NOT the post-norm-only
558 // topology `olmo2` and `exaone4` share. What differs is the norm
559 // FUNCTION: all three sites are
560 // `build_norm(x, NULL, NULL, LLM_NORM, il)`, a non-parametric
561 // LayerNorm, and `olmo.cpp:15-36` creates no norm tensor at all --
562 // no `attn_norm`, no `ffn_norm`, no `output_norm`.
563 //
564 // Its lm_head is TIED with a fallback (:21-25) and its RoPE is NORM
565 // (llama-model.cpp:2585). Its CLAMP -- `olmo.cpp:5` reads
566 // `{arch}.attention.clamp_kqv`, `llama-graph.cpp:1611-1652` applies
567 // it to Q, K and V inside `build_qkv`, and `conversion/olmo.py:23-25`
568 // really writes it for OLMo-7B-Twin-2T and OLMo-1.7-7B -- was a
569 // refusal by name and is implemented now (`crate::clamp_kqv`), with
570 // the clamped fixture matched against libllama rather than refused:
571 // `dbrx` below needed the same clamp as a REQUIRED key.
572 "olmo",
573 // tests/dbrx_graphs.rs: DBRX, NEW CODE on three blockers that each
574 // extended a seam landed the day before. `dbrx.cpp:69-71`, `:110-112`
575 // and `:140-142` norm with `LLM_NORM` and a weight but no bias, which
576 // is `crate::norm::NormOp::LayerNorm` -- the variant the OLMo-1 work
577 // deliberately left unwritten until a row called it; `dbrx.cpp:5`
578 // reads `attention.clamp_kqv` as REQUIRED, which `crate::clamp_kqv`
579 // applies after the bias through the ONE helper every host body
580 // shares (`decoder/qkv_bias.rs`); and `dbrx.cpp:34,110-113` keep the
581 // pre-FFN norm under `blk.N.attn_output_norm`, which
582 // `crate::norm_sites` reads into the same slot `gpt-oss` keeps under
583 // `post_attention_norm`. Fused `attn_qkv` (:31), NEOX RoPE
584 // (llama-model.cpp:2617), SiLU MoE with softmax gating and top-k
585 // renormalisation (:115-125), untied lm_head (:24).
586 "dbrx",
587 // tests/grok_graphs.rs: Grok-1, NEW CODE on the MiniCPM shape --
588 // `grok.cpp:5-12` seeds SEVEN hyper-parameters before `:14-27` let
589 // the file override them, so a file declaring none is still scaled
590 // by all of them and a key-presence gate sees nothing.
591 // `scalar_multipliers::MultiplierDefaults::Grok` is that hook:
592 // `embedding_scale` (78.38), `logit_scale` as a MULTIPLY (:211, the
593 // `LogitScaleUse::AsIs` variant the module had named as absent),
594 // `attention.output_scale` (0.0884, a fifth key that resolves into
595 // the `attention_scale` slot) and the attention softcap default of
596 // 30. The attention itself is `kq_scale = 1.0f` (:137) with the
597 // real scale inside the tanh (llama-graph.cpp:2572-2582), which is
598 // exactly "pre-scale Q, then softcap"; `router_logit_softcapping`
599 // and `attention.temperature_length` are read at :20,:23 and applied
600 // NOWHERE in the graph (measured: no other reference in `src/`), so
601 // frink ignores them the same way. `blk.N.attn_output_norm` is the
602 // POST-attention norm here (:143-146, before the residual add at
603 // :148) and `layer_output_norm` / `post_ffw_norm` the post-FFN one
604 // (:75-78, :185-188): a `crate::norm_sites` row. GELU MoE with
605 // softmax gating (:158-168), NEOX RoPE (llama-model.cpp:2616),
606 // tied-with-fallback lm_head (:46-51). Grok-2's parallel dense FFN
607 // (`:171-184`, `sqrt(2)/2` on the sum) is refused BY NAME in
608 // `loader.rs`, so this row is admitted for Grok-1.
609 "grok",
610 // tests/ungated_ffn_graphs.rs: Arcee AFM, NEW CODE on ONE fact --
611 // the FFN has no gate. `arcee.cpp:39-40` creates `ffn_up` and
612 // `ffn_down` only, and `:123-128` is `build_ffn` with a NULL gate,
613 // `LLM_FFN_RELU_SQR` and `LLM_FFN_SEQ`: `down(relu(up(x))^2)`.
614 // frink spells that as `FfnActivation::ReluSqr`, which the loader
615 // serves by ALIASING the expert's gate to its up matrix and
616 // `frink_moe::GluAct::ReluSqr` (reads `up` alone), so the gated
617 // struct and every gated path are untouched and `relu(up)^2` is
618 // what they compute; the dense hot paths skip the aliased matmul.
619 // (`GluAct::Reglu`, `relu(gate) * up`, served it until
620 // `smallthinker` needed that op on a REAL gate; see `uses_reglu`.)
621 // No fused device kernel spells it, so `fused_kernel_gelu_flag`
622 // returns `None` and `metal_can_serve_model` keeps the model off
623 // the stacks -- which replaced six `gelu = !is_swiglu()` sites
624 // that would have run a third activation as GELU. Everything else
625 // is `llama` (:6 says so): NORM RoPE (llama-model.cpp:2600),
626 // optional `output.weight` with a tied fallback (:20-25),
627 // `n_embd_head == n_rot` asserted (:51-52). The same FFN is in
628 // `plm`, `nemotron`, `jais2` and `nemotron-h`, each of which refuses
629 // for something else; see `uses_relu_sqr`.
630 "arcee",
631 // tests/per_layer_shape_graphs.rs: the PER-LAYER-SHAPE pair, two
632 // rows on one seam (`crate::layer_shapes`). llama.cpp reads
633 // `head_count`, `head_count_kv` and `feed_forward_length` as
634 // scalar-or-array for every architecture and hands most graphs
635 // layer 0; these two index the arrays in both their tensor loader
636 // and their graph, and frink carried all three as scalars. The
637 // scan that sized the seam is recorded in
638 // `layer_shapes::PER_LAYER_SHAPE_ARCHS`.
639 //
640 // `deci` (deci.cpp:30-34 loader, :103-105 graph): all three per
641 // layer AND a three-way branch on them -- `n_head == 0` passes the
642 // residual through with no norm (:107-109), `n_head_kv == 0` runs
643 // `attn_norm` then `wo` alone (:115-118), `n_ff == 0` skips the FFN
644 // (:147-149). `AttnShape::{Gqa, Linear, Absent}` and
645 // `LayerShape::ffn_dim` are those, and the fixture has one layer of
646 // each kind. A second fixture is the DeciLM-7B shape
647 // (conversion/deci.py:114-118: `head_count_kv` alone as an array).
648 // The FFN-free layer WITH attention is refused: `:147-149`
649 // `continue`s before the residual add, and scaling that layer's
650 // attention weights by 3 leaves libllama's logits byte-identical
651 // (measured), so the branch is dead in the reference graph and
652 // frink will not pin it. NORM RoPE (llama-model.cpp:2576).
653 "deci",
654 // `openelm` (openelm.cpp:26-28 loader, :67-69 graph): all three per
655 // layer, one fused `wqkv` per layer sized `(2*n_head_kv(i) +
656 // n_head(i)) * n_embd_head_k` (:34) -- `qkv_fused::FusedQkvRows::of`
657 // takes the layer now -- per-head QK-norm before RoPE (:82-102),
658 // NEOX RoPE (llama-model.cpp:2650), a tied lm_head with no fallback
659 // (:22). The fixture's three layers share no KV width and no FFN
660 // width. Its converter writes the arrays (conversion/openelm.py:
661 // 57-59), which `layer_shapes::read_u64_per_layer` reads where
662 // `GgufValue::as_u64` used to die on them.
663 "openelm",
664 // tests/gated_attention_graphs.rs: the GATED-ATTENTION pair, two
665 // rows on one seam (`crate::attn_gate`). `afmoe.cpp:73,154,183-185`,
666 // `laguna.cpp:110-124,211,246-257` and `step35.cpp:96,268-284` each
667 // project a gate from the SAME normed input Q/K/V read and multiply
668 // the attention output by it BEFORE `wo`; they differ in the
669 // activation (sigmoid / softplus), in the width (per element / per
670 // head / decided by the tensor's shape) and in whether the tensor
671 // may be absent. Read side by side before being called one cause:
672 // the three graphs, six create sites measured over all 155.
673 //
674 // `afmoe` (afmoe.cpp:73,120,154,183-185): sigmoid, per element,
675 // REQUIRED. The other afmoe-only fact is `:120`, `sqrt(n_embd)` on
676 // the embeddings from arithmetic -- the only non-Gemma graph that
677 // does it (`embeddings_scaled_by_sqrt_n_embd`). Everything else it
678 // needs it already had, and the fixture carries all of it: dual
679 // norms on both blocks, per-head QK norm before RoPE, leading
680 // dense, `exp_probs_b`, one shared expert, sigmoid gating with NO
681 // key (`:29-30`), the NoPE layer from `crate::rope_layers`, and a
682 // window with its own `rope.freq_base_swa`. NEOX RoPE
683 // (llama-model.cpp:2676-2677).
684 "afmoe",
685 // `laguna` (laguna.cpp:110-124,211,246-257): SOFTPLUS, per head OR
686 // per element -- `:112-123` reads the width off the stored tensor
687 // and aborts on any other -- REQUIRED. Two fixtures, one per width:
688 // the M.1 shape (no window, per element, uniform heads) and the
689 // XS.2 shape (window, period 4 dense-first, per head, and
690 // `head_count` as a per-layer ARRAY, which `crate::layer_shapes`
691 // carries). One thing stays refused by name in `loader.rs`, from a
692 // fixture that has it: a window together with a RoPE scaling
693 // (`:48,184-192` run the sliding layers with YaRN off, the Olmo-3
694 // rule). `rope.dimension_count_swa` (`:50`) differing from
695 // `rope.dimension_count` -- a second rotary width -- was the other
696 // and is SERVED since `step35` closed on the same two-valued width
697 // (`ModelConfig::rope_dim_swa`, `crate::swa_geometry`); the
698 // XS.2-shaped fixture that carries it matches libllama. NEOX RoPE
699 // (llama-model.cpp:2676-2677).
700 "laguna",
701 // `mellum` (mellum.cpp:12-17,45-68,108-197): the per-layer
702 // sliding-window ARRAY, honoured -- the scalar overload of
703 // `get_key_or_arr` first, the array overload on its `false`, and
704 // `conversion/mellum.py:28` always writes the array. The one
705 // generic-path graph that honours it, so the fixture's array
706 // [T, T, F, T] deliberately disagrees with the seeded period-4
707 // [T, T, T, F] on two layers and the golden is the file's layout,
708 // not the seed's. Everything else is machinery it already had: NEOX
709 // RoPE (llama-model.cpp:2682), per-head QK norm before RoPE
710 // (`:50-51,120-124`), softmax top-k renormalised (`:186`), the
711 // expert width from its own key (`:5`). A window together with a
712 // RoPE scaling -- `:128-142`, the Olmo-3 rule, and what every real
713 // Mellum2 export declares -- stays refused by name
714 // (`crate::swa_geometry`).
715 "mellum",
716 // tests/per_layer_activation_graphs.rs: `apertus` (apertus.cpp:6-9,
717 // 45-46, 93-96, 129-142), the first architecture whose FFN
718 // activation takes PARAMETERS THAT VARY BY LAYER -- xIELU with four
719 // `n_layer`-long arrays (`xielu.alpha_n`, `.alpha_p`, `.beta`,
720 // `.eps`, no architecture prefix) that `ggml_xielu` folds through
721 // a softplus at graph build. `crate::act_layers` reads them exactly
722 // as `get_key_or_arr` does (an array at `n_layer` length or a
723 // scalar broadcast; a second fixture carries the scalar form and
724 // libllama honours the broadcast), `frink_moe::GluAct::Xielu`
725 // carries one layer's four, and `ModelConfig::layer_ffn_act(il)`
726 // replaced the model-wide `GluAct::from(ffn_activation)` at every
727 // FFN body, so no site can take the activation without saying
728 // which layer's. The FFN is UNGATED like `arcee`'s and takes the
729 // same gate-to-up alias; per-head RMS QK-norm before RoPE; NEOX.
730 // Its optional `attn_q_norm.bias` / `attn_k_norm.bias` are created
731 // and never read upstream (`crate::unread_tensors`, measured). No
732 // fused Metal kernel spells xIELU, so every Metal launch refuses
733 // it through `ModelConfig::model_ffn_act`.
734 "apertus",
735 "step35",
736 // tests/gated_attention_graphs.rs: `spark2_5` (Spark-2.5 1.7B),
737 // the first architecture closed against the pin moved on
738 // 2026-09-19. Its one blocker was the per-head sigmoid attention
739 // gate, which is `crate::attn_gate`'s existing pair with the
740 // tensor REQUIRED (`src/models/spark2-5.cpp:41,97-105`); the
741 // fixture carries the window ARRAY with its own RoPE base, the
742 // per-layer head counts that size the gate, and a full-attention
743 // layer in the middle of sliding ones.
744 "spark2_5",
745 // tests/no_rope_layer_graphs.rs: `maple` (Maple-20B), the second
746 // row closed against the pin moved on 2026-09-19. Its one blocker
747 // was the per-layer RoPE gate -- `maple.cpp:88` rotates the
748 // sliding layers and not the full ones, `RopeLayers::SlidingOnly`
749 // -- and the fixture carries the window array, the per-layer
750 // expert widths, the per-head QK norm and the clamp arrays beside
751 // it, with layer 2 the unrotated one.
752 "maple",
753 // tests/granite_swa_graphs.rs: `granite_swa` (Granite 4.1), the
754 // third row closed against the moved pin. Its blockers were two
755 // per-layer tables: the `expert_used_count` ARRAY, which the
756 // loader reads scalar-or-array since the pin moved, and
757 // `attention.rope_pattern`, the FIRST upstream graph that lets the
758 // file say which layers rotate (`RopeLayers::FileMask`). The
759 // fixture's rope pattern and window array disagree about which
760 // layer is special, so a loader that read one into the other is
761 // caught.
762 "granite_swa",
763 // tests/muse_glimmer_graphs.rs: `muse-glimmer`, the fourth row
764 // closed against the moved pin. Two norm facts no other
765 // architecture has -- a weightless RMS on the EMBEDDINGS and a
766 // post-norm epsilon that is a literal in the graph rather than the
767 // model's key -- on top of four tables that each gained one name.
768 "muse-glimmer",
769 // tests/hrm_text_graphs.rs: `hrm_text` (DFM Mimir 1B), the fifth
770 // row closed against the moved pin and the first decoder here with
771 // TWO residual streams. `crate::hrm` holds them and
772 // `crate::layer_loops::LayerLoops::Hrm` is the schedule that says
773 // which stack a logical layer runs and which stream it writes.
774 "hrm_text",
775 // tests/attn_temperature_graphs.rs: `mistral3` (mistral3.cpp:5,
776 // 14-17, 153-156), every Ministral-3 export. Its one blocker was
777 // the PER-POSITION ATTENTION TEMPERATURE, `attention.temperature_scale`,
778 // which llama-graph.cpp:163-167 turns into `log(floor(pos /
779 // floor_scale) + 1) * scale + 1` per token and the graph multiplies
780 // into Q after RoPE; `crate::attn_temperature` is the seam, with
781 // the census (three graphs of 155 build the input, this the only
782 // generic-path one) and the floor resolved as `llama-model.cpp:
783 // 1164-1165` resolves it -- `context_length` first, the YaRN key
784 // over it -- which the second fixture measures. Two corrections
785 // to its verdict: the graph is either dense or MoE on every layer
786 // with NO leading-dense split and NO shared expert (`:64-84` create
787 // `_shexp` only under an `n_ff_shexp` its hparams never set, and
788 // no graph line reads them); and `rope.scaling.yarn_log_multiplier`
789 // (`:9`) adjusts a YaRN MAGNITUDE term frink turned out not to
790 // apply at all -- `crate::yarn_magnitude`, evidenced on two more
791 // fixtures with the factor at 4. NORM RoPE, `1/sqrt(head_dim)`.
792 "mistral3",
793 // tests/router_input_graphs.rs: `smallthinker`, NEW CODE on the
794 // ROUTER OPERAND -- `smallthinker.cpp:111` computes the router
795 // logits from `inpL`, the raw layer input before `attn_norm` and
796 // before attention, and `:151-161` passes them into `build_moe_ffn`
797 // as a precomputed `probs` with a NULL `ffn_gate_inp`. Four graphs
798 // of 155 pass `probs_in` (measured, `crate::router_input`); this is
799 // the only one on the generic path whose operand is not the normed
800 // FFN input the experts read. `RouterInput::RawLayerInput`, captured
801 // in ONE function (`Decoder::router_operand`) where each host body
802 // applies `attn_norm`; the GPU router paths refuse it through
803 // `gpu_router_matches_host_routing`. Its experts are `LLM_FFN_RELU`
804 // (`:158`) with a REAL gate -- `ggml_reglu_split`, `relu(gate) *
805 // up`, `FfnActivation::Reglu` -- which is NOT `arcee`'s ungated
806 // `relu(up)^2`; the one graph that passes it (`uses_reglu`). `:8`
807 // pins `n_swa = 4096` over whatever window the file declares
808 // (`swa_window_override`; libllama's logits are byte-identical for
809 // a declared 3 and a declared 4096, measured). NoPE on `il % 4 ==
810 // 0` from the `n_no_rope_layer_step` default (`crate::rope_layers`),
811 // everything rotated without a window (`:18`). Sigmoid or softmax
812 // gating from `expert_gating_func` (`conversion/smallthinker.py:
813 // 27-30`), `norm_w = true` literal, no shared expert, NEOX RoPE.
814 // Three fixtures: the window-declared shape, the no-window shape,
815 // and a hand-written `sliding_window_pattern = 2` with
816 // `rope.freq_base_swa` that pins the SWA period reading the key
817 // while the NoPE step stays the literal 4.
818 "smallthinker",
819 // tests/sub_norm_graphs.rs: `bitnet`, NEW CODE on the two norms
820 // INSIDE the blocks. `bitnet.cpp:24,36` require `attn_sub_norm`
821 // `{n_embd}` and `ffn_sub_norm` `{n_ff}`; `:101-106` RMS-norm the
822 // attention output BEFORE `wo` (the other side of that matmul from
823 // Gemma's `post_attention_norm`), and `:127-141` call `build_ffn`
824 // with a NULL down projection, norm the `silu(gate) * up` product,
825 // and apply `ffn_down` by hand. One graph of 155 has either tensor
826 // (measured, `crate::sub_norms`). `ModelConfig::block_sub_norms` is
827 // the one fact: the loader REQUIRES the pair on it, every fused
828 // Metal launch refuses on it, and the arithmetic sits in the one
829 // attention tail (`attn_out_to_residual_rows`) and the one dense
830 // FFN row body (`frink_moe::run_expert_sub_normed`, which shares
831 // its gate/up half with `run_expert` and cannot reach the fused
832 // on-device SwiGLU). No `output` tensor (`:14-17,164`: the LM head
833 // is `tok_embd`), `rope.scaling.type = linear` at factor 1
834 // (`conversion/bitnet.py:19-20`), NEOX RoPE (llama-model.cpp:2625),
835 // plain SwiGLU, `1/sqrt(head_dim)`. Its optional per-projection
836 // `.scale` tensors (`:27-43`), which llama.cpp multiplies in and the
837 // current converter no longer writes, are REFUSED by name
838 // (`crate::weight_scales`) from a fixture that carries them and
839 // whose libllama logits differ from the unscaled file's (measured).
840 "bitnet",
841 // tests/split_kv_head_dim_graphs.rs: `mimo2` (MiMo-V2-Flash), NEW
842 // CODE on a V HEAD WIDTH THAT DIFFERS FROM THE K HEAD WIDTH --
843 // `head_dim: 192, v_head_dim: 128` in every real export
844 // (`conversion/mimo.py:154`), `mimo2.cpp:47-48,132-140,152-154`
845 // sizing and viewing K and V separately and `wo` at `n_embd_head_v
846 // * n_head` (`:52`). `crate::kv_head_dims` is the seam: fourteen
847 // converters write `value_length`, three write it apart from
848 // `key_length`, one on this engine (measured). `ModelConfig::
849 // v_head_dim` is the one value; `KvCache` / `PagedKvStore` size V by
850 // it, `causal_gqa_attention_row` -- ONE kernel now for the plain,
851 // windowed, softcapped and sink-bearing arms, which were three
852 // copies -- and the batched prefill kernel accumulate over it, the
853 // projection check and the fused-QKV cut read it, and every fused
854 // Metal launch, the CUDA resident hook, the slot file and the KV
855 // block file refuse a model whose two widths differ. Its second
856 // half, `attention.value_scale` (`:14-17,180-183`, 0.707 on every
857 // export), is `crate::attn_value_scale`: one reader of 155,
858 // applied after `wo` in the one attention tail. Everything else the
859 // row needs had landed: the per-layer `head_count_kv` array, the
860 // per-layer window array with `rope.freq_base_swa`, sinks by
861 // tensor, NextN blocks inside `block_count`, sigmoid gating with
862 // `exp_probs_b` and `expert_weights_scale`, dense-or-MoE per layer
863 // by tensor presence, partial NEOX RoPE. `mimo2.cpp:227` passes the
864 // SIGMOID literal into `build_moe_ffn`, so the key is never read
865 // (`GATING_LITERAL_ARCHITECTURES`, measured over every call). Three
866 // fixtures: the converter's fused `attn_qkv` (K rows at 12, V rows
867 // at 8), the split spelling, and the same file without the value
868 // scale.
869 "mimo2",
870 // tests/llama4_graphs.rs: `llama4` (Llama 4 Scout 17B-16E, Maverick
871 // 17B-128E), NEW CODE on the CHUNKED window: `llama4.cpp:13-14`
872 // set `LLAMA_SWA_TYPE_CHUNKED` at a literal 8192 on the branch
873 // every export takes, and `llama-hparams.h:419-425` mask every key
874 // before the query's own chunk, so a query at `p` sees `p % 8192 +
875 // 1` positions where a sliding layer sees a constant. One graph of
876 // 140 sets the type (`crate::chunked_swa`); the row's other three
877 // facts each landed on a seam that existed with a per-layer gate:
878 // the literal temperature 0.1 / 8192 / 1.0 on the layers that do
879 // NOT rotate (`:15-17,175-176`, `attn_temperature::
880 // LITERAL_ATTN_TEMPERATURE`), a weightless per-head RMS on Q and K
881 // AFTER RoPE on the layers that do, for every expert count but 128
882 // (`:43,182-188`, `crate::weightless_qk_norm`), and the interleave
883 // step the TENSOR LOADER honours (`:64`, unlike ERNIE's,
884 // `moe_interleave::INTERLEAVE_STEP_HONOURED_BY_LOADER`) with a
885 // shared expert at `n_ff_exp` on the MoE layers, SIGMOID from a
886 // literal with `norm_w = false` (`:228-230`). A declared window of
887 // ZERO (`:8-11`, the converter's spelling for an all-full-attention
888 // MobileLLM) is refused by name because libllama aborts on it
889 // (llama-graph.cpp:159), and zero experts because `:49-51` throw.
890 // Two fixtures: 16 experts at step 2 and 128 experts at step 1
891 // with a separate `output.weight` (no QK norm).
892 "llama4",
893 // tests/cohere2moe_graphs.rs: `cohere2moe` (Cohere2 MoE, the 49-layer
894 // 30B-A3B), the `cohere2` graph -- the shared-norm parallel
895 // residual, a REQUIRED window and `logit_scale`, NORM RoPE -- with
896 // routed experts on three rows: a layer rotates when it slides OR
897 // sits in the dense prefix (`cohere2moe.cpp:177-179,192`,
898 // `RopeLayers::SlidingOrLeadingDense`); `(moe_out + shexp) * 0.5`
899 // on a layer with a shared expert (`:248-260`,
900 // `parallel_dense_ffn::SHARED_EXPERT_SUM_SCALE`); the norm FUNCTION
901 // from which epsilon key the file carries (`:4-11,166`,
902 // `norm::NORM_BY_RMS_EPS_KEY`: LayerNorm for every real export, RMS
903 // under a nonzero `layer_norm_rms_epsilon`). Sigmoid when the gating
904 // key is absent, `expert_weights_norm` / `_scale` read, the
905 // per-layer window array, an MTP block skipped. Four fixtures:
906 // LayerNorm, RMS, the MTP block (libllama byte-identical to the
907 // trunk's golden), softmax with `norm_w = true`.
908 "cohere2moe",
909 // tests/layer_loop_graphs.rs: `nanbeige`, NEW CODE on RUNNING THE
910 // SAME PHYSICAL LAYERS MORE THAN ONCE. `nanbeige.cpp:6-12` read
911 // `num_loops` / `skip_loop_final_norm`, `:19-31` set `n_layer_all =
912 // n_phys * n_loops` and replicate the per-layer arrays, `:69-73`
913 // alias `layers[i + j * n_phys] = layers[i]`, and `:167-175` norm
914 // the residual with `output_norm` after every pass but the last
915 // unless the flag skips it. One graph of 155 reads either key
916 // (measured, `crate::layer_loops`). The weights are shared and the
917 // KV is not, and the seam says that rather than copying weights:
918 // `Decoder::layers` stays physical, `ModelConfig::n_layers` is the
919 // logical count every KV cache and per-layer table is sized by,
920 // `Decoder::layer_for(l)` / `physical_index(l)` are the ONE mapping
921 // the three host bodies, the gpt-oss side table and the residency
922 // plan go through, and the loop norm sits at the end of BOTH FFN
923 // bodies so every caller gets it. The fused Metal launches refuse a
924 // looped model (one `l` for weights and KV). Everything inside a
925 // pass is plain Llama (NORM RoPE, `LlamaModel` converter). Three
926 // fixtures: two passes over two layers with the loop norm, the same
927 // with `skip_loop_final_norm`, and `num_loops = 1`, which is the
928 // plain path every real export without looping takes.
929 "nanbeige",
930 // tests/skip_stream_graphs.rs: `talkie`, NEW CODE on FOUR things,
931 // each one graph of 155 (measured). No norm weights: every
932 // `build_norm` is `(x, nullptr, nullptr, LLM_NORM_RMS)` (`talkie.cpp:
933 // 50,68,90,110,137`) -- `NormOp::RmsNoParams`, the RMS twin of
934 // OLMo-1's `LayerNormNoParams`, through the same `NormFunction`
935 // table, so no site loads a tensor the file does not have. A
936 // per-head SCALAR Q gain (`attn_q_norm` is `{1, n_head}`, `:26`)
937 // applied AFTER RoPE with a weightless per-head K norm beside it
938 // (`:82-91`) -- `QkNormStyle::PerHeadScalar`, decided by
939 // architecture because the weight's length is ambiguous with
940 // `head_dim`. The embedding skip stream: the embeddings normed
941 // before layer 0 (`:50`) and added into every layer's output times
942 // `layer_output_scale` (`:123-126`) -- `crate::skip_stream`, one
943 // `bool` for both halves, the norm at the ONE embedding site and the
944 // add at the end of BOTH FFN bodies. And the two `{1}` companions
945 // its converter writes (`conversion/talkie.py:26-31`),
946 // `attn_output.scale` / `ffn_down.scale`, multiplied onto `wo` and
947 // `down` as `build_lora_mm` multiplies them -- `AttnWeights::o_scale`
948 // / `MoeWeights::down_scale`, the two `crate::weight_scales` serves
949 // for any architecture, the rest still refused. `logit_scale`
950 // REQUIRED and multiplied (`:5,141`; `MultiplierSupport::TALKIE`, the
951 // `grok` use). Every fused Metal launch refuses the model. Two
952 // fixtures: the converter's shape with the gains, and the same file
953 // without them, whose golden differs.
954 "talkie",
955 // tests/parallel_dense_ffn_graphs.rs: a dense SiLU FFN sized
956 // `{n_embd, n_embd}` on EVERY layer (`arctic.cpp:38-42`) summed with
957 // the routed experts (`:154`), and the routed branch -- router and
958 // experts -- reading `ffn_norm_exps(inpSA)`, the layer INPUT under
959 // a second norm (`:45,135-152`), while the dense half reads
960 // `ffn_norm(ffn_inp)` (`:118-132`). `crate::parallel_dense_ffn`
961 // (two rows, `grok` the other) and `RouterInput::NormedLayerInput`
962 // (one row). `norm_w = true` literal, softmax,
963 // `expert_weights_scale` read by nothing (`:3-14`; a second fixture
964 // declares it and libllama's logits are byte-identical). NORM RoPE
965 // (llama-model.cpp:2588). Every fused Metal MoE launch refuses the
966 // model (shared experts on every layer, a non-default router
967 // operand).
968 "arctic",
969 // tests/glm4moe_graphs.rs: GLM-4.5 / GLM-4.5-Air / GLM-4.6. Plain
970 // GQA with Q/K/V biases (`glm4-moe.cpp:62`), an OPTIONAL per-head
971 // Q/K RMSNorm before RoPE (`:68-71,175-182`, the 355B variant),
972 // NEOX RoPE (llama-model.cpp:2700), and its pre-FFN norm stored as
973 // `blk.N.post_attention_norm` with no `ffn_norm` (`:75,215`;
974 // `norm_sites::PRE_FFN_NORM_IS_POST_ATTENTION_NORM`). The FFN is
975 // DeepSeek-V3's: a leading dense block, sigmoid routing with
976 // `exp_probs_b`, `expert_weights_norm` and `expert_weights_scale`
977 // read from the file (`:13-17`), a shared expert `n_ff_exp *
978 // n_expert_shared` wide (`:96-104`), summed with the routed output
979 // (`:252`). NextN blocks inside `block_count` are skipped
980 // (`crate::mtp_blocks`). Two fixtures: the 355B shape with the Q/K
981 // norms and the Air shape without. A file whose
982 // `rope.dimension_sections` declare M-RoPE (a GLM-4.5V text tower)
983 // rotates NEOX here, which is what M-RoPE computes on text
984 // positions (measured byte-identical; `crate::mrope`).
985 "glm4moe",
986 // tests/glm4_graphs.rs: GLM-4-0414 (9B, 32B), GLM-Z1, GLM-OCR.
987 // Plain GQA with Q/K/V biases (`glm4.cpp:42`), NORM RoPE over the
988 // first half of each head (`partial_rotary_factor = 0.5`,
989 // llama-model.cpp:2699), Gemma-2's `post_attention_norm` and
990 // `post_ffw_norm` in Gemma-2's slots (`:144-148,166-169`) beside the
991 // ordinary `attn_norm` / `ffn_norm` (`:41,48`), a FUSED SwiGLU `ffn_up`
992 // of `{n_embd, 2 * n_ff}` with no gate (`:50,158-163`, the Phi-3
993 // split), NextN blocks inside `block_count` for GLM-OCR (`:8,54-64`,
994 // `crate::mtp_blocks`), a tied lm_head when `output` is absent. A
995 // GLM-4.1V text tower's `rope.dimension_sections` is REFUSED
996 // (`crate::mrope`): llama.cpp rotates that file M-RoPE over weights
997 // the converter permuted to NEOX, and its logits differ from the
998 // plain file's by 0.72 (measured).
999 "glm4",
1000 // tests/biased_layer_norm_graphs.rs: the two rows of the old
1001 // "LayerNorm-with-bias group" that needed only the norm
1002 // (`BIASED_LAYER_NORM`, `NormOp::LayerNormBias`). `orion`
1003 // (Orion-14B): a Llama whose every norm is `build_norm(x, w, b,
1004 // LLM_NORM)` (`orion.cpp:63-66,104-107,127-130`), NEOX RoPE with no
1005 // `rope.dimension_count` and no `rope.freq_base` in the file. `nemotron`
1006 // (Nemotron-4, Minitron): the same norm (`nemotron.cpp:71-74,111-114,
1007 // 136-139`), the ungated ReLU-squared FFN (`:118-123`), partial NEOX
1008 // RoPE, `rope.scaling.type` `none` or `linear`; its OPTIONAL
1009 // `attn_output.bias` / `ffn_up.bias` / `ffn_down.bias` (`:31,40-41`)
1010 // are refused as unread when a file carries them.
1011 "orion",
1012 "nemotron",
1013 // tests/proj_bias_graphs.rs: the three rows of the old
1014 // "LayerNorm-with-bias group" whose other blocker was the projection
1015 // biases (`crate::proj_bias`: `attn_output.bias`, `ffn_up.bias`,
1016 // `ffn_down.bias`, all REQUIRED). `starcoder2` (StarCoder2-3B/7B/15B):
1017 // the biased LayerNorm, Q/K/V biases, an ungated GELU FFN
1018 // (`FfnActivation::GeluUngated`, `starcoder2.cpp:125-131`), NEOX RoPE.
1019 // `codeshell` (CodeShell-7B): the same shape with a partial rotary
1020 // (`codeshell.cpp:26,81-95`). `jais2` (Jais-2): the biased LayerNorm,
1021 // Q/K/V biases, the ungated ReLU-squared FFN (`jais2.cpp:130-136`),
1022 // NEOX RoPE, a tied lm_head when `output` is absent (`:16-19`).
1023 "starcoder2",
1024 "codeshell",
1025 "jais2",
1026 // tests/stablelm_graphs.rs: `stablelm` (StableLM-2-1.6B, StableLM-3B-
1027 // 4E1T), the sixth row of the old group, on the same
1028 // `NormOp::LayerNormBias` with the OPTIONAL `ffn_norm.bias`
1029 // (`stablelm.cpp:39`) required beside its weight, Q/K/V biases
1030 // through `create_tensor_qkv`, partial NEOX RoPE, SwiGLU. Two shapes
1031 // behind the same string are refused by name: a layer with no
1032 // `ffn_norm` is the PARALLEL residual (`:129-138`,
1033 // `crate::parallel_residual`) and a layer with `attn_q_norm` applies
1034 // a per-head LAYERNORM (`:34-35,84-97`, `crate::qk_layer_norm`);
1035 // StableLM-2-12B has both. `use_parallel_residual` is read by
1036 // nothing in the graph and ignored here as there (measured).
1037 "stablelm",
1038 // tests/parallel_residual_graphs.rs: the PARALLEL residual
1039 // (`crate::parallel_residual`). `gptneox` (Pythia, GPT-NeoX-20B):
1040 // `x + attn(ln1(x)) + ffn(ln2(x))` under `use_parallel_residual`
1041 // (`gptneox.cpp:5,143-166`) and the sequential form under `false`
1042 // (`:167-195`), both matched; the biased LayerNorm, a fused
1043 // `attn_qkv` with its bias, REQUIRED `attn_output.bias` and FFN
1044 // biases (`crate::proj_bias`), the ungated GELU FFN, a partial NEOX
1045 // rotary, no `head_count_kv` in the file. `plamo` (PLaMo-13B): a
1046 // Llama whose FFN reads the vector attention read (`plamo.cpp:
1047 // 64,97-98,111-112`), one RMSNorm per layer, GQA 8:1, NEOX.
1048 "gptneox",
1049 "plamo",
1050 // tests/command_r_graphs.rs: `command-r` (Command-R 35B, Aya-23).
1051 // `command-r.cpp:68` is `build_norm(inpL, attn_norm, NULL, LLM_NORM)`,
1052 // the weighted LayerNorm without a bias `dbrx` gave its caller
1053 // (`WEIGHTED_LAYER_NORM`); `:106-119` the shared-norm parallel
1054 // residual (`crate::parallel_residual`); `:137-138` a `logit_scale`
1055 // MULTIPLY on the logits (`crate::scalar_multipliers`, the `grok`
1056 // use, optional); a tied lm_head (`:21`, `TENSOR_DUPLICATED`), NORM
1057 // RoPE, `rope.scaling.type = none` written by its converter.
1058 // Command-R+ (64 layers) carries the per-head LayerNorm QK norm
1059 // `:28-31` REQUIRE at that depth and is refused by name from a
1060 // 64-layer fixture libllama runs (`crate::qk_layer_norm`).
1061 "command-r",
1062 // tests/falcon_graphs.rs: `falcon` (Falcon-7B / 40B / 180B).
1063 // `falcon.cpp:71-74,124-135` the shared-norm parallel residual over
1064 // the biased LayerNorm; `:35-36,79-85` the OPTIONAL `attn_norm_2`
1065 // that Falcon-40B carries, which norms the layer input FOR
1066 // ATTENTION while `attn_norm` keeps feeding the FFN -- the two-norm
1067 // arm with the names crossed (`norm_sites::
1068 // ATTN_NORM_2_FEEDS_ATTENTION`, per layer); `:38` a fused
1069 // `attn_qkv` with no bias, multi-query at 7B; `:127-131` the
1070 // ungated GELU with no biases; NEOX over the whole head; `output`
1071 // optional. Both shapes matched.
1072 "falcon",
1073 // tests/phi2_graphs.rs: `phi2` (Phi-2, Phi-1.5). `phi2.cpp:67,108,
1074 // 116-117` the shared-norm parallel residual over the biased
1075 // LayerNorm; `:30` Q/K/V biases through `create_tensor_qkv` (split
1076 // or fused, both matched); `:33,36,39` REQUIRED `attn_output.bias`,
1077 // `ffn_down.bias`, `ffn_up.bias` (`crate::proj_bias`); `:108-114`
1078 // the ungated GELU; `:22,136` an `output.bias` on the LM head,
1079 // REQUIRED, added right after the head (`Decoder::output_bias`);
1080 // `rope.dimension_count = partial_rotary_factor * head_dim`, NEOX.
1081 "phi2",
1082 // tests/cohere2_graphs.rs: `cohere2` (Command-R7B, Command-A).
1083 // `command-r.cpp` with a window: `cohere2.cpp:78` the weighted
1084 // LayerNorm without a bias, `:120-134` the shared-norm parallel
1085 // residual, `:14,153-154` `logit_scale` REQUIRED and multiplied,
1086 // `:4-7,13` `swa_type = STANDARD`, period 4 seeded and overridable by
1087 // the scalar key, the window REQUIRED (refused when absent,
1088 // `swa_geometry::window_required`), `:9-12` the sliding layers' base
1089 // following the model's, `:72,91` ONLY the sliding layers rotated
1090 // (`rope_layers::SlidingOnly`, the `exaone-moe` rule the first
1091 // census missed), a tied lm_head, NORM RoPE, no biases.
1092 "cohere2",
1093 // tests/phimoe_graphs.rs: `phimoe` (Phi-3.5-MoE-instruct). `phi3`'s
1094 // graph (`models.h:632`) on `phimoe.cpp`'s tensors: the RMSNorm with
1095 // a bias at every site (`:20-21,28-29,35-36`, `NormOp::RmsBias`),
1096 // Q/K/V biases through `create_tensor_qkv`, `attn_output.bias` and
1097 // `output.bias` REQUIRED (`crate::proj_bias`), softmax top-2
1098 // routing renormalised (`phi3.cpp:153-163`), LongRoPE's
1099 // `rope_factors_long` / `_short` pair with `rope.scaling.attn_factor`,
1100 // NEOX. `phimoe.cpp:3-10` read no window key, so the
1101 // `attention.sliding_window` every export writes is dead metadata
1102 // (`swa_window_override`, the `phi3` answer; libllama `n_swa = 0`,
1103 // measured).
1104 "phimoe",
1105 // tests/position_embd_graphs.rs: `gpt2` (GPT-2) and `starcoder`
1106 // (StarCoder, SantaCoder), ONE graph (`gpt2.cpp` and `starcoder.cpp`
1107 // differ in `head_count_kv 1` and a size table): the biased
1108 // LayerNorm, a fused `attn_qkv` with its bias, REQUIRED
1109 // `attn_output.bias` and FFN biases, the ungated GELU, a sequential
1110 // residual, `output` tied when absent, and `position_embd.weight`
1111 // `{n_embd, n_ctx_train}` ADDED to the token embedding before layer 0
1112 // (`:19,74-77`) with no `ggml_rope` anywhere (`crate::position_embd`,
1113 // `rope_layers::RopeLayers::Never`).
1114 "gpt2",
1115 "starcoder",
1116 // tests/alibi_graphs.rs: the four ALiBi rows (`crate::alibi`), no
1117 // rotation (`rope_layers::RopeLayers::Never`), the bias `slope_h *
1118 // (p_key - p_query)` on every score. `refact.cpp:12` (the literal 8;
1119 // RMSNorm, split Q/K/V, SwiGLU, multi-query), `bloom.cpp:18` (the
1120 // literal; the biased LayerNorm on the embeddings and every site, a
1121 // fused `attn_qkv` with bias, the required projection biases, the
1122 // ungated GELU), `mpt.cpp:6` (`attention.max_alibi_bias`; the
1123 // weighted LayerNorm, its biases and `position_embd` optional, the
1124 // ungated GELU, `clamp_kqv`), `jais.cpp:5` (the key; the biased
1125 // LayerNorm, the required projection biases with `ffn_gate.bias`,
1126 // SwiGLU). Baichuan-13B is the same seam on a row that was audited
1127 // for the 7B: `baichuan.cpp:11-14` at 40 layers.
1128 "refact",
1129 "bloom",
1130 "mpt",
1131 "jais",
1132 // tests/minimax_m2_graphs.rs: `minimax-m2` (MiniMax-M2, 230B MoE).
1133 // `minimax-m2.cpp:26,30-31,96-106,131-141`: plain GQA, ONE RMSNorm
1134 // over the whole Q projection and one over K (`attn_q_norm` is
1135 // `n_embd_head_k * n_head` wide), partial NEOX RoPE (`n_rot 64` of
1136 // `head_dim 128`), one SiLU MoE on every layer with `exp_probs_b`,
1137 // `norm_w = true` and the gating function from the key (SIGMOID on
1138 // every real export; the default aborts upstream). No dense layer,
1139 // no shared expert, no biases; `expert_weights_scale` is never read
1140 // by its hparams. Its refusal had said "a fixture away" for a week
1141 // while the fixture sat in `tests/fixtures/`.
1142 "minimax-m2",
1143 // tests/minimax_01_graphs.rs: `minimax-01` (MiniMax-Text-01). The
1144 // lightning-attention block (`crate::lightning`) on the layers
1145 // `attention.recurrent_layers` / `full_attention_interval` name
1146 // (`minimax-01.cpp:11-17`), plain GQA with partial NEOX RoPE
1147 // elsewhere, a softmax MoE on every layer, and the pre-norm
1148 // residual topology (`crate::normed_residual`) its REQUIRED
1149 // `residual_scale` multiplies.
1150 "minimax-01",
1151 // tests/lfm2_graphs.rs: `lfm2` (LFM2-350M / 700M / 1.2B / 2.6B), the
1152 // first HYBRID row on the generic path. `lfm2.cpp:9-11` marks a
1153 // layer recurrent when `n_head_kv(il) == 0`, and `:192-208` is ONE
1154 // residual topology for both kinds: `attn_norm`, the short
1155 // convolution (`crate::shortconv`, `AttnShape::ShortConv`) or GQA,
1156 // the residual add, `ffn_norm`, SwiGLU. The attention layers have a
1157 // PER-HEAD RMS QK norm (`{n_embd_head_k}`, :74-75), NEOX RoPE
1158 // (llama-model.cpp:2666), a fused or split QKV; the final norm is
1159 // stored as `token_embd_norm` (`norm_sites::
1160 // OUTPUT_NORM_UNDER_EMBEDDING_NAME`); `output` tied when absent.
1161 // Four fixtures: split, the converter's fused `attn_qkv`, a separate
1162 // `output.weight`; the fourth declares a window and is REFUSED by
1163 // name (lfm2.cpp:24-29 windows the attention layers alone).
1164 "lfm2",
1165 // tests/lfm2_graphs.rs: `lfm2moe` (LFM2-8B-A1B, LFM2-24B-A2B) is
1166 // `lfm2`'s graph (`models.h:1899`) with `leading_dense_block_count`
1167 // dense layers and a sigmoid MoE on the rest, `exp_probs_b` REQUIRED
1168 // (`lfm2moe.cpp:8,38-47`), `norm_w = true` (lfm2.cpp:118); the
1169 // gating function comes from the key, which the converter writes
1170 // as SIGMOID (`conversion/lfm2.py:109`). `expert_weights_scale` is
1171 // read by nothing in its hparams (the fixture declares 2.5 and the
1172 // golden is unscaled).
1173 "lfm2moe",
1174 // tests/pangu_embedded_graphs.rs: `pangu-embedded` (openPangu-
1175 // Embedded-1B / 7B), a decoder LLM that had been filed as an
1176 // embedding model from its name. `pangu-embed.cpp` is `llama.cpp`'s
1177 // graph with a REQUIRED `attn_output.bias` (`:37`), NEOX RoPE,
1178 // `n_rot == n_embd_head` (`:59`), fused or split QKV, `output` tied
1179 // when absent. Three fixtures: split, fused, separate `output`.
1180 "pangu-embedded",
1181 // tests/granite_hybrid_graphs.rs: `granitehybrid` (Granite-4.0-H
1182 // Micro / Tiny / Small) and its frink alias. `granite.cpp`'s four
1183 // multipliers and optional biases with a MAMBA-2 block on the
1184 // zero-KV layers (`granite-hybrid.cpp:17-19,163`; `crate::mamba2`,
1185 // `AttnShape::Mamba2`, the state as `RecurrentState` beside the
1186 // layer's cache), dense or MoE with the shared expert, and
1187 // `rope.scaling.finetuned = false` (every real export) rotating
1188 // nothing. Three fixtures: NoPE dense, rotated dense (Bamba's
1189 // shape), NoPE MoE with the shared expert.
1190 "granitehybrid",
1191 "granite-hybrid",
1192 // tests/nemotron_h_graphs.rs: `nemotron_h` (Nemotron-H 8B / 47B /
1193 // 56B, Nemotron-3 Nano dense). Every layer ONE block -- Mamba-2
1194 // (`n_head_kv == 0 && n_ff == 0`), attention (`n_ff == 0`, no RoPE,
1195 // optional `attn_output.bias`) or the ungated ReLU-squared FFN
1196 // (optional biases) -- under `attn_norm` with one residual add
1197 // (`nemotron-h.cpp:9-11,143-158`). Three fixtures: plain, the three
1198 // optional biases, a separate `output.weight`.
1199 "nemotron_h",
1200 // tests/nemotron_h_graphs.rs: `nemotron_h_moe` (Nemotron-3 Nano
1201 // 30B-A3B). The same layers with the FFN layer a sigmoid MoE
1202 // (`nemotron-h.cpp:206-231`: the gating function a LITERAL, the
1203 // router bias REQUIRED, `expert_weights_norm` / `_scale` from the
1204 // file) of UNGATED ReLU-squared experts, plus an ungated
1205 // ReLU-squared shared expert; the gate is aliased to `up` on both
1206 // as the dense ungated FFN's is. `moe_latent_size` (Nemotron-3
1207 // Super) is refused by name.
1208 "nemotron_h_moe",
1209 // tests/falcon_h1_graphs.rs: `falcon-h1` (Falcon-H1 0.5B to 34B).
1210 // Attention and the Mamba-2 block IN PARALLEL on every layer, both
1211 // reading `attn_norm(x)`, summed before the one residual add
1212 // (`falcon-h1.cpp:137-161`); NEOX RoPE; `ssm_norm` optional (`:70`);
1213 // `attn_output.bias` created and never read (`:76,154`,
1214 // `crate::unread_tensors`); `ffn_norm` under the two-argument
1215 // `LLM_TN` spelling (`:80`, no `.weight`). Every multiplier is folded
1216 // into the weights by the converter. Three fixtures: plain, without
1217 // `ssm_norm`, a separate `output.weight`.
1218 "falcon-h1",
1219 // tests/mamba_graphs.rs: `jamba` (AI21 Jamba-v0.1 / 1.5): the
1220 // Mamba-1 block (`crate::mamba1`, `mamba-base.cpp:4-148`, with the
1221 // REQUIRED dt / B / C norms, `jamba.cpp:49,52-53`) where
1222 // `head_count_kv` is 0, attention with no RoPE elsewhere (`:98`),
1223 // dense or MoE per layer by the router's presence (`:89-101,152`;
1224 // softmax, `norm_w = false`, `:164`). `mamba` (Mamba-130M to 2.8B,
1225 // FalconMamba-7B: `ssm.dt_b_c_rms`, the weightless dt / B / C
1226 // norms) and `mamba2` (Mamba-Codestral-7B): every layer the block,
1227 // no attention, no FFN, head_dim 0 (`layer_shapes::PURE_RECURRENT`).
1228 "jamba",
1229 "mamba",
1230 "mamba2",
1231 // tests/plamo2_graphs.rs: `plamo2` (PLaMo-2 1B / 2B / 8B). PLaMo-2's
1232 // own SSM block (`crate::plamo2_ssm`: Mamba-1's dt / B / C path,
1233 // B-C-dt order, REQUIRED norms, feeding Mamba-2's per-head scan;
1234 // z / x interleaved per head) where the KV count is zero, attention
1235 // with the per-head QK RMSNorm with a distinct row per head
1236 // (`QkNormStyle::PerHeadDistinct`) elsewhere.
1237 "plamo2",
1238 // tests/qwen35_graphs.rs: `qwen35` (Qwen3.5 0.8B to 27B). The gated
1239 // delta net (`crate::gdn`: `qwen35.cpp:236-317` over
1240 // `delta-net-base.cpp:289-365`, V heads TILED over K heads) on the
1241 // layers `attention.recurrent_layers` / `full_attention_interval`
1242 // name (`:17-24`), gated full attention elsewhere (`:186-234`: the
1243 // gate interleaved in `wq`, per-head QK norm, partial IMROPE over
1244 // `rope.dimension_sections`, NEOX on text positions), the pre-FFN
1245 // norm stored as `post_attention_norm` (`:65,146-148`), SwiGLU,
1246 // `nextn_predict_layers` skipped as an MTP block. Three fixtures:
1247 // the interval, the array, a separate `output.weight`.
1248 "qwen35",
1249 // tests/qwen35_graphs.rs: `qwen35moe` (Qwen3.5-35B-A3B and up), the
1250 // same layers with `qwen2moe`'s FFN on every one
1251 // (`qwen35moe.cpp:98-107,496-538`: softmax, `norm_w = true`, a
1252 // shared expert scaled by `sigmoid(ffn_gate_inp_shexp . x)`).
1253 "qwen35moe",
1254 // tests/qwen35_graphs.rs: `qwen3next` (Qwen3-Next-80B-A3B), the
1255 // same layers with the V heads GROUPED over the K heads
1256 // (`qwen3next.cpp:521-539`, `HeadMap::Grouped`), beta and alpha in
1257 // one `ssm_ba` projection (`:96,422-436`, `BetaAlpha::Fused`) and
1258 // plain NEOX RoPE (`:282-291`). The legacy fused `ssm_in` is refused
1259 // by name.
1260 "qwen3next",
1261];
1262
1263/// Is this architecture's use of the shared generic path backed by
1264/// evidence?
1265pub fn is_audited_generic(arch: &str) -> bool {
1266 AUDITED_GENERIC_GQA.contains(&arch)
1267}
1268
1269/// Architectures whose layers have **no pre-attention norm and no
1270/// pre-FFN norm at all**: the post-norm-only residual topology.
1271///
1272/// ```text
1273/// ffn_inp = x + post_attn_norm(attn(x))
1274/// out = ffn_inp + post_ffn_norm(ffn(ffn_inp))
1275/// ```
1276///
1277/// Not a family resemblance -- the two graphs were read side by side
1278/// and are the same statement for statement. `src/models/olmo2.cpp`
1279/// creates only `attn_q_norm`, `attn_k_norm`, `attn_post_norm` and
1280/// `ffn_post_norm` per layer (:45-52) and reads the raw residual at
1281/// both sublayers (`cur = inpL` at :92, `build_ffn(ffn_inp, ...)` at
1282/// :169), norming each branch's OUTPUT before its residual add
1283/// (:160-165, :177-182). `src/models/exaone4.cpp` is the same list
1284/// (:60-67) and the same four lines (:118, :159, :152-155, :166-169).
1285///
1286/// Both are refused unless [`AUDITED_GENERIC_GQA`] names them, and
1287/// `crate::norm::NormOp` is the one implementation they share.
1288/// Adding a third name here means having read a third `*.cpp`: this
1289/// list decides whether `loader.rs` demands `blk.N.attn_norm.weight`
1290/// from a file, so a wrong entry is a load that fails or a norm that
1291/// silently disappears.
1292pub const POST_NORM_ONLY_ARCHITECTURES: &[&str] = &["olmo2", "exaone4"];
1293
1294/// Does this architecture read the raw residual at both sublayers?
1295/// See [`POST_NORM_ONLY_ARCHITECTURES`].
1296pub fn is_post_norm_only(arch: &str) -> bool {
1297 POST_NORM_ONLY_ARCHITECTURES.contains(&arch)
1298}
1299
1300/// Architectures that normalise with a **non-parametric LayerNorm** --
1301/// subtract the mean, divide by the standard deviation, no learned
1302/// weight and no bias -- at every norm site.
1303///
1304/// `olmo` (OLMo-1), and llama.cpp has no second one. `olmo.cpp:27-35`
1305/// creates Q/K/V, `attn_output` and gate/up/down and NOT ONE norm
1306/// tensor, and its graph is `build_norm(x, NULL, NULL, LLM_NORM, il)`
1307/// at :65-67 (pre-attention), :104-106 (pre-FFN) and :128-130 (final).
1308///
1309/// It is pre-norm like `llama`, so this is orthogonal to
1310/// [`POST_NORM_ONLY_ARCHITECTURES`]: the difference is the norm
1311/// FUNCTION, not the residual wiring, and a name cannot be on both
1312/// lists (`loader.rs`'s
1313/// `the_norm_slot_and_function_lists_cannot_contradict`).
1314///
1315/// **This list will not grow, and that is a measured claim rather than
1316/// an expectation.** Every `build_norm` call in all of llama.cpp's
1317/// `src/models/*.cpp` graphs was scanned for a null weight argument:
1318/// three calls pass one to `LLM_NORM`, and all three are `olmo.cpp`.
1319/// `talkie.cpp` passes a null weight to `LLM_NORM_RMS` at five sites,
1320/// which is a non-parametric RMSNorm -- a different function, and a row
1321/// this list does not serve.
1322///
1323/// The LayerNorm *function* with a learned weight is a different list,
1324/// [`WEIGHTED_LAYER_NORM`], and it exists now because `dbrx` gave it a
1325/// caller.
1326pub const NON_PARAMETRIC_LAYER_NORM: &[&str] = &["olmo"];
1327
1328/// Does this architecture normalise without any learned parameters?
1329/// See [`NON_PARAMETRIC_LAYER_NORM`].
1330pub fn uses_non_parametric_layer_norm(arch: &str) -> bool {
1331 NON_PARAMETRIC_LAYER_NORM.contains(&arch)
1332}
1333
1334/// Architectures that normalise with a **non-parametric RMSNorm** --
1335/// `build_norm(x, nullptr, nullptr, LLM_NORM_RMS, il)` -- at every norm
1336/// site: no `attn_norm`, `ffn_norm` or `output_norm` tensor in the file.
1337///
1338/// The RMS twin of [`NON_PARAMETRIC_LAYER_NORM`], and measured the same
1339/// way: every `build_norm` call with a null weight across all 155
1340/// graphs is `olmo.cpp` (three, `LLM_NORM`) and `talkie.cpp` (five,
1341/// `LLM_NORM_RMS`: the embeddings at `:50`, `attn_norm` at `:68`, the K
1342/// norm at `:90`, `ffn_norm` at `:110`, the final norm at `:137`).
1343/// `NormOp::RmsNoParams` is the function; `crate::skip_stream` is the
1344/// rest of `talkie`.
1345///
1346/// **Re-measured 2026-09-19, when the pin moved to `5b59b83`, and the
1347/// answer CHANGED**: two graphs that landed upstream in the six weeks
1348/// since the last census pass a null weight to `LLM_NORM_RMS` too --
1349/// `hrm-text.cpp` at three sites (`:107,144,162`) and
1350/// `muse-glimmer.cpp` at one (`:69`, the embeddings). So the function
1351/// is no longer one architecture's, and `talkie` is no longer the
1352/// hoped-for lone row; both new ones refuse for OTHER reasons today
1353/// (`capability::NEOX_ROPE_TRIAGED`, `NORM_ROPE_TRIAGED`) and neither
1354/// is admitted here, because a name in this list is a promise that
1355/// every norm site of that architecture is served, which nobody has
1356/// checked for either. `muse-glimmer`'s is also the first WEIGHTLESS
1357/// norm at the EMBEDDING site, where `crate::norm_sites`' row is
1358/// `bloom`'s weighted one.
1359pub const NON_PARAMETRIC_RMS_NORM: &[&str] = &["talkie", "hrm_text"];
1360
1361/// See [`NON_PARAMETRIC_RMS_NORM`].
1362pub fn uses_non_parametric_rms_norm(arch: &str) -> bool {
1363 NON_PARAMETRIC_RMS_NORM.contains(&arch)
1364}
1365
1366/// Architectures that normalise with a **LayerNorm with a learned
1367/// weight and no bias** -- `build_norm(x, w, NULL, LLM_NORM, il)` -- at
1368/// every norm site.
1369///
1370/// `dbrx`: `src/models/dbrx.cpp:4` reads `LLM_KV_ATTENTION_LAYERNORM_EPS`
1371/// (not the RMS one) and the graph passes `LLM_NORM` with a weight and
1372/// a null bias at all three sites -- `:69-71` pre-attention, `:110-112`
1373/// pre-FFN (on `attn_out_norm`, its pre-FFN tensor; see
1374/// `crate::norm_sites`) and `:140-142` final. `dbrx.cpp:29,34,23`
1375/// create the three weights and no bias tensor at all.
1376///
1377/// The variant is `crate::norm::NormOp::LayerNorm`. It was deliberately
1378/// not written alongside the parameterless one, because every row that
1379/// needed it refused for more than the norm; `dbrx` needed two more
1380/// things and both were one implementation each (`crate::clamp_kqv`,
1381/// `crate::norm_sites`), which is what made it worth landing.
1382///
1383/// **What this list does NOT close**, so nobody adds a name on the
1384/// strength of "it is LayerNorm too": the `nemotron` / `orion` /
1385/// `stablelm` / `codeshell` / `jais2` / `starcoder` / `starcoder2` /
1386/// `phimoe` group all create `*_norm.bias` as REQUIRED and `build_norm`
1387/// adds it after the multiply. That is the `LayerNorm(w, b)` variant,
1388/// [`BIASED_LAYER_NORM`], which arrived on 2026-09-12 when `orion` and
1389/// `nemotron` turned out to need nothing else; six of the group are on
1390/// it now and `starcoder` / `phimoe` still refuse for something on top.
1391///
1392/// The second caller of THIS variant is `command-r` (Command-R 35B):
1393/// `command-r.cpp:68,127` pass `attn_norm` / `output_norm` with a NULL
1394/// bias to `LLM_NORM`, over the shared-norm parallel residual
1395/// (`crate::parallel_residual`) with a `logit_scale` MULTIPLY
1396/// (`crate::scalar_multipliers`); `tests/command_r_graphs.rs`. The
1397/// third is `cohere2` (Command-R7B), the same graph with a window
1398/// (`cohere2.cpp:78,147`); `tests/cohere2_graphs.rs`.
1399pub const WEIGHTED_LAYER_NORM: &[&str] = &["dbrx", "command-r", "cohere2", "cohere2moe", "mpt"];
1400
1401/// Does this architecture normalise with a weighted LayerNorm?
1402/// See [`WEIGHTED_LAYER_NORM`].
1403pub fn uses_weighted_layer_norm(arch: &str) -> bool {
1404 WEIGHTED_LAYER_NORM.contains(&arch)
1405}
1406
1407/// Architectures that normalise with a **LayerNorm with a learned
1408/// weight AND bias** -- `build_norm(x, w, b, LLM_NORM, il)` -- at every
1409/// norm site, the weights and the biases all REQUIRED.
1410///
1411/// The `(w, b)` variant [`WEIGHTED_LAYER_NORM`] had named as having no
1412/// caller. It has two now, and they are the two rows of the old
1413/// "LayerNorm-with-bias group" that need NOTHING ELSE of the generic
1414/// decoder (`NormOp::LayerNormBias`, `tests/biased_layer_norm_graphs.rs`):
1415///
1416/// - `orion` (Orion-14B): `orion.cpp:17-18,24-25,30-31` create the six
1417/// tensors and `:63-66,104-107,127-130` pass each pair to `LLM_NORM`;
1418/// the rest is a Llama with NEOX RoPE (llama-model.cpp's NEOX group),
1419/// no `rope.dimension_count` and no `rope.freq_base` in the file
1420/// (`conversion/orion.py:13-37` writes neither).
1421/// - `nemotron` (Nemotron-4, Minitron): `nemotron.cpp:18-19,25-26,33-34`
1422/// the same six, plus the ungated ReLU-squared FFN `arcee` already
1423/// serves (`uses_relu_sqr`), partial NEOX RoPE, and OPTIONAL
1424/// `attn_output.bias` / `ffn_up.bias` / `ffn_down.bias` (`:31,40-41`,
1425/// `TENSOR_NOT_REQUIRED`) that a file carrying them leaves UNREAD
1426/// here, which `assert_every_tensor_consumed` refuses.
1427///
1428/// Three more closed the same day once `crate::proj_bias` served the
1429/// REQUIRED `attn_output.bias` / `ffn_up.bias` / `ffn_down.bias` that
1430/// had been their other blocker: `starcoder2` (`starcoder2.cpp:23,35,44`,
1431/// an ungated GELU FFN), `codeshell` (`codeshell.cpp:24,31,39`, the
1432/// same with a partial rotary) and `jais2` (`jais2.cpp:20,30,44`, the
1433/// ReLU-squared FFN); `tests/proj_bias_graphs.rs`.
1434///
1435/// `stablelm` followed (`stablelm.cpp:20-21,27-28,38-39`; the pre-FFN
1436/// pair is `TENSOR_NOT_REQUIRED`, and its absence is the shared-norm
1437/// parallel residual `crate::parallel_residual` serves), with its
1438/// per-head LayerNorm QK norm refused by name
1439/// (`crate::qk_layer_norm`); `tests/stablelm_graphs.rs`. `gptneox`
1440/// (`gptneox.cpp:57-58,63-64,72-73`, all six REQUIRED) followed on the
1441/// parallel residual's other arm; `tests/parallel_residual_graphs.rs`.
1442/// `falcon` (`falcon.cpp:20-21,32-33`, plus the OPTIONAL `attn_norm_2`
1443/// pair at `:35-36`) followed it; `tests/falcon_graphs.rs`. `phi2`
1444/// (`phi2.cpp:19-20,27-28`) followed on `output.bias`;
1445/// `tests/phi2_graphs.rs`.
1446///
1447/// The two the group still holds, each for something ELSE on top of
1448/// this norm (the norm is done for both): `starcoder` a learned
1449/// `position_embd` with no RoPE; `phimoe` an `output.bias` on the LM
1450/// head and LongRoPE. `tests/attn_bias.rs` pins both as refused with
1451/// the bias named.
1452pub const BIASED_LAYER_NORM: &[&str] = &[
1453 "orion",
1454 "nemotron",
1455 "starcoder2",
1456 "codeshell",
1457 "jais2",
1458 "stablelm",
1459 "gptneox",
1460 "falcon",
1461 "phi2",
1462 "gpt2",
1463 "starcoder",
1464 "bloom",
1465 "jais",
1466];
1467
1468/// See [`BIASED_LAYER_NORM`].
1469pub fn uses_biased_layer_norm(arch: &str) -> bool {
1470 BIASED_LAYER_NORM.contains(&arch)
1471}
1472
1473/// Architectures that normalise with an **RMSNorm with a learned weight
1474/// AND bias** -- `build_norm(x, w, b, LLM_NORM_RMS, il)` -- at every
1475/// norm site, all REQUIRED: `phimoe` (Phi-3.5-MoE), whose tensors are
1476/// `phimoe.cpp:20-21,28-29,35-36` and whose graph is `phi3`'s
1477/// (`phi3.cpp:99-102,137-139,174-177` pass the bias; `phi3` never
1478/// creates one). Measured: `grep -B3 LLM_NORM_RMS src/models/*.cpp |
1479/// grep norm_b` is `phi3` (this row's graph), `chameleon` (passes NULL),
1480/// and `deepseek32` / `glm-dsa` / `rwkv6qwen2` / `arwkv7` on other
1481/// engines. [`crate::norm::NormOp::RmsBias`]; `tests/phimoe_graphs.rs`.
1482pub const BIASED_RMS_NORM: &[&str] = &["phimoe"];
1483
1484/// See [`BIASED_RMS_NORM`].
1485pub fn uses_biased_rms_norm(arch: &str) -> bool {
1486 BIASED_RMS_NORM.contains(&arch)
1487}
1488
1489/// How the generic `Decoder` / `ModelConfig::from_gguf` path treats a
1490/// GGUF architecture string.
1491#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1492pub enum ArchPath {
1493 /// Standard GQA (+ optional MoE) decoder; RoPE layout is known.
1494 GenericGqa { rope: RopeLayout },
1495 /// In-repo test fixtures (`ferroxtest*`) -- not a real model
1496 /// family.
1497 ///
1498 /// The `ferrox` spelling is DELIBERATE and is the one thing the
1499 /// 2026-09-19 rename to Frink did not touch: these are
1500 /// `general.architecture` VALUES written inside committed binary
1501 /// GGUF fixtures (`tests/fixtures/frink_real_*.gguf`), and a GGUF
1502 /// string is length-prefixed, so renaming them means regenerating
1503 /// the fixtures and the goldens that go with them. A wire value is
1504 /// not branding; it is data that has to match what the file says.
1505 TestFixture { rope: RopeLayout },
1506 /// Real architecture, but must not be loaded through the generic
1507 /// GQA decoder (wrong attention / residual math).
1508 DedicatedOnly { reason: &'static str },
1509 /// In the llama.cpp inventory but out of Frink scope for now.
1510 Deferred { reason: &'static str },
1511}
1512
1513/// Load-time resolved profile for one GGUF `general.architecture` string.
1514#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1515pub struct ArchProfile {
1516 pub gguf_name: &'static str,
1517 pub scope: ArchScope,
1518 pub family: DecoderFamily,
1519 pub memory: MemoryKind,
1520 pub rope: RopeLayout,
1521 pub path: ArchPath,
1522 /// Default QK-norm style when norm tensors are present; loader may
1523 /// refine from tensor length.
1524 pub qk_norm: QkNormStyle,
1525 /// For an UNAUDITED [`ArchPath::GenericGqa`] row: how far it is from
1526 /// running, read against llama.cpp's own graph. `None` on audited
1527 /// rows (which run) and on rows still in [`TRIAGE_PENDING`].
1528 pub triage: Option<UnauditedTriage>,
1529}
1530
1531impl ArchProfile {
1532 /// Attach a triage verdict to a catalog row. Private on purpose:
1533 /// verdicts are data of the catalog, not something a caller supplies.
1534 fn triaged(mut self, class: TriageClass, blocker: &'static str) -> Self {
1535 self.triage = Some(UnauditedTriage { class, blocker });
1536 self
1537 }
1538}
1539
1540fn prof(
1541 name: &'static str,
1542 scope: ArchScope,
1543 fam: DecoderFamily,
1544 mem: MemoryKind,
1545 rope: RopeLayout,
1546 path: ArchPath,
1547 qk: QkNormStyle,
1548) -> ArchProfile {
1549 ArchProfile {
1550 gguf_name: name,
1551 scope,
1552 family: fam,
1553 memory: mem,
1554 rope,
1555 path,
1556 qk_norm: qk,
1557 triage: None,
1558 }
1559}
1560
1561fn gqa_norm(name: &'static str) -> ArchProfile {
1562 prof(
1563 name,
1564 ArchScope::TextGeneration,
1565 DecoderFamily::StandardGqa,
1566 MemoryKind::KvGqa,
1567 RopeLayout::Norm,
1568 ArchPath::GenericGqa {
1569 rope: RopeLayout::Norm,
1570 },
1571 QkNormStyle::WholeVector,
1572 )
1573}
1574
1575fn gqa_neox(name: &'static str) -> ArchProfile {
1576 prof(
1577 name,
1578 ArchScope::TextGeneration,
1579 DecoderFamily::StandardGqa,
1580 MemoryKind::KvGqa,
1581 RopeLayout::Neox,
1582 ArchPath::GenericGqa {
1583 rope: RopeLayout::Neox,
1584 },
1585 QkNormStyle::WholeVector,
1586 )
1587}
1588
1589fn dedicated(name: &'static str, reason: &'static str) -> ArchProfile {
1590 prof(
1591 name,
1592 ArchScope::TextGeneration,
1593 DecoderFamily::Dedicated,
1594 MemoryKind::KvGqa,
1595 RopeLayout::Norm,
1596 ArchPath::DedicatedOnly { reason },
1597 QkNormStyle::WholeVector,
1598 )
1599}
1600
1601fn deferred_scope(name: &'static str, scope: ArchScope, reason: &'static str) -> ArchProfile {
1602 prof(
1603 name,
1604 scope,
1605 DecoderFamily::StandardGqa,
1606 MemoryKind::None,
1607 RopeLayout::Neox,
1608 ArchPath::Deferred { reason },
1609 QkNormStyle::WholeVector,
1610 )
1611}
1612
1613/// Triaged rows of the generic **Norm**-RoPE group, with the llama.cpp
1614/// line that decides each verdict. Consumed by
1615/// [`architecture_catalog`]; a name here must not also appear in the
1616/// untriaged list above it or in [`TRIAGE_PENDING`], which
1617/// `catalog_has_unique_names` and
1618/// `every_unaudited_generic_architecture_is_triaged_or_listed_as_pending`
1619/// between them enforce.
1620const NORM_ROPE_TRIAGED: &[(&str, TriageClass, &str)] = &[
1621 // --- Landed upstream AFTER the 2026-08-04 pin, read on 2026-09-19
1622 // when the pin moved to `5b59b83` (792 commits, 15 new graphs).
1623 // None of the four below has a fixture yet; each says what it
1624 // needs, measured against the graph, not guessed from the name.
1625 // `granite_swa` (Granite 4.1) was HERE for one PR, NEW CODE on two
1626 // small per-layer tables, and is audited now: the
1627 // `expert_used_count` ARRAY is read scalar-or-array by the loader
1628 // (a fix that came out of the same pin move), and
1629 // `attention.rope_pattern` is `RopeLayers::FileMask` -- the first
1630 // upstream graph that lets the FILE say which layers rotate, one
1631 // line of 155 (`rope_layers::ROPE_PATTERN_READERS`). Everything
1632 // else it needed was served and each table gained one name: the
1633 // four Granite multipliers, the window ARRAY, the REQUIRED
1634 // per-layer sinks, the optional projection biases and the
1635 // `attention.scale` override. `tests/granite_swa_graphs.rs`.
1636 (
1637 "graniteswitch",
1638 TriageClass::NewCode,
1639 "a per-token ADAPTER selection. `src/models/granite-switch.cpp` threads an `adapter_ids` \
1640 tensor through the layer body so each token's FFN reads a different expert \
1641 adapter, which is a second indexing dimension the MoE layer here does not have \
1642 (frink routes tokens to experts; this routes them to adapters OF an expert). \
1643 Its other half is small and named: `:9-12` read `rope.scaling.finetuned` and fill \
1644 `rope_pattern` with it, which is `rope_finetuned::unrotated` plus the per-layer \
1645 array `granite_swa` needs",
1646 ),
1647 // `muse-glimmer` was HERE for one PR, NEW CODE on two norm facts,
1648 // and is audited now: the WEIGHTLESS embedding norm is
1649 // `norm_sites::WEIGHTLESS_EMBEDDING_NORM` with `NormOp::
1650 // RmsNoParams` at the site `bloom`'s weighted one already had, and
1651 // the post-norm epsilon literal is `norm::POST_NORM_EPS_LITERAL`
1652 // read through `ModelConfig::post_norm_eps()` at the three host
1653 // post-norm sites, with the fused Metal launches and the CUDA
1654 // prefill refusing a model whose two epsilons differ. The rest was
1655 // served and each table gained one name: the per-element sigmoid
1656 // gate, `RopeLayers::SlidingOnly`, the `logit_scale` multiply with
1657 // the final tanh softcap, and the window pattern read scalar-then-
1658 // array. `tests/muse_glimmer_graphs.rs`.
1659 // `ernie4_5-moe` was HERE, ONE MATCH ARM on
1660 // `{arch}.interleave_moe_layer_step`. Building its fixture found the
1661 // arm is not implementable against a reference: llama.cpp's tensor
1662 // loader (ernie4-5.cpp:49) and its graph (ernie4-5-moe.cpp:64)
1663 // disagree about which layers are MoE, and only the graph has the
1664 // step, so a checkpoint whose interleave interleaves cannot be
1665 // loaded by llama.cpp at all. The arm landed as a REFUSAL
1666 // (`crate::moe_interleave`) and the step every real checkpoint
1667 // carries is audited (`tests/one_match_arm_graphs.rs`), so the row
1668 // is in AUDITED_GENERIC_GQA and carries no verdict.
1669 //
1670 // `granite`, `granitemoe` and the `granite-moe` alias were HERE,
1671 // NEW CODE on the four scalar multipliers. All three are audited
1672 // now: `crate::scalar_multipliers` implements the multipliers ONCE,
1673 // parameterised by architecture, and `tests/granite_family_graphs.rs`
1674 // is the libllama-golden evidence. The `rope_finetuned` half of that
1675 // verdict landed as a REFUSAL rather than an implementation
1676 // (`crate::rope_finetuned`), because llama.cpp runs such a file with
1677 // no rotation at all and frink cannot express that.
1678 //
1679 // `chatglm` was HERE, ONE MATCH ARM on the fused `attn_qkv.bias`.
1680 // The arm landed (`crate::qkv_fused`, which now resolves the
1681 // projections and their biases from ONE decision about which
1682 // spelling the file uses) and is evidenced against libllama in
1683 // `tests/one_match_arm_graphs.rs`, so the row is in
1684 // AUDITED_GENERIC_GQA and carries no verdict.
1685 //
1686 // Its verdict said implementing the arm "closes chatglm and qwen
1687 // together". It did not, and that is the finding: `qwen` needs the
1688 // same bias AND a second, unrelated arm the verdict did not name --
1689 // `qwen.cpp:33-35` sizes every FFN matrix `n_ff/2`, because
1690 // Qwen-1's `intermediate_size` counts gate and up together. `qwen`
1691 // stays refused, with that added to its reason.
1692 // `deci` was HERE, NEW CODE on PER-LAYER SHAPES, and is audited
1693 // now with `openelm` on one seam (`crate::layer_shapes`,
1694 // `tests/per_layer_shape_graphs.rs`). Its three layer kinds are
1695 // `AttnShape::{Gqa, Linear, Absent}` plus `ffn_dim == 0`; the one
1696 // combination llama.cpp's graph handles by discarding a computed
1697 // branch (`deci.cpp:147-149` before `:150-153`) is refused by name
1698 // from a fixture that has it, with the drop MEASURED rather than
1699 // read.
1700 // `olmo` was HERE, NEW CODE on the non-parametric LayerNorm, and is
1701 // audited now: `crate::norm::NormOp::LayerNormNoParams` implements
1702 // the function and `tests/olmo_graphs.rs` carries the fixture. Its
1703 // verdict called the clamp "an optional key nothing here applies";
1704 // that half stayed a REFUSAL rather than an implementation, because
1705 // `llama-graph.cpp:1611-1652` really does clamp Q, K and V and
1706 // `conversion/olmo.py:23-25` really does write the key. See
1707 // `crate::clamp_kqv`.
1708 // `arctic` was HERE, NEW CODE on a PARALLEL dense + MoE layer whose
1709 // MoE branch reads the pre-attention residual, and is audited now
1710 // on two seams (`tests/parallel_dense_ffn_graphs.rs`): the dense FFN
1711 // summed with the experts is `crate::parallel_dense_ffn` -- the
1712 // shared-expert slot under the dense names plus the row's scale on
1713 // the sum, whose second row is Grok-2, refused by name until then --
1714 // and the branch operand `ffn_norm_exps(inpSA)` is
1715 // `RouterInput::NormedLayerInput`, one graph of 155. The verdict had
1716 // said `router_input` "does not reach it" because the operand feeds
1717 // a whole expert bank; it reaches it as a third variant carrying
1718 // that fact (`experts_read_router_operand`).
1719 // `mistral3` was HERE, NEW CODE on the PER-POSITION ATTENTION
1720 // TEMPERATURE, and is audited now: `crate::attn_temperature` is the
1721 // seam and `tests/attn_temperature_graphs.rs` carries five
1722 // fixtures. Its verdict's "leading-dense + MoE + shared expert" was
1723 // wrong on two counts (see the AUDITED entry), and its
1724 // `yarn_log_multiplier` half found that YaRN's magnitude term was
1725 // missing for every architecture (`crate::yarn_magnitude`). The
1726 // reach was measured before a line was written: `llama4` seeds the
1727 // same three constants from literals and gates the multiply on
1728 // its no-RoPE layers (`llama4.cpp:15-17,175-176`), and `deepseek2`
1729 // / `mistral4` read the same key with `attention.temperature_length`
1730 // as the floor (`deepseek2.cpp:46-49`) -- the MLA loader REFUSES
1731 // that by name now, where it used to drop it, because that engine
1732 // has no golden to check an implementation against.
1733 // `nanbeige` was HERE, NEW CODE on running the same physical layers
1734 // more than once, and is audited now: `crate::layer_loops` is the
1735 // seam and `tests/layer_loop_graphs.rs` carries three fixtures.
1736 // The verdict's last sentence was the design: "the copy has no
1737 // home" -- it has one now, and it is a mapping, not a copy. See
1738 // `AUDITED_GENERIC_GQA`.
1739 // `arcee` was HERE, NEW CODE on `UNGATED_RELU_SQR`, and is audited
1740 // now: the FFN is `FfnActivation::ReluSqr` and
1741 // `tests/ungated_ffn_graphs.rs` carries the fixture.
1742 // `plm` was HERE, NEW CODE on `UNGATED_RELU_SQR`'s second half,
1743 // DeepSeek-2 MLA attention on a dense model, and it is on the MLA
1744 // engine now (`DecoderFamily::Mla`, below): the engine gained the
1745 // direct-Q form (`crate::mla_q_proj`), the per-architecture table
1746 // (`crate::mla_arch`) and its FIRST libllama-golden fixture
1747 // (tests/plm_graphs.rs) with it. That row is not in
1748 // AUDITED_GENERIC_GQA because it never ran on the generic path.
1749];
1750
1751/// Shared by the three frink-only alias rows `mistral`, `mixtral` and
1752/// `yi`: the reason none of them is an architecture at all.
1753///
1754/// These were UNKNOWN, and the open question was "what would settle
1755/// it?" -- a real GGUF whose `general.architecture` is literally one of
1756/// the three. **The investigation that settled it (2026-09-10) did not
1757/// find one, and found the reason no such file exists.** Three
1758/// measurements, not readings:
1759///
1760/// 1. `grep '"mistral' src/llama-arch.cpp` returns `mistral3` and
1761/// `mistral4` and nothing else; `mixtral` and `yi` return nothing.
1762/// Neither is in gguf-py's `MODEL_ARCH_NAMES` either.
1763/// 2. A GGUF written with `general.architecture = "mistral"` (and
1764/// `mixtral`, and `yi`) is REFUSED by libllama with
1765/// `llama_model_load: error loading model: unknown model
1766/// architecture: 'mistral'`. So no golden reference for these rows
1767/// can ever exist, at the evidence standard every audited row in
1768/// this file meets.
1769/// 3. The two real checkpoints in `models/` --
1770/// `Mistral-7B-Instruct-v0.2-Q4_K_M.gguf` and
1771/// `Yi-1.5-6B-Chat-Q4_K_M.gguf` -- both declare
1772/// `general.architecture = llama`, which is audited and runs.
1773///
1774/// So the rows are refused as strings rather than triaged as
1775/// architectures, and the refusal says the actionable thing: your file
1776/// is spelled `llama`. Leaving them on the generic path would have kept
1777/// a live hazard: the catalog gave all three NEOX RoPE while `llama`,
1778/// the graph they really are, is in `llama_model_rope_type`'s NORM
1779/// group (llama-model.cpp, the `case LLM_ARCH_LLAMA:` arm), so a file
1780/// spelling `mistral` would have been rotated on the wrong pairs of
1781/// every Q/K head -- the exact defect behind the Llama-3.1-8B
1782/// wrong-logits bug -- and `rope_layout_matches_llama_cpp` cannot see
1783/// it, because its lookup miss on a frink-only name is a `continue`.
1784///
1785/// `phi4` is the same shape and is deliberately NOT changed here: it is
1786/// still `GenericGqa` + UNKNOWN, because unlike these three it names a
1787/// concrete, checkable hypothesis (phi3's fused-QKV graph) that a real
1788/// file would confirm or refute. These three name none.
1789const NO_UPSTREAM_ARCH: &str =
1790 "this is not a GGUF architecture. `mistral`, `mixtral` and `yi` appear in neither LLM_ARCH_NAMES (src/llama-arch.cpp lists `mistral3` and `mistral4` and nothing else under that prefix) nor gguf-py's MODEL_ARCH_NAMES, and libllama REFUSES a file declaring one of them: `unknown model architecture: 'mistral'` -- measured, on a synthetic llama-shaped file written under each of the three strings. Every real Mistral, Mixtral and Yi checkpoint converts to `llama` instead, which frink audits and runs: the two in this repo's own models/ directory (Mistral-7B-Instruct-v0.2-Q4_K_M.gguf, Yi-1.5-6B-Chat-Q4_K_M.gguf) both declare `general.architecture = llama`. IF YOUR FILE REALLY SPELLS THIS, it came from a converter neither engine has read, so its RoPE variant, its norm placement and its FFN shape are all undetermined and frink will not guess -- re-convert it with llama.cpp's convert_hf_to_gguf.py and it will load as `llama`. These rows used to sit on the generic path with NEOX RoPE, while `llama` is in llama_model_rope_type's NORM group, so such a file would have been rotated on the wrong pairs of every Q/K head";
1791
1792// `UNGATED_RELU_SQR` was here: the verdict `arcee` and `plm` shared,
1793// and after `arcee` closed (2026-09-11) the one that said why `plm` had
1794// not -- DeepSeek-2 MLA attention on a dense model, `plm.cpp:16-19,
1795// 32-36,84-166`, which frink had only inside the dedicated `MlaEngine`,
1796// arch-gated to `deepseek2` / `mistral4`, with no dense ReLU-squared FFN
1797// and no libllama-golden evidence of its own. `plm` closed on that
1798// engine on 2026-09-12 (`crate::mla_arch`, `crate::mla_q_proj`,
1799// tests/plm_graphs.rs), and the same fixture is the engine's first
1800// golden. The FFN half is `uses_relu_sqr` below.
1801
1802/// Triaged rows of the generic **NEOX**-RoPE group. Same rules as
1803/// [`NORM_ROPE_TRIAGED`].
1804const NEOX_ROPE_TRIAGED: &[(&str, TriageClass, &str)] = &[
1805 // --- Landed upstream AFTER the 2026-08-04 pin (see the NORM group).
1806 // `maple` was HERE for one PR, ONE MATCH ARM on the per-layer RoPE
1807 // gate, and is audited now: `src/models/maple.cpp:88` rotates only
1808 // the sliding layers, which is `RopeLayers::SlidingOnly` --
1809 // `cohere2`'s rule, one row of `crate::rope_layers`. The other
1810 // things its verdict listed were already served and each table
1811 // gained one name: the window ARRAY (`crate::swa_layers`), the
1812 // per-layer `expert_feed_forward_length` array
1813 // (`crate::layer_shapes`) and the SwiGLU clamp arrays
1814 // (`crate::act_layers`). `tests/no_rope_layer_graphs.rs`.
1815 // `spark2_5` was HERE for one PR, ONE MATCH ARM on the attention
1816 // gate, and is audited now: `src/models/spark2-5.cpp:41,97-105`
1817 // is `step35`'s corner of `crate::attn_gate` with the tensor
1818 // REQUIRED instead of optional, which is one row of
1819 // `ATTN_GATE_ARCHS`, and the rest of its graph (the window ARRAY
1820 // with `rope.freq_base_swa`, per-layer head counts, a gated GELU
1821 // FFN, NEOX RoPE) was already served. The libllama golden is
1822 // `tests/gated_attention_graphs.rs`; it is the first row closed
1823 // against the MOVED pin, and it took a fixture and an hour, which
1824 // is what a ONE MATCH ARM verdict is supposed to mean.
1825 // `hrm_text` (DFM Mimir 1B) was HERE for one PR, NEW CODE on its
1826 // two-stack cycle schedule, and is audited now. The schedule is a
1827 // second variant of `crate::layer_loops` -- two stacks of `lps`
1828 // blocks replayed over `h * (l + 1)` passes, each pass aliasing
1829 // one of the two -- and the TWO residual streams it recombines at
1830 // every stack boundary are `crate::hrm`, one type the four host
1831 // bodies call rather than four copies of "hold two vectors". Its
1832 // other facts were served or one table row each: weightless RMS
1833 // norms (`NON_PARAMETRIC_RMS_NORM`), a per-element sigmoid gate,
1834 // an `embedding_scale`, and NO `output_norm` tensor at all
1835 // (`norm_sites::NO_OUTPUT_NORM`, because the last stack's own norm
1836 // is the final one). `tests/hrm_text_graphs.rs`.
1837 (
1838 "qwen4exp",
1839 TriageClass::NewCode,
1840 "the largest graph upstream has (`src/models/qwen4exp.cpp`, 1297 lines). A gated delta-net (`:1`, \
1841 `build_layer_attn_linear`, the fourth caller of the helper `crate::gdn` serves \
1842 for the other three) over a hybrid memory INDEX (`llama-memory-hybrid-idx.h`, a \
1843 new memory class), with an attention gate, MoE, and an IMROPE rotation. The \
1844 delta-net half is the seam frink has; the memory index is not, and it decides \
1845 which state a layer reads",
1846 ),
1847 // `mellum` was HERE, NEW CODE on two things. The first -- its
1848 // sliding layers roped with the model's YaRN switched OFF
1849 // (`mellum.cpp:128-142`), the Olmo-3 rule -- is a REFUSAL BY NAME
1850 // in `crate::swa_geometry` for a file declaring both a window and
1851 // a RoPE scaling, which every real Mellum2 export does. The second
1852 // -- the per-layer sliding-window ARRAY that `:12-17` honour and
1853 // `conversion/mellum.py:28` always writes -- is `crate::swa_layers`
1854 // now, and `mellum` is the ONE generic-path architecture whose
1855 // graph honours the array, so it is the row that evidences that
1856 // branch against libllama (`tests/window_array_graphs.rs`). A
1857 // Mellum without a scaling runs; a Mellum2 stops on the first
1858 // thing, by name.
1859 // `talkie` was HERE, NEW CODE on four things, and is audited now:
1860 // the weightless norms are `NormOp::RmsNoParams`, the per-head
1861 // scalar Q gain and weightless K norm are `QkNormStyle::
1862 // PerHeadScalar`, the embedding skip stream is `crate::skip_stream`,
1863 // and the two projection gains its converter writes are the two
1864 // `crate::weight_scales` serves. `tests/skip_stream_graphs.rs`
1865 // carries two fixtures. See `AUDITED_GENERIC_GQA`.
1866 // `mimo2` was HERE, NEW CODE on the split K/V head width, and is
1867 // audited now: `crate::kv_head_dims` is the seam,
1868 // `crate::attn_value_scale` its small second half, and
1869 // `tests/split_kv_head_dim_graphs.rs` carries three fixtures. Its
1870 // verdict had already said the NextN blocks, the window array, the
1871 // sinks and the per-layer shapes were no longer blockers; they were
1872 // not, and the fixture carries all four. See `AUDITED_GENERIC_GQA`.
1873 // `afmoe` was HERE, NEW CODE on the gated attention (`afmoe.cpp:73`)
1874 // and the `sqrt(n_embd)` embedding scale (`:120`). Both are
1875 // implemented -- `crate::attn_gate` and
1876 // `embeddings_scaled_by_sqrt_n_embd` -- and the row is audited on a
1877 // libllama-golden fixture (`tests/gated_attention_graphs.rs`). Its
1878 // sigmoid default for `expert_gating_func` (`:29-30`) had been in
1879 // `SIGMOID_GATING_ARCHITECTURES` since 2026-09-01; the fixture
1880 // declares no gating key so that default is what it measures.
1881 // `apertus` was HERE, NEW CODE on xIELU with four PER-LAYER
1882 // parameter arrays (`apertus.cpp:6-9,132-138`). The arrays are
1883 // `crate::act_layers` (read as `get_key_or_arr` reads them, an
1884 // array at `n_layer` length or a scalar broadcast), the activation
1885 // is `frink_moe::GluAct::Xielu` carrying that layer's four, and
1886 // `ModelConfig::layer_ffn_act(il)` is the one accessor every FFN
1887 // body asks -- the row is audited on a libllama-golden fixture
1888 // (`tests/per_layer_activation_graphs.rs`). The verdict's third
1889 // sentence was wrong: `:50,52` CREATE `attn_q_norm.bias` /
1890 // `attn_k_norm.bias` and `:93,96` pass `NULL` as the bias, so they
1891 // are never read; measured (libllama's logits byte-identical with
1892 // and without them) and recorded in `crate::unread_tensors`.
1893 (
1894 "grovemoe",
1895 TriageClass::NewCode,
1896 "a SECOND bank of experts, not just a scale. src/models/grovemoe.cpp:57-59 creates \
1897 `ffn_gate_chexps` / `ffn_down_chexps` / `ffn_up_chexps` -- `n_expert / \
1898 n_group_experts` \"chunk\" experts with their own width n_ff_chexp -- and the graph \
1899 runs build_moe_ffn TWICE (:137 over the ordinary experts, :153 over the chunk \
1900 experts) before :167 adds `scale(moe_out, expert_group_scale)` to the residual. The \
1901 inventory recorded only the post-sum group scale and called this small; the second \
1902 expert bank with its own routing is the larger half and frink's MoE layer holds \
1903 one bank. Both n_group_experts and expert_group_scale are REQUIRED keys (:6-7). \
1904 QK-norm is before RoPE (:100-109), which is the one thing that would otherwise have \
1905 been a blocker. READ ON 2026-09-12 AGAINST THE REFERENCE MODEL, and not closed \
1906 for a reason the count cannot show: llama.cpp's graph disagrees with \
1907 `modeling_grove_moe.py` in two places. (1) grovemoe.cpp:148-149 sets `cur = \
1908 moe_out` and :152 feeds THAT -- the routed experts' OUTPUT -- into the chunk \
1909 experts, where the reference (`GroveMoeSparseMoeBlock.forward`:369) feeds them \
1910 the same `hidden_states` the routed experts read; upstream PR #15510's own debug \
1911 dump shows `MUL_MAT_ID(ffn_gate_chexps, ffn_moe_out)`. (2) llama-graph.cpp: \
1912 2035-2039 divides the selected expert ids by `n_group_experts` and then gathers \
1913 the weights from the softmax probs AT THE CHUNK INDEX, where the reference (:324, \
1914 :370) gathers them at the ORIGINAL expert index; the two agree only when the \
1915 selected expert's index equals its chunk's. Both are shipped upstream (master \
1916 2026-09) and neither was discussed in the PR. So there is no single graph to \
1917 match: reproducing llama.cpp reproduces a divergence from the model, and matching \
1918 the model has no libllama golden. Refused by name until upstream settles it; the \
1919 reach of the mechanism (a precomputed `probs`) is `crate::router_input`'s census",
1920 ),
1921 // `hunyuan-dense` was HERE, ONE MATCH ARM on the NTK-alpha RoPE
1922 // base rescale. The arm landed (`crate::rope_ntk_alpha`), the
1923 // post-RoPE QK-norm half was already implemented, and both are
1924 // evidenced against libllama in `tests/one_match_arm_graphs.rs`, so
1925 // the row is audited and carries no verdict. Its verdict cited
1926 // `conversion/hunyuan.py:356` as the line that writes
1927 // `{arch}.rope.scaling.alpha` for this architecture; that line is in
1928 // HunyuanVLTextModel, whose model_arch is HUNYUAN_VL. The
1929 // HUNYUAN_DENSE converter (:254-281) does the same arithmetic in
1930 // Python and writes the already-scaled base instead.
1931 // `laguna` was HERE, NEW CODE on the gated attention (`laguna.cpp:124`,
1932 // softplus, per head or per element) and a second rotary width
1933 // (`:50`). The gate is `crate::attn_gate` and the row is audited on
1934 // two libllama-golden fixtures, one per width
1935 // (`tests/gated_attention_graphs.rs`). The second rotary width is a
1936 // REFUSAL by name in `loader.rs` for one day -- a file whose
1937 // `rope.dimension_count_swa` differs from `rope.dimension_count` --
1938 // and is served now (`ModelConfig::rope_dim_swa`, with `step35`);
1939 // a window with a RoPE scaling (`:48,184-192`, the Olmo-3 rule,
1940 // `swa_layers_unscaled_rope`) stays refused, from a fixture that
1941 // has it. Real Laguna-M.1 has neither; real Laguna-XS.2 has both
1942 // and stops at the scaling.
1943 // `step35` was HERE, NEW CODE on its per-layer SwiGLU clamp arrays
1944 // (`step35.cpp:28-29`, applied by llama.cpp's generic
1945 // `build_moe_ffn` / `build_ffn` at `llama-graph.cpp:2146-2164` /
1946 // `:1751-1768`) and its half-width rotary on the full layers
1947 // (`:9`). The clamp is the second body on the per-layer activation
1948 // seam `apertus` opened -- `crate::act_layers::SwigluClamps`, read
1949 // by SITE, `frink_moe::GluAct::SwigluClamped` -- and the width is
1950 // `ModelConfig::rope_dim_swa` (`crate::swa_geometry`, the same
1951 // two-valued `n_rot(il)` that lifted Laguna-XS.2's refusal). Three
1952 // libllama-golden fixtures (`tests/clamped_swiglu_graphs.rs`):
1953 // clamped, unclamped, and with a NextN block. Everything else the
1954 // verdict had crossed off is carried by them rather than assumed.
1955 // `mistral`, `mixtral` and `yi` were HERE, UNKNOWN on
1956 // NO_UPSTREAM_ARCH. The question that verdict asked -- "is there a
1957 // real GGUF spelling one of these?" -- was answered NO, with a
1958 // measurement: libllama refuses all three strings outright. They
1959 // are refused as strings now, not triaged as architectures. See
1960 // NO_UPSTREAM_ARCH.
1961 // `grok` and `dbrx` were HERE, both NEW CODE, and both closed on
1962 // seams that had landed the day before. `grok`'s verdict named
1963 // five hardcoded defaults (`grok.cpp:5-12` -- there are seven at
1964 // this checkout), a `kq_scale = 1.0f` attention with the real
1965 // scale folded into a tanh softcap, and `blk.N.attn_output_norm`
1966 // as an unread tensor name: the defaults are a
1967 // `scalar_multipliers::MultiplierDefaults` variant like MiniCPM's,
1968 // the attention is `attention_scale` plus the existing softcap, and
1969 // the tensor name is a `crate::norm_sites` row. Its Grok-2 shape --
1970 // a dense GELU FFN summed with the MoE at sqrt(2)/2 (`:171-184`)
1971 // -- is refused BY NAME in `loader.rs`, so the row is admitted for
1972 // Grok-1. `dbrx`'s verdict named the weighted LayerNorm, the
1973 // REQUIRED `attention.clamp_kqv`, and `attn_output_norm` as its
1974 // pre-FFN norm: `crate::norm::NormOp::LayerNorm`, `crate::clamp_kqv`
1975 // (which closed `olmo`'s clip_qkv sub-refusal with it) and the
1976 // same `norm_sites` table. See `AUDITED_GENERIC_GQA`.
1977 // `smallthinker` was HERE, NEW CODE on the ROUTER OPERAND, and is
1978 // audited now (`crate::router_input`, `tests/router_input_graphs.rs`).
1979 // Its verdict named three things and all three landed: the router
1980 // reading `inpL` (`RouterInput::RawLayerInput`), the gated
1981 // `LLM_FFN_RELU` experts (`FfnActivation::Reglu`, which the verdict
1982 // called "one match arm" and which needed a variant because
1983 // `ffn_is_ungated` and `layer_ffn_acts` must agree about whether
1984 // the gate is real), and the `n_swa = 4096` pin
1985 // (`swa_window_override`). The reach was measured before a line
1986 // was written and came back with ONE: `grovemoe` also passes a
1987 // precomputed `probs` but routes on the normed FFN input, and the
1988 // two graphs whose operand really differs (`gemma4`, `nemotron-h`)
1989 // are on other engines. See `AUDITED_GENERIC_GQA`.
1990 // `bitnet` was HERE, NEW CODE on the two norms INSIDE the blocks
1991 // (`bitnet.cpp:24,36`), and is audited now: `crate::sub_norms` is
1992 // the seam and `tests/sub_norm_graphs.rs` carries the fixture. Its
1993 // verdict's third sentence, the per-projection `.scale` tensors, is
1994 // a refusal by name now (`crate::weight_scales`) rather than an
1995 // unread-tensor error, and its fourth (no `output` tensor) was
1996 // already served by the tied lm_head. See `AUDITED_GENERIC_GQA`.
1997 // `openelm` was HERE, NEW CODE on PER-LAYER SHAPES, and is audited
1998 // now with `deci` (`crate::layer_shapes`,
1999 // `tests/per_layer_shape_graphs.rs`). The misleading missing-hparam
2000 // message its verdict named is gone with it:
2001 // `layer_shapes::read_u64_per_layer` reads the arrays
2002 // `conversion/openelm.py:57-59` writes.
2003];
2004
2005/// Full inventory keyed by GGUF `general.architecture` string.
2006/// Kept in sync with `.scratch/llama.cpp/src/llama-arch.cpp` `LLM_ARCH_NAMES`.
2007pub fn architecture_catalog() -> &'static [ArchProfile] {
2008 use std::sync::OnceLock;
2009 use ArchScope::*;
2010 use DecoderFamily::*;
2011 use MemoryKind::*;
2012 use QkNormStyle::*;
2013 use RopeLayout::*;
2014
2015 static CAT: OnceLock<Vec<ArchProfile>> = OnceLock::new();
2016 CAT.get_or_init(|| {
2017 let mut v = Vec::with_capacity(160);
2018 // --- Verified / standard GQA (Norm RoPE) ---
2019 //
2020 // `llama` is the only untriaged name left in this group: it is
2021 // audited, so it runs and needs no verdict. Every other
2022 // Norm-RoPE row moved into `NORM_ROPE_TRIAGED` below when it was
2023 // read against llama.cpp's graph.
2024 v.push(gqa_norm("llama"));
2025 // Audited too, each by a libllama-golden fixture -- see
2026 // `AUDITED_GENERIC_GQA` for the arm each one needed and
2027 // `tests/one_match_arm_graphs.rs` for the evidence.
2028 for n in ["bailingmoe", "deepseek", "maincoder"] {
2029 v.push(gqa_norm(n));
2030 }
2031 // Were FIXTURE-AWAY in this group and now have the fixture:
2032 // `tests/fixture_away_graphs.rs`, same evidence standard.
2033 for n in ["baichuan", "ernie4_5", "internlm2", "xverse"] {
2034 v.push(gqa_norm(n));
2035 }
2036 // `ernie4_5-moe` was ONE MATCH ARM in `NORM_ROPE_TRIAGED` and is
2037 // audited now: the step every real checkpoint carries has a
2038 // libllama-golden fixture (`tests/one_match_arm_graphs.rs`) and
2039 // any other step is refused by name (`crate::moe_interleave`).
2040 v.push(gqa_norm("ernie4_5-moe"));
2041 // `chatglm` was the LAST ONE MATCH ARM row anywhere in this
2042 // file. Its arm -- the fused `attn_qkv.bias` -- landed in
2043 // `crate::qkv_fused` and has a libllama-golden fixture
2044 // (`tests/one_match_arm_graphs.rs`), so the class is empty now.
2045 v.push(gqa_norm("chatglm"));
2046 // `nanbeige` was NEW CODE in `NORM_ROPE_TRIAGED` on the layer
2047 // loop (`nanbeige.cpp:19-31`), audited now on
2048 // `crate::layer_loops` (`tests/layer_loop_graphs.rs`). NORM RoPE:
2049 // its converter is `LlamaModel` (`conversion/nanbeige.py:8`) and
2050 // `LLM_ARCH_NANBEIGE` sits in the NORM group, which
2051 // `tests/rope_layout.rs` pins.
2052 v.push(gqa_norm("nanbeige"));
2053 // The Granite family. All three were NEW CODE in
2054 // `NORM_ROPE_TRIAGED` on the four scalar multipliers, which
2055 // `crate::scalar_multipliers` now implements once for all of
2056 // them (`tests/granite_family_graphs.rs`). `granite-moe` is a
2057 // frink-only alias -- `llama-arch.cpp:101` spells it
2058 // `granitemoe` -- and is here rather than anywhere else so it
2059 // cannot be given a different path from the row it aliases.
2060 for n in ["granite", "granitemoe", "granite-moe"] {
2061 v.push(gqa_norm(n));
2062 }
2063 // Granite 4.0 (`granitehybrid`; `granite-hybrid` is the frink
2064 // alias every Granite row carries). `granite-hybrid.cpp` is the
2065 // Granite graph with a Mamba-2 block where `head_count_kv` is 0
2066 // (`crate::mamba2`, `AttnShape::Mamba2`), and its converter
2067 // writes `rope.scaling.finetuned = false` for every export with
2068 // a Mamba layer, so the attention layers rotate NOTHING
2069 // (`crate::rope_finetuned`, `RopeLayers::Never`). Audited on
2070 // tests/granite_hybrid_graphs.rs.
2071 for n in ["granitehybrid", "granite-hybrid"] {
2072 v.push(prof(
2073 n,
2074 TextGeneration,
2075 DecoderFamily::Hybrid,
2076 MemoryKind::Hybrid,
2077 Norm,
2078 ArchPath::GenericGqa { rope: Norm },
2079 WholeVector,
2080 ));
2081 }
2082 // Nemotron-H (`nemotron_h`: Nemotron-H 8B / 47B / 56B, Nemotron-3
2083 // Nano dense). One block per layer -- Mamba-2, attention or an
2084 // ungated ReLU-squared FFN -- under ONE `attn_norm` and one
2085 // residual add (`nemotron-h.cpp:143-158`; `layer_shapes::
2086 // BLOCK_WITHOUT_FFN_KEEPS_ITS_OUTPUT`, `ZeroKvLayer::
2087 // Mamba2UnlessFfn`, `norm_sites::ONE_NORM_PER_LAYER`). Its
2088 // attention never calls `ggml_rope_ext` (`:181-193`): the NEOX
2089 // group entry (llama-model.cpp:2671) is a filler and
2090 // `rope_layers` answers `Never`. Audited on
2091 // tests/nemotron_h_graphs.rs. `nemotron_h_moe` (Nemotron-3 Nano
2092 // 30B-A3B) is the same graph with a sigmoid MoE of UNGATED
2093 // ReLU-squared experts and an ungated shared expert on the FFN
2094 // layers (`:206-231`); its latent variant (`moe_latent_size`,
2095 // Nemotron-3 Super) is refused by name
2096 // (`unsupported_feature_keys`).
2097 for n in ["nemotron_h", "nemotron_h_moe"] {
2098 v.push(prof(
2099 n,
2100 TextGeneration,
2101 DecoderFamily::Hybrid,
2102 MemoryKind::Hybrid,
2103 Neox,
2104 ArchPath::GenericGqa { rope: Neox },
2105 WholeVector,
2106 ));
2107 }
2108 // Falcon-H1 (`falcon-h1`: 0.5B / 1.5B / 3B / 7B / 34B): attention
2109 // AND the Mamba-2 block on EVERY layer, in parallel on the same
2110 // `attn_norm` output, summed before the residual
2111 // (`falcon-h1.cpp:137-161`; `crate::mamba2::
2112 // PARALLEL_WITH_ATTENTION`, `ModelConfig::parallel_ssm`). NEOX
2113 // RoPE (llama-model.cpp:2615). Audited on
2114 // tests/falcon_h1_graphs.rs.
2115 v.push(prof(
2116 "falcon-h1",
2117 TextGeneration,
2118 DecoderFamily::Hybrid,
2119 MemoryKind::Hybrid,
2120 Neox,
2121 ArchPath::GenericGqa { rope: Neox },
2122 WholeVector,
2123 ));
2124 // OLMo-1 was NEW CODE in `NORM_ROPE_TRIAGED` on its
2125 // non-parametric LayerNorm, which `crate::norm::NormOp` now
2126 // implements (`tests/olmo_graphs.rs`). NORM RoPE:
2127 // `llama_model_rope_type` puts LLM_ARCH_OLMO in the
2128 // consecutive-pairs group (llama-model.cpp:2585), which is also
2129 // why `conversion/olmo.py:33-36` permutes q_proj and k_proj the
2130 // way `LlamaModel` does. Its `olmo.attention.clamp_kqv` was
2131 // refused by name and is applied now (`crate::clamp_kqv`),
2132 // since `dbrx` needed the same clamp.
2133 v.push(gqa_norm("olmo"));
2134 // `smollm3` was refused OUTRIGHT, in the "No RoPE at all" group
2135 // below, and it was the only row there whose graph is the plain
2136 // pre-norm llama one. What it needed was a way to say WHICH
2137 // LAYERS ROTATE: `smollm3.cpp:5,69` skip `(il + 1) % 4 == 0`,
2138 // nine layers of a 36-layer SmolLM3-3B, with no GGUF key.
2139 // `crate::rope_layers` says it now, once, for the six
2140 // architectures llama.cpp gates per layer, and
2141 // `tests/no_rope_layer_graphs.rs` is the libllama-golden
2142 // evidence. NORM RoPE: llama-model.cpp puts LLM_ARCH_SMOLLM3 in
2143 // the consecutive-pairs group (:2600).
2144 v.push(gqa_norm("smollm3"));
2145 // `arcee` was NEW CODE in `NORM_ROPE_TRIAGED` on the ungated
2146 // ReLU-squared FFN, which `FfnActivation::ReluSqr` implements
2147 // (`tests/ungated_ffn_graphs.rs`). NORM RoPE: LLM_ARCH_ARCEE is
2148 // in the consecutive-pairs group (llama-model.cpp:2600).
2149 v.push(gqa_norm("arcee"));
2150 // `deci` was NEW CODE in `NORM_ROPE_TRIAGED` on per-layer
2151 // shapes, which `crate::layer_shapes` implements
2152 // (`tests/per_layer_shape_graphs.rs`). NORM RoPE: LLM_ARCH_DECI
2153 // is in the consecutive-pairs group (llama-model.cpp:2576).
2154 v.push(gqa_norm("deci"));
2155 // `mistral3` was NEW CODE in `NORM_ROPE_TRIAGED` on the
2156 // per-position attention temperature, which
2157 // `crate::attn_temperature` implements
2158 // (`tests/attn_temperature_graphs.rs`). NORM RoPE:
2159 // LLM_ARCH_MISTRAL3 is in the consecutive-pairs group
2160 // (llama-model.cpp:2604), which `tests/rope_layout.rs` pins.
2161 v.push(gqa_norm("mistral3"));
2162 // `arctic` was NEW CODE in `NORM_ROPE_TRIAGED` on the parallel
2163 // dense + MoE layer, audited now (`crate::parallel_dense_ffn`,
2164 // `RouterInput::NormedLayerInput`, tests/parallel_dense_ffn_graphs.rs).
2165 // NORM RoPE: llama-model.cpp:2588.
2166 v.push(gqa_norm("arctic"));
2167 // `glm4` was a `dedicated` refusal sent to the GLM-5.2 MLA loader;
2168 // audited now on the generic NORM path (tests/glm4_graphs.rs).
2169 // NORM RoPE: llama-model.cpp:2699 (M-RoPE files refused,
2170 // `crate::mrope`).
2171 v.push(gqa_norm("glm4"));
2172 // `orion` and `nemotron` were DedicatedOnly on their REQUIRED
2173 // LayerNorm biases; audited now on `NormOp::LayerNormBias`
2174 // (tests/biased_layer_norm_graphs.rs). NEOX RoPE:
2175 // llama-model.cpp:2653-2654.
2176 v.push(gqa_neox("orion"));
2177 v.push(gqa_neox("nemotron"));
2178 // The three whose LAST blocker was the projection biases
2179 // (`crate::proj_bias`, tests/proj_bias_graphs.rs). NEOX RoPE:
2180 // llama-model.cpp:2649 (starcoder2), :2652 (codeshell), :2662
2181 // (jais2).
2182 v.push(gqa_neox("starcoder2"));
2183 v.push(gqa_neox("codeshell"));
2184 v.push(gqa_neox("jais2"));
2185 // `stablelm` was DedicatedOnly on its REQUIRED LayerNorm biases;
2186 // audited now for the sequential shape, its parallel residual
2187 // (`crate::parallel_residual`) and per-head LayerNorm QK norm
2188 // (`crate::qk_layer_norm`) refused by name from fixtures libllama
2189 // runs (tests/stablelm_graphs.rs). NEOX RoPE: llama-model.cpp:2624.
2190 v.push(gqa_neox("stablelm"));
2191 // The parallel residual's two arms, each on a real graph
2192 // (`crate::parallel_residual`, tests/parallel_residual_graphs.rs):
2193 // `gptneox` (Pythia, GPT-NeoX-20B) under `use_parallel_residual`
2194 // with two norms, `plamo` (PLaMo-13B) with the one shared norm.
2195 // NEOX RoPE: llama-model.cpp:2651 (gptneox), :2639 (plamo).
2196 v.push(gqa_neox("gptneox"));
2197 v.push(gqa_neox("plamo"));
2198 // `command-r` (Command-R 35B, Aya-23): the shared-norm parallel
2199 // residual over the weighted LayerNorm WITHOUT a bias
2200 // (`WEIGHTED_LAYER_NORM`'s second caller) and a `logit_scale`
2201 // multiply (tests/command_r_graphs.rs). NORM RoPE:
2202 // llama-model.cpp:2582.
2203 v.push(gqa_norm("command-r"));
2204 // `falcon` (Falcon-7B / 40B / 180B): the shared-norm parallel
2205 // residual at 7B and the two-norm one at 40B, whose second norm
2206 // is `attn_norm_2` FOR ATTENTION (`norm_sites::
2207 // ATTN_NORM_2_FEEDS_ATTENTION`), over the biased LayerNorm, a
2208 // fused `attn_qkv` with no bias, the ungated GELU FFN
2209 // (tests/falcon_graphs.rs). NEOX RoPE: llama-model.cpp:2651.
2210 v.push(gqa_neox("falcon"));
2211 // `phi2` (Phi-2, Phi-1.5): the shared-norm parallel residual
2212 // over the biased LayerNorm, Q/K/V biases, `attn_output.bias`
2213 // and the FFN biases, the ungated GELU, and an `output.bias` on
2214 // the LM head (`proj_bias::OUTPUT_BIAS_CREATORS`); partial NEOX
2215 // rotary (tests/phi2_graphs.rs). NEOX RoPE: llama-model.cpp:2636.
2216 v.push(gqa_neox("phi2"));
2217 // `cohere2` (Command-R7B, Command-A): `command-r`'s graph with a
2218 // REQUIRED window whose sliding layers alone are rotated
2219 // (`crate::rope_layers::RopeLayers::SlidingOnly`), the
2220 // `logit_scale` REQUIRED (tests/cohere2_graphs.rs). NORM RoPE:
2221 // llama-model.cpp:2583.
2222 v.push(gqa_norm("cohere2"));
2223 // `phimoe` (Phi-3.5-MoE): `phi3`'s graph on routed experts with
2224 // the biased RMSNorm (`BIASED_RMS_NORM`), `attn_output.bias`
2225 // and `output.bias` (`crate::proj_bias`), LongRoPE, its window
2226 // key dead metadata as `phi3`'s (tests/phimoe_graphs.rs). NEOX
2227 // RoPE: llama-model.cpp:2638.
2228 v.push(gqa_neox("phimoe"));
2229 // `gpt2` and `starcoder` (GPT-2, StarCoder / SantaCoder): one
2230 // graph, the `gptneox` sequential layer with a learned position
2231 // table added to the embeddings and NO rotation
2232 // (`crate::position_embd`, `rope_layers::RopeLayers::Never`;
2233 // tests/position_embd_graphs.rs). The layout here is a filler
2234 // nothing reads: `llama_model_rope_type` answers NONE for `gpt2`
2235 // and NORM for `starcoder`, and neither graph calls `ggml_rope`.
2236 v.push(gqa_norm("gpt2"));
2237 v.push(gqa_norm("starcoder"));
2238 // The ALiBi rows (`crate::alibi`; tests/alibi_graphs.rs): no
2239 // rotation, the bias added to every score. The layout is a
2240 // filler nothing reads. `refact` (Refact-1.6B): RMSNorm, split
2241 // Q/K/V, SwiGLU, multi-query, the literal 8. `bloom` (BLOOM):
2242 // the biased LayerNorm on the embeddings too
2243 // (`norm_sites::EMBEDDING_NORM_ARCHITECTURES`), fused `attn_qkv`
2244 // with bias, the required projection biases, the ungated GELU,
2245 // the literal 8. `mpt` (MPT-7B / 30B): the weighted LayerNorm
2246 // (its biases are all optional and MPT has none), fused
2247 // `attn_qkv`, optional projection biases, the ungated GELU,
2248 // `attention.max_alibi_bias` from the key with `clamp_kqv` and
2249 // an optional `position_embd`. `jais` (Jais-13B / 30B): the
2250 // biased LayerNorm, fused `attn_qkv` with bias, the required
2251 // projection biases INCLUDING `ffn_gate.bias`, SwiGLU, the key.
2252 v.push(gqa_norm("refact"));
2253 v.push(gqa_norm("bloom"));
2254 v.push(gqa_norm("mpt"));
2255 v.push(gqa_norm("jais"));
2256 // Same generic Norm-RoPE path, but READ against llama.cpp's own
2257 // graph -- see [`TriageClass`]. Each row below refuses with its
2258 // class and its blocker instead of the generic
2259 // "nobody has checked this" paragraph.
2260 for (n, class, blocker) in NORM_ROPE_TRIAGED {
2261 v.push(gqa_norm(n).triaged(*class, blocker));
2262 }
2263 for n in [
2264 "olmoe", "qwen2", "qwen2moe",
2265 // llama-model.cpp `llama_model_rope_type`: LLM_ARCH_OPENAI_MOE
2266 // falls in the `return LLAMA_ROPE_TYPE_NEOX` group, and a live
2267 // load of a gpt-oss GGUF prints `rope type = 2` (= NEOX).
2268 // frink had it on the interleaved (NORM) list, which rotates
2269 // the wrong pairs of every Q/K head.
2270 "gpt-oss",
2271 // Same audit, run over every arch at once against
2272 // `llama_model_rope_type`'s NEOX group
2273 // (llama-model.cpp:2613-2683). These 24 were on frink's
2274 // interleaved (NORM) list and reach the generic GQA decoder,
2275 // so every one of them rotated the wrong pairs of every Q/K
2276 // head and answered fluently and wrongly. Pinned by
2277 // `rope_layout_matches_llama_cpp` below; dots1 additionally
2278 // checked end-to-end against llama.cpp's own logits in
2279 // `tests/moe_routing_bias.rs`.
2280 "dots1",
2281 // Audited by libllama-golden fixtures in
2282 // `tests/one_match_arm_graphs.rs`: `hunyuan-moe` needed the
2283 // post-RoPE QK-norm order, `seed_oss` the gpt-oss pre-FFN
2284 // norm slot.
2285 "hunyuan-moe",
2286 "seed_oss",
2287 // `hunyuan-dense` was ONE MATCH ARM in `NEOX_ROPE_TRIAGED`
2288 // and is audited now: the NTK-alpha RoPE base rescale
2289 // (`crate::rope_ntk_alpha`) plus the post-RoPE QK-norm order
2290 // it shares with `hunyuan-moe`, both against libllama's own
2291 // logits.
2292 "hunyuan-dense",
2293 // Were FIXTURE-AWAY and now have the fixture
2294 // (`tests/fixture_away_graphs.rs`). EXAONE 3.x only:
2295 // `exaone4` and `exaone-moe` are different graphs and stay
2296 // in `NEOX_ROPE_TRIAGED` below. `bailingmoe2` is Ling-2.0
2297 // and is unrelated to the NORM-RoPE `bailingmoe` row above.
2298 "exaone",
2299 "bailingmoe2",
2300 "plamo3",
2301 // Were NEW CODE in `NEOX_ROPE_TRIAGED` and are audited now.
2302 // One residual topology, `crate::norm`, shared by both:
2303 // no pre-attention norm and no pre-FFN norm, each branch's
2304 // OUTPUT normed before its residual add. The evidence is
2305 // `tests/post_norm_only_graphs.rs`, one libllama-golden
2306 // fixture each. `olmo2` with a sliding window AND a RoPE
2307 // scaling (Olmo-3) and `exaone4` with 64 layers (the 32B)
2308 // are refused by name in `loader.rs` and are NOT covered by
2309 // these two rows.
2310 "olmo2",
2311 "exaone4",
2312 // Was NEW CODE in `NEOX_ROPE_TRIAGED` on ONE blocker: its
2313 // GLOBAL layers get no RoPE (`exaone-moe.cpp:136,155-161`).
2314 // That is the SAME RULE as `exaone4`'s -- :4 pins
2315 // `swa_type` to STANDARD, which makes `exaone4.cpp:116`'s
2316 // second disjunct false and the two predicates identical --
2317 // so both rows take one implementation,
2318 // `crate::rope_layers`, with a libllama-golden fixture each
2319 // in `tests/no_rope_layer_graphs.rs`. Everything else it
2320 // needed (leading dense, `exp_probs_b`, shared expert,
2321 // sigmoid gating from metadata, a per-head QK-norm) frink
2322 // already had, and its fixture carries all of it rather
2323 // than asserting so. The two things a REAL export carries
2324 // on top -- K-EXAONE's one NextN block inside `block_count`
2325 // (`exaone.py:132,146`) and the window pattern as a bool
2326 // ARRAY (`:84`) that `exaone-moe.cpp:7` never reads -- are
2327 // `crate::mtp_blocks` and `crate::swa_layers`, with a
2328 // fixture carrying both (`tests/window_array_graphs.rs`).
2329 "exaone-moe",
2330 // Was a `DedicatedOnly` bias refusal, not an unaudited row:
2331 // its only dropped bias was the FUSED `attn_qkv.bias`, which
2332 // `crate::qkv_fused` applies now. Qwen-1 only; qwen2 and
2333 // later store the split spelling and were already audited.
2334 "qwen",
2335 // Were NEW CODE in `NEOX_ROPE_TRIAGED` and are audited now,
2336 // each on seams that landed the day before: `dbrx` on the
2337 // weighted LayerNorm (`crate::norm`), the QKV clamp
2338 // (`crate::clamp_kqv`) and the `attn_output_norm` slot
2339 // (`crate::norm_sites`); `grok` on the defaults hook
2340 // (`scalar_multipliers::MultiplierDefaults::Grok`), the
2341 // scale-inside-softcap attention and the same `norm_sites`
2342 // table. NEOX RoPE: llama-model.cpp:2616-2617 put both in
2343 // the `n_rot/2`-offset group. `tests/dbrx_graphs.rs`,
2344 // `tests/grok_graphs.rs`.
2345 "dbrx",
2346 "grok",
2347 // Was NEW CODE in `NEOX_ROPE_TRIAGED` on per-layer shapes,
2348 // audited now with `deci` on `crate::layer_shapes`
2349 // (`tests/per_layer_shape_graphs.rs`). NEOX RoPE:
2350 // llama-model.cpp:2650.
2351 "openelm",
2352 // Were NEW CODE in `NEOX_ROPE_TRIAGED` on the gated
2353 // attention, audited now on `crate::attn_gate`
2354 // (`tests/gated_attention_graphs.rs`). NEOX RoPE:
2355 // llama-model.cpp:2676-2677.
2356 "afmoe",
2357 "laguna",
2358 // Was NEW CODE in `NEOX_ROPE_TRIAGED` on the sliding-window
2359 // ARRAY (`mellum.cpp:12-17`), audited now on
2360 // `crate::swa_layers` (`tests/window_array_graphs.rs`); its
2361 // window-with-YaRN half stays refused by name. NEOX RoPE:
2362 // llama-model.cpp:2682.
2363 "mellum",
2364 // Was NEW CODE in `NEOX_ROPE_TRIAGED` on xIELU's per-layer
2365 // parameter arrays, audited now on `crate::act_layers`
2366 // (`tests/per_layer_activation_graphs.rs`). NEOX RoPE:
2367 // llama-model.cpp:2671.
2368 "apertus",
2369 "step35",
2370 // Was ONE MATCH ARM in `NEOX_ROPE_TRIAGED` for one PR on
2371 // the per-head attention gate, audited now on
2372 // `crate::attn_gate` (`tests/gated_attention_graphs.rs`).
2373 // NEOX RoPE: `llama_model_rope_type` puts
2374 // LLM_ARCH_SPARK2_5 in the NEOX group, which
2375 // `tests/rope_layout.rs` pins.
2376 "spark2_5",
2377 // Was NEW CODE in `NEOX_ROPE_TRIAGED` on the router
2378 // operand (`smallthinker.cpp:111`), audited now on
2379 // `crate::router_input` (`tests/router_input_graphs.rs`).
2380 // NEOX RoPE: llama-model.cpp puts LLM_ARCH_SMALLTHINKER in
2381 // the `LLAMA_ROPE_TYPE_NEOX` group, which
2382 // `tests/rope_layout.rs` pins.
2383 "smallthinker",
2384 // Was NEW CODE in `NEOX_ROPE_TRIAGED` on the two norms
2385 // INSIDE the blocks (`bitnet.cpp:24,36`), audited now on
2386 // `crate::sub_norms` (`tests/sub_norm_graphs.rs`). NEOX
2387 // RoPE: llama-model.cpp:2625.
2388 "bitnet",
2389 // Was NEW CODE in `NEOX_ROPE_TRIAGED` on the split K/V head
2390 // width (`mimo2.cpp:47-48`), audited now on
2391 // `crate::kv_head_dims` (`tests/split_kv_head_dim_graphs.rs`).
2392 // NEOX RoPE: `LLM_ARCH_MIMO2` is in the NEOX group,
2393 // `tests/rope_layout.rs` pins it.
2394 "mimo2",
2395 // Was NEW CODE in `NEOX_ROPE_TRIAGED` on its weightless norms,
2396 // per-head scalar Q gain, skip stream and projection gains,
2397 // audited now (`crate::skip_stream`,
2398 // `tests/skip_stream_graphs.rs`). NEOX RoPE:
2399 // llama-model.cpp:2681.
2400 "talkie",
2401 // Was a `dedicated` refusal on its pre-FFN norm slot; audited
2402 // now (`norm_sites::PRE_FFN_NORM_IS_POST_ATTENTION_NORM`,
2403 // tests/glm4moe_graphs.rs). NEOX RoPE: llama-model.cpp:2700
2404 // (M-RoPE when `rope.dimension_sections` says so, which on
2405 // text positions is the same rotation; `crate::mrope`).
2406 "glm4moe",
2407 ] {
2408 v.push(gqa_neox(n));
2409 }
2410 // `minimax-01` (MiniMax-Text-01, 456B-A45B) was NEW CODE in
2411 // `NEOX_ROPE_TRIAGED` and is audited now. Its recurrent mask is
2412 // the Qwen3.5 one -- the same two keys, read by
2413 // `crate::gdn::recurrent_layers`, with the interval defaulting
2414 // to 8 instead of 4 -- and the BLOCK those layers run is
2415 // lightning attention (`crate::lightning`, `AttnShape::
2416 // Lightning`), whose state is one `head_dim x head_dim` KV per
2417 // head. Two things the tensor shapes do not show, both from
2418 // `minimax-01.cpp:303-309`: the fused `attn_qkv` runs through
2419 // SiLU BEFORE it is split, and it is HEAD-major (`[q|k|v]` per
2420 // head) rather than three blocks. And the residual topology is
2421 // its own (`crate::normed_residual`): each sublayer's pre-norm
2422 // output, times a REQUIRED `residual_scale`, REPLACES the
2423 // stream its branch joins, so the layer input is discarded.
2424 // `tests/minimax_01_graphs.rs`.
2425 v.push(prof(
2426 "minimax-01",
2427 TextGeneration,
2428 DecoderFamily::Hybrid,
2429 MemoryKind::Hybrid,
2430 Neox,
2431 ArchPath::GenericGqa { rope: Neox },
2432 QkNormStyle::WholeVector,
2433 ));
2434 // Triaged NEOX-RoPE rows; see `NORM_ROPE_TRIAGED` above.
2435 for (n, class, blocker) in NEOX_ROPE_TRIAGED {
2436 v.push(gqa_neox(n).triaged(*class, blocker));
2437 }
2438 // --- No RoPE at all -------------------------------------------
2439 //
2440 // `llama_model_rope_type` opens with a `LLAMA_ROPE_TYPE_NONE`
2441 // group, and five of its rows sat on frink's NEOX list once:
2442 // each loaded, ran at full speed, and answered fluently from
2443 // positions the checkpoint never encodes that way. They were
2444 // refused by name here until the position they DO encode was
2445 // served: `gpt2`'s learned table (`crate::position_embd`) and
2446 // the ALiBi bias of `mpt`, `refact`, `bloom` and `jais`
2447 // (`crate::alibi`, whose table also carries Baichuan-13B), with
2448 // `rope_layers::RopeLayers::Never` as the other half of each.
2449 // `tests/rope_layout.rs`'s `LLAMA_NO_ROPE` pins that a row of
2450 // that group reaches the generic path ONLY under `Never`.
2451 // `hrm_text` was NEW CODE in `NEOX_ROPE_TRIAGED` for one PR on
2452 // its two-stack schedule and is audited now
2453 // (`tests/hrm_text_graphs.rs`). NEOX RoPE:
2454 // `llama_model_rope_type` puts LLM_ARCH_HRM_TEXT in the NEOX
2455 // group, which `tests/rope_layout.rs` pins.
2456 v.push(prof(
2457 "hrm_text",
2458 TextGeneration,
2459 StandardGqa,
2460 KvGqa,
2461 Neox,
2462 ArchPath::GenericGqa { rope: Neox },
2463 WholeVector,
2464 ));
2465 // `muse-glimmer` was NEW CODE in `NORM_ROPE_TRIAGED` for one
2466 // PR on its two norm facts and is audited now
2467 // (`tests/muse_glimmer_graphs.rs`). NORM RoPE:
2468 // `llama_model_rope_type` puts LLM_ARCH_MUSE_GLIMMER in the
2469 // NORM group, which `tests/rope_layout.rs` pins. Per-head QK
2470 // norm: `muse-glimmer.cpp:40-41` stores `{n_embd_head_k}`
2471 // weights and `:106-107` apply them per head.
2472 v.push(prof(
2473 "muse-glimmer",
2474 TextGeneration,
2475 StandardGqa,
2476 KvIswa,
2477 RopeLayout::Norm,
2478 ArchPath::GenericGqa {
2479 rope: RopeLayout::Norm,
2480 },
2481 PerHead,
2482 ));
2483 // `granite_swa` was NEW CODE in `NORM_ROPE_TRIAGED` for one PR
2484 // on its two per-layer tables and is audited now
2485 // (`tests/granite_swa_graphs.rs`). NORM RoPE:
2486 // `llama_model_rope_type` puts LLM_ARCH_GRANITE_SWA in the
2487 // NORM group, which `tests/rope_layout.rs` pins.
2488 v.push(prof(
2489 "granite_swa",
2490 TextGeneration,
2491 StandardGqa,
2492 KvIswa,
2493 RopeLayout::Norm,
2494 ArchPath::GenericGqa {
2495 rope: RopeLayout::Norm,
2496 },
2497 WholeVector,
2498 ));
2499 // `maple` was ONE MATCH ARM in `NEOX_ROPE_TRIAGED` for one PR
2500 // on the per-layer RoPE gate and is audited now on
2501 // `crate::rope_layers` (`tests/no_rope_layer_graphs.rs`). It is
2502 // pushed here rather than in the `gqa_neox` list above because
2503 // its QK norm is PER HEAD (`maple.cpp:49-50,84-88`, a
2504 // `{head_dim}` weight applied to each head), and `gqa_neox`
2505 // hands out `WholeVector` -- which loads, runs and normalises
2506 // over the whole projection, the silent-wrong shape this
2507 // column exists to prevent.
2508 v.push(prof(
2509 "maple",
2510 TextGeneration,
2511 StandardGqa,
2512 KvIswa,
2513 Neox,
2514 ArchPath::GenericGqa { rope: Neox },
2515 PerHead,
2516 ));
2517 v.push(prof(
2518 "qwen3",
2519 TextGeneration,
2520 Qwen3Family,
2521 KvGqa,
2522 Neox,
2523 ArchPath::GenericGqa { rope: Neox },
2524 PerHead,
2525 ));
2526 v.push(prof(
2527 "qwen3moe",
2528 TextGeneration,
2529 Qwen3Family,
2530 KvGqa,
2531 Neox,
2532 ArchPath::GenericGqa { rope: Neox },
2533 PerHead,
2534 ));
2535 // `gemma` was FIXTURE-AWAY here until it got its fixture
2536 // (`tests/fixture_away_graphs.rs`); it is audited now and
2537 // carries no verdict at all.
2538 v.push(prof(
2539 "gemma",
2540 TextGeneration,
2541 GemmaFamily,
2542 KvGqa,
2543 Neox,
2544 ArchPath::GenericGqa { rope: Neox },
2545 PerHead,
2546 ));
2547 v.push(prof(
2548 "gemma2",
2549 TextGeneration,
2550 GemmaFamily,
2551 KvIswa,
2552 Neox,
2553 ArchPath::GenericGqa { rope: Neox },
2554 PerHead,
2555 ));
2556 v.push(prof(
2557 "gemma3",
2558 TextGeneration,
2559 GemmaFamily,
2560 KvIswa,
2561 Neox,
2562 ArchPath::GenericGqa { rope: Neox },
2563 PerHead,
2564 ));
2565 // Gemma-4 text GGUFs (E2B): per-layer embeddings, shared-KV
2566 // layers, and split SWA/full head dims -- dedicated
2567 // [`crate::gemma4_engine::Gemma4Engine`] (not GenericGqa).
2568 for n in ["gemma4", "gemma4-assistant"] {
2569 v.push(prof(
2570 n,
2571 TextGeneration,
2572 GemmaFamily,
2573 KvIswa,
2574 Neox,
2575 ArchPath::DedicatedOnly {
2576 reason: "use load_gemma4_engine_from_path / ServedEngine::Gemma4",
2577 },
2578 PerHead,
2579 ));
2580 }
2581 // The parallel residual `x + attn(norm(x)) + ffn(norm(x))` is
2582 // SERVED (`crate::parallel_residual`), and every row that was
2583 // refused for it is audited now: `gptneox`, `plamo`
2584 // (tests/parallel_residual_graphs.rs), `command-r`
2585 // (tests/command_r_graphs.rs), `falcon` (tests/falcon_graphs.rs),
2586 // `phi2` (tests/phi2_graphs.rs), `cohere2`
2587 // (tests/cohere2_graphs.rs), and `cohere2moe`
2588 // (tests/cohere2moe_graphs.rs, 2026-09-14): the `cohere2` graph
2589 // with routed experts, on `rope_layers::RopeLayers::
2590 // SlidingOrLeadingDense`, `parallel_dense_ffn::
2591 // SHARED_EXPERT_SUM_SCALE`, `norm::NORM_BY_RMS_EPS_KEY`.
2592 v.push(gqa_norm("cohere2moe"));
2593 // MiniCPM was the case `unsupported_scaling_keys` cannot catch:
2594 // `src/models/minicpm.cpp:5-7` *hardcodes* an embedding
2595 // multiplier of 12.0, a residual multiplier of
2596 // `1.4/sqrt(n_layer)` and a logit multiplier of `256/n_embd`,
2597 // and only then (`:12-14`) lets the GGUF override them. An older
2598 // MiniCPM export carrying none of the three keys is still scaled
2599 // by all three, so a key-presence gate sees nothing.
2600 //
2601 // It is generic now, on the same evidence every other row here
2602 // has: `scalar_multipliers::MultiplierDefaults` applies the
2603 // three, and `tests/minicpm_graphs.rs` drives a fixture that
2604 // declares NONE of them against llama.cpp's own logits. Its RoPE
2605 // is NORM (`llama_model_rope_type`, llama-model.cpp:2580, the
2606 // consecutive-pairs group), and it is deliberately NOT in
2607 // `rope_finetuned::ROPE_GATED_ON_FINETUNED`: it runs Granite's
2608 // graph, whose RoPE is gated on `hparams.rope_finetuned`, but
2609 // `minicpm.cpp:17` pins that true with no key read at all, so
2610 // the switch Granite exposes is unreachable here.
2611 v.push(gqa_norm("minicpm"));
2612 v.push(prof(
2613 "phi3",
2614 TextGeneration,
2615 PhiFamily,
2616 KvGqa,
2617 Neox,
2618 ArchPath::GenericGqa { rope: Neox },
2619 WholeVector,
2620 ));
2621 // Phi-4 GGUFs share the phi3 fused-QKV / fused gate+up graph
2622 // (PhiFamily). Many community checkpoints still tag `phi3`; admit
2623 // `phi4` the same way so either string can load. Receipts / head-dim
2624 // FA-vec coverage remain P6 evidence work -- not a speed claim.
2625 v.push(
2626 prof(
2627 "phi4",
2628 TextGeneration,
2629 PhiFamily,
2630 KvGqa,
2631 Neox,
2632 ArchPath::GenericGqa { rope: Neox },
2633 WholeVector,
2634 )
2635 .triaged(
2636 TriageClass::Unknown,
2637 "there is no llama.cpp graph to diff against. `phi4` is NOT in LLM_ARCH_NAMES \
2638 -- src/llama-arch.cpp:44 lists \"phi3\" and there is no phi4 entry -- so this \
2639 row is a frink-only alias and no llama.cpp-produced GGUF can carry the \
2640 string. frink admits it as PhiFamily/NEOX, i.e. phi3's fused-QKV and fused \
2641 gate+up graph, on the assumption that a file spelling it means the same \
2642 thing. WHAT WOULD SETTLE IT: a real GGUF whose general.architecture is \
2643 literally `phi4`. If its blk.0 carries attn_qkv.weight it is phi3's graph \
2644 and this row is fixture-away behind an already-audited phi3; if it carries \
2645 split attn_q/attn_k/attn_v it is a Llama-shaped graph and belongs on a \
2646 different row",
2647 ),
2648 );
2649 // Llama 4 (Scout, Maverick): was a `DedicatedOnly` refusal on
2650 // an engine that never existed, audited now on
2651 // tests/llama4_graphs.rs. The chunked window is
2652 // `crate::chunked_swa`, the literal temperature on the unrotated
2653 // layers `attn_temperature::LITERAL_ATTN_TEMPERATURE`, the
2654 // weightless post-RoPE QK norm `crate::weightless_qk_norm`, the
2655 // honoured interleave step `moe_interleave::
2656 // INTERLEAVE_STEP_HONOURED_BY_LOADER`. NORM RoPE:
2657 // llama-model.cpp's `LLM_ARCH_LLAMA4` sits in the NORM group,
2658 // pinned by `tests/rope_layout.rs`.
2659 v.push(gqa_norm("llama4"));
2660 // MiniMax M2 and M3 are two DIFFERENT architectures and were
2661 // wrong to share one reason. Both used to refuse with "256-expert
2662 // sigmoid MoE + MTP"; neither clause is true.
2663 //
2664 // MTP: `minimax-m2.cpp` and `minimax-m3.cpp` create no `nextn.*`
2665 // tensor at all, and `gguf-py/gguf/constants.py`'s
2666 // `MODEL_ARCH.MINIMAXM2` / `.MINIMAXM3` tensor lists contain no
2667 // `NEXTN_*` entry -- so no converter can even emit MTP weights for
2668 // these files. `minimax-m3.cpp:9` says it outright: "MTP is not
2669 // in released model weights."
2670 //
2671 // Sigmoid MoE: frink HAS it. `loader.rs` reads
2672 // `{arch}.expert_gating_func` into `GatingFunction::Sigmoid`,
2673 // loads `blk.N.exp_probs_b.bias`, and reads
2674 // `expert_weights_scale` / `expert_weights_norm`. Expert count is
2675 // an hparam, not a ceiling.
2676 //
2677 // llama-arch.cpp puts both in the NEOX RoPE group.
2678 // `minimax-m2` was HERE as "UNAUDITED, not unimplemented" -- plain
2679 // GQA, whole-vector QK-norm, partial NEOX RoPE, a sigmoid MoE
2680 // with `exp_probs_b` -- and it is audited now on the fixture that
2681 // had evidenced the claim (tests/minimax_m2_graphs.rs). NEOX
2682 // RoPE: llama-model.cpp:2672.
2683 v.push(gqa_neox("minimax-m2"));
2684 // `pangu-embedded` is openPangu-Embedded-1B / 7B (Huawei), a
2685 // DECODER LLM: `PanguEmbeddedForCausalLM`, `conversion/pangu.py`
2686 // is a `TextModel` with an `lm_head`, and "Embedded" means edge
2687 // devices. It was filed here as "embedding variant; deferred"
2688 // and in `embedding_model::NOT_YET` from the name alone.
2689 // `pangu-embed.cpp` is `llama.cpp`'s graph with one REQUIRED
2690 // `attn_output.bias` (`:37`; `proj_bias::ATTN_OUT_BIAS_CREATORS`),
2691 // NEOX RoPE (llama-model.cpp:2675). Audited on
2692 // tests/pangu_embedded_graphs.rs.
2693 v.push(gqa_neox("pangu-embedded"));
2694 v.push(prof(
2695 "minimax-m3",
2696 TextGeneration,
2697 Dedicated,
2698 KvGqa,
2699 Neox,
2700 ArchPath::DedicatedOnly {
2701 reason: "minimax-m3 needs MiniMax Sparse Attention: a per-layer indexer \
2702 (index_q_proj/index_k_proj/index_q_norm/index_k_norm, minimax-m3.cpp:76-82) \
2703 driving its own MSA KV cache (llama-kv-cache-msa.h) with position<->cell \
2704 maps, plus SWIGLU_OAI experts and shared experts. frink has only the \
2705 block-selection rule (frink_core::block_sparse), none of the rest",
2706 },
2707 // minimax-m3.cpp:53-55 -- `{n_embd_head_k}`, with llama.cpp's
2708 // own comment "per-head QK-norm: a single head_dim vector
2709 // applied to every head". M2 and M3 DIFFER here, which is why
2710 // the shared entry was wrong for M3.
2711 PerHead,
2712 ));
2713 // MiniCPM3 is MLA, not generic GQA, and the catalog said
2714 // otherwise: it claimed `StandardGqa`/`KvGqa`, which is false
2715 // about the model rather than merely unaudited.
2716 // `src/models/minicpm3.cpp:5-6` requires `q_lora_rank` and
2717 // `kv_lora_rank`, and `:41-46` creates
2718 // `attn_q_a`/`attn_q_b`/`attn_kv_a_mqa`/`attn_kv_b` -- the
2719 // DeepSeek-2 tensor set. There is no `attn_q.weight` in any
2720 // MiniCPM3 checkpoint, so the generic path could never have
2721 // loaded one whatever the audit said.
2722 //
2723 // Reclassified 2026-09-01 by the unaudited-refusal triage. This
2724 // is a MESSAGE-QUALITY fix, not a correctness one: the old
2725 // failure was already a clean missing-tensor error. It stops the
2726 // user being told "unaudited" for something that is not merely
2727 // unaudited.
2728 v.push(prof(
2729 "minicpm3",
2730 TextGeneration,
2731 Mla,
2732 KvMla,
2733 Neox,
2734 ArchPath::DedicatedOnly {
2735 reason: "MiniCPM3 is an MLA model (src/models/minicpm3.cpp:5-6,41-46 -- \
2736 q_lora_rank/kv_lora_rank and the attn_q_a/attn_q_b/attn_kv_a_mqa/\
2737 attn_kv_b tensor set), so it needs the MLA engine and not the \
2738 generic GQA decoder. It ALSO hardcodes MiniCPM's multipliers with \
2739 no GGUF key to read them from -- scale_embd = 12.0, \
2740 scale_depth = 1.4, n_embd_base = 256 at :65-67, applied at :81 -- \
2741 which is the same blind spot `minicpm` is refused for",
2742 },
2743 WholeVector,
2744 ));
2745 v.push(prof(
2746 "deepseek2",
2747 TextGeneration,
2748 Mla,
2749 KvMla,
2750 Norm,
2751 ArchPath::DedicatedOnly {
2752 reason: "DeepSeek-2 MLA needs the MLA engine, not generic GQA",
2753 },
2754 WholeVector,
2755 ));
2756 // PLM-1.8B: `deepseek2.cpp`'s naive MLA branch on a dense
2757 // ReLU-squared model with a direct `attn_q` and a tied lm_head
2758 // (`plm.cpp`); the three differences are `crate::mla_arch`'s
2759 // row. Checked against libllama in tests/plm_graphs.rs, NORM
2760 // RoPE (llama-model.cpp:2592).
2761 v.push(prof(
2762 "plm",
2763 TextGeneration,
2764 Mla,
2765 KvMla,
2766 Norm,
2767 ArchPath::DedicatedOnly {
2768 reason: "PLM is DeepSeek-2 MLA attention on a dense model and runs on the MLA \
2769 engine (`mla_gguf_loader`), not generic GQA",
2770 },
2771 WholeVector,
2772 ));
2773 v.push(prof(
2774 "deepseek32",
2775 TextGeneration,
2776 Mla,
2777 KvDsa,
2778 Norm,
2779 ArchPath::DedicatedOnly {
2780 reason: "DeepSeek-3.2 DSA/MLA needs the dedicated sparse/MLA stack",
2781 },
2782 WholeVector,
2783 ));
2784 v.push(prof(
2785 "mistral4",
2786 TextGeneration,
2787 Mla,
2788 KvMla,
2789 Norm,
2790 ArchPath::DedicatedOnly {
2791 reason: "mistral4 reuses DeepSeek-2 MLA loader/graph in llama.cpp",
2792 },
2793 WholeVector,
2794 ));
2795 // The three frink-only alias rows. Refused as STRINGS, not
2796 // triaged as architectures: libllama refuses all three outright
2797 // and every real checkpoint of all three declares `llama`. See
2798 // `NO_UPSTREAM_ARCH` for the three measurements. Note the
2799 // `dedicated` helper gives them NORM RoPE, which is at least the
2800 // layout of the graph they claim to be; they had NEOX while
2801 // sitting on the generic path.
2802 for n in ["mistral", "mixtral", "yi"] {
2803 v.push(dedicated(n, NO_UPSTREAM_ARCH));
2804 }
2805 v.push(dedicated(
2806 "glm-dsa",
2807 "use frink_models::glm52_decoder / glm52_gguf_loader (DSA), not the generic GQA Decoder",
2808 ));
2809 // `glm4` -- GLM-4-0414 9B / 32B, GLM-Z1, GLM-OCR -- was HERE,
2810 // sent to the GLM-5.2 MLA loader for four keys `glm4.cpp:3-9`
2811 // never read: the `glm4moe` defect a second time. It is plain
2812 // GQA with Gemma-2's two post norms in Gemma-2's slots and a
2813 // fused SwiGLU, audited on the generic NORM path
2814 // (`tests/glm4_graphs.rs`); see `AUDITED_GENERIC_GQA`.
2815 // `glm4moe` -- GLM-4.5 / GLM-4.5-Air / GLM-4.6 -- was HERE as a
2816 // `dedicated` refusal, twice over: first pointing at
2817 // `glm52_gguf_loader` (which asks for a `q_lora_rank` no glm4moe
2818 // file carries; it is not MLA), then naming the ONE thing that
2819 // was missing, its pre-FFN norm stored as
2820 // `blk.N.post_attention_norm` (`glm4-moe.cpp:75,215`, gpt-oss's
2821 // slot). That slot is one row in
2822 // `norm_sites::PRE_FFN_NORM_IS_POST_ATTENTION_NORM` now and the
2823 // row is audited on the generic NEOX path
2824 // (`tests/glm4moe_graphs.rs`); see `AUDITED_GENERIC_GQA`.
2825 v.push(dedicated(
2826 "deepseek4",
2827 "DeepSeek V4 needs CSA/HCA + mHC assembly; generic GQA Decoder is not valid",
2828 ));
2829 v.push(dedicated(
2830 "kimi-linear",
2831 "use frink_models::kimi_decoder / kimi_loader, not the generic GQA Decoder",
2832 ));
2833 // `kimi-k3`, with a HYPHEN. This row spelled it `kimi_k3` until
2834 // 2026-09-19, and `src/llama-arch.cpp:155` writes
2835 // `{ LLM_ARCH_KIMI_K3, "kimi-k3" }` -- so the refusal could not
2836 // fire on any real file, and a Kimi-K3 export fell through to
2837 // the unknown-architecture message instead of the one naming
2838 // its loader. frink's own preset and Kimi loader
2839 // (`frink-cli/src/main.rs:509`, `kimi_gguf_loader.rs:1022`)
2840 // had the hyphen all along, which is the disagreement this
2841 // repo keeps paying for: two spellings of one name with
2842 // nothing comparing them.
2843 v.push(dedicated(
2844 "kimi-k3",
2845 "use frink_models::kimi_decoder / kimi_loader, not the generic GQA Decoder. \
2846 Upstream's own graph (src/models/kimi-k3.cpp:3-12, new since the 2026-08-04 \
2847 pin) is kimi-linear's KDA + MLA hybrid plus five things it lists itself: \
2848 cross-layer residual attention, a latent MoE, a `situ` activation in place of \
2849 SwiGLU everywhere, a sigmoid gate on the MLA output, and a full-rank KDA gate",
2850 ));
2851 // Landed upstream after the pin, each needing an attention this
2852 // engine does not have; `dedicated` rather than a triaged
2853 // generic row because the generic decoder is not a candidate.
2854 v.push(dedicated(
2855 "bailingmoe3",
2856 "MLA and KDA in one model (src/models/bailingmoe3.cpp:5-14: the `_mla` key \
2857 lengths, `attention.kv_lora_rank`, an SSM conv kernel and `kda.head_dim`). \
2858 The MLA half is frink_models::mla; the KDA half is a linear-attention block \
2859 the gated-delta-net seam does not cover, and the two alternate by layer",
2860 ));
2861 v.push(dedicated(
2862 "dots3note",
2863 "a DSA indexer in front of an absorbed MLA (src/models/dots3note.cpp:2-3 \
2864 includes llama-kv-cache-dsa.h; its own header says it is deepseek32.cpp's \
2865 indexer with step35.cpp's head-wise output gate). frink's DSA lives in the \
2866 GLM-5.2 engine and its MLA in frink_models::mla; this needs the pair plus \
2867 the gate",
2868 ));
2869 v.push(dedicated(
2870 "hy_v4",
2871 "independent hyper-connections: several residual streams reduced before each \
2872 layer and redistributed after (src/models/hy-v4.cpp:6-8, the DeepSeek-V4 \
2873 hyper-connection layout without the comb term), over a DSA cache. Every \
2874 decoder here carries ONE residual stream",
2875 ));
2876 // Qwen3.5 dense (`qwen35`: 0.8B / 2B / 4B / 9B / 27B) left the
2877 // hybrid group on 2026-09-14: the gated delta net is a block
2878 // where attention would be (`crate::gdn`, `AttnShape::Gdn`,
2879 // decided by `gdn::recurrent_layers`), its full-attention layers
2880 // gate through a double-width `wq` (`attn_gate::
2881 // Q_INTERLEAVED_GATE_ARCHS`), per-head QK norm, partial IMROPE
2882 // (NEOX on text positions, `crate::mrope`), the pre-FFN norm
2883 // under `post_attention_norm` (`norm_sites`). Audited on
2884 // tests/qwen35_graphs.rs.
2885 // `qwen35moe` (Qwen3.5-35B-A3B, 122B-A10B, 397B-A17B) is the same
2886 // layers with `qwen2moe`'s FFN (`qwen35moe.cpp:496-538`: softmax,
2887 // `norm_w = true`, the shared expert scaled by its own sigmoid
2888 // gate), which the generic path has served since OLMoE.
2889 // `qwen3next` (Qwen3-Next-80B-A3B) is `qwen35moe`'s layers with
2890 // the V heads GROUPED over the K heads and beta / alpha in one
2891 // `ssm_ba` projection (`gdn::GROUPED_HEAD_ARCHITECTURES`,
2892 // `gdn::BetaAlpha::Fused`), plain NEOX RoPE with no sections
2893 // (llama-model.cpp:2678).
2894 for n in ["qwen35", "qwen35moe", "qwen3next"] {
2895 v.push(prof(
2896 n,
2897 TextGeneration,
2898 DecoderFamily::Hybrid,
2899 MemoryKind::Hybrid,
2900 Neox,
2901 ArchPath::GenericGqa { rope: Neox },
2902 PerHead,
2903 ));
2904 }
2905 // PLaMo-2 (`plamo2`: PLaMo-2 1B / 2B / 8B). Its own SSM block
2906 // where the KV count is zero (`crate::plamo2_ssm`, `ZeroKvLayer::
2907 // Plamo2`), attention elsewhere with a fused `attn_qkv`, the
2908 // per-head QK RMSNorm with a DISTINCT row per head
2909 // (`QkNormStyle::PerHeadDistinct`, `plamo2.cpp:92-93,163,166`),
2910 // NEOX RoPE (llama-model.cpp:2640), post-attention and post-FFN
2911 // norms, the Phi-3 fused `ffn_up`. `kq_scale` is `1/sqrt(v_dim)`
2912 // (`:171`), which is `1/sqrt(head_dim)` on every export
2913 // (`conversion/plamo.py:99-100` write one width for both); a file
2914 // whose two widths differ is refused (`crate::kv_head_dims`).
2915 // Audited on tests/plamo2_graphs.rs.
2916 v.push(prof(
2917 "plamo2",
2918 TextGeneration,
2919 DecoderFamily::Hybrid,
2920 MemoryKind::Hybrid,
2921 Neox,
2922 ArchPath::GenericGqa { rope: Neox },
2923 PerHeadDistinct,
2924 ));
2925 // `lfm2` left the hybrid group on 2026-09-14: its recurrent
2926 // block is a short convolution at the attention site
2927 // (`crate::shortconv`), served on the generic path with a
2928 // per-head QK norm (`lfm2.cpp:74-75`) and NEOX RoPE
2929 // (llama-model.cpp:2666). `lfm2moe` shares its graph
2930 // (`models.h:1899`) and followed on the same seam.
2931 for n in ["lfm2", "lfm2moe"] {
2932 v.push(prof(
2933 n,
2934 TextGeneration,
2935 DecoderFamily::Hybrid,
2936 MemoryKind::Hybrid,
2937 Neox,
2938 ArchPath::GenericGqa { rope: Neox },
2939 PerHead,
2940 ));
2941 }
2942 // `mamba` and `mamba2` (Mamba-130M to 2.8B, FalconMamba-7B;
2943 // Mamba-Codestral-7B) left the recurrent group on 2026-09-14:
2944 // every layer is the one block and no FFN
2945 // (`layer_shapes::PURE_RECURRENT`), served by `crate::mamba1` /
2946 // `crate::mamba2` on the generic path with head_dim 0 and no
2947 // attention anywhere. `jamba` (AI21 Jamba) left the hybrid
2948 // group with them: Mamba-1 where `head_count_kv` is 0
2949 // (`ZeroKvLayer::Mamba1`), attention with NO RoPE elsewhere
2950 // (`jamba.cpp:98`; `rope_layers` answers `Never`, the NEOX entry
2951 // below is a filler as `gpt2`'s), dense or MoE per layer by the
2952 // router's presence (`moe_interleave::
2953 // DENSE_LAYER_BY_ROUTER_ABSENCE`). Audited on
2954 // tests/mamba_graphs.rs.
2955 for n in ["mamba", "mamba2"] {
2956 v.push(prof(
2957 n,
2958 TextGeneration,
2959 DecoderFamily::Recurrent,
2960 MemoryKind::Recurrent,
2961 Neox,
2962 ArchPath::GenericGqa { rope: Neox },
2963 WholeVector,
2964 ));
2965 }
2966 v.push(prof(
2967 "jamba",
2968 TextGeneration,
2969 DecoderFamily::Hybrid,
2970 MemoryKind::Hybrid,
2971 Neox,
2972 ArchPath::GenericGqa { rope: Neox },
2973 WholeVector,
2974 ));
2975 for n in ["rwkv6", "rwkv6qwen2", "rwkv7", "arwkv7"] {
2976 v.push(prof(
2977 n,
2978 TextGeneration,
2979 DecoderFamily::Recurrent,
2980 MemoryKind::Recurrent,
2981 Neox,
2982 ArchPath::DedicatedOnly {
2983 reason: "recurrent engine not yet on the serve path",
2984 },
2985 WholeVector,
2986 ));
2987 }
2988 v.push(prof(
2989 "t5",
2990 TextGeneration,
2991 EncoderDecoder,
2992 None,
2993 Neox,
2994 ArchPath::DedicatedOnly {
2995 reason: "T5 encoder-decoder engine not yet on the serve path",
2996 },
2997 WholeVector,
2998 ));
2999 for (n, scope, reason) in [
3000 (
3001 "t5encoder",
3002 DeferredEncoderEmbedding,
3003 "encoder-only; deferred from text-generation parity",
3004 ),
3005 // Deferred from the *decoder* path, and that is still
3006 // right: a `bert` GGUF has no output head, so
3007 // `ensure_generic_decoder` must keep refusing it. It is no
3008 // longer deferred outright -- it loads and embeds through
3009 // `bert_gguf_loader` / `bert_encoder`, checked against
3010 // llama.cpp by `tests/bert_llama_cpp_parity.rs`.
3011 (
3012 "bert",
3013 DeferredEncoderEmbedding,
3014 "encoder; no output head, so never a decoder -- served by \
3015 frink_models::EmbeddingModel on /v1/embeddings",
3016 ),
3017 (
3018 "modern-bert",
3019 DeferredEncoderEmbedding,
3020 "encoder/embedding; deferred",
3021 ),
3022 // Served since 2026-09-19 on the SAME encoder as `bert`:
3023 // its two deltas from that graph are NEOX RoPE on Q/K
3024 // (`bert.cpp:126-133`) and a gated SiLU FFN (`:195-201`),
3025 // both read from the architecture through
3026 // `bert_gguf_loader::ENCODER_ARCHS` and checked against
3027 // llama.cpp's own pooled embedding
3028 // (`tests/nomic_bert_graphs.rs`). Deferred from the
3029 // DECODER path, as `bert` is: neither has an output head.
3030 (
3031 "nomic-bert",
3032 DeferredEncoderEmbedding,
3033 "encoder; no output head, so never a decoder -- served by \
3034 frink_models::EmbeddingModel on /v1/embeddings",
3035 ),
3036 (
3037 "nomic-bert-moe",
3038 DeferredEncoderEmbedding,
3039 "encoder/embedding; deferred",
3040 ),
3041 (
3042 "neo-bert",
3043 DeferredEncoderEmbedding,
3044 "encoder/embedding; deferred",
3045 ),
3046 // Served since 2026-09-19 on the same encoder as `bert`
3047 // (ALiBi, GEGLU and two optional norms);
3048 // deferred from the DECODER path, which is where the
3049 // scope column speaks from -- it has no output head.
3050 (
3051 "jina-bert-v2",
3052 DeferredEncoderEmbedding,
3053 "encoder; no output head, so never a decoder -- served by \
3054 frink_models::EmbeddingModel on /v1/embeddings",
3055 ),
3056 // Served since 2026-09-19 on the same encoder as `bert`
3057 // (its rotation with `bert`'s FFN);
3058 // deferred from the DECODER path, which is where the
3059 // scope column speaks from -- it has no output head.
3060 (
3061 "jina-bert-v3",
3062 DeferredEncoderEmbedding,
3063 "encoder; no output head, so never a decoder -- served by \
3064 frink_models::EmbeddingModel on /v1/embeddings",
3065 ),
3066 (
3067 "eurobert",
3068 DeferredEncoderEmbedding,
3069 "encoder/embedding; deferred",
3070 ),
3071 (
3072 "llama-embed",
3073 DeferredEncoderEmbedding,
3074 "embedding variant; deferred",
3075 ),
3076 (
3077 "gemma-embedding",
3078 DeferredEncoderEmbedding,
3079 "embedding variant; deferred",
3080 ),
3081 ("yi-vl", DeferredMultimodal, "Yi vision-language; deferred"),
3082 ("qwen2vl", DeferredMultimodal, "vision-language; deferred"),
3083 ("qwen3vl", DeferredMultimodal, "vision-language; deferred"),
3084 ("qwen3vlmoe", DeferredMultimodal, "vision-language; deferred"),
3085 ("cogvlm", DeferredMultimodal, "vision-language; deferred"),
3086 ("chameleon", DeferredMultimodal, "multimodal; deferred"),
3087 ("hunyuan_vl", DeferredMultimodal, "vision-language; deferred"),
3088 ("paddleocr", DeferredMultimodal, "OCR multimodal; deferred"),
3089 ("hy_v3", DeferredMultimodal, "multimodal; deferred"),
3090 ("deepseek2-ocr", DeferredMultimodal, "OCR multimodal; deferred"),
3091 ("dream", DeferredDiffusion, "diffusion LM; deferred"),
3092 ("llada", DeferredDiffusion, "diffusion LM; deferred"),
3093 ("llada-moe", DeferredDiffusion, "diffusion LM; deferred"),
3094 ("rnd1", DeferredDiffusion, "diffusion LM; deferred"),
3095 (
3096 "wavtokenizer-dec",
3097 DeferredAudio,
3098 "audio tokenizer; deferred",
3099 ),
3100 // Landed upstream after the 2026-08-04 pin. Both are
3101 // text-to-speech: `pockettts.cpp` is a small LayerNorm
3102 // decoder that emits audio codes, `qwen3tts.cpp` is a
3103 // three-line shim over it. Deferred with the audio scope
3104 // rather than triaged as text generation, because what
3105 // they need is an audio OUTPUT path, not a decoder arm.
3106 ("pockettts", DeferredAudio, "text-to-speech; deferred"),
3107 ("qwen3tts", DeferredAudio, "text-to-speech; deferred"),
3108 (
3109 "eagle3",
3110 EnumOnly,
3111 "speculative draft head; not a standalone decoder target",
3112 ),
3113 (
3114 "dflash",
3115 EnumOnly,
3116 "speculative draft head; not a standalone decoder target",
3117 ),
3118 ("clip", EnumOnly, "quantize dummy only"),
3119 ("gptj", EnumOnly, "enum-only in llama.cpp factory gap"),
3120 ("(unknown)", EnumOnly, "llama.cpp unknown sentinel"),
3121 ] {
3122 v.push(deferred_scope(n, scope, reason));
3123 }
3124 v.push(prof(
3125 "gemma3n",
3126 TextGeneration,
3127 GemmaFamily,
3128 KvIswa,
3129 Neox,
3130 ArchPath::DedicatedOnly {
3131 reason: "gemma3n AltUp/Laurel tensors not implemented in the generic decoder",
3132 },
3133 PerHead,
3134 ));
3135 for n in ["ferroxtest", "ferroxtestmoe", "ferroxtestmixed"] {
3136 v.push(prof(
3137 n,
3138 TextGeneration,
3139 TestFixture,
3140 KvGqa,
3141 Neox,
3142 ArchPath::TestFixture { rope: Neox },
3143 WholeVector,
3144 ));
3145 }
3146 v
3147 })
3148 .as_slice()
3149}
3150
3151/// Resolve a GGUF `general.architecture` value to its profile.
3152pub fn resolve_profile(arch: &str) -> Option<&'static ArchProfile> {
3153 architecture_catalog().iter().find(|p| p.gguf_name == arch)
3154}
3155
3156/// Resolve a GGUF `general.architecture` value. `None` means the string
3157/// is not in the registry -- callers must fail closed rather than guess.
3158pub fn resolve_architecture(arch: &str) -> Option<ArchPath> {
3159 resolve_profile(arch).map(|p| p.path)
3160}
3161
3162/// llama.cpp's hardcoded alternating sliding-window layout for one
3163/// architecture: the period, *and* which end of each period is the
3164/// full-attention layer.
3165///
3166/// `llama_hparams::set_swa_pattern` (`src/llama-hparams.cpp:8-22`) has
3167/// two phases, and they are not interchangeable:
3168///
3169/// - `dense_first = false`: `is_swa[il] = il % p < (p - 1)` -- the
3170/// **last** layer of every period is full attention.
3171/// - `dense_first = true`: `is_swa[il] = il % p != 0` -- the **first**
3172/// layer of every period is full attention.
3173///
3174/// For a 32-layer period-4 model the two disagree on 16 of the 32
3175/// layers. Storing only the period would therefore not be a partial
3176/// transcription, it would be a wrong one for the four architectures
3177/// llama.cpp passes `dense_first = true`.
3178#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3179pub struct SwaPattern {
3180 /// llama.cpp's `swa_period` seed literal.
3181 pub period: usize,
3182 /// llama.cpp's `dense_first` argument to `set_swa_pattern`.
3183 pub dense_first: bool,
3184}
3185
3186/// Every architecture for which llama.cpp seeds a sliding-window period
3187/// *before* letting `{arch}.attention.sliding_window_pattern` override
3188/// it, transcribed from `src/models/*.cpp`.
3189///
3190/// The period is not in the file for these families -- llama.cpp
3191/// hardcodes it per architecture and only lets the metadata key override
3192/// it (`ml.get_key_or_arr(LLM_KV_ATTENTION_SLIDING_WINDOW_PATTERN,
3193/// swa_period, false)` after seeding `swa_period` with the literal
3194/// below). A missing key therefore does **not** mean "every layer is
3195/// windowed", which is what frink assumed: `layer_sliding_window`
3196/// returns the window for all layers when `swa_pattern` is `None`, so a
3197/// gpt-oss or cohere2 checkpoint ran its full-attention layers through a
3198/// 128-token window and answered from a truncated history.
3199///
3200/// Two llama.cpp spellings are deliberately absent, because neither is
3201/// a per-arch *period*:
3202///
3203/// - `set_swa_pattern(0)` (`deepseek4.cpp:68`, `dflash.cpp:54`) makes
3204/// **every** layer sliding, which is what frink already does for a
3205/// declared window with no pattern.
3206/// - `set_swa_pattern(1)` (`phi3.cpp:23`) makes **no** layer sliding,
3207/// and phi3 zeroes `n_swa` and sets `swa_type = NONE` on the same
3208/// branch, so there is no window left to place.
3209///
3210/// Architectures that only ever read a per-layer *array*
3211/// (`get_key_or_arr(..., hparams.is_swa_impl, n_layer)`: `gemma4`,
3212/// `gemma4-assistant`, `step35`, `mimo2`, `dflash`) seed no scalar and
3213/// so have no default to pin.
3214///
3215/// Pinned by `tests/swa_pattern.rs`.
3216/// Architectures where llama.cpp DISABLES sliding-window attention even
3217/// though the checkpoint declares a window.
3218///
3219/// `src/models/phi3.cpp:12-24`: if `attention.sliding_window` is present
3220/// and non-zero, llama.cpp warns, then sets `n_swa = 0`,
3221/// `swa_type = LLAMA_SWA_TYPE_NONE` and `set_swa_pattern(1)` -- i.e. NO
3222/// layer slides. Its own comment says the conversion scripts populate
3223/// the key wrongly and links the PR that turned it off.
3224///
3225/// frink read the key and, having no per-architecture period for
3226/// `phi3`, windowed EVERY layer. So a Phi-3 or Phi-4 model attended over
3227/// a truncated history on every layer where llama.cpp attends over the
3228/// whole context. `phi3` is in [`AUDITED_GENERIC_GQA`], and
3229/// `models/Phi-4-mini-instruct-Q4_K_M.gguf` really does declare
3230/// `phi3.attention.sliding_window = 262144` -- so this was live on a
3231/// model in the benchmark suite, not hypothetical.
3232///
3233/// This is deliberately a REFUSAL TO HONOUR the key rather than a
3234/// transcribed period: llama.cpp is not choosing a different window
3235/// here, it is declining to use the one in the file.
3236///
3237/// # The second cause, and why it shares this predicate
3238///
3239/// `src/models/exaone4.cpp:4-14` wraps the ENTIRE SWA setup --
3240/// `swa_type`, `n_swa`, `set_swa_pattern`, both SWA RoPE fields -- in
3241/// `if (hparams.n_layer() == 64)`, and only then reads
3242/// `LLM_KV_ATTENTION_SLIDING_WINDOW` at :16 into an `hparams.n_swa` no
3243/// layer consults. So EXAONE-4 1.2B (30 layers) attends over the whole
3244/// context on every layer no matter what its file declares, and
3245/// EXAONE-4 32B (64) does not.
3246///
3247/// It is the same QUESTION as `phi3`'s -- "does this file get a window
3248/// at all" -- so it is the same predicate rather than a second one
3249/// beside it. `crate::rope_layers::rope_layers` takes this function's
3250/// answer, not the raw presence of the key, and getting that wrong
3251/// would rope the 1.2B as if it were the 32B: `exaone4.cpp:116` gates
3252/// rotation on `is_swa(il)`, so a spurious window would silently stop
3253/// three layers in four from rotating.
3254pub fn swa_disabled_by_arch(arch: &str, n_layers: usize) -> bool {
3255 matches!(swa_window_override(arch, n_layers), SwaWindowOverride::Drop)
3256}
3257
3258/// What llama.cpp does with a nonzero `attention.sliding_window` the
3259/// file declares, for the architectures whose `load_arch_hparams` does
3260/// not simply honour it.
3261///
3262/// Three answers, one table: HONOUR (every architecture not named),
3263/// DROP (the two [`swa_disabled_by_arch`] rows -- no layer slides), and
3264/// PIN (the window is replaced by a literal, and the layers still
3265/// slide). [`swa_disabled_by_arch`] is DERIVED from this so the two
3266/// cannot disagree about which rows decline the file's value.
3267#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3268pub enum SwaWindowOverride {
3269 /// The file's value is the window.
3270 Honour,
3271 /// No window at all, whatever the file says.
3272 Drop,
3273 /// This window, whatever the file says.
3274 Pin(usize),
3275}
3276
3277/// The third case's one row: `src/models/smallthinker.cpp:4-8` reads
3278/// `attention.sliding_window` into `n_swa`, tests it for `> 0`, and
3279/// on that branch assigns `hparams.n_swa = 4096` -- the value it just
3280/// read is used as a flag and then overwritten. So a SmallThinker file
3281/// declaring 3 slides at 4096, and libllama's logits for a fixture
3282/// declaring 3 and the same fixture declaring 4096 are BYTE-IDENTICAL
3283/// (measured, `tests/router_input_graphs.rs`). A file declaring 0 or
3284/// nothing takes the other branch (`:16-18`): no window, every layer
3285/// rotated.
3286///
3287/// This is a PIN rather than a DROP because the layers still slide
3288/// (`:11` calls `set_swa_pattern`) and the SWA RoPE base still applies
3289/// (`:13-15`); only the width is upstream's literal. Honouring the
3290/// file's value here would mask three layers in four over a window
3291/// the graph never uses. `conversion/smallthinker.py:32-38` writes the
3292/// real `sliding_window_size` (4096 on every published SmallThinker),
3293/// so on a real export the pin and the file agree and a reader cannot
3294/// tell them apart; the fixture declares 3 so that they cannot.
3295pub const SMALLTHINKER_PINNED_WINDOW: usize = 4096;
3296
3297/// See [`SwaWindowOverride`].
3298pub fn swa_window_override(arch: &str, n_layers: usize) -> SwaWindowOverride {
3299 match arch {
3300 "phi3" => SwaWindowOverride::Drop,
3301 // phimoe.cpp:3-10 read no window key at all, so `swa_type` stays
3302 // NONE and the key `conversion/phi.py:171` writes for every
3303 // export is dead metadata: libllama reports `n_swa = 0` for a
3304 // file declaring one (measured, tests/phimoe_graphs.rs).
3305 "phimoe" => SwaWindowOverride::Drop,
3306 // exaone4.cpp:4. NOT `>= 64` and not a range: llama.cpp tests
3307 // equality, so a hypothetical 63- or 65-layer EXAONE-4 gets no
3308 // window there either.
3309 "exaone4" if n_layers != 64 => SwaWindowOverride::Drop,
3310 // smallthinker.cpp:8.
3311 "smallthinker" => SwaWindowOverride::Pin(SMALLTHINKER_PINNED_WINDOW),
3312 _ => SwaWindowOverride::Honour,
3313 }
3314}
3315
3316/// Architectures whose FFN gate uses GELU rather than SiLU, i.e. GeGLU
3317/// rather than SwiGLU.
3318///
3319/// llama.cpp picks this PER ARCHITECTURE -- it is the `LLM_FFN_GELU` vs
3320/// `LLM_FFN_SILU` argument each `src/models/*.cpp` passes to `build_ffn`
3321/// / `build_moe_ffn` -- and frink picked it per FAMILY, which is not
3322/// the same partition. `grok` is the case that proves it:
3323/// `src/models/grok.cpp:165` passes `LLM_FFN_GELU` to `build_moe_ffn`,
3324/// but `grok` is `DecoderFamily::StandardGqa`, so frink handed it
3325/// SwiGLU and would have computed a different FFN on every layer.
3326///
3327/// It was latent while `grok` refused as unaudited, and it is LIVE
3328/// now: `tests/grok_graphs.rs` compares the GELU experts against
3329/// libllama, at the GeGLU tolerance that llama.cpp's f16 GELU table
3330/// forces on every GeGLU row.
3331///
3332/// The other `LLM_FFN_GELU` users upstream -- `bert`, `bloom`,
3333/// `codeshell`, `falcon`, `gpt2`, `gptneox`, `mpt`, `phi2`, `starcoder`,
3334/// `starcoder2`, `t5`, `wavtokenizer-dec` -- are all `Deferred` or
3335/// `DedicatedOnly` here, so none reaches the generic path and none is
3336/// listed. The Gemma lineage is GELU too and stays on the family rule,
3337/// because every Gemma row IS `GemmaFamily`.
3338pub fn uses_geglu(arch: &str) -> bool {
3339 // `spark2_5` joined on 2026-09-19 with the pin move:
3340 // `src/models/spark2-5.cpp:124` passes `LLM_FFN_GELU` under
3341 // `LLM_FFN_PAR` to `build_ffn`, i.e. a GATED GELU, and the row is
3342 // `StandardGqa` like `grok` -- so the family rule would have given
3343 // it SwiGLU and a different FFN on every layer. Its golden
3344 // (`tests/gated_attention_graphs.rs`) holds at the same GeGLU
3345 // tolerance llama.cpp's f16 GELU table forces.
3346 matches!(arch, "grok" | "spark2_5")
3347}
3348
3349/// Architectures whose FFN is the UNGATED ReLU-squared MLP:
3350/// `build_ffn(up, NULL gate, down, LLM_FFN_RELU_SQR, LLM_FFN_SEQ)`,
3351/// i.e. `down(relu(up(x))^2)` (`arcee.cpp:123-128`).
3352///
3353/// Five graphs pass `LLM_FFN_RELU_SQR` upstream -- measured, by
3354/// grepping `src/models/*.cpp`: `arcee`, `plm`, `nemotron`, `jais2`,
3355/// `nemotron-h` (the GGUF string is `nemotron_h`; the MoE sibling's
3356/// dense shared expert and its experts pass it too, `:190,227`). All
3357/// five serve it: `plm` on the MLA engine (`crate::mla_arch` reads the
3358/// same fact from its own table, and
3359/// `mla_arch_and_this_table_agree_about_plm` pins that they agree), the
3360/// rest on the generic path.
3361pub fn uses_relu_sqr(arch: &str) -> bool {
3362 matches!(
3363 arch,
3364 "arcee" | "plm" | "nemotron" | "jais2" | "nemotron_h" | "nemotron_h_moe"
3365 )
3366}
3367
3368/// Architectures whose FFN is the UNGATED GELU MLP:
3369/// `build_ffn(up, up_b, NULL gate, down, down_b, LLM_FFN_GELU,
3370/// LLM_FFN_SEQ)`, i.e. `down(gelu(up(x) + up_b)) + down_b`
3371/// (`starcoder2.cpp:125-131`, `codeshell.cpp:120-126`).
3372///
3373/// Eleven graphs pass `LLM_FFN_GELU` under `LLM_FFN_SEQ` upstream --
3374/// measured, `grep -l 'LLM_FFN_GELU, *LLM_FFN_SEQ' src/models/*.cpp`:
3375/// `bert`, `bloom`, `codeshell`, `falcon`, `gptneox`, `gpt2`, `mpt`,
3376/// `phi2`, `starcoder`, `starcoder2`, `wavtokenizer-dec`. The two
3377/// listed reach the generic path with nothing else in the way once the
3378/// projection biases are served (`crate::proj_bias`); `bert` and
3379/// `wavtokenizer-dec` are not decoders, `bloom` / `gpt2` / `mpt` /
3380/// `starcoder` has no RoPE; `gptneox`, `falcon` and `phi2` joined once
3381/// the parallel residual was served (`crate::parallel_residual`). The
3382/// five here map to `FfnActivation::GeluUngated`.
3383pub fn uses_gelu_ungated(arch: &str) -> bool {
3384 matches!(
3385 arch,
3386 "starcoder2"
3387 | "codeshell"
3388 | "gptneox"
3389 | "falcon"
3390 | "phi2"
3391 | "gpt2"
3392 | "starcoder"
3393 | "bloom"
3394 | "mpt"
3395 )
3396}
3397
3398#[cfg(test)]
3399mod relu_sqr_tests {
3400 use super::*;
3401
3402 /// Two tables say what `plm`'s dense FFN is -- this one, read by
3403 /// the generic loader, and `crate::mla_arch`'s row, read by the MLA
3404 /// loader. They must agree, and the MLA table must say ReluSqr for
3405 /// exactly the rows this one names.
3406 #[test]
3407 fn mla_arch_and_this_table_agree_about_plm() {
3408 for row in crate::mla_arch::MLA_ENGINE_ARCHS {
3409 let ungated = row.dense_act.ungated().is_some();
3410 assert_eq!(
3411 ungated,
3412 uses_relu_sqr(row.name),
3413 "`{}`: mla_arch says ungated={ungated}, uses_relu_sqr disagrees",
3414 row.name
3415 );
3416 }
3417 assert!(matches!(
3418 crate::mla_arch::mla_arch("plm").unwrap().dense_act,
3419 frink_moe::GluAct::ReluSqr
3420 ));
3421 }
3422}
3423
3424/// Architectures whose experts are the GATED ReLU MLP:
3425/// `build_moe_ffn(..., LLM_FFN_RELU, ...)` with `gate_exps` present,
3426/// which `llama-graph.cpp:2195-2197` runs as `ggml_reglu_split(gate,
3427/// up)`, i.e. `down(relu(gate(x)) * up(x))` (`smallthinker.cpp:62,158`).
3428///
3429/// ONE graph passes `LLM_FFN_RELU` to `build_moe_ffn` upstream --
3430/// measured, `grep -n 'LLM_FFN_RELU[^_]' src/models/*.cpp` over all
3431/// 140: `smallthinker.cpp:158`. The only other two hits, `t5.cpp:243,
3432/// 345`, are `build_ffn` with a NULL gate (ungated `relu(up)`, a
3433/// different op again) on an encoder-decoder engine, so they are not
3434/// listed. Distinct from [`uses_relu_sqr`] on purpose: that is
3435/// `LLM_FFN_RELU_SQR` with NO gate, served by aliasing gate to up, and
3436/// a loader that aliased this one would compute `relu(up) * up` on a
3437/// file whose gate tensor it had silently dropped.
3438pub fn uses_reglu(arch: &str) -> bool {
3439 matches!(arch, "smallthinker")
3440}
3441
3442pub fn default_swa_layout(arch: &str) -> Option<SwaPattern> {
3443 let last_dense = |period| {
3444 Some(SwaPattern {
3445 period,
3446 dense_first: false,
3447 })
3448 };
3449 let dense_first = |period| {
3450 Some(SwaPattern {
3451 period,
3452 dense_first: true,
3453 })
3454 };
3455 match arch {
3456 // src/models/openai-moe.cpp:9
3457 "gpt-oss" => last_dense(2),
3458 // src/models/gemma2.cpp:6
3459 "gemma2" => last_dense(2),
3460 // src/models/gemma3.cpp:7
3461 "gemma3" => last_dense(6),
3462 // src/models/gemma3n.cpp:4 says 5, NOT 6. This was transcribed
3463 // as 6 alongside gemma3 and is simply wrong. Inert only because
3464 // `gemma3n` refuses for other reasons today.
3465 "gemma3n" => last_dense(5),
3466 // src/models/gemma-embedding.cpp:5. Deferred (embedding scope),
3467 // so latent rather than live.
3468 "gemma-embedding" => last_dense(6),
3469 // src/models/cohere2.cpp:5, exaone4.cpp:7, olmo2.cpp:9
3470 "cohere2" | "exaone4" | "olmo2" => last_dense(4),
3471 // Added after an audit found this table covered 6 architectures
3472 // where llama.cpp hardcodes a period for 17. A MISSING entry is
3473 // not neutral: with no period, every layer gets windowed, so a
3474 // model whose full-attention layers should see the whole context
3475 // sees only a window instead. That is a different model, and it
3476 // fails silently.
3477 //
3478 // src/models/mellum.cpp:11
3479 "mellum" => last_dense(4),
3480 // src/models/exaone-moe.cpp:6. SWA is unconditional there
3481 // with n_swa = 128, so without this every layer ran with a
3482 // 128-token history.
3483 "exaone-moe" => last_dense(4),
3484 // src/models/afmoe.cpp:17. LIVE: `afmoe` is audited, and its
3485 // fixture's window is narrower than the prompt
3486 // (`tests/gated_attention_graphs.rs`).
3487 "afmoe" => last_dense(4),
3488 // src/models/plamo3.cpp:9. LIVE: `plamo3` is audited, and its
3489 // fixture drives a period of 2 from the file with a window
3490 // narrower than the prompt, so both the period override and
3491 // this phase are exercised end to end against libllama.
3492 "plamo3" => last_dense(8),
3493 // src/models/llama4.cpp:19 ("pattern: 3 chunked - 1 full").
3494 // LIVE: the chunked window is `crate::chunked_swa`, and
3495 // tests/llama4_graphs.rs drives a period of 2 from the file.
3496 "llama4" => last_dense(4),
3497 // --- dense_first = true -----------------------------------
3498 //
3499 // These four put the full-attention layer at `il % p == 0`, not
3500 // at `il % p == p - 1`. `ModelConfig::layer_sliding_window`
3501 // implements BOTH phases and carries this flag as
3502 // `swa_dense_first`; it used to implement only the first, which
3503 // is why `smallthinker` and `laguna` windowed every layer.
3504 //
3505 // src/models/smallthinker.cpp:9-11. Latent: `smallthinker` is
3506 // triaged NEW CODE on its raw-input router and ReLU experts, so
3507 // it refuses before this row is consulted. This used to say
3508 // LIVE, and was wrong: the triage row predates the comment.
3509 "smallthinker" => dense_first(4),
3510 // src/models/laguna.cpp:39-41 (its own comment: "XS.2: FULL at
3511 // il%4==0"). LIVE: `laguna` is on the generic GQA path.
3512 "laguna" => dense_first(4),
3513 // src/models/cohere2moe.cpp:31-33. `DedicatedOnly` today
3514 // (parallel attention+FFN residual), so latent.
3515 "cohere2moe" => dense_first(4),
3516 // src/models/modern-bert.cpp:8-10. Deferred (encoder scope), so
3517 // latent.
3518 "modern-bert" => dense_first(3),
3519 _ => None,
3520 }
3521}
3522
3523/// True when this architecture's SWA layers use the model's own RoPE
3524/// base rather than llama.cpp's `rope_freq_base_train_swa` default of
3525/// `10000`.
3526///
3527/// `llama_hparams` defaults that field to `10000.0f`
3528/// (`src/llama-hparams.h:127`) and the Gemma-3 lineage relies on the
3529/// default; the architectures listed here instead open with
3530/// `hparams.rope_freq_base_train_swa = hparams.rope_freq_base_train;`
3531/// before letting `rope.freq_base_swa` override it. frink applied the
3532/// Gemma default to everything, which rotates a gpt-oss SWA layer at
3533/// theta 10000 instead of its real 150000.
3534pub fn swa_rope_base_follows_model(arch: &str) -> bool {
3535 matches!(
3536 arch,
3537 "afmoe"
3538 | "cohere2"
3539 | "cohere2moe"
3540 | "dflash"
3541 | "exaone-moe"
3542 | "exaone4"
3543 | "gemma2"
3544 | "laguna"
3545 | "llama4"
3546 | "mellum"
3547 | "olmo2"
3548 | "gpt-oss"
3549 | "smallthinker"
3550 )
3551}
3552
3553/// True when this architecture's SWA layers inherit the model's TRAINED
3554/// RoPE position scale rather than llama.cpp's
3555/// `rope_freq_scale_train_swa` default of `1.0`.
3556///
3557/// The sibling of [`swa_rope_base_follows_model`], and deliberately NOT
3558/// derived from it: llama.cpp defaults both fields
3559/// (`src/llama-hparams.h:127,129`) and each architecture assigns them
3560/// independently, so the two lists differ. `olmo2.cpp:13-14` and
3561/// `laguna.cpp:47-48` seed the BASE from the model and then pin the
3562/// SCALE to `1.0` -- laguna's own comment is "SWA uses plain RoPE (no
3563/// YaRN scaling); do NOT inherit full layers 1/factor". Collapsing the
3564/// two tables into one would rope those two architectures wrong in
3565/// exactly the way this function exists to stop.
3566///
3567/// The default matters more than the list. `gemma3.cpp:11` reads only
3568/// `LLM_KV_ROPE_FREQ_BASE_SWA` and never touches
3569/// `rope_freq_scale_train_swa`, so Gemma-3's sliding layers rope at
3570/// scale `1.0` while its full-attention layers use the trained scale --
3571/// and the converter agrees, writing `rope.scaling.factor` from
3572/// `rope_parameters["full_attention"]` alone (`conversion/base.py:1222`,
3573/// whose own comment is "TODO: Handle sliding_attention similarly when
3574/// models start implementing it").
3575///
3576/// Every name here is a `hparams.rope_freq_scale_train_swa =
3577/// hparams.rope_freq_scale_train;` in `src/models/`, at the line given.
3578pub fn swa_rope_scale_follows_model(arch: &str) -> bool {
3579 matches!(
3580 arch,
3581 "afmoe" // afmoe.cpp:22
3582 | "cohere2" // cohere2.cpp:10
3583 | "cohere2moe" // cohere2moe.cpp:39
3584 | "dflash" // dflash.cpp:59, :71
3585 | "exaone-moe" // exaone-moe.cpp:10
3586 | "exaone4" // exaone4.cpp:12
3587 | "gemma2" // gemma2.cpp:11
3588 | "llama4" // llama4.cpp:24
3589 | "mellum" // mellum.cpp:20
3590 | "gpt-oss" // openai-moe.cpp:14
3591 | "smallthinker" // smallthinker.cpp:14
3592 )
3593}
3594
3595/// True when this architecture's graph multiplies every token
3596/// embedding by `sqrt(n_embd)` as ARITHMETIC, reading no key for it.
3597///
3598/// Measured over all 155 `src/models/*.cpp` for
3599/// `ggml_scale(ctx0, inpL, sqrtf(...n_embd...))`: every Gemma graph
3600/// (`gemma.cpp:49`, `gemma2.cpp:70`, `gemma3.cpp:93`, `gemma3n.cpp:104`,
3601/// `gemma4.cpp:155`, `gemma-embedding.cpp:85`) and exactly ONE other,
3602/// `afmoe.cpp:120` ("MuP scaling"). The Gemma side was a `family`
3603/// match in `loader.rs`; `afmoe` is not a Gemma and does the same
3604/// thing, so the fact is a table here rather than a second `if`
3605/// beside the first.
3606///
3607/// This is about the ARITHMETIC, not the key. A file for one of these
3608/// declaring `{arch}.embedding_scale` describes something its graph
3609/// does not do, and `scalar_multipliers::multiplier_support` --
3610/// which lists none of them -- refuses the key before this is asked.
3611pub fn embeddings_scaled_by_sqrt_n_embd(arch: &str, family: DecoderFamily) -> bool {
3612 matches!(family, DecoderFamily::GemmaFamily) || arch == "afmoe"
3613}
3614
3615/// llama.cpp's `hparams.f_attention_scale`, but only when it DIFFERS
3616/// from the `1/sqrt(head_dim)` every frink attention kernel already
3617/// applies. `None` means "the kernels' own scale is already right", so
3618/// a caller stores it straight into `ModelConfig::attention_scale`.
3619///
3620/// Only the Gemma-2 and Gemma-3 27B checkpoints answer `Some`:
3621///
3622/// ```cpp
3623/// // src/models/gemma3.cpp:30-33 (src/models/gemma2.cpp:26-29 identical in shape)
3624/// hparams.f_attention_scale = type == LLM_TYPE_27B
3625/// ? 1.0f / std::sqrt(float(hparams.n_embd / hparams.n_head(0)))
3626/// : 1.0f / std::sqrt(float(hparams.n_embd_head_k()));
3627/// ```
3628///
3629/// and llama.cpp applies it as an explicit `ggml_scale` on Q followed by
3630/// `build_attn(..., 1.0f)` (`gemma3.cpp:154`, `gemma2.cpp:110`), which is
3631/// what [`crate::config::ModelConfig::attention_scale`] means here.
3632///
3633/// **The selector is the LAYER COUNT, not a comparison of the two
3634/// widths.** `LLM_TYPE_27B` comes from `switch (hparams.n_layer())`
3635/// (`gemma3.cpp:20-28` `case 62`, `gemma2.cpp:19-23` `case 46`), and
3636/// deriving it instead from `n_embd / n_head != head_dim` would be
3637/// wrong for EVERY other Gemma size -- all of them have
3638/// `n_embd / n_head != head_dim` too, and all of them take llama.cpp's
3639/// `1/sqrt(n_embd_head_k)` branch. See
3640/// `gemma_27b_is_the_only_size_that_overrides_the_kernel_scale`.
3641///
3642/// `hidden_dim / n_heads` is integer division on purpose: llama.cpp
3643/// divides two `uint32_t` and only then converts to float.
3644///
3645/// `jais` is the one other graph with a literal: `jais.cpp:81-83`
3646/// passes `kq_scale = 1.0f / float(n_embd_head)` -- `1/d`, not
3647/// `1/sqrt(d)` (Jais's muP attention) -- to `build_attn` on every layer.
3648/// Measured: `grep -n "1.0f/float(n_embd_head)" src/models/*.cpp` over
3649/// all 155 graphs is that one file.
3650pub fn attention_scale_override(
3651 arch: &str,
3652 n_layers: usize,
3653 hidden_dim: usize,
3654 n_heads: usize,
3655 head_dim: usize,
3656) -> Option<f32> {
3657 // `case 62` / `case 46` in the `switch (hparams.n_layer())` that
3658 // picks `LLM_TYPE_27B`. Every other Gemma architecture
3659 // (`gemma-embedding`, `gemma3n`, `gemma4`) sets `f_attention_scale`
3660 // unconditionally and has no 27B branch at all.
3661 let is_27b = match arch {
3662 "gemma2" => n_layers == 46,
3663 "gemma3" => n_layers == 62,
3664 _ => false,
3665 };
3666 if head_dim == 0 {
3667 return None;
3668 }
3669 if arch == "jais" {
3670 return Some(1.0 / head_dim as f32);
3671 }
3672 if !is_27b || n_heads == 0 {
3673 return None;
3674 }
3675 let scale = 1.0 / ((hidden_dim / n_heads) as f32).sqrt();
3676 let kernel_scale = 1.0 / (head_dim as f32).sqrt();
3677 (scale != kernel_scale).then_some(scale)
3678}
3679
3680/// Architectures outside the Gemma family whose graph applies the two
3681/// logit softcaps frink implements -- `attn_logit_softcapping` on the
3682/// attention scores and `final_logit_softcapping` after the lm_head.
3683///
3684/// `grok`: `llama-graph.cpp:2572-2582` applies
3685/// `30 * tanh(kq * f_attn_out_scale / 30)` before the softmax, which is
3686/// frink's `attn_logit_softcap` over a Q pre-scaled by
3687/// `ModelConfig::attention_scale`; `grok.cpp:214-218` applies the final
3688/// softcap when the file declares one (default 0, off). The converter
3689/// (`conversion/grok.py:34`) writes `attn_logit_softcapping` for EVERY
3690/// Grok export, so without this list no real Grok file could load.
3691///
3692/// The Gemma family is not here because it is exempted as a family
3693/// below; a name here is one whose graph was read for both softcaps.
3694pub const LOGIT_SOFTCAP_ARCHITECTURES: &[&str] = &["grok", "muse-glimmer"];
3695
3696/// Metadata keys that, when present with a nonzero value, require math
3697/// frink's generic decoder does not implement *unless* the architecture
3698/// profile opts into those features (Gemma family), or the architecture
3699/// is named in [`LOGIT_SOFTCAP_ARCHITECTURES`] for the softcaps.
3700pub fn unsupported_feature_keys(arch: &str) -> Vec<(String, &'static str)> {
3701 let profile = resolve_profile(arch);
3702 // Gemma family implements softcap + SWA pattern; others still refuse.
3703 if matches!(profile.map(|p| p.family), Some(DecoderFamily::GemmaFamily)) {
3704 return Vec::new();
3705 }
3706 let key = |suffix: &str| format!("{arch}.{suffix}");
3707 let mut out = Vec::new();
3708 if !LOGIT_SOFTCAP_ARCHITECTURES.contains(&arch) {
3709 out.push((
3710 key("attention.logit_softcapping"),
3711 "attention logit soft-capping (Gemma 2+); not implemented in the generic decoder",
3712 ));
3713 // The spelling llama.cpp's converters ACTUALLY write
3714 // (`llama-arch.cpp:213` is `%s.attn_logit_softcapping`). The
3715 // line above is a spelling no converter emits, so this gate has
3716 // never fired for any non-Gemma architecture -- while
3717 // `loader.rs` reads BOTH spellings and applies the value.
3718 //
3719 // A checkpoint declaring an attention softcap was therefore not
3720 // refused; it ran with the generic formula. For `grok` that is a
3721 // wrong answer rather than an approximation: `grok.cpp` folds
3722 // the real attention scale INTO the softcap and passes
3723 // `kq_scale = 1.0f`, which the generic path does not do.
3724 //
3725 // A gate that cannot fire is not a gate, and it looked exactly
3726 // like one.
3727 out.push((
3728 key("attn_logit_softcapping"),
3729 "attention logit soft-capping (Gemma 2+); not implemented in the generic decoder",
3730 ));
3731 out.push((
3732 key("final_logit_softcapping"),
3733 "final logit soft-capping (Gemma 2+); not implemented in the generic decoder",
3734 ));
3735 }
3736 // `{arch}.nextn_predict_layers` WAS refused here, for every
3737 // architecture, with the reason that frink's `n_layers` IS
3738 // `block_count` and it would run the MTP head as decoder layers.
3739 // `crate::mtp_blocks::trunk_layers` subtracts the blocks now for
3740 // exactly the seventeen graphs whose `load_arch_hparams` reads the
3741 // key (`NEXTN_READERS`, measured) and still refuses a nonzero value
3742 // on any other -- where llama.cpp itself would run every block and
3743 // then fail on the unread `nextn.*` tensors. One place decides both
3744 // halves, so the reader table and the refusal cannot drift apart.
3745 // `{arch}.attention.sliding_window_pattern` WAS refused here,
3746 // with the reason "not implemented in the generic decoder".
3747 // That reason was false, and had been for some time: the
3748 // alternating pattern lives in `ModelConfig::layer_sliding_window`,
3749 // which implements BOTH phases and which `gpt-oss` -- a
3750 // `StandardGqa` row, not a Gemma one -- has relied on since it
3751 // was audited against libllama.
3752 //
3753 // What the gate really did was make the loader's own read of
3754 // that key (`swa_pattern`) unreachable for every non-Gemma
3755 // architecture: llama.cpp lets the file override the
3756 // architecture's hardcoded period, frink refused any file that
3757 // tried. `plamo3` is the case that proves it -- its converter
3758 // writes the key verbatim (`conversion/plamo.py:178`) -- and
3759 // `tests/fixture_away_graphs.rs` now drives a period of 2 out of
3760 // a plamo3 fixture and compares against llama.cpp's own graph on
3761 // all three forward paths, with the phase and the window
3762 // sabotaged separately.
3763 //
3764 // The real gap the key could hide was NOT the pattern: it was
3765 // that llama.cpp accepts the value as a scalar OR an n_layer-long
3766 // ARRAY (`ml.get_key_or_arr`), and frink carried one scalar
3767 // period. The array is `crate::swa_layers` now, read the way each
3768 // graph reads it -- ignored, honoured, or broadcast -- so neither
3769 // shape is refused here or anywhere else.
3770 //
3771 // `{arch}.moe_latent_size` (`LLM_KV_MOE_LATENT_SIZE`,
3772 // `nemotron-h.cpp:21,36,82-85,206-208`): the routed experts run in a
3773 // LATENT width the layer projects into with `ffn_latent_down` and
3774 // out of with `ffn_latent_up`, while the router and the shared
3775 // expert read the unprojected input. Nemotron-3 Nano writes no such
3776 // key; Nemotron-3 Super does. The generic MoE bodies run their
3777 // experts at `hidden_dim`, so a nonzero value stops here, by name.
3778 out.push((
3779 key("moe_latent_size"),
3780 "a latent MoE (nemotron-h.cpp:206-208: the experts read `ffn_latent_down(x)` and \
3781 their sum is `ffn_latent_up`ed back), which the generic MoE bodies, which run \
3782 the experts at hidden_dim, do not have",
3783 ));
3784 out
3785}
3786
3787/// Scalar multipliers a checkpoint can declare in **metadata** that the
3788/// generic decoder does not apply, with the value that means "no-op".
3789///
3790/// These are the blind spot left by
3791/// [`crate::loader::assert_every_tensor_consumed`]: that gate catches a
3792/// missing *tensor*, but Granite / MiniCPM / Command-R style multipliers
3793/// are hparams, not weights, so a checkpoint carrying them loads
3794/// cleanly, runs at full speed, and computes a graph scaled differently
3795/// from the one the checkpoint was trained as. Nothing says so.
3796///
3797/// llama.cpp key names (`llama-arch.cpp`):
3798/// `%s.logit_scale` (`LLM_KV_LOGIT_SCALE`), `%s.residual_scale`,
3799/// `%s.embedding_scale`, `%s.attention.scale`. Granite reads all four
3800/// (`src/models/granite.cpp::load_arch_hparams`); MiniCPM and
3801/// Command-R/Cohere2 read the subset they use.
3802///
3803/// **This list is DERIVED, never restated.** Which of the four an
3804/// architecture applies lives in
3805/// [`crate::scalar_multipliers::multiplier_support`], and this function
3806/// is exactly its complement: a key appears here if and only if that
3807/// table says the graph does not apply it. Two hand-written lists is the
3808/// shape that once let this repo refuse a key it implemented and
3809/// implement a key it refused, and the Gemma family used to be exempted
3810/// from ALL FOUR of these wholesale on the strength of implementing two,
3811/// so a hand-written `gemma3.residual_scale` would have loaded and been
3812/// ignored.
3813///
3814/// `residual_scale` is the one that reaches furthest: it multiplies the
3815/// attention and FFN branch outputs before every residual add, so on an
3816/// architecture that does not implement it a declared value would have
3817/// to be dropped by every CPU decode/prefill/multi-seq path *and* by the
3818/// fused Metal kernels that fold the residual in.
3819///
3820/// The no-op value differs by key: the three `*_scale` multipliers are
3821/// `1.0`, while llama.cpp's `f_attention_scale` uses `0.0` as its
3822/// "unset, use 1/sqrt(head_dim)" sentinel.
3823pub fn unsupported_scaling_keys(arch: &str) -> Vec<(String, &'static str, f32)> {
3824 use crate::scalar_multipliers::{AttentionScaleKey, LogitScaleUse, ResidualScaleUse};
3825 let support = crate::scalar_multipliers::multiplier_support(arch);
3826 let key = |suffix: &str| format!("{arch}.{suffix}");
3827 let mut out = Vec::new();
3828 if support.logit == LogitScaleUse::NotApplied {
3829 out.push((
3830 key("logit_scale"),
3831 "logit multiplier (Granite / Command-R `logits_scaling`); not applied by the generic decoder",
3832 1.0,
3833 ));
3834 }
3835 if support.residual == ResidualScaleUse::NotRead {
3836 out.push((
3837 key("residual_scale"),
3838 "residual multiplier (Granite `residual_multiplier`); not applied by the generic decoder",
3839 1.0,
3840 ));
3841 }
3842 if !support.embedding {
3843 out.push((
3844 key("embedding_scale"),
3845 "embedding multiplier (Granite / MiniCPM `embedding_multiplier`); the generic decoder only scales embeddings for the Gemma and Granite families",
3846 1.0,
3847 ));
3848 }
3849 // Two spellings of one slot, and an architecture reads at most one
3850 // of them: the OTHER stays refused. `grok` reads `output_scale` and
3851 // never `scale`; Granite the reverse; everyone else neither.
3852 if support.attention != AttentionScaleKey::Scale {
3853 out.push((
3854 key("attention.scale"),
3855 "explicit attention score scale (Granite `attention_multiplier`); the generic decoder always uses 1/sqrt(head_dim)",
3856 0.0,
3857 ));
3858 }
3859 if support.attention != AttentionScaleKey::OutputScale {
3860 // Applied as-is by the one graph that reads it (`grok.cpp`, no
3861 // sentinel), so there is no value that means "off" -- the
3862 // no-op here is the kernels' own scale expressed as a key, which
3863 // no converter writes for a non-Grok architecture. A file
3864 // declaring ANY other value is refused.
3865 out.push((
3866 key("attention.output_scale"),
3867 "attention output scale (Grok `attn_output_multiplier`, applied inside its tanh softcap); the generic decoder always uses 1/sqrt(head_dim)",
3868 0.0,
3869 ));
3870 }
3871 out
3872}
3873
3874/// Markdown coverage table for docs / CI drift checks.
3875pub fn coverage_report_markdown() -> String {
3876 let mut lines = vec![
3877 "# Architecture coverage manifest".to_string(),
3878 String::new(),
3879 "Generated from `frink_models::capability::architecture_catalog`.".to_string(),
3880 "Source of truth for names: pinned llama.cpp `LLM_ARCH_NAMES`.".to_string(),
3881 String::new(),
3882 "| GGUF arch | Scope | Family | Memory | Path |".to_string(),
3883 "|---|---|---|---|---|".to_string(),
3884 ];
3885 for p in architecture_catalog() {
3886 let path = match p.path {
3887 ArchPath::GenericGqa { .. } => "generic-gqa",
3888 ArchPath::TestFixture { .. } => "test-fixture",
3889 ArchPath::DedicatedOnly { .. } => "dedicated",
3890 ArchPath::Deferred { .. } => "deferred",
3891 };
3892 lines.push(format!(
3893 "| `{}` | {:?} | {:?} | {:?} | {} |",
3894 p.gguf_name, p.scope, p.family, p.memory, path
3895 ));
3896 }
3897 lines.push(String::new());
3898 lines.join("\n")
3899}
3900
3901#[cfg(test)]
3902mod audit_tests {
3903 use super::*;
3904
3905 /// Every audited name must actually be on the generic path.
3906 ///
3907 /// A name here that resolves to a dedicated engine, or to nothing,
3908 /// is a stale entry claiming evidence for a path it does not use.
3909 #[test]
3910 fn every_audited_name_is_actually_on_the_generic_path() {
3911 for name in AUDITED_GENERIC_GQA {
3912 let profile = resolve_profile(name)
3913 .unwrap_or_else(|| panic!("audited arch `{name}` is not in the catalog"));
3914 assert!(
3915 matches!(profile.path, ArchPath::GenericGqa { .. }),
3916 "`{name}` is listed as an audited GENERIC-path arch but resolves to {:?}",
3917 profile.path
3918 );
3919 }
3920 }
3921
3922 /// The five architectures that were caught computing the wrong
3923 /// thing must never appear here.
3924 ///
3925 /// They are refused outright now, but this pins the intent: the
3926 /// audited list is evidence of correctness, and these are the
3927 /// counter-examples that motivated it.
3928 #[test]
3929 fn the_architectures_that_were_wrong_are_not_claimed_as_audited() {
3930 // `gpt2` left this list on 2026-09-14: it IS audited now, on a
3931 // rule that rotates nothing (`rope_layers::RopeLayers::Never`)
3932 // with its table added (`crate::position_embd`), which is what
3933 // the finding asked for.
3934 assert!(is_audited_generic("gpt2"));
3935 assert_eq!(
3936 crate::rope_layers::rope_layers("gpt2", 12, false, 0),
3937 crate::rope_layers::RopeLayers::Never
3938 );
3939 // The four ALiBi rows followed `gpt2` the same way
3940 // (`crate::alibi`, tests/alibi_graphs.rs): audited, and under
3941 // `Never`.
3942 for name in ["mpt", "refact", "bloom", "jais"] {
3943 assert!(is_audited_generic(name));
3944 assert_eq!(
3945 crate::rope_layers::rope_layers(name, 24, false, 0),
3946 crate::rope_layers::RopeLayers::Never,
3947 "`{name}` positions by ALiBi and must rotate nothing"
3948 );
3949 }
3950 }
3951
3952 /// Every unaudited generic-path architecture either carries a
3953 /// triage verdict or is named on [`TRIAGE_PENDING`] -- never both,
3954 /// never neither.
3955 ///
3956 /// This is the anti-drift gate. Adding a new architecture to the
3957 /// generic catalog without either reading it against llama.cpp or
3958 /// admitting on the pending list that nobody has, fails here.
3959 #[test]
3960 fn every_unaudited_generic_architecture_is_triaged_or_listed_as_pending() {
3961 for p in architecture_catalog() {
3962 if !matches!(p.path, ArchPath::GenericGqa { .. }) || is_audited_generic(p.gguf_name) {
3963 continue;
3964 }
3965 let pending = TRIAGE_PENDING.contains(&p.gguf_name);
3966 match (p.triage, pending) {
3967 (Some(_), false) | (None, true) => {}
3968 (Some(t), true) => panic!(
3969 "`{}` carries a {:?} verdict AND is still on TRIAGE_PENDING; remove it \
3970 from the pending list",
3971 p.gguf_name, t.class
3972 ),
3973 (None, false) => panic!(
3974 "`{}` is on the generic path, is not audited, has no triage verdict and \
3975 is not on TRIAGE_PENDING. Read \
3976 .scratch/llama.cpp/src/models/ for it, or say so on the pending list",
3977 p.gguf_name
3978 ),
3979 }
3980 }
3981 }
3982
3983 /// A name on [`TRIAGE_PENDING`] that is not an unaudited generic row
3984 /// is a stale to-do: it would keep claiming work that no longer
3985 /// exists, or point at an architecture the loader never asks about.
3986 #[test]
3987 fn nothing_on_the_pending_list_is_stale() {
3988 for name in TRIAGE_PENDING {
3989 let p = resolve_profile(name)
3990 .unwrap_or_else(|| panic!("TRIAGE_PENDING names `{name}`, not in the catalog"));
3991 assert!(
3992 matches!(p.path, ArchPath::GenericGqa { .. }),
3993 "`{name}` is on TRIAGE_PENDING but resolves to {:?}, which never reaches the \
3994 unaudited refusal",
3995 p.path
3996 );
3997 assert!(
3998 !is_audited_generic(name),
3999 "`{name}` is audited and runs; it does not need a triage verdict"
4000 );
4001 }
4002 // The list is empty because the triage finished, not because it
4003 // was never populated. If a future architecture lands on the
4004 // generic path with no verdict, it belongs here and
4005 // `every_unaudited_generic_architecture_is_triaged_or_listed_as_pending`
4006 // will say so; until then, empty is the completed state.
4007 assert!(
4008 TRIAGE_PENDING.is_empty(),
4009 "TRIAGE_PENDING regrew to {:?}; that is fine, but say so in docs/MODELS.md too",
4010 TRIAGE_PENDING
4011 );
4012 }
4013
4014 /// An audited architecture runs. A triage verdict on one would be a
4015 /// refusal class attached to something that never refuses.
4016 #[test]
4017 fn an_audited_architecture_carries_no_triage_verdict() {
4018 for name in AUDITED_GENERIC_GQA {
4019 assert!(
4020 unaudited_triage(name).is_none(),
4021 "`{name}` is audited and runs, so it must not carry a triage verdict"
4022 );
4023 }
4024 }
4025
4026 /// A verdict has to say something. An empty blocker, or one that
4027 /// cites no llama.cpp source line, is the failure mode this whole
4028 /// item exists to prevent: a refusal that names a blocker nobody
4029 /// checked.
4030 #[test]
4031 fn every_triage_verdict_cites_the_llama_cpp_line_that_decides_it() {
4032 let mut seen = 0;
4033 for p in architecture_catalog() {
4034 let Some(t) = p.triage else { continue };
4035 seen += 1;
4036 assert!(
4037 t.blocker.len() > 80,
4038 "`{}`'s blocker is too short to name anything: {:?}",
4039 p.gguf_name,
4040 t.blocker
4041 );
4042 let cites_llama_cpp =
4043 t.blocker.contains("src/models/") || t.blocker.contains("src/llama-arch.cpp");
4044 assert!(
4045 cites_llama_cpp,
4046 "`{}`'s blocker cites no llama.cpp source: {}",
4047 p.gguf_name, t.blocker
4048 );
4049 if t.class == TriageClass::Unknown {
4050 assert!(
4051 t.blocker.contains("WOULD SETTLE IT"),
4052 "`{}` is UNKNOWN but does not say what would settle it",
4053 p.gguf_name
4054 );
4055 }
4056 }
4057 assert!(
4058 seen == 4,
4059 "every unaudited generic architecture is triaged; found {seen}. \
4060 It was 47 until the triage found `minicpm3` was an MLA model on the \
4061 generic-GQA row and it moved to DedicatedOnly, 46 until five ONE MATCH ARM \
4062 rows -- deepseek, bailingmoe, seed_oss, maincoder, hunyuan-moe -- were admitted \
4063 with libllama-golden fixtures, 41 until seven FIXTURE-AWAY rows -- \
4064 internlm2, xverse, ernie4_5, baichuan, exaone, bailingmoe2, plamo3 -- got \
4065 theirs (tests/fixture_away_graphs.rs), 34 until `gemma`, `hunyuan-dense` \
4066 and `ernie4_5-moe` got theirs, 31 until `olmo2` and `exaone4` -- the \
4067 POST-NORM-ONLY pair, ONE topology and one implementation \
4068 (`crate::norm`) -- got theirs (tests/post_norm_only_graphs.rs), 29 until \
4069 `chatglm` -- the LAST ONE MATCH ARM row -- got its fused-QKV-bias arm and \
4070 its fixture, 28 until `mistral`, `mixtral` and `yi` turned out not to be \
4071 architectures at all (libllama refuses all three strings) and moved to \
4072 DedicatedOnly, and 25 until the three Granite rows -- granite, granitemoe \
4073 and the granite-moe alias -- closed together on ONE implementation of their \
4074 four scalar multipliers (tests/granite_family_graphs.rs), and 22 until \
4075 `olmo` closed on the non-parametric LayerNorm (`crate::norm`, \
4076 tests/olmo_graphs.rs). `olmo` is the FIRST NEW CODE row to close on its own, \
4077 and it says something the other closures do not: its cause is not shared. \
4078 Every `build_norm` call in llama.cpp's 155 graphs was scanned for a null \
4079 weight and all three hits are `olmo.cpp`, so this variant was never going to \
4080 take a second row with it -- see `NON_PARAMETRIC_LAYER_NORM`. `gemma` was the \
4081 last fixture-away row and `chatglm` the last one-match-arm row, so BOTH \
4082 classes are empty, and 21 until `exaone-moe` closed on the per-layer RoPE \
4083 gate (`crate::rope_layers`, tests/no_rope_layer_graphs.rs) -- which is ONE \
4084 cause behind three refusals, and the count moved by one only because the \
4085 other two were not in it: EXAONE-4 32B was refused BY NAME in loader.rs \
4086 and `smollm3` sat in the \"No RoPE at all\" DedicatedOnly group, so both \
4087 raise the audited number without lowering this one, and 20 until `grok` \
4088 and `dbrx` closed together on seams that had landed the day before -- the \
4089 defaults hook and the norm-site table for `grok`, the LayerNorm variant, \
4090 the QKV clamp and the same table for `dbrx` (tests/grok_graphs.rs, \
4091 tests/dbrx_graphs.rs) -- with the clamp also closing `olmo`'s clip_qkv \
4092 refusal by name, and 18 until `arcee` closed on the ungated ReLU-squared FFN \
4093 (`FfnActivation::ReluSqr`, tests/ungated_ffn_graphs.rs) -- ALONE, because the \
4094 constant it shared with `plm` had named the FFN and missed `plm`'s MLA \
4095 attention -- and `deci` and `openelm` closed together on the per-layer shape \
4096 seam (`crate::layer_shapes`, tests/per_layer_shape_graphs.rs), which the scan \
4097 that sized it says reaches `laguna`, `mimo2` and `step35` too, each of which \
4098 still needed something else, and 15 until `afmoe` and `laguna` closed together \
4099 on the gated attention (`crate::attn_gate`, tests/gated_attention_graphs.rs) \
4100 -- one op with two free parameters behind three verdicts, read side by side \
4101 before being called one cause; `step35` keeps its clamp arrays and window \
4102 array and says the gate is done, and `mimo2`'s sinks moved off the gpt-oss \
4103 name onto the tensor without closing it, and 13 until `mellum` closed on the \
4104 per-layer sliding-window ARRAY (`crate::swa_layers`, \
4105 tests/window_array_graphs.rs) -- the seam three verdicts named, and `mellum` \
4106 is the one generic-path graph that HONOURS the array; the same seam lifted the \
4107 over-refusal of every real EXAONE-4 32B / EXAONE-MoE / Olmo-3 export, whose \
4108 array llama.cpp IGNORES (measured: libllama's logits do not move when it is \
4109 inverted), and `crate::mtp_blocks` landed beside it and skips the NextN \
4110 blocks `mimo2` and `step35` named, so both lead with what is left, and 12 \
4111 until `apertus` and `step35` closed together on the per-layer ACTIVATION \
4112 PARAMETER seam (`crate::act_layers`, tests/per_layer_activation_graphs.rs, \
4113 tests/clamped_swiglu_graphs.rs) -- one plumbing question, `layer il runs its \
4114 FFN activation with these scalars`, and two bodies, xIELU and the clamped \
4115 SwiGLU, read side by side before being called one cause; `step35`'s \
4116 half-width rotary landed on `crate::swa_geometry` as a two-valued width and \
4117 lifted Laguna-XS.2's `rope.dimension_count_swa` refusal by name with it, and \
4118 10 until `mistral3` closed on the per-position attention temperature \
4119 (`crate::attn_temperature`, tests/attn_temperature_graphs.rs) -- the reach \
4120 measured first: three graphs of 155 build the input, `llama4` from literals \
4121 on its own engine and `deepseek2` / `mistral4` on the MLA engine, which \
4122 REFUSES the key by name now where it dropped it; and its `yarn_log_multiplier` \
4123 half found YaRN's magnitude term missing for EVERY architecture \
4124 (`crate::yarn_magnitude`), and 9 until `smallthinker` closed on the router \
4125 operand (`crate::router_input`, tests/router_input_graphs.rs) -- the reach \
4126 measured first over every `build_moe_ffn` call site: four graphs pass a \
4127 precomputed `probs_in`, and it is the only one on the generic path whose \
4128 operand is not the normed FFN input; its gated ReLU experts split \
4129 `GluAct::ReluSqr` from `GluAct::Reglu`, because the one variant that had \
4130 served `arcee` by aliasing would have skipped a real gate, and 8 until \
4131 `bitnet` closed on the two norms INSIDE the blocks (`crate::sub_norms`, \
4132 tests/sub_norm_graphs.rs) -- the reach measured first: one graph of 155 \
4133 creates either tensor, so the seam is a `bool` and it closed alone, and 7 \
4134 until `mimo2` closed on the split K/V head width (`crate::kv_head_dims`, \
4135 tests/split_kv_head_dim_graphs.rs) -- the reach measured over the fourteen \
4136 converters that write `value_length`: three write it apart from \
4137 `key_length`, two on the MLA engine, one here, and 6 until `nanbeige` closed \
4138 on the layer loop (`crate::layer_loops`, tests/layer_loop_graphs.rs) -- one \
4139 graph of 155 reads `num_loops`, and the seam is a mapping from logical to \
4140 physical layer rather than a copy of the weights, and 5 until `talkie` closed \
4141 on four things at once (`crate::skip_stream`, `NormOp::RmsNoParams`, \
4142 `QkNormStyle::PerHeadScalar`, the two served `.scale` companions; \
4143 tests/skip_stream_graphs.rs), each one graph of 155, and 4 until `plm` closed \
4144 on the MLA engine (`crate::mla_arch`, `crate::mla_q_proj`, tests/plm_graphs.rs) \
4145 -- the reach measured first: six graphs of 155 create `attn_kv_a_mqa`, three \
4146 have a direct `attn_q` beside it, and on this engine that is `plm` and every \
4147 lite `deepseek2`, which the loader had refused for a key llama.cpp does not \
4148 read; the fixture is the engine's FIRST libllama golden, and 3 until `arctic` \
4149 closed on the parallel dense + MoE layer (`crate::parallel_dense_ffn`, \
4150 `RouterInput::NormedLayerInput`, tests/parallel_dense_ffn_graphs.rs) -- the reach \
4151 measured first: two graphs of 155 sum a dense FFN with their routed output, and \
4152 the other, Grok-2, had been refused by name from a fixture that now has a golden; \
4153 the branch operand is one graph of 155 and a third variant of the seam \
4154 `smallthinker` opened. \
4155 What is left is 1 NEW CODE (`grovemoe`) and one UNKNOWN (`phi4`). The NEW CODE rows \
4156 that have closed are `olmo2`, `exaone4`, the three Granite rows, `exaone-moe`, \
4157 `grok`, `dbrx`, `arcee`, `deci`, `openelm`, `afmoe`, `laguna`, `mellum`, `apertus`, \
4158 `step35`, `mistral3`, `smallthinker`, `bitnet`, `mimo2`, `nanbeige`, `talkie`, \
4159 `plm` and `arctic`, and each closure but `olmo`'s, `arcee`'s, `mellum`'s, \
4160 `mistral3`'s, `smallthinker`'s, `bitnet`'s, `mimo2`'s, `nanbeige`'s, `talkie`'s \
4161 and `plm`'s took more than one row at a time because each found ONE cause \
4162 behind several refusals; `mellum`'s cause IS shared and moved three verdicts, \
4163 but only one of them was closable by it, `mistral3`'s is shared with two rows \
4164 on other engines, `smallthinker`'s mechanism (a precomputed `probs`) is shared \
4165 with three rows whose CAUSE it is not, `bitnet`'s is shared with nothing, and \
4166 `mimo2`'s is shared with the MLA engine, which has carried the two widths \
4167 since it existed, `nanbeige`'s and `talkie`'s with nothing, `plm`'s with \
4168 the lite DeepSeek-V2 checkpoints on the same engine, and `arctic`'s with \
4169 Grok-2, whose refusal by name lifted with it. \
4170 THEN THE COUNT WENT BACK UP, 2 to 10, and that is the honest shape of \
4171 parity with a moving target: the pinned llama.cpp was six weeks and 792 \
4172 commits old on 2026-09-19, and moving the pin to `5b59b83` added fifteen \
4173 graphs. Eight of them are generic-path candidates and are triaged here \
4174 (`granite_swa`, `graniteswitch`, `muse-glimmer`, `maple`, `spark2_5`, \
4175 `hrm_text`, `minimax-01`, `qwen4exp`); four need an attention this engine \
4176 does not have and are `dedicated` refusals (`bailingmoe3`, `dots3note`, \
4177 `hy_v4`, `kimi-k3`); two are text-to-speech and are deferred with the audio \
4178 scope. TWO of the eight were ONE MATCH ARM -- `maple` needs one \
4179 `crate::rope_layers` row and `spark2_5` needed one `crate::attn_gate` row -- \
4180 and BOTH closed the same day, `spark2_5` on exactly the row its \
4181 verdict named and `maple` on that row PLUS one thing no reading of \
4182 `maple.cpp` alone could have found: `llama-graph.cpp:2228` sends four \
4183 architectures, `maple` among them, to `ggml_swiglu_clamp`, which clamps the \
4184 gate BEFORE the SiLU where every other graph clamps the SiLU's output \
4185 (`frink_moe::ClampForm`). `granite_swa` and `muse-glimmer` closed the same day too, the \
4186 first on \
4187 `RopeLayers::FileMask` -- `attention.rope_pattern`, one line of 155 and the \
4188 FIRST upstream graph that lets the FILE say which layers rotate -- so the \
4189 count is 6, and the second on two norm facts nothing else upstream has (a \
4190 WEIGHTLESS RMS on the embeddings and a post-norm epsilon written as a \
4191 literal in the graph). Four of the eight rows the pin brought in closed the \
4192 day it moved, `hrm_text` made it five the day after, and `minimax-01` six the \
4193 day after that, which is what leaves 4: its lightning-attention block is \
4194 `crate::lightning` on the `AttnShape` seam the Qwen3.5 rows built, its \
4195 recurrent mask is the same two keys `crate::gdn::recurrent_layers` already \
4196 read, and the one thing neither reached is the residual topology \
4197 (`crate::normed_residual`: each sublayer's PRE-NORM output, scaled by a \
4198 REQUIRED `residual_scale`, REPLACES the stream its branch joins), which is ONE \
4199 graph of the 155"
4200 );
4201 }
4202
4203 /// The class reaches the message. Two architectures in different
4204 /// classes must not read the same, which is the defect being fixed.
4205 #[test]
4206 fn the_refusal_detail_distinguishes_the_classes() {
4207 // TWO of the four classes have no rows left. `gemma` was the
4208 // last FIXTURE-AWAY row and `chatglm` the last ONE MATCH ARM
4209 // one, and both are audited now, so neither renders a detail at
4210 // all -- `every_triage_verdict_cites_the_llama_cpp_line...`
4211 // pins the count that says so. The two live classes are sampled
4212 // from the catalog; the two empty ones are sampled from
4213 // `headline()` below, because a class with no rows still has to
4214 // render distinctly the day something lands in it again.
4215 //
4216 // `grovemoe`, which used to be `arctic`, `talkie`, `bitnet`,
4217 // `smallthinker`, `dbrx`, `olmo`: the sample keeps moving because
4218 // the rows keep closing. `olmo`'s non-parametric LayerNorm,
4219 // `dbrx`'s weighted one plus its clamp and its `attn_output_norm`
4220 // slot, `smallthinker`'s router operand and gated ReLU experts,
4221 // `bitnet`'s two inner norms, `talkie`'s weightless norms,
4222 // per-head scalar gain, skip stream and projection gains, and
4223 // `arctic`'s parallel dense + MoE layer are all implemented now.
4224 // `grovemoe`'s second expert bank has no single graph to match
4225 // (its verdict says why).
4226 let new_code = unaudited_refusal_detail("grovemoe");
4227 // `phi4` is the only UNKNOWN row left: `mistral`, `mixtral` and
4228 // `yi` used to be the other three and are refused as strings
4229 // now (see `NO_UPSTREAM_ARCH`).
4230 let unknown = unaudited_refusal_detail("phi4");
4231 // TRIAGE_PENDING is empty now that all 47 are read, so the
4232 // untriaged branch is exercised through a name the catalog does
4233 // not carry. The branch has to keep working: it is what a NEW
4234 // architecture added to the catalog would render until somebody
4235 // reads it.
4236 let untriaged = unaudited_refusal_detail("an-arch-nobody-has-read");
4237 assert!(new_code.contains("NEW CODE"), "{new_code}");
4238 assert!(unknown.contains("UNKNOWN"), "{unknown}");
4239 assert!(
4240 untriaged.contains("not done for `an-arch-nobody-has-read` yet"),
4241 "{untriaged}"
4242 );
4243 for a in [&new_code, &unknown, &untriaged] {
4244 for b in [&new_code, &unknown, &untriaged] {
4245 if !std::ptr::eq(a, b) {
4246 assert_ne!(a, b, "two refusal details are identical");
4247 }
4248 }
4249 }
4250 // The blocker itself, not only the class label, has to be in the
4251 // message -- a class with no specifics is the old refusal with a
4252 // new adjective.
4253 assert!(new_code.contains("grovemoe.cpp"), "{new_code}");
4254 assert!(unknown.contains("LLM_ARCH_NAMES"), "{unknown}");
4255 // The two empty classes still have to be distinguishable.
4256 let labels = [
4257 TriageClass::FixtureAway,
4258 TriageClass::OneMatchArm,
4259 TriageClass::NewCode,
4260 TriageClass::Unknown,
4261 ];
4262 for (i, a) in labels.iter().enumerate() {
4263 for b in &labels[i + 1..] {
4264 assert_ne!(a.label(), b.label());
4265 assert_ne!(a.headline(), b.headline());
4266 }
4267 }
4268 }
4269
4270 /// An architecture nobody has checked is not audited, which is the
4271 /// whole point of the inversion.
4272 #[test]
4273 fn an_unchecked_architecture_is_not_audited() {
4274 assert!(!is_audited_generic("grovemoe"));
4275 assert!(!is_audited_generic("phi4"));
4276 assert!(!is_audited_generic("an-arch-that-does-not-exist"));
4277 }
4278}
4279
4280#[cfg(test)]
4281mod tests {
4282 use super::*;
4283
4284 #[test]
4285 fn known_mainstream_families_resolve() {
4286 assert_eq!(
4287 resolve_architecture("llama"),
4288 Some(ArchPath::GenericGqa {
4289 rope: RopeLayout::Norm
4290 })
4291 );
4292 assert_eq!(
4293 resolve_architecture("qwen2moe"),
4294 Some(ArchPath::GenericGqa {
4295 rope: RopeLayout::Neox
4296 })
4297 );
4298 // `mistral`, `mixtral` and `yi` are NOT here any more. They are
4299 // resolved, but refused: no converter writes those strings and
4300 // libllama refuses them outright, so they are alias rows that
4301 // exist to say "your file is spelled `llama`", not families
4302 // that load. Pinned by
4303 // `the_alias_rows_are_refused_as_strings_no_converter_writes`.
4304 for alias in ["mistral", "mixtral", "yi"] {
4305 assert!(
4306 matches!(
4307 resolve_architecture(alias),
4308 Some(ArchPath::DedicatedOnly { .. })
4309 ),
4310 "`{alias}` must be refused, not routed to the generic decoder"
4311 );
4312 }
4313 assert_eq!(
4314 resolve_architecture("phi3"),
4315 Some(ArchPath::GenericGqa {
4316 rope: RopeLayout::Neox
4317 })
4318 );
4319 assert_eq!(
4320 resolve_architecture("phi4"),
4321 Some(ArchPath::GenericGqa {
4322 rope: RopeLayout::Neox
4323 })
4324 );
4325 assert_eq!(
4326 resolve_profile("phi4").map(|p| p.family),
4327 Some(DecoderFamily::PhiFamily)
4328 );
4329 assert_eq!(
4330 resolve_architecture("gemma3"),
4331 Some(ArchPath::GenericGqa {
4332 rope: RopeLayout::Neox
4333 })
4334 );
4335 for arch in ["gemma4", "gemma4-assistant"] {
4336 assert!(
4337 matches!(
4338 resolve_architecture(arch),
4339 Some(ArchPath::DedicatedOnly { .. })
4340 ),
4341 "{arch} uses dedicated Gemma4 engine"
4342 );
4343 assert_eq!(
4344 resolve_profile(arch).map(|p| p.family),
4345 Some(DecoderFamily::GemmaFamily)
4346 );
4347 }
4348 assert!(matches!(
4349 resolve_architecture("gemma3n"),
4350 Some(ArchPath::DedicatedOnly { .. })
4351 ));
4352 assert_eq!(
4353 resolve_architecture("deepseek"),
4354 Some(ArchPath::GenericGqa {
4355 rope: RopeLayout::Norm
4356 })
4357 );
4358 assert_eq!(
4359 resolve_profile("qwen3").map(|p| p.qk_norm),
4360 Some(QkNormStyle::PerHead)
4361 );
4362 }
4363
4364 #[test]
4365 fn deepseek2_is_dedicated_mla_not_generic() {
4366 assert!(matches!(
4367 resolve_architecture("deepseek2"),
4368 Some(ArchPath::DedicatedOnly { .. })
4369 ));
4370 }
4371
4372 #[test]
4373 fn unknown_architecture_is_none() {
4374 assert_eq!(resolve_architecture("totally-unknown-arch"), None);
4375 // t5 is registered as dedicated encoder-decoder stub
4376 assert!(matches!(
4377 resolve_architecture("t5"),
4378 Some(ArchPath::DedicatedOnly { .. })
4379 ));
4380 }
4381
4382 #[test]
4383 fn dedicated_paths_are_not_generic() {
4384 assert!(matches!(
4385 resolve_architecture("glm-dsa"),
4386 Some(ArchPath::DedicatedOnly { .. })
4387 ));
4388 assert!(matches!(
4389 resolve_architecture("deepseek4"),
4390 Some(ArchPath::DedicatedOnly { .. })
4391 ));
4392 assert!(
4393 matches!(
4394 resolve_architecture("minimax-m3"),
4395 Some(ArchPath::DedicatedOnly { .. })
4396 ),
4397 "minimax-m3 must fail closed, not silent generic GQA"
4398 );
4399 // `llama4` was a `DedicatedOnly` refusal here and is an audited
4400 // generic row now (tests/llama4_graphs.rs).
4401 assert!(is_audited_generic("llama4"));
4402 // `glm4` and `glm4moe` were DedicatedOnly refusals here and are
4403 // audited generic rows now (tests/glm4_graphs.rs,
4404 // tests/glm4moe_graphs.rs); `glm-dsa` stays on its engine.
4405 assert!(matches!(
4406 resolve_architecture("glm-dsa"),
4407 Some(ArchPath::DedicatedOnly { .. })
4408 ));
4409 assert!(matches!(
4410 resolve_architecture("glm4"),
4411 Some(ArchPath::GenericGqa {
4412 rope: RopeLayout::Norm
4413 })
4414 ));
4415 assert!(is_audited_generic("glm4"));
4416 assert!(matches!(
4417 resolve_architecture("glm4moe"),
4418 Some(ArchPath::GenericGqa {
4419 rope: RopeLayout::Neox
4420 })
4421 ));
4422 assert!(is_audited_generic("glm4moe"));
4423 }
4424
4425 #[test]
4426 fn test_fixtures_remain_loadable() {
4427 for arch in ["ferroxtest", "ferroxtestmoe", "ferroxtestmixed"] {
4428 assert!(matches!(
4429 resolve_architecture(arch),
4430 Some(ArchPath::TestFixture { .. })
4431 ));
4432 }
4433 }
4434
4435 #[test]
4436 fn catalog_has_unique_names() {
4437 let mut seen = std::collections::HashSet::new();
4438 for p in architecture_catalog() {
4439 assert!(
4440 seen.insert(p.gguf_name),
4441 "duplicate arch name {}",
4442 p.gguf_name
4443 );
4444 }
4445 }
4446
4447 #[test]
4448 fn gemma_family_does_not_fail_closed_on_softcap_keys() {
4449 assert!(unsupported_feature_keys("gemma3").is_empty());
4450 assert!(!unsupported_feature_keys("llama").is_empty());
4451 }
4452
4453 /// `grok` applies both softcaps, so neither key refuses it -- while
4454 /// every OTHER gate in that list still does, and every name on the
4455 /// softcap list is an audited row.
4456 ///
4457 /// The first half is what lets a real Grok file load at all:
4458 /// `conversion/grok.py:34` writes `attn_logit_softcapping` for every
4459 /// export. The second half is what keeps the exemption from
4460 /// widening into "softcaps are fine everywhere": `llama` must still
4461 /// refuse them, and the NextN gate must still reach `grok`.
4462 #[test]
4463 fn grok_is_exempt_from_the_softcap_keys_and_nothing_else() {
4464 let keys: Vec<String> = unsupported_feature_keys("grok")
4465 .into_iter()
4466 .map(|(k, _)| k)
4467 .collect();
4468 for softcap in [
4469 "grok.attention.logit_softcapping",
4470 "grok.attn_logit_softcapping",
4471 "grok.final_logit_softcapping",
4472 ] {
4473 assert!(
4474 !keys.iter().any(|k| k == softcap),
4475 "{softcap} must not refuse grok"
4476 );
4477 }
4478 // `nextn_predict_layers` used to be the "non-softcap gate still
4479 // applies" witness here. It is `crate::mtp_blocks` now, keyed by
4480 // which graphs read it, and `grok` is not one: its refusal
4481 // there is `a_non_reader_with_a_nonzero_count_is_refused_and_zero_is_not`.
4482 assert!(
4483 !keys.iter().any(|k| k.ends_with("nextn_predict_layers")),
4484 "nextn_predict_layers is decided by mtp_blocks::trunk_layers, not here: {keys:?}"
4485 );
4486 let llama: Vec<String> = unsupported_feature_keys("llama")
4487 .into_iter()
4488 .map(|(k, _)| k)
4489 .collect();
4490 assert!(llama.iter().any(|k| k == "llama.attn_logit_softcapping"));
4491 for arch in LOGIT_SOFTCAP_ARCHITECTURES {
4492 assert!(
4493 is_audited_generic(arch),
4494 "`{arch}` is on LOGIT_SOFTCAP_ARCHITECTURES without a fixture proving both \
4495 softcaps"
4496 );
4497 }
4498 }
4499
4500 /// The derived scaling refusals for `grok`: the two keys its graph
4501 /// does not read stay refused, the three it reads do not, and the
4502 /// OTHER attention spelling is refused for Granite.
4503 ///
4504 /// This is the half of `AttentionScaleKey` that a hand-written list
4505 /// could have got wrong silently: `attention.output_scale` had no
4506 /// refusal at all before `grok`, so a Granite file declaring it
4507 /// would have loaded and been ignored.
4508 #[test]
4509 fn the_scaling_refusals_for_grok_are_derived_from_its_attention_key() {
4510 let refused = |arch: &str| -> Vec<String> {
4511 unsupported_scaling_keys(arch)
4512 .into_iter()
4513 .map(|(k, _, _)| k)
4514 .collect()
4515 };
4516 let grok = refused("grok");
4517 assert_eq!(
4518 grok,
4519 vec![
4520 "grok.residual_scale".to_string(),
4521 "grok.attention.scale".to_string()
4522 ],
4523 "{grok:?}"
4524 );
4525 let granite = refused("granite");
4526 assert_eq!(granite, vec!["granite.attention.output_scale".to_string()]);
4527 let llama = refused("llama");
4528 assert!(llama.contains(&"llama.attention.output_scale".to_string()));
4529 assert!(llama.contains(&"llama.attention.scale".to_string()));
4530 assert_eq!(llama.len(), 5, "{llama:?}");
4531 }
4532
4533 /// The parallel residual is served now (`crate::parallel_residual`),
4534 /// and every row that was refused for it is audited: what this test
4535 /// pins is that no row is refused for the residual any more.
4536 /// `cohere2moe` was the last to leave (2026-09-14) and is checked
4537 /// with the rest.
4538 ///
4539 /// `minicpm` used to be on this list and is NOT a residual-topology
4540 /// row -- it runs Granite's graph verbatim
4541 /// (`models.h:1594-1601`). It was here because its three hardcoded
4542 /// multipliers are invisible to a key-presence gate the same way a
4543 /// parallel residual is, which made the list's name wrong about one
4544 /// of its own members. `scalar_multipliers::MultiplierDefaults`
4545 /// applies them now and `tests/minicpm_graphs.rs` is the evidence.
4546 #[test]
4547 fn architectures_with_a_different_residual_topology_are_refused() {
4548 // The sequential-residual siblings stay on the generic path --
4549 // this is a named list, not a family-wide ban.
4550 //
4551 // `phimoe`, `starcoder2` and `nemotron` used to be checked here
4552 // too. They left the generic path for an unrelated reason (the
4553 // required bias tensors pinned by `tests/attn_bias.rs`); what
4554 // still has to hold is that neither they nor the archs below
4555 // are refused for a *residual* reason they do not have.
4556 // `nemotron` and `starcoder2` are generic again
4557 // (`BIASED_LAYER_NORM`, `crate::proj_bias`); `phimoe` is not.
4558 for arch in [
4559 "phi3",
4560 "plamo3",
4561 "qwen2",
4562 "llama",
4563 "nemotron",
4564 "orion",
4565 "starcoder2",
4566 "codeshell",
4567 "jais2",
4568 "stablelm",
4569 "gptneox",
4570 "plamo",
4571 "command-r",
4572 "falcon",
4573 "phi2",
4574 "cohere2",
4575 "cohere2moe",
4576 "phimoe",
4577 "gpt2",
4578 "starcoder",
4579 ] {
4580 assert!(
4581 matches!(
4582 resolve_architecture(arch),
4583 Some(ArchPath::GenericGqa { .. })
4584 ),
4585 "{arch} must stay generic"
4586 );
4587 }
4588 }
4589
4590 /// Every architecture appears exactly once, so a refusal added next
4591 /// to an existing entry cannot be shadowed by whichever the lookup
4592 /// happens to find first.
4593 #[test]
4594 fn no_architecture_is_listed_twice() {
4595 let mut seen = std::collections::HashSet::new();
4596 for p in architecture_catalog() {
4597 assert!(seen.insert(p.gguf_name), "{} listed twice", p.gguf_name);
4598 }
4599 }
4600
4601 /// Every key this gate refuses must be a key a converter actually
4602 /// writes, or the gate cannot fire.
4603 ///
4604 /// `unsupported_feature_keys` listed `{arch}.attention.logit_softcapping`.
4605 /// llama.cpp writes `{arch}.attn_logit_softcapping`
4606 /// (`llama-arch.cpp:213`), and no converter emits the first
4607 /// spelling -- so that arm never matched anything, for any non-Gemma
4608 /// architecture, ever. Meanwhile `loader.rs` reads BOTH spellings,
4609 /// so the value was read and applied with the generic formula
4610 /// instead of being refused. For `grok` that is a wrong answer:
4611 /// `grok.cpp` folds the real attention scale into the softcap and
4612 /// passes `kq_scale = 1.0f`.
4613 ///
4614 /// A gate that cannot fire is worse than a missing gate, because it
4615 /// reads as coverage.
4616 #[test]
4617 fn every_refused_key_is_one_a_converter_actually_writes() {
4618 let keys: Vec<String> = unsupported_feature_keys("llama")
4619 .into_iter()
4620 .map(|(k, _)| k)
4621 .collect();
4622
4623 // Transcribed from `llama-arch.cpp`'s LLM_KV_NAMES.
4624 // `llama.attention.sliding_window_pattern` was on this list and
4625 // is deliberately off it: the alternating pattern IS
4626 // implemented (`ModelConfig::layer_sliding_window`, both
4627 // phases), so refusing it was a gate with a false reason that
4628 // also made the loader's own read of the key unreachable. See
4629 // the comment where it used to be. The array-valued case is
4630 // `crate::swa_layers` (`tests/window_array_graphs.rs`).
4631 for real in [
4632 "llama.attn_logit_softcapping",
4633 "llama.final_logit_softcapping",
4634 ] {
4635 assert!(
4636 keys.iter().any(|k| k == real),
4637 "{real} is a key llama.cpp writes and this gate must refuse it; \
4638 gate currently holds {keys:?}"
4639 );
4640 }
4641
4642 // Gemma implements all three, so it must still be exempt --
4643 // otherwise "fix the spelling" would have turned into "refuse
4644 // every Gemma checkpoint".
4645 assert!(
4646 unsupported_feature_keys("gemma2").is_empty(),
4647 "the Gemma family implements softcap and the SWA pattern"
4648 );
4649 // And the pattern key must not come back: a file carrying it
4650 // gets its period READ, which is what llama.cpp does.
4651 assert!(
4652 !keys.iter().any(|k| k.ends_with("sliding_window_pattern")),
4653 "the SWA pattern is implemented; refusing it makes the loader's read of the \
4654 key dead code: {keys:?}"
4655 );
4656 }
4657
4658 /// llama.cpp picks Gemma's `f_attention_scale` on the LAYER COUNT
4659 /// (`gemma3.cpp:20-33`, `gemma2.cpp:19-29`), and every published
4660 /// Gemma size -- not just 27B -- has `n_embd / n_head != head_dim`.
4661 /// An override derived from "the two widths disagree" would fire on
4662 /// all eight rows below and mis-scale six of them, which is why this
4663 /// walks the real sizes rather than asserting the 27B number alone.
4664 ///
4665 /// Shipped broken: `loader.rs` hardcoded `attention_scale = None`
4666 /// beside a comment naming the 27B exception, so Gemma-2-27B scored
4667 /// `sqrt(144/128)` and Gemma-3-27B `sqrt(168/128)` too large on
4668 /// every layer -- a sharper softmax than the trained one, with no
4669 /// error.
4670 #[test]
4671 fn gemma_27b_is_the_only_size_that_overrides_the_kernel_scale() {
4672 /// One published Gemma size, as its GGUF header declares it.
4673 struct Size {
4674 arch: &'static str,
4675 n_layers: usize,
4676 n_embd: usize,
4677 n_head: usize,
4678 /// `attention.key_length`, llama.cpp's `n_embd_head_k()`.
4679 head_dim: usize,
4680 /// The denominator llama.cpp's 27B branch produces, or
4681 /// `None` where it takes the `1/sqrt(n_embd_head_k)` branch.
4682 want_denom: Option<f32>,
4683 }
4684 let size = |arch, n_layers, n_embd, n_head, head_dim, want_denom| Size {
4685 arch,
4686 n_layers,
4687 n_embd,
4688 n_head,
4689 head_dim,
4690 want_denom,
4691 };
4692 let sizes = [
4693 size("gemma2", 26, 2304, 8, 256, None), // Gemma-2-2B
4694 size("gemma2", 42, 3584, 16, 256, None), // Gemma-2-9B
4695 size("gemma2", 46, 4608, 32, 128, Some(144.0)), // Gemma-2-27B
4696 size("gemma3", 18, 640, 4, 256, None), // Gemma-3-270M
4697 size("gemma3", 26, 1152, 4, 256, None), // Gemma-3-1B
4698 size("gemma3", 34, 2560, 8, 256, None), // Gemma-3-4B
4699 size("gemma3", 48, 3840, 16, 256, None), // Gemma-3-12B
4700 size("gemma3", 62, 5376, 32, 128, Some(168.0)), // Gemma-3-27B
4701 ];
4702 for &Size {
4703 arch,
4704 n_layers,
4705 n_embd,
4706 n_head,
4707 head_dim,
4708 want_denom,
4709 } in &sizes
4710 {
4711 // The premise of the whole test: no Gemma size has
4712 // `n_embd / n_head == head_dim`, so "the widths disagree"
4713 // cannot be the selector.
4714 assert_ne!(
4715 n_embd / n_head,
4716 head_dim,
4717 "{arch}/{n_layers}L: if this ever holds, re-read the derivation"
4718 );
4719 let got = attention_scale_override(arch, n_layers, n_embd, n_head, head_dim);
4720 match want_denom {
4721 None => assert_eq!(
4722 got, None,
4723 "{arch}/{n_layers}L takes llama.cpp's 1/sqrt(n_embd_head_k) branch, \
4724 which the attention kernels already apply"
4725 ),
4726 Some(denom) => {
4727 let want = 1.0 / denom.sqrt();
4728 let got = got.unwrap_or_else(|| {
4729 panic!("{arch}/{n_layers}L is llama.cpp's LLM_TYPE_27B; scale must be set")
4730 });
4731 assert!(
4732 (got - want).abs() < 1e-7,
4733 "{arch}/{n_layers}L: want 1/sqrt({denom}) = {want}, got {got}"
4734 );
4735 // The direction of the correction: the kernels' own
4736 // scale is the LARGER one, so the override shrinks
4737 // the scores rather than growing them.
4738 let kernel = 1.0f32 / (head_dim as f32).sqrt();
4739 assert!(
4740 kernel > got,
4741 "{arch}/{n_layers}L: kernel scale {kernel} must exceed {got}"
4742 );
4743 }
4744 }
4745 }
4746 // `gemma-embedding`, `gemma3n` and `gemma4` set
4747 // `f_attention_scale` unconditionally in llama.cpp and have no
4748 // `LLM_TYPE_27B` branch; nothing outside gemma2/gemma3 reaches
4749 // this at all.
4750 for arch in ["gemma-embedding", "gemma3n", "gemma4", "llama", "qwen3"] {
4751 assert_eq!(
4752 attention_scale_override(arch, 62, 5376, 32, 128),
4753 None,
4754 "{arch} has no LLM_TYPE_27B branch in llama.cpp"
4755 );
4756 }
4757 }
4758}