Skip to main content

frink_models/
layer_shapes.rs

1//! Per-layer attention and FFN shapes: llama.cpp's `n_head(il)`,
2//! `n_head_kv(il)` and `n_ff(il)`.
3//!
4//! llama.cpp reads `{arch}.attention.head_count`, `.head_count_kv` and
5//! `{arch}.feed_forward_length` as a scalar OR an `n_layer`-long array
6//! for EVERY architecture (`get_key_or_arr`, `llama-model.cpp:1149-1158`)
7//! and keeps three `std::array<uint32_t, LLAMA_MAX_LAYERS>`
8//! (`llama-hparams.h:83-85`). `LLAMA_LOAD_LOCALS` then hands most
9//! graphs layer 0's value (`n_head()` defaults `il = 0`,
10//! `llama-hparams.h:320-324`), so an array only matters to the graphs
11//! that index it. **Measured, not assumed**: every one of the 140
12//! `src/models/*.cpp` was scanned for `n_head(i)`, `n_head_kv(i)`,
13//! `n_ff(i)`, `n_embd_k_gqa(i)`, `n_embd_v_gqa(i)` and the `_arr`
14//! fields, in both the tensor loader and the graph. The architectures
15//! that honour a per-layer shape in BOTH are [`PER_LAYER_SHAPE_ARCHS`];
16//! `granite.cpp:204` reads `n_head(il)` in its graph but sizes its
17//! tensors from layer 0 (`:68`), so a heterogeneous Granite file fails
18//! in llama.cpp's own loader and is not in the list.
19//!
20//! frink's [`ModelConfig`] carries `n_heads`, `n_kv_heads` and
21//! `moe.expert_ffn_dim` as scalars, and every host body read them once
22//! above its layer loop. This module is the seam that makes them
23//! per-layer, with the uniform case being the one where every layer
24//! agrees: [`ModelConfig::layer_shape`] is the ONLY way a layer body
25//! learns its head counts, and the scalars are documented as the
26//! WIDEST layer's, which is what a memory budget needs and what no
27//! per-layer computation may read.
28//!
29//! Where disagreement is made to fail rather than drift:
30//!
31//! - [`ModelConfig::new_kv_caches`] sizes each layer's cache from its
32//!   own shape, and `KvCache::push` asserts the row width, so a cache
33//!   built from the scalar for a narrower layer panics on the first
34//!   token rather than storing a misaligned history.
35//! - `Decoder::metal_can_serve_model` refuses every fused Metal launch
36//!   for a non-uniform model: those launches take ONE `n_heads`
37//!   argument and one `MetalKvBuffers` geometry.
38//! - [`AttnShape`] is an enum, so a layer with no attention or with
39//!   deci's `wo`-only "linear attention" cannot be spelled as
40//!   `n_heads = 0` and fall through a `0..n_heads` loop doing nothing.
41//!
42//! What it does NOT close, and says so: `nanbeige` rewrites the arrays
43//! to walk its physical layers more than once (`nanbeige.cpp:13-31`);
44//! `mimo2` and `step35` read per-layer heads AND something else (both closed since, on
45//! the seams their entries name)
46//! (a V head width differing from K's; per-layer clamp arrays and a
47//! half-width rotary -- their window arrays and NextN blocks are
48//! `crate::swa_layers` and `crate::mtp_blocks` now); `laguna` closed the day after, when its other thing (the
49//! gated attention, `crate::attn_gate`) landed; the hybrid recurrent rows (`jamba`, `lfm2`, `nemotron-h`,
50//! `plamo2`, `granite-hybrid`, `kimi-linear`) use `n_head_kv(i) == 0`
51//! to mean "this layer is recurrent", a different graph entirely.
52
53use crate::config::ModelConfig;
54use crate::decoder::AttnWeights;
55use crate::loader::{load_weight_matrix, LoadError};
56use crate::norm::NormOp;
57use crate::norm_sites::NormSites;
58use frink_core::cache::{KvCache, PagedKvStore, SharedPagedKv};
59use frink_core::{Tensor, WeightMatrix};
60use frink_gguf::{GgufValue, TensorSource};
61use frink_moe::ExpertWeights;
62
63/// Architectures whose llama.cpp tensor loader AND graph both index the
64/// per-layer arrays, with the lines. Anything else gets layer 0 from
65/// `LLAMA_LOAD_LOCALS` upstream, so a file whose layers disagree cannot
66/// load there either, and frink refuses it by name rather than picking
67/// a layer to believe.
68///
69/// Rows marked `generic` run on frink's generic GQA path and are what
70/// this seam serves; the rest are listed so the reach of the seam is
71/// recorded where the next person will look, and each names what else
72/// it needs.
73pub const PER_LAYER_SHAPE_ARCHS: &[(&str, &str)] = &[
74    (
75        "deci",
76        "generic. deci.cpp:30-34 (loader) and :103-105 (graph): all three per layer, with \
77         n_head == 0 an attention-free layer, n_head_kv == 0 a wo-only layer and n_ff == 0 \
78         an FFN-free layer",
79    ),
80    (
81        "openelm",
82        "generic. openelm.cpp:26-28 (loader) and :67-69 (graph): all three per layer, sizing \
83         one fused wqkv per layer",
84    ),
85    (
86        "plamo3",
87        "generic. plamo3.cpp:39-44 (loader) and :110-111 (graph); no published PLaMo-3 \
88         export writes an array (conversion/plamo.py:27-30 writes scalars), so the seam is \
89         latent there",
90    ),
91    (
92        "laguna",
93        "generic. laguna.cpp:87-88 (loader) and :176-177 (graph) read n_head(i) per layer; \
94         KV heads uniform (:86). Closed with the gated attention (`crate::attn_gate`); the \
95         second rotary width at :50 is `ModelConfig::rope_dim_swa` (`crate::swa_geometry`)",
96    ),
97    (
98        "mimo2",
99        "mimo2.cpp:47-49,111-112 read heads per layer (`swa_num_key_value_heads` on the \
100         sliding layers, the converter's array). Closed with the split K/V head width \
101         (`crate::kv_head_dims`, the V width :47-48 sizes apart from K's) and the value \
102         scale (`crate::attn_value_scale`, :16,181); the sinks at :58 are \
103         `AttnWeights::sinks`, the is_swa array at :12 is `crate::swa_layers` and the NEXTN \
104         blocks at :19 are `crate::mtp_blocks`",
105    ),
106    (
107        "step35",
108        "generic. step35.cpp:76-78,208-209 (loader and graph) read heads and KV widths per \
109         layer. Closed with the per-layer activation seam (`crate::act_layers`, the clamp \
110         arrays at :28-29) and the two-valued rotary width (`crate::swa_geometry`, :9); the \
111         gate at :96 is `crate::attn_gate`, the is_swa array at :26 `crate::swa_layers`, \
112         the NEXTN blocks at :32 `crate::mtp_blocks`",
113    ),
114    (
115        "spark2_5",
116        "generic. spark2-5.cpp:33-37 (loader) and :76-77 (graph) read n_head(i) and \
117         n_head_kv(i) per layer, sizing the per-head attention gate (:41) by each layer's \
118         own count. Landed upstream after the 2026-08-04 pin and closed on 2026-09-19 with \
119         one `crate::attn_gate` row",
120    ),
121    (
122        "maple",
123        "generic. maple.cpp:6 reads `expert_feed_forward_length` as an ARRAY at \
124         n_layer_all length; the tensors are sized from n_ff_exp() (layer 0) at :27, so \
125         the array must LOAD even where every entry agrees. Landed upstream after the \
126         2026-08-04 pin and closed on 2026-09-19 with one `crate::rope_layers` row",
127    ),
128    (
129        "nanbeige",
130        "nanbeige.cpp:24-26 copies each physical layer's arrays to every logical slot; \
131         `LayerShapes::replicated` does the same and `crate::layer_loops` is the seam the row \
132         closed on",
133    ),
134    (
135        "gemma4",
136        "dedicated engine. gemma4.cpp:64-67,91 (loader) and :179-184 (graph)",
137    ),
138    (
139        "gemma4-assistant",
140        "dedicated engine. gemma4-assistant.cpp:53-55 (loader) and :134-138 (graph)",
141    ),
142    (
143        "jamba",
144        "generic. jamba.cpp:8-10 (hparams), :37-58 (loader) and :90-92 (graph): n_head_kv(i) \
145         == 0 marks a Mamba-1 layer (`crate::mamba1`), served since 2026-09-14",
146    ),
147    (
148        "lfm2",
149        "hybrid: n_head_kv(il) == 0 marks a recurrent layer (lfm2.cpp:10,72,130-132)",
150    ),
151    (
152        "lfm2moe",
153        "hybrid: n_head_kv(il) == 0 marks a recurrent layer (lfm2moe.cpp:13,63)",
154    ),
155    (
156        "nemotron_h",
157        "generic. nemotron-h.cpp:9-11 (hparams), :53-98 (loader) and :146-153 (graph): \
158         n_head_kv(i) == 0 && n_ff(i) == 0 marks a Mamba-2 layer, n_ff(i) == 0 alone an \
159         attention layer, the rest an FFN-only layer; one block per layer \
160         (`BLOCK_WITHOUT_FFN_KEEPS_ITS_OUTPUT`), served since 2026-09-14",
161    ),
162    (
163        "nemotron_h_moe",
164        "nemotron-h.cpp:9-11, the same rule; its latent ungated ReLU-squared MoE (:79-90) is \
165         not served yet",
166    ),
167    (
168        "plamo2",
169        "hybrid: n_head_kv(i) == 0 marks a recurrent layer (plamo2.cpp:19,82-84,218-219)",
170    ),
171    (
172        "granitehybrid",
173        "generic. granite-hybrid.cpp:17-19 (hparams), :58-77 (loader) and :137-140 (graph): \
174         n_head_kv(i) == 0 marks a Mamba-2 layer (`crate::mamba2`), served since 2026-09-14",
175    ),
176    (
177        "granite-hybrid",
178        "generic. the frink alias of `granitehybrid` (granite-hybrid.cpp:17-19), the same rule",
179    ),
180    (
181        "kimi-linear",
182        "hybrid: n_head_kv(i) == 0 marks a KDA layer (kimi-linear.cpp:18)",
183    ),
184];
185
186/// True when llama.cpp itself honours a per-layer shape for `arch`.
187pub fn per_layer_shapes_read_by_llama_cpp(arch: &str) -> bool {
188    PER_LAYER_SHAPE_ARCHS.iter().any(|(a, _)| *a == arch)
189}
190
191/// What one layer's attention block is.
192///
193/// An enum rather than two counts, because two of deci's three layer
194/// kinds are spelled with a zero count upstream and a zero count is
195/// exactly what a `for h in 0..n_heads` loop silently accepts.
196#[derive(Debug, Clone, Copy, PartialEq, Eq)]
197pub enum AttnShape {
198    /// Grouped-query attention with this many query and KV heads.
199    Gqa { n_heads: usize, n_kv_heads: usize },
200    /// deci's "linear attention" (`deci.cpp:36-40`, `:115-118`):
201    /// `n_head > 0 && n_head_kv == 0`. The block is `attn_norm` then
202    /// `wo` alone -- `{n_embd, n_embd}`, no Q/K/V, no RoPE, nothing
203    /// cached -- and its output joins the residual like any attention.
204    Linear,
205    /// deci's attention-free layer (`deci.cpp:107-109`, `:150-153`):
206    /// `n_head == 0`. No norm, no projection; the residual passes
207    /// straight into the FFN's input.
208    Absent,
209    /// LFM2's short convolution (`lfm2.cpp:9-11`, `:139-189`): the same
210    /// `n_head > 0 && n_head_kv == 0` counts as [`AttnShape::Linear`],
211    /// meaning a different block, decided by architecture
212    /// ([`ZeroKvLayer`]). `attn_norm`, then `crate::shortconv`, then
213    /// the residual add; the layer's cache holds its conv inputs
214    /// ([`Self::cache_geometry`]).
215    ShortConv,
216    /// A Mamba-2 block (`build_mamba2_layer`, `mamba-base.cpp:149-288`;
217    /// `granite-hybrid.cpp:163`): the same two counts again, decided
218    /// by architecture ([`ZeroKvLayer`]). `attn_norm`, then
219    /// `crate::mamba2`, then the residual add; the layer's cache holds
220    /// no rows and carries a `RecurrentState` instead.
221    Mamba2,
222    /// A Mamba-1 block (`build_mamba_layer`, `mamba-base.cpp:4-148`;
223    /// `jamba.cpp:128`, `mamba.cpp:106`): as [`AttnShape::Mamba2`] with
224    /// `crate::mamba1`'s block.
225    Mamba1,
226    /// PLaMo-2's block (`plamo2.cpp:218-343`; `crate::plamo2_ssm`): as
227    /// [`AttnShape::Mamba2`] with that block.
228    Plamo2Ssm,
229    /// The gated delta net (`qwen35.cpp:236-317`; `crate::gdn`): as
230    /// [`AttnShape::Mamba2`] with that block. Decided by
231    /// `crate::gdn::recurrent_layers`, not by the head counts, which
232    /// are uniform on such a file.
233    Gdn,
234    /// MiniMax-01's lightning attention (`minimax-01.cpp:293-420`;
235    /// `crate::lightning`): as [`AttnShape::Gdn`] with that block,
236    /// decided by the same mask read from the same two keys.
237    Lightning,
238}
239
240/// Architectures whose graph ADDS a block's output to the residual on a
241/// layer with `feed_forward_length 0`, so "attention with no FFN" and
242/// "Mamba-2 with no FFN" are layers rather than a defect.
243///
244/// `nemotron-h.cpp:157` adds `cur` for every kind of layer. deci is the
245/// other reading: `deci.cpp:147-149` `continue`s BEFORE the add, so its
246/// attention output is DISCARDED, and `LayerShapes::resolve` refuses
247/// that combination for every architecture not listed here rather than
248/// pin a dropped branch as the reference.
249pub const BLOCK_WITHOUT_FFN_KEEPS_ITS_OUTPUT: &[&str] =
250    &["nemotron_h", "nemotron_h_moe", "mamba", "mamba2"];
251
252/// Architectures with NO attention anywhere: every layer is the named
253/// block and nothing else (`mamba.cpp:73-88`, `mamba2.cpp`; the
254/// converter writes `head_count 0` and `feed_forward_length 0`,
255/// `conversion/mamba.py:155-156`). `LayerShapes::resolve` builds every
256/// layer from this table, because the counts alone -- `(0, 0)` -- are
257/// deci's attention-free layer on every other architecture.
258pub const PURE_RECURRENT: &[(&str, ZeroKvLayer)] = &[
259    ("mamba", ZeroKvLayer::Mamba1),
260    ("mamba2", ZeroKvLayer::Mamba2),
261];
262
263/// The block every layer of a pure recurrent model is, or `None`.
264pub fn pure_recurrent_block(arch: &str) -> Option<ZeroKvLayer> {
265    PURE_RECURRENT
266        .iter()
267        .find(|(a, _)| *a == arch)
268        .map(|(_, k)| *k)
269}
270
271/// What `head_count_kv == 0` with `head_count > 0` MEANS for an
272/// architecture, because two graphs spell two different blocks with the
273/// same two counts and the counts alone cannot tell them apart.
274#[derive(Debug, Clone, Copy, PartialEq, Eq)]
275pub enum ZeroKvLayer {
276    /// `deci.cpp:115-118`: `attn_norm` then `wo`.
277    Linear,
278    /// `lfm2.cpp:197`: the short convolution (`crate::shortconv`).
279    ShortConv,
280    /// `granite-hybrid.cpp:163`: the Mamba-2 block (`crate::mamba2`).
281    Mamba2,
282    /// `jamba.cpp:128`: the Mamba-1 block (`crate::mamba1`).
283    Mamba1,
284    /// `plamo2.cpp:218`: PLaMo-2's own block (`crate::plamo2_ssm`).
285    Plamo2,
286    /// `nemotron-h.cpp:9-11`: the Mamba-2 block when the layer's FFN
287    /// width is ALSO zero, and an FFN-only layer (no attention block at
288    /// all, [`AttnShape::Absent`]) when it is not -- every Nemotron-H
289    /// layer is one block, and `feed_forward_length` is the second
290    /// array that says which.
291    Mamba2UnlessFfn,
292    /// A recurrent block frink has no body for; the reason names it.
293    Unserved(&'static str),
294}
295
296impl ZeroKvLayer {
297    /// The table. Every architecture in [`PER_LAYER_SHAPE_ARCHS`] whose
298    /// graph reads `n_head_kv(il) == 0` as a layer kind has a row here;
299    /// anything else that reaches a zero is `Linear`, the reading the
300    /// generic path had before the table existed, which only deci's
301    /// converter writes.
302    pub fn for_arch(arch: &str) -> Self {
303        if crate::shortconv::is_shortconv_architecture(arch) {
304            return ZeroKvLayer::ShortConv;
305        }
306        match arch {
307            // `granite-hybrid.cpp:17-19,163`: Mamba-2 where the KV count
308            // is zero, attention elsewhere, an FFN on every layer.
309            "granitehybrid" | "granite-hybrid" => ZeroKvLayer::Mamba2,
310            // `jamba.cpp:8-10,128`: `build_mamba_layer`, Mamba-1.
311            "jamba" => ZeroKvLayer::Mamba1,
312            // `falcon-h1.cpp:161` runs the Mamba-2 block IN PARALLEL with
313            // attention on every layer (`crate::mamba2::
314            // PARALLEL_WITH_ATTENTION`), so its KV count is never zero;
315            // a zero here is not that graph.
316            "falcon-h1" => ZeroKvLayer::Unserved(
317                "no falcon-h1 layer has a zero KV count: falcon-h1.cpp:137-161 runs attention \
318                 AND the Mamba-2 block on every layer (`ModelConfig::parallel_ssm`)",
319            ),
320            // `nemotron-h.cpp:9-11,143-152`: a layer is ONE of Mamba-2,
321            // attention, or FFN, with one residual add. On the generic
322            // layer that is a block with `ffn_dim 0`
323            // ([`BLOCK_WITHOUT_FFN_KEEPS_ITS_OUTPUT`]) or an FFN with no
324            // block.
325            "nemotron_h" | "nemotron_h_moe" => ZeroKvLayer::Mamba2UnlessFfn,
326            // `plamo2.cpp:19,142-146`: PLaMo-2's block where the KV
327            // count is zero, attention elsewhere; served since 2026-09-18
328            // by its own spelling (`crate::plamo2_ssm`).
329            "plamo2" => ZeroKvLayer::Plamo2,
330            "kimi-linear" => ZeroKvLayer::Unserved(
331                "a KDA block (kimi-linear.cpp:18), served by `crate::kimi_decoder` and not \
332                 the generic path",
333            ),
334            _ => ZeroKvLayer::Linear,
335        }
336    }
337}
338
339impl AttnShape {
340    /// llama.cpp's three-way branch on the two counts (`deci.cpp:107-137`).
341    ///
342    /// `n_head == 0` with `n_head_kv > 0` is refused: the graph would
343    /// take the attention-free branch while the loader (`:36-45`)
344    /// would create a zero-wide Q, which is not a shape any converter
345    /// writes.
346    ///
347    /// `ffn_dim` is this layer's FFN width, which one rule
348    /// ([`ZeroKvLayer::Mamba2UnlessFfn`]) reads.
349    pub fn from_counts(
350        n_heads: usize,
351        n_kv_heads: usize,
352        ffn_dim: usize,
353        zero_kv: ZeroKvLayer,
354    ) -> Result<Self, String> {
355        match (n_heads, n_kv_heads) {
356            // `plamo2.cpp:19` reads ONLY `n_head_kv(il)`, and
357            // `conversion/plamo.py:87-88` writes BOTH arrays as 0 on an
358            // SSM layer, so on that architecture the pair means the
359            // block and not deci's attention-free layer.
360            (0, 0) if zero_kv == ZeroKvLayer::Plamo2 => Ok(AttnShape::Plamo2Ssm),
361            (0, 0) => Ok(AttnShape::Absent),
362            (0, kv) => Err(format!(
363                "head_count 0 with head_count_kv {kv}: deci.cpp:107 would skip attention while \
364                 :44 sizes a zero-wide Q projection"
365            )),
366            (_, 0) => match zero_kv {
367                ZeroKvLayer::Linear => Ok(AttnShape::Linear),
368                ZeroKvLayer::ShortConv => Ok(AttnShape::ShortConv),
369                ZeroKvLayer::Mamba2 => Ok(AttnShape::Mamba2),
370                ZeroKvLayer::Mamba1 => Ok(AttnShape::Mamba1),
371                ZeroKvLayer::Plamo2 => Ok(AttnShape::Plamo2Ssm),
372                // nemotron-h.cpp:9-11: `n_head_kv == 0 && n_ff == 0`.
373                ZeroKvLayer::Mamba2UnlessFfn if ffn_dim == 0 => Ok(AttnShape::Mamba2),
374                // :152-153: the FFN alone, under `attn_norm` (:145).
375                ZeroKvLayer::Mamba2UnlessFfn => Ok(AttnShape::Absent),
376                ZeroKvLayer::Unserved(what) => Err(format!(
377                    "head_count_kv 0 marks {what}; `layer_shapes::ZeroKvLayer` is the table"
378                )),
379            },
380            (q, kv) if q % kv != 0 => Err(format!(
381                "head_count {q} is not a multiple of head_count_kv {kv}"
382            )),
383            (n_heads, n_kv_heads) => Ok(AttnShape::Gqa {
384                n_heads,
385                n_kv_heads,
386            }),
387        }
388    }
389
390    /// KV heads this layer caches: zero for the two shapes that write
391    /// no history.
392    pub fn n_kv_heads(self) -> usize {
393        match self {
394            AttnShape::Gqa { n_kv_heads, .. } => n_kv_heads,
395            AttnShape::Linear
396            | AttnShape::Absent
397            | AttnShape::ShortConv
398            | AttnShape::Mamba2
399            | AttnShape::Mamba1
400            | AttnShape::Plamo2Ssm
401            | AttnShape::Gdn
402            | AttnShape::Lightning => 0,
403        }
404    }
405
406    /// Query heads, zero where there are none.
407    pub fn n_heads(self) -> usize {
408        match self {
409            AttnShape::Gqa { n_heads, .. } => n_heads,
410            AttnShape::Linear
411            | AttnShape::Absent
412            | AttnShape::ShortConv
413            | AttnShape::Mamba2
414            | AttnShape::Mamba1
415            | AttnShape::Plamo2Ssm
416            | AttnShape::Gdn
417            | AttnShape::Lightning => 0,
418        }
419    }
420
421    /// True for a block whose state between tokens is a
422    /// `RecurrentState` rather than rows (`crate::mamba2`).
423    pub fn is_recurrent(self) -> bool {
424        matches!(
425            self,
426            AttnShape::Mamba2
427                | AttnShape::Mamba1
428                | AttnShape::Plamo2Ssm
429                | AttnShape::Gdn
430                | AttnShape::Lightning
431        )
432    }
433
434    /// The layer's cache as `(n_kv_heads, k_head_dim, v_head_dim)`, the
435    /// three numbers every `KvCache` / `PagedKvStore` constructor takes.
436    ///
437    /// A GQA layer's is its head geometry; the two attention-less
438    /// shapes and the Mamba-2 block write no history and get an empty
439    /// cache (the Mamba-2 block keeps its state beside it and pushes
440    /// EMPTY rows so the cache still counts positions); a short-conv
441    /// layer keeps its conv inputs as ONE row of `hidden_dim` per token
442    /// with no V (`crate::shortconv`). ONE function, because
443    /// `ModelConfig::new_kv_caches` and its three siblings each build
444    /// the caches and a fourth reading of the shape would be a fourth
445    /// place to disagree.
446    pub fn cache_geometry(
447        self,
448        head_dim: usize,
449        v_head_dim: usize,
450        hidden_dim: usize,
451    ) -> (usize, usize, usize) {
452        match self {
453            AttnShape::Gqa { n_kv_heads, .. } => (n_kv_heads, head_dim, v_head_dim),
454            AttnShape::Linear
455            | AttnShape::Absent
456            | AttnShape::Mamba2
457            | AttnShape::Mamba1
458            | AttnShape::Plamo2Ssm
459            | AttnShape::Gdn
460            | AttnShape::Lightning => (0, head_dim, v_head_dim),
461            AttnShape::ShortConv => (1, hidden_dim, 0),
462        }
463    }
464}
465
466/// One layer's shape.
467#[derive(Debug, Clone, Copy, PartialEq, Eq)]
468pub struct LayerShape {
469    pub attention: AttnShape,
470    /// The dense FFN width, `n_ff(il)`. Zero is deci's FFN-free layer
471    /// (`deci.cpp:147-149`): no `ffn_norm`, no gate/up/down.
472    pub ffn_dim: usize,
473}
474
475/// Every layer's shape, or the statement that they all agree.
476///
477/// `Uniform` is not `PerLayer(vec![same; n])`: the fused Metal stacks,
478/// the CUDA resident KV and the slot-file format each hold ONE
479/// geometry, and "is this model uniform" is a question they ask
480/// through [`Self::is_uniform`] rather than by comparing entries.
481#[derive(Debug, Clone, PartialEq, Eq, Default)]
482pub enum LayerShapes {
483    /// Every layer is `ModelConfig::{n_heads, n_kv_heads,
484    /// moe.expert_ffn_dim}`.
485    #[default]
486    Uniform,
487    /// One entry per layer, at least two of which differ.
488    PerLayer(Vec<LayerShape>),
489}
490
491impl LayerShapes {
492    pub fn is_uniform(&self) -> bool {
493        matches!(self, LayerShapes::Uniform)
494    }
495
496    /// The same shapes for `n_loops` passes over the layers, as
497    /// `nanbeige.cpp:24-26` copies each physical layer's arrays to every
498    /// logical slot (`crate::layer_loops`). Uniform stays uniform.
499    pub fn replicated(self, n_loops: usize) -> Self {
500        match self {
501            LayerShapes::PerLayer(v) if n_loops > 1 => {
502                LayerShapes::PerLayer(v.iter().copied().cycle().take(v.len() * n_loops).collect())
503            }
504            other => other,
505        }
506    }
507
508    /// Builds the table from the three per-layer arrays, collapsing to
509    /// `Uniform` when nothing varies and refusing a varying file for an
510    /// architecture llama.cpp itself reads at layer 0.
511    ///
512    /// `ffn` may be absent (a MoE file that declares only
513    /// `expert_feed_forward_length`); then every layer takes
514    /// `expert_ffn_dim`.
515    ///
516    /// `recurrent` is `crate::gdn::recurrent_layers`' answer: the layers
517    /// that run a recurrent block AND which block, on an architecture
518    /// whose head counts are uniform and say nothing about either.
519    pub fn resolve(
520        arch: &str,
521        heads: &[u64],
522        kv_heads: &[u64],
523        ffn: Option<&[u64]>,
524        expert_ffn_dim: usize,
525        recurrent: Option<&crate::gdn::RecurrentMask>,
526    ) -> Result<Self, LoadError> {
527        let n = heads.len();
528        assert_eq!(kv_heads.len(), n);
529        if let Some(recurrent) = recurrent {
530            assert_eq!(recurrent.layers.len(), n);
531            let zero_kv = ZeroKvLayer::for_arch(arch);
532            let mut shapes = Vec::with_capacity(n);
533            for il in 0..n {
534                let ffn_dim = ffn.map_or(expert_ffn_dim, |f| f[il] as usize);
535                let attention = if recurrent.layers[il] {
536                    recurrent.block
537                } else {
538                    AttnShape::from_counts(
539                        heads[il] as usize,
540                        kv_heads[il] as usize,
541                        ffn_dim,
542                        zero_kv,
543                    )
544                    .map_err(|why| {
545                        LoadError::UnsupportedFeature(arch.to_string(), format!("blk.{il}: {why}"))
546                    })?
547                };
548                shapes.push(LayerShape { attention, ffn_dim });
549            }
550            return Ok(LayerShapes::PerLayer(shapes));
551        }
552        // A pure recurrent model: every layer the one block, no FFN
553        // (`PURE_RECURRENT`). Its arrays are uniform zeros, which would
554        // otherwise read as a zero-head GQA model.
555        if let Some(kind) = pure_recurrent_block(arch) {
556            let shape = AttnShape::from_counts(1, 0, 0, kind)
557                .map_err(|why| LoadError::UnsupportedFeature(arch.to_string(), why))?;
558            for il in 0..n {
559                let ffn_dim = ffn.map_or(0, |f| f[il] as usize);
560                if heads[il] != 0 || kv_heads[il] != 0 || ffn_dim != 0 {
561                    return Err(LoadError::UnsupportedFeature(
562                        arch.to_string(),
563                        format!(
564                            "blk.{il}: head_count {} / head_count_kv {} / feed_forward_length \
565                             {ffn_dim} on a pure recurrent architecture, whose converter writes \
566                             0 for all three (conversion/mamba.py:155-156) and whose graph has no \
567                             attention and no FFN (mamba.cpp:73-88)",
568                            heads[il], kv_heads[il]
569                        ),
570                    ));
571                }
572            }
573            return Ok(LayerShapes::PerLayer(vec![
574                LayerShape {
575                    attention: shape,
576                    ffn_dim: 0
577                };
578                n
579            ]));
580        }
581        let uniform = heads.windows(2).all(|w| w[0] == w[1])
582            && kv_heads.windows(2).all(|w| w[0] == w[1])
583            && ffn.is_none_or(|f| f.windows(2).all(|w| w[0] == w[1]));
584        if uniform {
585            return Ok(LayerShapes::Uniform);
586        }
587        if !per_layer_shapes_read_by_llama_cpp(arch) {
588            return Err(LoadError::UnsupportedFeature(
589                arch.to_string(),
590                format!(
591                    "per-layer head_count / head_count_kv / feed_forward_length arrays whose \
592                     entries differ (heads {heads:?}, kv {kv_heads:?}, ff {ffn:?}). llama.cpp \
593                     reads these arrays for every architecture (llama-model.cpp:1149-1158) but \
594                     this one's graph takes layer 0 through LLAMA_LOAD_LOCALS \
595                     (llama-model.h:760-767), so such a file cannot load there either; \
596                     `layer_shapes::PER_LAYER_SHAPE_ARCHS` lists the ones that index per layer"
597                ),
598            ));
599        }
600        let mut shapes = Vec::with_capacity(n);
601        let zero_kv = ZeroKvLayer::for_arch(arch);
602        let keeps_output = BLOCK_WITHOUT_FFN_KEEPS_ITS_OUTPUT.contains(&arch);
603        for il in 0..n {
604            let ffn_dim = ffn.map_or(expert_ffn_dim, |f| f[il] as usize);
605            let attention =
606                AttnShape::from_counts(heads[il] as usize, kv_heads[il] as usize, ffn_dim, zero_kv)
607                    .map_err(|why| {
608                        LoadError::UnsupportedFeature(arch.to_string(), format!("blk.{il}: {why}"))
609                    })?;
610            if ffn_dim == 0 && attention != AttnShape::Absent && !keeps_output {
611                // deci.cpp:147-149 `continue`s BEFORE the residual add
612                // at :150-153, so the attention output computed at
613                // :115-137 is discarded and `inpL` is left untouched.
614                // That is llama.cpp's graph and it is almost certainly
615                // not the model's (HF's DeciLM adds the attention
616                // residual before asking whether the FFN is a no-op).
617                // frink will not pin a dropped branch as the golden
618                // answer, so the combination is refused by name; the
619                // attention-free FFN-free layer, where both agree the
620                // layer is the identity, is admitted.
621                return Err(LoadError::UnsupportedFeature(
622                    arch.to_string(),
623                    format!(
624                        "blk.{il}: feed_forward_length 0 on a layer WITH attention \
625                         (head_count {}). deci.cpp:147-149 `continue`s before the residual \
626                         add at :150-153, discarding the attention output that :115-137 \
627                         computed, and frink will not reproduce a dropped branch as the \
628                         reference. An FFN-free layer with head_count 0 is supported",
629                        heads[il]
630                    ),
631                ));
632            }
633            shapes.push(LayerShape { attention, ffn_dim });
634        }
635        Ok(LayerShapes::PerLayer(shapes))
636    }
637}
638
639/// llama.cpp's `get_key_or_arr` (`llama-model-loader.cpp:446-470`): a
640/// scalar is broadcast to every layer, an array must be exactly
641/// `n_layers` long, and an absent key is `None`.
642///
643/// `GgufValue::as_u64` returns `None` for an array, which is how
644/// `openelm` used to die on a missing-hparam error for a key its file
645/// carries; this is the read that sees both spellings.
646/// [`read_u64_per_layer`] for a file that may carry NextN/MTP blocks:
647/// the array is length-checked against `block_count`, which is what
648/// llama.cpp passes (`llama-model.cpp:1148-1156` read the three shape
649/// arrays with `hparams.n_layer()` BEFORE `load_arch_hparams` at `:1233`
650/// has read `nextn_predict_layers`, so `n_layer()` is still
651/// `n_layer_all`, and `conversion/mimo.py:146-150` writes the arrays at
652/// that length with the MTP entries appended), and only the trunk's
653/// entries are returned.
654///
655/// The loader reads every per-layer shape through this and never
656/// through the raw function, so a call site cannot hand the trunk count
657/// to the length check by mistake.
658pub fn read_u64_trunk_layers(
659    file: &impl TensorSource,
660    key: &str,
661    trunk: &crate::mtp_blocks::TrunkLayers,
662) -> Result<Option<Vec<u64>>, LoadError> {
663    Ok(
664        read_u64_per_layer(file, key, trunk.block_count)?.map(|mut v| {
665            v.truncate(trunk.n_layers);
666            v
667        }),
668    )
669}
670
671pub fn read_u64_per_layer(
672    file: &impl TensorSource,
673    key: &str,
674    n_layers: usize,
675) -> Result<Option<Vec<u64>>, LoadError> {
676    let Some(value) = file.metadata(key) else {
677        return Ok(None);
678    };
679    match value {
680        GgufValue::Array(items) => {
681            if items.len() != n_layers {
682                return Err(LoadError::UnsupportedFeature(
683                    key.to_string(),
684                    format!(
685                        "array of {} entries for {n_layers} layers; llama.cpp refuses this too \
686                         (`key has wrong array length`, llama-model-loader.cpp:464-465)",
687                        items.len()
688                    ),
689                ));
690            }
691            let mut out = Vec::with_capacity(n_layers);
692            for (il, item) in items.iter().enumerate() {
693                out.push(item.as_u64().ok_or_else(|| {
694                    LoadError::UnsupportedFeature(
695                        key.to_string(),
696                        format!("entry {il} is not an unsigned integer: {item:?}"),
697                    )
698                })?);
699            }
700            Ok(Some(out))
701        }
702        scalar => scalar
703            .as_u64()
704            .map(|v| Some(vec![v; n_layers]))
705            .ok_or_else(|| LoadError::MissingHparam(key.to_string())),
706    }
707}
708
709/// A projection with no rows: the placeholder held by a layer that has
710/// no such projection, so that `AttnWeights` / `ExpertWeights` -- which
711/// have thirty construction sites and no `Option` in them -- can carry
712/// deci's two attention-less shapes without a new struct.
713///
714/// Never applied: every host body branches on [`AttnShape`] /
715/// `LayerShape::ffn_dim` before it reaches a projection. If one did
716/// not, `apply` of a zero-row matrix is an empty vector, and every
717/// kernel downstream of an empty Q panics on its own arithmetic rather
718/// than answering.
719fn no_rows(cols: usize) -> WeightMatrix {
720    WeightMatrix::F32(Tensor::new(Vec::new(), vec![0, cols]))
721}
722
723/// The attention weights of a [`AttnShape::Linear`], [`AttnShape::
724/// Absent`] or [`AttnShape::ShortConv`] layer.
725///
726/// Linear (`deci.cpp:36-40`): `attn_norm` and a `{n_embd, n_embd}`
727/// `attn_output`, nothing else. Absent (`:107-109`): nothing at all --
728/// no norm tensor exists for the layer, and `NormOp::None` is what the
729/// body applies before a block it then skips. ShortConv (`lfm2.cpp:
730/// 70,80-82`): `attn_norm` and the three `shortconv.*` tensors
731/// (`crate::shortconv`).
732pub(crate) fn load_non_gqa_attention(
733    shape: AttnShape,
734    file: &impl TensorSource,
735    arch: &str,
736    layer: usize,
737    norm_sites: &NormSites,
738    config: &ModelConfig,
739) -> Result<AttnWeights, LoadError> {
740    let hidden_dim = config.hidden_dim;
741    let mut shortconv = None;
742    let mut ssm = None;
743    let (norm_weight, o_proj) = match shape {
744        AttnShape::Linear => (
745            norm_sites.load_pre_norm(norm_sites.attn, file, Some(layer))?,
746            load_weight_matrix(file, &format!("blk.{layer}.attn_output.weight"))?,
747        ),
748        AttnShape::Absent => (NormOp::None, no_rows(0)),
749        AttnShape::ShortConv => {
750            shortconv = Some(crate::shortconv::ShortConv::load(
751                file, arch, layer, hidden_dim,
752            )?);
753            (
754                norm_sites.load_pre_norm(norm_sites.attn, file, Some(layer))?,
755                no_rows(0),
756            )
757        }
758        AttnShape::Mamba2 => {
759            ssm = Some(crate::ssm_block::SsmBlock::Mamba2(
760                crate::mamba2::Mamba2::load(file, arch, layer, hidden_dim)?,
761            ));
762            (
763                norm_sites.load_pre_norm(norm_sites.attn, file, Some(layer))?,
764                no_rows(0),
765            )
766        }
767        AttnShape::Mamba1 => {
768            ssm = Some(crate::ssm_block::SsmBlock::Mamba1(
769                crate::mamba1::Mamba1::load(file, arch, layer, hidden_dim)?,
770            ));
771            (
772                norm_sites.load_pre_norm(norm_sites.attn, file, Some(layer))?,
773                no_rows(0),
774            )
775        }
776        AttnShape::Plamo2Ssm => {
777            ssm = Some(crate::ssm_block::SsmBlock::Plamo2(
778                crate::plamo2_ssm::Plamo2Ssm::load(file, arch, layer, hidden_dim)?,
779            ));
780            (
781                norm_sites.load_pre_norm(norm_sites.attn, file, Some(layer))?,
782                no_rows(0),
783            )
784        }
785        AttnShape::Gdn => {
786            ssm = Some(crate::ssm_block::SsmBlock::Gdn(crate::gdn::Gdn::load(
787                file, arch, layer, hidden_dim,
788            )?));
789            (
790                norm_sites.load_pre_norm(norm_sites.attn, file, Some(layer))?,
791                no_rows(0),
792            )
793        }
794        AttnShape::Lightning => {
795            ssm = Some(crate::ssm_block::SsmBlock::Lightning(
796                crate::lightning::Lightning::load(
797                    file,
798                    layer,
799                    config.n_layers,
800                    config.n_heads,
801                    config.head_dim,
802                    hidden_dim,
803                )?,
804            ));
805            (
806                norm_sites.load_pre_norm(norm_sites.attn, file, Some(layer))?,
807                no_rows(0),
808            )
809        }
810        AttnShape::Gqa { .. } => unreachable!("a GQA layer loads its projections"),
811    };
812    if let AttnShape::Linear = shape {
813        if o_proj.rows() != hidden_dim || o_proj.cols() != hidden_dim {
814            return Err(LoadError::UnsupportedFeature(
815                format!("blk.{layer}.attn_output.weight"),
816                format!(
817                    "a wo-only layer's projection is {{n_embd, n_embd}} (deci.cpp:39); this one \
818                     is {}x{} for hidden_dim {hidden_dim}",
819                    o_proj.rows(),
820                    o_proj.cols()
821                ),
822            ));
823        }
824    }
825    Ok(AttnWeights {
826        q_proj: no_rows(hidden_dim),
827        k_proj: no_rows(hidden_dim),
828        v_proj: no_rows(hidden_dim),
829        o_proj,
830        norm_weight,
831        q_norm: None,
832        k_norm: None,
833        q_bias: None,
834        k_bias: None,
835        v_bias: None,
836        // `plamo2.cpp:150` norms the BLOCK's output with `attn_post_norm`
837        // on every layer, SSM or attention; the table's row is read for
838        // that shape (and `Decoder::recurrent_block` applies it). No
839        // other recurrent graph creates one on a recurrent layer, and
840        // the two attention-less shapes have no output to norm.
841        post_attn_norm: match shape {
842            AttnShape::Plamo2Ssm => NormSites::load_post_norm(norm_sites.post_attn, file, layer)?,
843            _ => None,
844        },
845        // The FFN's post-norm lives on this struct; a layer with no
846        // attention may still have one, so the table decides.
847        post_ffn_norm: NormSites::load_post_norm(norm_sites.post_ffn, file, layer)?,
848        output_gate: None,
849        sinks: None,
850        // No attention, no attention output to norm; the table row
851        // that has these (`bitnet`) has a uniform GQA shape.
852        attn_sub_norm: None,
853        o_scale: None,
854        o_bias: None,
855        shortconv,
856        ssm,
857        q_gate_interleaved: false,
858    })
859}
860
861/// The expert of an FFN-free layer (`deci.cpp:63-67` creates no
862/// gate/up/down when `n_ff == 0`): three placeholders, never applied.
863///
864/// `down` has ZERO rows rather than `hidden_dim` rows of nothing, on
865/// purpose: a `{hidden_dim, 0}` placeholder would project an empty
866/// activation to a vector of zeros, and a host body that forgot to
867/// skip the FFN would add zeros to the residual and be right by
868/// accident. With no rows the forgotten branch is an empty vector, and
869/// `residual_add`'s length check turns the omission into a panic.
870pub(crate) fn absent_ffn(hidden_dim: usize) -> ExpertWeights {
871    ExpertWeights {
872        gate: no_rows(hidden_dim),
873        up: no_rows(hidden_dim),
874        down: no_rows(0),
875    }
876}
877
878/// llama.cpp's `check_tensor_dims` for the three projections, against
879/// THIS layer's shape: `create_tensor_qkv` sizes Q `{n_embd,
880/// n_embd_head_k * n_head}` and K/V `{n_embd, n_embd_head_k *
881/// n_head_kv}` (`llama-model.cpp:2886-2900`), and `wo` is
882/// `{n_embd_head_k * n_head, n_embd}`. A file whose tensors disagree
883/// with its own header is refused there, and was silently accepted here.
884pub(crate) fn check_gqa_projection_widths(
885    layer: usize,
886    shape: AttnShape,
887    head_dim: usize,
888    v_head_dim: usize,
889    hidden_dim: usize,
890    attn: &AttnWeights,
891) -> Result<(), LoadError> {
892    let AttnShape::Gqa {
893        n_heads,
894        n_kv_heads,
895    } = shape
896    else {
897        unreachable!("only GQA layers have Q/K/V to check")
898    };
899    // `qwen35.cpp:59`: the query and its gate share one projection
900    // (`crate::attn_gate::Q_INTERLEAVED_GATE_ARCHS`).
901    let q_rows = if attn.q_gate_interleaved { 2 } else { 1 } * n_heads * head_dim;
902    let want = [
903        ("attn_q", attn.q_proj.rows(), q_rows),
904        ("attn_k", attn.k_proj.rows(), n_kv_heads * head_dim),
905        // V and the output projection at the V width: `mimo2.cpp:52`
906        // creates `wo` as `{n_embd_head_v * n_head, n_embd}`
907        // (`crate::kv_head_dims`); one width everywhere else.
908        ("attn_v", attn.v_proj.rows(), n_kv_heads * v_head_dim),
909        ("attn_output (rows)", attn.o_proj.rows(), hidden_dim),
910        (
911            "attn_output (cols)",
912            attn.o_proj.cols(),
913            n_heads * v_head_dim,
914        ),
915    ];
916    for (name, got, expected) in want {
917        if got != expected {
918            return Err(LoadError::UnsupportedFeature(
919                format!("blk.{layer}.{name}.weight"),
920                format!(
921                    "{got} does not match this layer's head_count {n_heads} / head_count_kv \
922                     {n_kv_heads} x head_dim {head_dim} / v_head_dim {v_head_dim} (expected \
923                     {expected}); llama.cpp's check_tensor_dims refuses the same file"
924                ),
925            ));
926        }
927    }
928    Ok(())
929}
930
931impl ModelConfig {
932    /// True when any layer carries a `RecurrentState` between tokens
933    /// (`AttnShape::is_recurrent`): the fact every caller that rolls a
934    /// cache back to a middle position -- speculative verification, the
935    /// draft model, the prefix cache -- is fenced on
936    /// (`frink_core::recurrent_state`).
937    pub fn has_recurrent_layers(&self) -> bool {
938        self.parallel_ssm
939            || (0..self.n_layers).any(|il| self.layer_shape(il).attention.is_recurrent())
940    }
941
942    /// Layer `il`'s cache geometry (`AttnShape::cache_geometry` at this
943    /// model's widths).
944    pub fn layer_cache_geometry(&self, il: usize) -> (usize, usize, usize) {
945        self.layer_shape(il).attention.cache_geometry(
946            self.head_dim,
947            self.v_head_dim(),
948            self.hidden_dim,
949        )
950    }
951
952    /// Layer `il`'s shape. THE accessor: every layer body reads its head
953    /// counts here and nowhere else.
954    pub fn layer_shape(&self, il: usize) -> LayerShape {
955        match &self.layer_shapes {
956            LayerShapes::Uniform => LayerShape {
957                attention: AttnShape::Gqa {
958                    n_heads: self.n_heads,
959                    n_kv_heads: self.n_kv_heads,
960                },
961                ffn_dim: self.moe.expert_ffn_dim,
962            },
963            LayerShapes::PerLayer(v) => v[il],
964        }
965    }
966
967    /// One contiguous cache per layer, each sized for that layer.
968    ///
969    /// The twenty-odd call sites that used to spell
970    /// `KvCache::new(config.n_kv_heads, config.head_dim)` per layer were
971    /// twenty copies of one geometry decision, and every one of them was
972    /// wrong for a model whose layers differ.
973    pub fn new_kv_caches(&self) -> Vec<KvCache> {
974        (0..self.n_layers)
975            .map(|il| {
976                let (n_kv_heads, head_dim, v_head_dim) = self.layer_cache_geometry(il);
977                KvCache::new_split(n_kv_heads, head_dim, v_head_dim)
978            })
979            .collect()
980    }
981
982    /// The same, pre-allocated for `max_seq_len` positions.
983    pub fn new_kv_caches_with_capacity(&self, max_seq_len: usize) -> Vec<KvCache> {
984        (0..self.n_layers)
985            .map(|il| {
986                let (n_kv_heads, head_dim, v_head_dim) = self.layer_cache_geometry(il);
987                KvCache::with_capacity_split(n_kv_heads, head_dim, v_head_dim, max_seq_len)
988            })
989            .collect()
990    }
991
992    /// The same, each layer's storage leased from `pool`. The first
993    /// layer that cannot be leased fails the whole set, as before.
994    pub fn new_kv_caches_with_pool(
995        &self,
996        pool: &std::sync::Arc<std::sync::Mutex<frink_core::cache::KvBlockPool>>,
997        max_seq_len: usize,
998    ) -> Result<Vec<KvCache>, frink_core::cache::KvPoolExhausted> {
999        (0..self.n_layers)
1000            .map(|il| {
1001                let (n_kv_heads, head_dim, v_head_dim) = self.layer_cache_geometry(il);
1002                KvCache::with_pool_split(
1003                    n_kv_heads,
1004                    head_dim,
1005                    v_head_dim,
1006                    std::sync::Arc::clone(pool),
1007                    max_seq_len,
1008                )
1009            })
1010            .collect()
1011    }
1012
1013    /// One paged store per layer, each sized for that layer.
1014    pub fn new_paged_kv(&self, block_size: usize, blocks_per_layer: usize) -> SharedPagedKv {
1015        SharedPagedKv::from_stores(
1016            (0..self.n_layers)
1017                .map(|il| {
1018                    let (n_kv_heads, head_dim, v_head_dim) = self.layer_cache_geometry(il);
1019                    PagedKvStore::new_split(
1020                        block_size,
1021                        blocks_per_layer,
1022                        n_kv_heads,
1023                        head_dim,
1024                        v_head_dim,
1025                    )
1026                })
1027                .collect(),
1028        )
1029    }
1030
1031    /// KV heads summed over every layer: what a per-token memory budget
1032    /// multiplies by `head_dim * elem_size`. `n_layers * n_kv_heads`
1033    /// for a uniform model, and an over-count for a heterogeneous one
1034    /// wherever it is still spelled that way.
1035    pub fn kv_heads_all_layers(&self) -> usize {
1036        (0..self.n_layers)
1037            .map(|il| self.layer_shape(il).attention.n_kv_heads())
1038            .sum()
1039    }
1040}
1041
1042#[cfg(test)]
1043mod tests {
1044    use super::*;
1045
1046    fn deci_like() -> Vec<LayerShape> {
1047        vec![
1048            LayerShape {
1049                attention: AttnShape::Gqa {
1050                    n_heads: 4,
1051                    n_kv_heads: 2,
1052                },
1053                ffn_dim: 16,
1054            },
1055            LayerShape {
1056                attention: AttnShape::Linear,
1057                ffn_dim: 8,
1058            },
1059            LayerShape {
1060                attention: AttnShape::Absent,
1061                ffn_dim: 16,
1062            },
1063            LayerShape {
1064                attention: AttnShape::Absent,
1065                ffn_dim: 0,
1066            },
1067        ]
1068    }
1069
1070    /// The three-way branch, pinned to deci.cpp's conditions.
1071    #[test]
1072    fn the_two_zero_counts_are_two_different_layer_kinds() {
1073        let deci = ZeroKvLayer::for_arch("deci");
1074        assert_eq!(
1075            AttnShape::from_counts(0, 0, 16, deci),
1076            Ok(AttnShape::Absent)
1077        );
1078        assert_eq!(
1079            AttnShape::from_counts(4, 0, 16, deci),
1080            Ok(AttnShape::Linear)
1081        );
1082        assert_eq!(
1083            AttnShape::from_counts(4, 2, 16, deci),
1084            Ok(AttnShape::Gqa {
1085                n_heads: 4,
1086                n_kv_heads: 2
1087            })
1088        );
1089        assert!(AttnShape::from_counts(0, 2, 16, deci).is_err());
1090        assert!(AttnShape::from_counts(3, 2, 16, deci).is_err());
1091        assert_eq!(AttnShape::Linear.n_kv_heads(), 0);
1092        assert_eq!(AttnShape::Absent.n_heads(), 0);
1093    }
1094
1095    /// The same two counts are a different block on LFM2 (lfm2.cpp:197)
1096    /// and an unserved one on the Mamba hybrids: the architecture
1097    /// decides, and the counts alone cannot.
1098    #[test]
1099    fn a_zero_kv_layer_means_what_the_architecture_says() {
1100        let lfm2 = ZeroKvLayer::for_arch("lfm2");
1101        assert_eq!(
1102            AttnShape::from_counts(4, 0, 16, lfm2),
1103            Ok(AttnShape::ShortConv)
1104        );
1105        // GQA layers are GQA on every architecture.
1106        assert!(matches!(
1107            AttnShape::from_counts(4, 2, 16, lfm2),
1108            Ok(AttnShape::Gqa { .. })
1109        ));
1110        assert_eq!(
1111            AttnShape::from_counts(4, 0, 16, ZeroKvLayer::for_arch("jamba")),
1112            Ok(AttnShape::Mamba1)
1113        );
1114        // plamo2: the block under either head-count spelling -- the
1115        // converter's `(0, 0)` (`conversion/plamo.py:87-88`) and the
1116        // scalar-heads `(4, 0)` -- because `plamo2.cpp:19` reads only
1117        // the KV count; `(0, 0)` is deci's attention-free layer
1118        // everywhere else.
1119        assert_eq!(
1120            AttnShape::from_counts(4, 0, 16, ZeroKvLayer::for_arch("plamo2")),
1121            Ok(AttnShape::Plamo2Ssm)
1122        );
1123        assert_eq!(
1124            AttnShape::from_counts(0, 0, 16, ZeroKvLayer::for_arch("plamo2")),
1125            Ok(AttnShape::Plamo2Ssm)
1126        );
1127        assert_eq!(
1128            AttnShape::from_counts(0, 0, 16, ZeroKvLayer::for_arch("jamba")),
1129            Ok(AttnShape::Absent)
1130        );
1131        // A pure recurrent model: every layer the block, from uniform
1132        // zeros that would otherwise read as a zero-head GQA model.
1133        let s = LayerShapes::resolve("mamba", &[0, 0], &[0, 0], Some(&[0, 0]), 0, None).unwrap();
1134        let LayerShapes::PerLayer(v) = s else {
1135            panic!("per layer");
1136        };
1137        assert!(v
1138            .iter()
1139            .all(|l| l.attention == AttnShape::Mamba1 && l.ffn_dim == 0));
1140        assert!(
1141            LayerShapes::resolve("mamba2", &[0, 0], &[0, 0], None, 0, None)
1142                .is_ok_and(|s| matches!(s, LayerShapes::PerLayer(_)))
1143        );
1144        assert!(LayerShapes::resolve("mamba", &[4, 4], &[0, 0], None, 0, None).is_err());
1145        // The cache: one row of n_embd per token, no V.
1146        assert_eq!(AttnShape::ShortConv.cache_geometry(6, 6, 24), (1, 24, 0));
1147        // Mamba-2: no rows at all, the state rides beside the cache.
1148        assert_eq!(
1149            AttnShape::from_counts(4, 0, 16, ZeroKvLayer::for_arch("granitehybrid")),
1150            Ok(AttnShape::Mamba2)
1151        );
1152        // nemotron-h.cpp:9-11: the FFN width is the second array.
1153        let nh = ZeroKvLayer::for_arch("nemotron_h");
1154        assert_eq!(AttnShape::from_counts(4, 0, 0, nh), Ok(AttnShape::Mamba2));
1155        assert_eq!(AttnShape::from_counts(4, 0, 40, nh), Ok(AttnShape::Absent));
1156        // Attention with no FFN: refused as deci's discarded branch,
1157        // served as Nemotron-H's one-block layer.
1158        assert!(LayerShapes::resolve("deci", &[4, 4], &[2, 2], Some(&[16, 0]), 16, None).is_err());
1159        let s = LayerShapes::resolve(
1160            "nemotron_h",
1161            &[4, 4, 4],
1162            &[0, 2, 0],
1163            Some(&[0, 0, 40]),
1164            16,
1165            None,
1166        )
1167        .unwrap();
1168        let LayerShapes::PerLayer(v) = s else {
1169            panic!("per layer");
1170        };
1171        assert_eq!(
1172            v.iter().map(|l| l.attention).collect::<Vec<_>>(),
1173            [
1174                AttnShape::Mamba2,
1175                AttnShape::Gqa {
1176                    n_heads: 4,
1177                    n_kv_heads: 2
1178                },
1179                AttnShape::Absent
1180            ]
1181        );
1182        assert_eq!(v.iter().map(|l| l.ffn_dim).collect::<Vec<_>>(), [0, 0, 40]);
1183        assert_eq!(AttnShape::Mamba2.cache_geometry(6, 6, 24), (0, 6, 6));
1184        assert!(AttnShape::Mamba2.is_recurrent() && !AttnShape::ShortConv.is_recurrent());
1185        assert_eq!(AttnShape::Linear.cache_geometry(6, 6, 24), (0, 6, 6));
1186        assert_eq!(AttnShape::ShortConv.n_kv_heads(), 0);
1187        let s = LayerShapes::resolve("plamo2", &[4, 4], &[2, 0], None, 16, None).unwrap();
1188        let LayerShapes::PerLayer(v) = s else {
1189            panic!("per layer");
1190        };
1191        assert_eq!(v[1].attention, AttnShape::Plamo2Ssm);
1192        assert!(AttnShape::Plamo2Ssm.is_recurrent());
1193        assert_eq!(AttnShape::Plamo2Ssm.cache_geometry(8, 8, 32), (0, 8, 8));
1194        let s = LayerShapes::resolve("lfm2", &[4, 4], &[0, 2], None, 16, None).unwrap();
1195        let LayerShapes::PerLayer(v) = s else {
1196            panic!("per layer");
1197        };
1198        assert_eq!(v[0].attention, AttnShape::ShortConv);
1199    }
1200
1201    /// Equal arrays are the uniform model, for ANY architecture: the
1202    /// converter is free to spell a scalar as an array.
1203    #[test]
1204    fn equal_arrays_collapse_to_uniform_even_for_a_layer_zero_architecture() {
1205        let s = LayerShapes::resolve("llama", &[4, 4], &[2, 2], Some(&[16, 16]), 16, None).unwrap();
1206        assert!(s.is_uniform());
1207    }
1208
1209    /// A varying array on an architecture whose graph reads layer 0 is
1210    /// refused, naming the table; on one that indexes per layer it is
1211    /// the per-layer table.
1212    #[test]
1213    fn a_varying_array_is_refused_unless_llama_cpp_indexes_it_per_layer() {
1214        let err = LayerShapes::resolve("llama", &[4, 4], &[2, 1], None, 16, None).unwrap_err();
1215        assert!(format!("{err}").contains("PER_LAYER_SHAPE_ARCHS"), "{err}");
1216        let s = LayerShapes::resolve(
1217            "deci",
1218            &[4, 4, 0, 0],
1219            &[2, 0, 0, 0],
1220            Some(&[16, 8, 16, 0]),
1221            16,
1222            None,
1223        )
1224        .unwrap();
1225        assert_eq!(s, LayerShapes::PerLayer(deci_like()));
1226    }
1227
1228    /// The dropped-branch combination is refused by name, and the
1229    /// combination both graphs agree on is not.
1230    #[test]
1231    fn an_ffn_free_layer_with_attention_is_refused_and_one_without_is_not() {
1232        let err =
1233            LayerShapes::resolve("deci", &[4, 4], &[2, 2], Some(&[16, 0]), 16, None).unwrap_err();
1234        let msg = format!("{err}");
1235        assert!(msg.contains("deci.cpp:147-149"), "{msg}");
1236        assert!(msg.contains("blk.1"), "{msg}");
1237        assert!(LayerShapes::resolve("deci", &[4, 0], &[2, 0], Some(&[16, 0]), 16, None).is_ok());
1238    }
1239
1240    /// The accessor and the two cache constructors read the same table.
1241    #[test]
1242    fn caches_are_sized_per_layer_and_the_scalar_is_never_consulted() {
1243        let mut cfg = crate::config::glm_5_2();
1244        cfg.n_layers = 4;
1245        cfg.n_heads = 4;
1246        cfg.n_kv_heads = 2;
1247        cfg.head_dim = 8;
1248        cfg.layer_shapes = LayerShapes::PerLayer(deci_like());
1249        let caches = cfg.new_kv_caches();
1250        assert_eq!(
1251            caches.iter().map(|c| c.n_kv_heads).collect::<Vec<_>>(),
1252            vec![2, 0, 0, 0]
1253        );
1254        assert_eq!(cfg.kv_heads_all_layers(), 2);
1255        assert_eq!(cfg.layer_shape(1).attention, AttnShape::Linear);
1256        assert_eq!(cfg.layer_shape(3).ffn_dim, 0);
1257        // A cache built from the scalar for the wo-only layer refuses
1258        // the first row: this is what turns a missed call site into a
1259        // panic instead of a misaligned history.
1260        let mut wrong = KvCache::new(cfg.n_kv_heads, cfg.head_dim);
1261        let res = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1262            wrong.push(&[], &[]).unwrap();
1263        }));
1264        assert!(res.is_err(), "push must assert the row width");
1265        cfg.layer_shapes = LayerShapes::Uniform;
1266        assert!(cfg
1267            .new_kv_caches()
1268            .iter()
1269            .all(|c| c.n_kv_heads == 2 && c.head_dim == 8));
1270        assert_eq!(cfg.kv_heads_all_layers(), 8);
1271    }
1272
1273    /// The projection-width check refuses a Q/K/V/wo whose rows disagree
1274    /// with the layer's own counts, naming the tensor, and passes one
1275    /// that agrees. Built by hand rather than from a fixture: a GGUF
1276    /// whose tensors disagree with its header is exactly what no
1277    /// converter writes, so this is the only way the refusal can be
1278    /// shown to fire.
1279    #[test]
1280    fn a_projection_sized_for_another_layer_s_counts_is_refused_naming_the_tensor() {
1281        let m = |rows: usize, cols: usize| {
1282            WeightMatrix::F32(Tensor::new(vec![0.0; rows * cols], vec![rows, cols]))
1283        };
1284        let shape = AttnShape::Gqa {
1285            n_heads: 4,
1286            n_kv_heads: 2,
1287        };
1288        let (head_dim, hidden) = (6, 24);
1289        let build = |q_rows: usize, k_rows: usize| AttnWeights {
1290            q_proj: m(q_rows, hidden),
1291            k_proj: m(k_rows, hidden),
1292            v_proj: m(k_rows, hidden),
1293            o_proj: m(hidden, q_rows),
1294            norm_weight: NormOp::None,
1295            q_norm: None,
1296            k_norm: None,
1297            q_bias: None,
1298            k_bias: None,
1299            v_bias: None,
1300            post_attn_norm: None,
1301            post_ffn_norm: None,
1302            output_gate: None,
1303            sinks: None,
1304            attn_sub_norm: None,
1305            o_scale: None,
1306            o_bias: None,
1307            shortconv: None,
1308            ssm: None,
1309            q_gate_interleaved: false,
1310        };
1311        assert!(
1312            check_gqa_projection_widths(0, shape, head_dim, head_dim, hidden, &build(24, 12))
1313                .is_ok()
1314        );
1315        // K sized for 3 KV heads on a 2-KV-head layer.
1316        let err = check_gqa_projection_widths(1, shape, head_dim, head_dim, hidden, &build(24, 18))
1317            .unwrap_err();
1318        let msg = format!("{err}");
1319        assert!(msg.contains("blk.1.attn_k.weight"), "{msg}");
1320        assert!(msg.contains("head_count_kv 2"), "{msg}");
1321        // Q sized for 3 heads on a 4-head layer.
1322        let err = check_gqa_projection_widths(2, shape, head_dim, head_dim, hidden, &build(18, 12))
1323            .unwrap_err();
1324        assert!(format!("{err}").contains("blk.2.attn_q.weight"), "{err}");
1325    }
1326
1327    /// Every row of the reach table cites a llama.cpp line, and the
1328    /// five that this seam serves are the ones on the generic path.
1329    #[test]
1330    fn the_reach_table_cites_its_lines_and_names_what_each_row_still_needs() {
1331        for (arch, note) in PER_LAYER_SHAPE_ARCHS {
1332            assert!(note.contains(".cpp:"), "`{arch}` cites no line: {note}");
1333        }
1334        let generic: Vec<&str> = PER_LAYER_SHAPE_ARCHS
1335            .iter()
1336            .filter(|(_, n)| n.starts_with("generic"))
1337            .map(|(a, _)| *a)
1338            .collect();
1339        assert_eq!(
1340            generic,
1341            [
1342                "deci",
1343                "openelm",
1344                "plamo3",
1345                "laguna",
1346                "step35",
1347                "spark2_5",
1348                "maple",
1349                "jamba",
1350                "nemotron_h",
1351                "granitehybrid",
1352                "granite-hybrid"
1353            ]
1354        );
1355        assert!(per_layer_shapes_read_by_llama_cpp("deci"));
1356        assert!(!per_layer_shapes_read_by_llama_cpp("granite"));
1357    }
1358}