Skip to main content

frink_models/
gdn.rs

1//! The gated delta-net block (Qwen3.5's linear attention), at the site
2//! attention occupies on a recurrent layer
3//! ([`crate::layer_shapes::AttnShape::Gdn`]).
4//!
5//! `qwen35.cpp:236-317` (`build_layer_attn_linear`), per token:
6//!
7//! ```text
8//! qkv   = attn_qkv(normed)                       {2 key_dim + value_dim}    :248-250
9//! z     = attn_gate(normed)                      {value_dim}                :250
10//! beta  = sigmoid(ssm_beta(normed))              {n_v_heads}                :251-253
11//! g     = softplus(ssm_alpha(normed) + ssm_dt) * ssm_a   {n_v_heads}        :254-262
12//! qkv   = silu(conv(qkv))                        causal, width d_conv, NO bias  :266-273
13//! q, k, v = qkv split [key_dim, key_dim, value_dim]                         :276-295
14//! q, k  = l2_norm(q), l2_norm(k) per head        eps = rms_eps              :296-297
15//! o     = delta_step(state, q / sqrt(S), k, v, g, beta)   frink_core::gdn   :302-308
16//! o     = rms_norm(o per head, ssm_norm) * silu(z per head)                 :311-313, :171-178
17//! out   = ssm_out(o)                                                        :315
18//! ```
19//!
20//! with `head_k_dim = head_v_dim = ssm.state_size`, `n_k_heads =
21//! ssm.group_count`, `n_v_heads = ssm.time_step_rank` and `ssm.inner_size
22//! = n_v_heads * head_v_dim` (`:52-58`), V head `h` reading K head
23//! `h % n_k_heads` (`HeadMap::Tiled`, `llama-model.cpp:524-526`; the
24//! converter reorders V heads into that order). The state is a
25//! [`RecurrentState`] on the sequence's layer cache: the conv window
26//! `[d_conv - 1][2 key_dim + value_dim]` and the delta state
27//! `[n_v_heads][S][S]`.
28//!
29//! # Which layers
30//!
31//! `qwen35.cpp:17-24`: `{arch}.attention.recurrent_layers` (a bool per
32//! layer over `n_layer_all`, the MTP block included) when the file
33//! carries it, else `(i + 1) % full_attention_interval != 0` with the
34//! interval from `{arch}.full_attention_interval` (default 4) and the
35//! MTP block never recurrent. [`recurrent_layers`] is that rule.
36//!
37//! # Reach
38//!
39//! Three graphs of 155 build the block (`grep -l build_layer_attn_linear
40//! src/models/*.cpp`: `qwen35`, `qwen35moe`, `qwen3next`) over the ONE
41//! `delta-net-base.cpp`. `qwen3next` differs in two places, both
42//! tables here: its V heads read K heads GROUPED (`h / ratio`,
43//! `qwen3next.cpp:521-539`, `llama-model.cpp:525`;
44//! [`GROUPED_HEAD_ARCHITECTURES`]), and beta and alpha come from ONE
45//! `ssm_ba` projection laid out `[k_group][beta * ratio, alpha * ratio]`
46//! (`:96,422-436`; [`BetaAlpha::Fused`]). Its legacy fused `ssm_in`
47//! (q/k/v/z in one, `:88-90,336-360`) is refused by name: every
48//! current export splits it (`conversion/qwen.py:389-416`).
49
50use frink_core::gdn::{delta_step, l2_normalize, DeltaDims, HeadMap};
51use frink_core::mamba2::{conv_step, softplus};
52use frink_core::matmul::rms_norm;
53use frink_core::recurrent_state::RecurrentState;
54use frink_core::weight_matrix::WeightMatrix;
55use frink_gguf::TensorSource;
56
57use crate::layer_shapes::AttnShape;
58use crate::loader::{load_f32_vec, load_weight_matrix, LoadError};
59
60/// Architectures whose V heads read K heads grouped (`HeadMap::Grouped`,
61/// `qwen3next.cpp:521-539`: `ggml_repeat_4d` over a `[head_dim, 1,
62/// n_k]` view repeats each K head `ratio` times consecutively). Every
63/// other reader of the block tiles (`llama-model.cpp:524-526`).
64pub const GROUPED_HEAD_ARCHITECTURES: &[&str] = &["qwen3next"];
65
66/// How `arch`'s V heads find their K heads.
67pub fn head_map(arch: &str) -> HeadMap {
68    if GROUPED_HEAD_ARCHITECTURES.contains(&arch) {
69        HeadMap::Grouped
70    } else {
71        HeadMap::Tiled
72    }
73}
74
75/// The five `ssm.*` hparams as one value (`qwen35.cpp:7-11`), and the
76/// architecture's head map.
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78pub struct GdnHparams {
79    pub d_conv: usize,
80    /// `ssm.state_size`: the key AND value head width.
81    pub head_dim: usize,
82    /// `ssm.group_count`.
83    pub n_k_heads: usize,
84    /// `ssm.time_step_rank`.
85    pub n_v_heads: usize,
86    pub map: HeadMap,
87}
88
89impl GdnHparams {
90    pub fn read(file: &impl TensorSource, arch: &str) -> Result<Self, LoadError> {
91        let key = |k: &str| format!("{arch}.ssm.{k}");
92        let read = |k: &str| {
93            file.metadata_u64(&key(k))
94                .map(|v| v as usize)
95                .ok_or_else(|| LoadError::MissingHparam(key(k)))
96        };
97        let h = Self {
98            d_conv: read("conv_kernel")?,
99            head_dim: read("state_size")?,
100            n_k_heads: read("group_count")?,
101            n_v_heads: read("time_step_rank")?,
102            map: head_map(arch),
103        };
104        let d_inner = read("inner_size")?;
105        if h.d_conv < 2
106            || h.head_dim == 0
107            || h.n_k_heads == 0
108            || !h.n_v_heads.is_multiple_of(h.n_k_heads)
109            || d_inner != h.n_v_heads * h.head_dim
110        {
111            return Err(LoadError::UnsupportedFeature(
112                arch.to_string(),
113                format!(
114                    "ssm.* hparams {h:?} with inner_size {d_inner}: qwen35.cpp:52-58 sizes the \
115                     block as inner_size = time_step_rank * state_size with time_step_rank a \
116                     multiple of group_count (delta-net-base.cpp:308)"
117                ),
118            ));
119        }
120        Ok(h)
121    }
122
123    pub fn key_dim(self) -> usize {
124        self.n_k_heads * self.head_dim
125    }
126
127    pub fn value_dim(self) -> usize {
128        self.n_v_heads * self.head_dim
129    }
130
131    /// The conv's channel count, `2 key_dim + value_dim`.
132    pub fn conv_dim(self) -> usize {
133        2 * self.key_dim() + self.value_dim()
134    }
135
136    pub fn delta_dims(self) -> DeltaDims {
137        DeltaDims {
138            n_k_heads: self.n_k_heads,
139            n_v_heads: self.n_v_heads,
140            head_dim: self.head_dim,
141            map: self.map,
142        }
143    }
144
145    /// Floats of state per sequence per layer: `n_embd_r + n_embd_s`.
146    pub fn state_floats(self) -> (usize, usize) {
147        (
148            (self.d_conv - 1) * self.conv_dim(),
149            self.delta_dims().state_len(),
150        )
151    }
152}
153
154/// Where beta and alpha come from.
155pub enum BetaAlpha {
156    /// `blk.N.ssm_beta.weight` and `blk.N.ssm_alpha.weight`, each
157    /// `[n_v_heads, n_embd]` (`qwen35.cpp:71-72`).
158    Split {
159        beta: WeightMatrix,
160        alpha: WeightMatrix,
161    },
162    /// `blk.N.ssm_ba.weight`, `[2 n_v_heads, n_embd]`, its rows laid out
163    /// per K group as `ratio` betas then `ratio` alphas
164    /// (`qwen3next.cpp:96,422-436`).
165    Fused { ba: WeightMatrix },
166}
167
168/// One GDN layer's weights (`qwen35.cpp:66-74`).
169pub struct Gdn {
170    pub h: GdnHparams,
171    /// `blk.N.attn_qkv.weight`, `[conv_dim, n_embd]`.
172    pub qkv: WeightMatrix,
173    /// `blk.N.attn_gate.weight`, `[value_dim, n_embd]`: the `z` gate.
174    pub z_proj: WeightMatrix,
175    /// `blk.N.ssm_conv1d.weight`, `[conv_dim][d_conv]`; no bias.
176    pub conv1d: Vec<f32>,
177    /// `blk.N.ssm_dt.bias`, `[n_v_heads]`.
178    pub dt_bias: Vec<f32>,
179    /// `blk.N.ssm_a`, `[n_v_heads]`, stored negative.
180    pub a: Vec<f32>,
181    pub beta_alpha: BetaAlpha,
182    /// `blk.N.ssm_norm.weight`, `[head_dim]`, one weight for every head.
183    pub norm: Vec<f32>,
184    /// `blk.N.ssm_out.weight`, `[n_embd, value_dim]`.
185    pub out_proj: WeightMatrix,
186}
187
188impl Gdn {
189    /// Loads layer `layer`'s tensors and checks them against
190    /// `qwen35.cpp:66-74`'s shapes.
191    pub fn load(
192        file: &impl TensorSource,
193        arch: &str,
194        layer: usize,
195        hidden_dim: usize,
196    ) -> Result<Self, LoadError> {
197        let h = GdnHparams::read(file, arch)?;
198        let name = |t: &str| format!("blk.{layer}.{t}");
199        let matrix = |t: &str, rows: usize, cols: usize| -> Result<WeightMatrix, LoadError> {
200            let m = load_weight_matrix(file, &name(t))?;
201            if m.rows() != rows || m.cols() != cols {
202                return Err(LoadError::UnsupportedFeature(
203                    name(t),
204                    format!(
205                        "{}x{}; qwen35.cpp:66-74 sizes it {rows}x{cols}",
206                        m.rows(),
207                        m.cols()
208                    ),
209                ));
210            }
211            Ok(m)
212        };
213        let vector = |t: &str, len: usize| -> Result<Vec<f32>, LoadError> {
214            let v = load_f32_vec(file, &name(t))?;
215            if v.len() != len {
216                return Err(LoadError::UnsupportedFeature(
217                    name(t),
218                    format!("{} elements; qwen35.cpp:66-74 sizes it {len}", v.len()),
219                ));
220            }
221            Ok(v)
222        };
223        // `:66` creates `attn_qkv` TENSOR_NOT_REQUIRED because qwen3next
224        // may store it fused as `ssm_in`; qwen35 has no other spelling.
225        if file.find_tensor(&name("attn_qkv.weight")).is_none() {
226            return Err(LoadError::UnsupportedFeature(
227                name("attn_qkv.weight"),
228                "absent: the gated delta net's fused q/k/v projection (qwen35.cpp:66); the \
229                 legacy `ssm_in` / `ssm_ba` spelling (qwen3next.cpp:88-95) is not served"
230                    .to_string(),
231            ));
232        }
233        let beta_alpha = if file.find_tensor(&name("ssm_ba.weight")).is_some() {
234            BetaAlpha::Fused {
235                ba: matrix("ssm_ba.weight", 2 * h.n_v_heads, hidden_dim)?,
236            }
237        } else {
238            BetaAlpha::Split {
239                beta: matrix("ssm_beta.weight", h.n_v_heads, hidden_dim)?,
240                alpha: matrix("ssm_alpha.weight", h.n_v_heads, hidden_dim)?,
241            }
242        };
243        Ok(Self {
244            h,
245            qkv: matrix("attn_qkv.weight", h.conv_dim(), hidden_dim)?,
246            z_proj: matrix("attn_gate.weight", h.value_dim(), hidden_dim)?,
247            conv1d: vector("ssm_conv1d.weight", h.d_conv * h.conv_dim())?,
248            dt_bias: vector("ssm_dt.bias", h.n_v_heads)?,
249            a: vector("ssm_a", h.n_v_heads)?,
250            beta_alpha,
251            norm: vector("ssm_norm.weight", h.head_dim)?,
252            out_proj: matrix("ssm_out.weight", hidden_dim, h.value_dim())?,
253        })
254    }
255
256    /// A fresh sequence's state for this layer.
257    pub fn zero_state(&self) -> RecurrentState {
258        let (conv, ssm) = self.h.state_floats();
259        RecurrentState::zeros(conv, ssm)
260    }
261
262    /// `rows` consecutive tokens of ONE sequence (`normed` is
263    /// `[rows][n_embd]`) through the block, advancing `state` in place.
264    pub fn forward_rows(
265        &self,
266        normed: &[f32],
267        rows: usize,
268        state: &mut RecurrentState,
269        rms_eps: f32,
270    ) -> Vec<f32> {
271        let h = self.h;
272        let n_embd = self.out_proj.rows();
273        assert_eq!(normed.len(), rows * n_embd);
274        let (conv_len, ssm_len) = h.state_floats();
275        assert_eq!(
276            state.conv.len(),
277            conv_len,
278            "conv state sized by these weights"
279        );
280        assert_eq!(
281            state.ssm.len(),
282            ssm_len,
283            "delta state sized by these weights"
284        );
285        let (s, n_k, n_v) = (h.head_dim, h.n_k_heads, h.n_v_heads);
286        let (key_dim, value_dim, conv_dim) = (h.key_dim(), h.value_dim(), h.conv_dim());
287        let dims = h.delta_dims();
288        let project = |m: &WeightMatrix| {
289            if rows == 1 {
290                m.apply(normed)
291            } else {
292                m.apply_batch(normed, rows)
293            }
294        };
295        // `qkv` and `z` read the same input: one launch on a GPU
296        // backend (`apply_pair`), two overlapped regions on the CPU.
297        //
298        // The gate projections are NOT in that launch, and that is a
299        // measurement: sending all of them together as one list was
300        // neutral on Bonsai (tg32 7.2 either way) because those
301        // matrices are tiny and unquantized, and it made the fused
302        // launch all-or-nothing across two different folds.
303        let (qkv_all, z_all) = if rows == 1 {
304            WeightMatrix::apply_pair(&self.qkv, &self.z_proj, normed)
305        } else {
306            // A batch rotates the shared input ONCE for the pair.
307            WeightMatrix::apply_batch_pair_with_acts(&self.qkv, &self.z_proj, normed, rows, None)
308        };
309        // The two per-head gate logits, whichever projection spells
310        // them: `[rows][n_v]` each.
311        let (beta_all, alpha_all) = match &self.beta_alpha {
312            BetaAlpha::Split { beta, alpha } => (project(beta), project(alpha)),
313            BetaAlpha::Fused { ba } => {
314                let mixed = project(ba);
315                let ratio = n_v / n_k;
316                let mut beta_all = vec![0.0f32; rows * n_v];
317                let mut alpha_all = vec![0.0f32; rows * n_v];
318                for r in 0..rows {
319                    let row = &mixed[r * 2 * n_v..(r + 1) * 2 * n_v];
320                    for hd in 0..n_v {
321                        // qwen3next.cpp:422-436: group `hd / ratio`, its
322                        // `ratio` betas then its `ratio` alphas.
323                        let base = (hd / ratio) * 2 * ratio + hd % ratio;
324                        beta_all[r * n_v + hd] = row[base];
325                        alpha_all[r * n_v + hd] = row[base + ratio];
326                    }
327                }
328                (beta_all, alpha_all)
329            }
330        };
331        let mut ys = vec![0.0f32; rows * value_dim];
332        let mut conv_out = vec![0.0f32; conv_dim];
333        let mut o = vec![0.0f32; value_dim];
334        let mut g = vec![0.0f32; n_v];
335        let mut beta = vec![0.0f32; n_v];
336        // A PREFILL batch takes the CHUNKED delta rule
337        // (`frink_core::gdn_chunk`): the same recurrence with the
338        // state read once per chunk of rows instead of once per row.
339        // The sequential step is bandwidth-bound (36 GB/s of state,
340        // measured) and a 128-token Bonsai prefill moves 38 GB through
341        // it, so trading 1.5x the multiply-adds for a 32nd of the
342        // traffic is 2.1x on the step. A decode token still steps one
343        // row at a time, where there is no traffic to amortise, and
344        // running the row step on the GPU is a loss three ways
345        // (`docs/plans/gdn-resident-state.md`).
346        //
347        // Chunking is also what finally makes the GPU worth it, and
348        // for the same reason it made the host faster: with the
349        // traffic amortised the step is compute-dense, which is the
350        // thing the three earlier attempts never had.
351        // `delta_chunk_rows` is the one place that choice is made.
352        // A DECODE token takes the whole branch on the device when
353        // the shapes allow, which is what removes the host step INSIDE
354        // a recurrent layer -- the thing that, priced in
355        // `docs/plans/gdn-resident-state.md`, is two thirds of a token's
356        // host time and the reason a layer cannot be one submission.
357        // It rides in the submission `ssm_out` already cost, so the
358        // count does not go up; the host body below is the fallback and
359        // the oracle.
360        #[cfg(feature = "metal")]
361        if rows == 1 {
362            if let Some(out) = self.device_branch_full(
363                state,
364                rms_eps,
365                None,
366                None,
367                Some((&qkv_all, &z_all, &beta_all, &alpha_all)),
368            ) {
369                return out;
370            }
371        }
372        if rows > 1 {
373            let (q_all, k_all, v_all, g_all, beta_gate) =
374                self.conv_and_gates_for_rows(rows, &qkv_all, &beta_all, &alpha_all, state, rms_eps);
375            let mut o_all = vec![0.0f32; rows * value_dim];
376            frink_core::gdn_chunk::delta_chunk_rows(
377                dims,
378                rows,
379                &mut state.ssm,
380                &q_all,
381                &k_all,
382                &v_all,
383                &g_all,
384                &beta_gate,
385                &mut o_all,
386            );
387            for r in 0..rows {
388                let o = &o_all[r * value_dim..(r + 1) * value_dim];
389                let y = &mut ys[r * value_dim..(r + 1) * value_dim];
390                let z = &z_all[r * value_dim..(r + 1) * value_dim];
391                for hd in 0..n_v {
392                    let normed_head = rms_norm(&o[hd * s..(hd + 1) * s], &self.norm, rms_eps);
393                    for i in 0..s {
394                        y[hd * s + i] = normed_head[i] * silu(z[hd * s + i]);
395                    }
396                }
397            }
398            return self.out_proj.apply_batch(&ys, rows);
399        }
400        for r in 0..rows {
401            // :251-262: the two per-head gates from the layer input.
402            for hd in 0..n_v {
403                beta[hd] = sigmoid(beta_all[r * n_v + hd]);
404                g[hd] = softplus(alpha_all[r * n_v + hd] + self.dt_bias[hd]) * self.a[hd];
405            }
406            // :266-273: the conv over this token and the state, SiLU.
407            conv_step(
408                &mut state.conv,
409                &self.conv1d,
410                h.d_conv,
411                &qkv_all[r * conv_dim..(r + 1) * conv_dim],
412                &mut conv_out,
413            );
414            for x in conv_out.iter_mut() {
415                *x = silu(*x);
416            }
417            let (q, rest) = conv_out.split_at_mut(key_dim);
418            let (k, v) = rest.split_at_mut(key_dim);
419            // :296-297: l2 per head, with the RMS epsilon.
420            for hd in 0..n_k {
421                l2_normalize(&mut q[hd * s..(hd + 1) * s], rms_eps);
422                l2_normalize(&mut k[hd * s..(hd + 1) * s], rms_eps);
423            }
424            // The recurrence stays on the HOST, and that is THREE
425            // measurements rather than an omission. Running it, the
426            // gated norm and `ssm_out` as one Metal submission works
427            // and is slower every way it has been tried on
428            // Bonsai-2-27B: 6.0 tok/s against 7.1 with the state copied
429            // both ways, 6.6 with it wrapped in place (the state buffer
430            // is page-aligned for exactly that, `AlignedF32`), and 6.9
431            // with the wrapper cached so the pages are mapped once.
432            // The kernels are real and pinned against this code
433            // (`frink_metal::gdn`); what beats a host recurrence is a
434            // CHUNKED delta rule, which is a different algorithm.
435            // `docs/plans/gdn-resident-state.md` carries all of it.
436            delta_step(dims, &mut state.ssm, q, k, v, &g, &beta, &mut o);
437            // :311-313 (`build_norm_gated`, :171-178): per head,
438            // rms_norm(o, ssm_norm) * silu(z).
439            let y = &mut ys[r * value_dim..(r + 1) * value_dim];
440            let z = &z_all[r * value_dim..(r + 1) * value_dim];
441            for hd in 0..n_v {
442                let normed_head = rms_norm(&o[hd * s..(hd + 1) * s], &self.norm, rms_eps);
443                for i in 0..s {
444                    y[hd * s + i] = normed_head[i] * silu(z[hd * s + i]);
445                }
446            }
447        }
448        if rows == 1 {
449            self.out_proj.apply(&ys)
450        } else {
451            self.out_proj.apply_batch(&ys, rows)
452        }
453    }
454}
455
456impl Gdn {
457    /// One decode token's WHOLE LAYER in one command buffer: this
458    /// branch, the residual add, the FFN norm, the FFN and the second
459    /// residual add. Returns the layer's output, or `None` when the
460    /// shapes are not ones the kernels serve and the host bodies run.
461    ///
462    /// This is the step that changes the submission COUNT, which
463    /// `docs/plans/gdn-resident-state.md` prices as the whole of what
464    /// is left between this engine and the reference's decode rate.
465    #[cfg(feature = "metal")]
466    pub fn fused_layer(
467        &self,
468        attn_norm: &[f32],
469        normed: &[f32],
470        state: &mut RecurrentState,
471        rms_eps: f32,
472        ffn: &crate::fused_layer::LayerFfnParts<'_>,
473        residual: &[f32],
474    ) -> Option<Vec<f32>> {
475        let BetaAlpha::Split { beta, alpha } = &self.beta_alpha else {
476            // The fused spelling's per-group interleave is host
477            // arithmetic with no kernel here.
478            return None;
479        };
480        // The HEAD on the device too, when its four matrices have
481        // kernels and agree about their input basis: then the layer is
482        // ONE submission and `normed` never leaves the GPU.
483        if let Some(head) = crate::fused_layer::LayerHeadParts::for_block(
484            attn_norm,
485            rms_eps,
486            &self.qkv,
487            &self.z_proj,
488            beta,
489            alpha,
490        ) {
491            if let Some(out) =
492                self.device_branch_full(state, rms_eps, Some(&head), Some((ffn, residual)), None)
493            {
494                return Some(out);
495            }
496        }
497        // Otherwise the projections run on the host, as they did.
498        let (qkv_all, z_all) = WeightMatrix::apply_pair(&self.qkv, &self.z_proj, normed);
499        let (beta_all, alpha_all) = (beta.apply(normed), alpha.apply(normed));
500        self.device_branch_full(
501            state,
502            rms_eps,
503            None,
504            Some((ffn, residual)),
505            Some((&qkv_all, &z_all, &beta_all, &alpha_all)),
506        )
507    }
508
509    /// One decode token's whole branch in ONE command buffer:
510    /// `frink_metal::gdn_branch`. `None` when this layer is not what
511    /// those kernels serve, and then the host body runs.
512    ///
513    /// The refusals are shapes the kernels state, not guesses: a head
514    /// width that is not a power of two (the two reductions halve their
515    /// stride from it), a tap count past the window the convolution
516    /// holds in registers, an output projection with no Metal launch,
517    /// and the FUSED beta/alpha spelling, whose per-group interleave
518    /// (`qwen3next.cpp:422-436`) is host arithmetic this has no kernel
519    /// for. Each one falls through rather than being approximated.
520    #[cfg(feature = "metal")]
521    /// Builds this layer's [`frink_metal::gdn_branch::BranchWeights`]
522    /// and hands it to `f`.
523    ///
524    /// A closure rather than a return value because the weights borrow
525    /// launches and fold plans that are locals here, and ONE
526    /// construction site because a second would be a second place that
527    /// has to remember the head map, the fold widths and which
528    /// `beta_alpha` spelling has a kernel.
529    #[cfg(feature = "metal")]
530    fn with_branch_weights<R>(
531        &self,
532        rms_eps: f32,
533        head: Option<&crate::fused_layer::LayerHeadParts<'_>>,
534        ffn: Option<&crate::fused_layer::LayerFfnParts<'_>>,
535        f: impl FnOnce(&frink_metal::gdn_branch::BranchWeights<'_>) -> R,
536    ) -> Option<R> {
537        if !frink_core::weight_matrix::metal_dense_enabled() {
538            return None;
539        }
540        if matches!(self.beta_alpha, BetaAlpha::Fused { .. }) {
541            return None;
542        }
543        let h = self.h;
544        let (base, fold) = self.out_proj.launch_parts();
545        let out_proj = crate::metal_launch::matvec(base)?;
546        let fold_y = match fold {
547            None => None,
548            Some(fd) => Some(fd.metal_plan(h.value_dim())?),
549        };
550        let ffn_launches = match ffn {
551            None => None,
552            Some(parts) => Some(parts.launches()?),
553        };
554        let head_launches = match head {
555            None => None,
556            Some(parts) => Some(parts.launches()?),
557        };
558        let w = frink_metal::gdn_branch::BranchWeights {
559            shape: frink_metal::gdn::DeltaShape {
560                n_k_heads: h.n_k_heads,
561                n_v_heads: h.n_v_heads,
562                head_dim: h.head_dim,
563                map: match h.map {
564                    HeadMap::Tiled => frink_metal::gdn::HeadMapKind::Tiled,
565                    HeadMap::Grouped => frink_metal::gdn::HeadMapKind::Grouped,
566                },
567            },
568            head: frink_metal::gdn_head::HeadShape {
569                n_k_heads: h.n_k_heads,
570                n_v_heads: h.n_v_heads,
571                head_dim: h.head_dim,
572                d_conv: h.d_conv,
573            },
574            conv1d: &self.conv1d,
575            dt_bias: &self.dt_bias,
576            a: &self.a,
577            ssm_norm: &self.norm,
578            eps: rms_eps,
579            out_proj: &out_proj,
580            fold_y: fold_y.as_ref(),
581            ffn: ffn_launches.as_ref().map(|l| l.as_metal()),
582            head_in: head_launches.as_ref().map(|l| l.as_metal()),
583        };
584        if !w.is_supported() {
585            return None;
586        }
587        Some(f(&w))
588    }
589
590    /// This layer appended to a RUN of layers sharing one residual
591    /// buffer and one wait.
592    ///
593    /// # Safety
594    ///
595    /// The state this borrows must stay valid and exclusively the
596    /// caller's until the run finishes, because the GPU has not
597    /// necessarily read it when this returns.
598    #[cfg(feature = "metal")]
599    pub unsafe fn run_layer(
600        &self,
601        run: &mut frink_metal::gdn_branch::GdnRun,
602        attn_norm: &[f32],
603        rms_eps: f32,
604        ffn: &crate::fused_layer::LayerFfnParts<'_>,
605        state: &mut RecurrentState,
606    ) -> Option<()> {
607        let BetaAlpha::Split { beta, alpha } = &self.beta_alpha else {
608            return None;
609        };
610        let head = crate::fused_layer::LayerHeadParts::for_block(
611            attn_norm,
612            rms_eps,
613            &self.qkv,
614            &self.z_proj,
615            beta,
616            alpha,
617        )?;
618        let conv_len = state.conv.len();
619        let (ssm_bytes, ssm_ptr) = (state.ssm.alloc_bytes(), state.ssm.as_ptr());
620        let (conv_bytes, conv_ptr) = (state.conv.alloc_bytes(), state.conv.as_ptr());
621        self.with_branch_weights(rms_eps, Some(&head), Some(ffn), |w| {
622            // SAFETY: the caller's contract, forwarded.
623            unsafe { run.layer(w, ssm_ptr, ssm_bytes, conv_ptr, conv_bytes, conv_len) }
624        })?
625        .ok()
626    }
627
628    #[cfg(feature = "metal")]
629    #[allow(clippy::type_complexity)]
630    fn device_branch_full(
631        &self,
632        state: &mut RecurrentState,
633        rms_eps: f32,
634        head: Option<&crate::fused_layer::LayerHeadParts<'_>>,
635        rest: Option<(&crate::fused_layer::LayerFfnParts<'_>, &[f32])>,
636        host_proj: Option<(&[f32], &[f32], &[f32], &[f32])>,
637    ) -> Option<Vec<f32>> {
638        if !frink_core::weight_matrix::metal_dense_enabled() {
639            return None;
640        }
641        // The fused spelling's interleave happens on the host before
642        // this is reached, so `beta_in` / `alpha_in` are already split
643        // either way; what this refuses is a layer whose gates ARE
644        // fused, because that path has not been measured here.
645        if matches!(self.beta_alpha, BetaAlpha::Fused { .. }) {
646            return None;
647        }
648        let h = self.h;
649        let (base, fold) = self.out_proj.launch_parts();
650        let out_proj = crate::metal_launch::matvec(base)?;
651        let fold_y = match fold {
652            None => None,
653            Some(f) => Some(f.metal_plan(h.value_dim())?),
654        };
655        let w = frink_metal::gdn_branch::BranchWeights {
656            shape: frink_metal::gdn::DeltaShape {
657                n_k_heads: h.n_k_heads,
658                n_v_heads: h.n_v_heads,
659                head_dim: h.head_dim,
660                map: match h.map {
661                    HeadMap::Tiled => frink_metal::gdn::HeadMapKind::Tiled,
662                    HeadMap::Grouped => frink_metal::gdn::HeadMapKind::Grouped,
663                },
664            },
665            head: frink_metal::gdn_head::HeadShape {
666                n_k_heads: h.n_k_heads,
667                n_v_heads: h.n_v_heads,
668                head_dim: h.head_dim,
669                d_conv: h.d_conv,
670            },
671            conv1d: &self.conv1d,
672            dt_bias: &self.dt_bias,
673            a: &self.a,
674            ssm_norm: &self.norm,
675            eps: rms_eps,
676            out_proj: &out_proj,
677            fold_y: fold_y.as_ref(),
678            ffn: None,
679            head_in: None,
680        };
681        // The rest of the layer, when the caller owns it: this is what
682        // turns three submissions a layer into one.
683        let ffn_launches = match rest {
684            None => None,
685            Some((parts, _)) => Some(parts.launches()?),
686        };
687        let head_launches = match head {
688            None => None,
689            Some(parts) => Some(parts.launches()?),
690        };
691        let w = frink_metal::gdn_branch::BranchWeights {
692            ffn: ffn_launches.as_ref().map(|l| l.as_metal()),
693            head_in: head_launches.as_ref().map(|l| l.as_metal()),
694            ..w
695        };
696        if !w.is_supported() {
697            return None;
698        }
699        let conv_len = state.conv.len();
700        // Both states travel as their own page-aligned bytes, so the
701        // 3.1 MB delta state and the 123 KB convolution window are read
702        // and written in place rather than copied either way.
703        let (ssm_bytes, ssm_ptr) = (state.ssm.alloc_bytes(), state.ssm.as_ptr());
704        let (conv_bytes, conv_ptr) = (state.conv.alloc_bytes(), state.conv.as_ptr());
705        // SAFETY: `AlignedF32` is page-aligned and page-rounded, both
706        // are borrowed mutably here so nothing else touches those
707        // bytes, and the launch waits for the GPU before returning.
708        unsafe {
709            frink_metal::gdn_branch::launch_gdn_branch(
710                &w,
711                ssm_ptr,
712                ssm_bytes,
713                conv_ptr,
714                conv_bytes,
715                conv_len,
716                host_proj.map(|(q, _, _, _)| q),
717                host_proj.map(|(_, z, _, _)| z),
718                host_proj.map(|(_, _, b, _)| b),
719                host_proj.map(|(_, _, _, a)| a),
720                // The residual is what the head norms and what the FFN
721                // adds to, so it travels when either does.
722                rest.map(|(_, residual)| residual),
723                // A single layer keeps its residual stream to itself;
724                // `frink_metal::gdn_branch::GdnRun` is what passes one
725                // from layer to layer without the host seeing it.
726                None,
727            )
728        }
729        .ok()
730    }
731
732    /// Every row's conv step, gates and l2 norms, which the chunked
733    /// recurrence needs up front: none of them reads the delta state,
734    /// so they do not have to interleave with it the way the
735    /// row-at-a-time loop does.
736    ///
737    /// Returns `(q, k, v, g, beta)`, each `[rows][...]`.
738    #[allow(clippy::type_complexity)]
739    fn conv_and_gates_for_rows(
740        &self,
741        rows: usize,
742        qkv_all: &[f32],
743        beta_all: &[f32],
744        alpha_all: &[f32],
745        state: &mut RecurrentState,
746        rms_eps: f32,
747    ) -> (Vec<f32>, Vec<f32>, Vec<f32>, Vec<f32>, Vec<f32>) {
748        let h = self.h;
749        let (s, n_k, n_v) = (h.head_dim, h.n_k_heads, h.n_v_heads);
750        let (key_dim, value_dim, conv_dim) = (h.key_dim(), h.value_dim(), h.conv_dim());
751        let mut q_all = vec![0.0f32; rows * key_dim];
752        let mut k_all = vec![0.0f32; rows * key_dim];
753        let mut v_all = vec![0.0f32; rows * value_dim];
754        let mut g_all = vec![0.0f32; rows * n_v];
755        let mut beta_gate = vec![0.0f32; rows * n_v];
756        let mut conv_out = vec![0.0f32; conv_dim];
757        for r in 0..rows {
758            for hd in 0..n_v {
759                beta_gate[r * n_v + hd] = sigmoid(beta_all[r * n_v + hd]);
760                g_all[r * n_v + hd] =
761                    softplus(alpha_all[r * n_v + hd] + self.dt_bias[hd]) * self.a[hd];
762            }
763            conv_step(
764                &mut state.conv,
765                &self.conv1d,
766                h.d_conv,
767                &qkv_all[r * conv_dim..(r + 1) * conv_dim],
768                &mut conv_out,
769            );
770            for x in conv_out.iter_mut() {
771                *x = silu(*x);
772            }
773            let (q, rest) = conv_out.split_at_mut(key_dim);
774            let (k, v) = rest.split_at_mut(key_dim);
775            for hd in 0..n_k {
776                l2_normalize(&mut q[hd * s..(hd + 1) * s], rms_eps);
777                l2_normalize(&mut k[hd * s..(hd + 1) * s], rms_eps);
778            }
779            q_all[r * key_dim..(r + 1) * key_dim].copy_from_slice(q);
780            k_all[r * key_dim..(r + 1) * key_dim].copy_from_slice(k);
781            v_all[r * value_dim..(r + 1) * value_dim].copy_from_slice(v);
782        }
783        (q_all, k_all, v_all, g_all, beta_gate)
784    }
785}
786
787#[inline]
788fn silu(x: f32) -> f32 {
789    x / (1.0 + (-x).exp())
790}
791
792#[inline]
793fn sigmoid(x: f32) -> f32 {
794    1.0 / (1.0 + (-x).exp())
795}
796
797/// Architectures whose recurrent layers are decided by
798/// `{arch}.attention.recurrent_layers` / `{arch}.full_attention_interval`
799/// rather than by a zero KV count (`qwen35.cpp:17-24`; `qwen35moe.cpp`
800/// and `qwen3next.cpp` read the same two keys).
801///
802/// The third column is the BLOCK those layers run, because the mask and
803/// the block are one fact: `minimax-01` reads the identical two keys
804/// and runs lightning attention where Qwen3.5 runs the gated delta net
805/// (`crate::lightning`). Carried here rather than looked up beside the
806/// mask so the two cannot be resolved from different tables.
807pub const INTERVAL_RECURRENT_ARCHITECTURES: &[(&str, usize, AttnShape)] = &[
808    ("qwen35", 4, AttnShape::Gdn),
809    ("qwen35moe", 4, AttnShape::Gdn),
810    ("qwen3next", 4, AttnShape::Gdn),
811    // `minimax-01.cpp:13` seeds 8 where `qwen35.cpp:21` seeds 4. The
812    // RULE is the same line in both -- layer `i` is recurrent unless
813    // `(i + 1) % interval == 0` -- so this is a default per
814    // architecture and not a second reader.
815    ("minimax-01", 8, AttnShape::Lightning),
816];
817
818/// Which layers are recurrent AND what block they run.
819///
820/// One value from one constructor ([`recurrent_layers`]): the mask
821/// alone would leave the block to be decided again somewhere else, and
822/// a file whose mask came from one architecture and whose block came
823/// from another is exactly this repo's dominant bug shape.
824#[derive(Debug, Clone, PartialEq, Eq)]
825pub struct RecurrentMask {
826    /// One entry per TRUNK layer.
827    pub layers: Vec<bool>,
828    pub block: AttnShape,
829}
830
831/// Which trunk layers of `arch` are recurrent, or `None` for an
832/// architecture that decides by its head counts.
833///
834/// `qwen35.cpp:17-24` and `minimax-01.cpp:11-17` are the same rule in
835/// two files: the array wins when present (read at `block_count`
836/// length, the MTP block included, and cut to the trunk here); else
837/// layer `i` is recurrent iff `(i + 1) % interval != 0`, with
838/// `interval` from `{arch}.full_attention_interval`. Only the DEFAULT
839/// differs between them, which is why the table carries it.
840pub fn recurrent_layers(
841    file: &impl TensorSource,
842    arch: &str,
843    block_count: usize,
844    n_layers: usize,
845) -> Result<Option<RecurrentMask>, LoadError> {
846    let Some((_, default_interval, block)) = INTERVAL_RECURRENT_ARCHITECTURES
847        .iter()
848        .find(|(a, _, _)| *a == arch)
849    else {
850        return Ok(None);
851    };
852    let key = format!("{arch}.attention.recurrent_layers");
853    if let Some(frink_gguf::GgufValue::Array(items)) = file.metadata(&key) {
854        if items.len() != block_count {
855            return Err(LoadError::UnsupportedFeature(
856                key,
857                format!(
858                    "{} entries for block_count {block_count}; llama.cpp reads it at n_layer_all \
859                     length (qwen35.cpp:17)",
860                    items.len()
861                ),
862            ));
863        }
864        let mut out = Vec::with_capacity(n_layers);
865        for (il, item) in items.iter().enumerate().take(n_layers) {
866            out.push(item.as_bool().ok_or_else(|| {
867                LoadError::UnsupportedFeature(key.clone(), format!("entry {il} is not a bool"))
868            })?);
869        }
870        return Ok(Some(RecurrentMask {
871            layers: out,
872            block: *block,
873        }));
874    }
875    let interval = file
876        .metadata_u64(&format!("{arch}.full_attention_interval"))
877        .unwrap_or(*default_interval as u64) as usize;
878    if interval == 0 {
879        return Err(LoadError::UnsupportedFeature(
880            format!("{arch}.full_attention_interval"),
881            "0: qwen35.cpp:22 takes `(i + 1) % interval`".to_string(),
882        ));
883    }
884    Ok(Some(RecurrentMask {
885        layers: (0..n_layers)
886            .map(|i| !(i + 1).is_multiple_of(interval))
887            .collect(),
888        block: *block,
889    }))
890}
891
892#[cfg(test)]
893mod tests {
894    use super::*;
895    use frink_core::Tensor;
896
897    fn hp() -> GdnHparams {
898        GdnHparams {
899            d_conv: 3,
900            head_dim: 2,
901            n_k_heads: 1,
902            n_v_heads: 2,
903            map: HeadMap::Tiled,
904        }
905    }
906
907    #[test]
908    fn the_widths_are_the_graph_s() {
909        let h = hp();
910        assert_eq!((h.key_dim(), h.value_dim(), h.conv_dim()), (2, 4, 8));
911        assert_eq!(h.state_floats(), (2 * 8, 2 * 2 * 2));
912    }
913
914    /// The device head IS this one: same convolution, same SiLU, same
915    /// per-head l2 norms, same two gates, and the same convolution
916    /// window left behind.
917    ///
918    /// It matters that this is measured against `conv_and_gates_for_rows`
919    /// rather than against a formula: the l2 norm sums in f64 on the
920    /// host and cannot on the device, and the softplus has a threshold
921    /// at 20 that a kernel taking the log anyway would miss exactly
922    /// where the argument is large. Both are drawn for here.
923    #[cfg(feature = "metal")]
924    #[test]
925    #[ignore = "needs a real Metal-capable GPU; run manually with --ignored on Apple Silicon"]
926    fn the_device_head_matches_this_one() {
927        use frink_metal::gdn_head::{launch_gdn_head, HeadShape};
928
929        for h in [
930            GdnHparams {
931                d_conv: 4,
932                head_dim: 128,
933                n_k_heads: 4,
934                n_v_heads: 48,
935                map: HeadMap::Tiled,
936            },
937            GdnHparams {
938                d_conv: 2,
939                head_dim: 4,
940                n_k_heads: 1,
941                n_v_heads: 2,
942                map: HeadMap::Grouped,
943            },
944        ] {
945            let n_embd = 8;
946            let mut seed = 424_242u32;
947            let mut rnd = |n: usize, scale: f32| -> Vec<f32> {
948                (0..n)
949                    .map(|_| {
950                        seed = seed.wrapping_mul(1664525).wrapping_add(1013904223);
951                        (((seed >> 9) as f32 / (1u32 << 23) as f32) - 0.5) * scale
952                    })
953                    .collect()
954            };
955            let mat = |rows: usize, cols: usize, v: Vec<f32>| {
956                WeightMatrix::F32(Tensor::new(v, vec![rows, cols]))
957            };
958            let conv_dim = h.conv_dim();
959            let m = Gdn {
960                h,
961                qkv: mat(conv_dim, n_embd, rnd(conv_dim * n_embd, 1.0)),
962                z_proj: mat(h.value_dim(), n_embd, rnd(h.value_dim() * n_embd, 1.0)),
963                conv1d: rnd(h.d_conv * conv_dim, 1.0),
964                dt_bias: rnd(h.n_v_heads, 1.0),
965                a: rnd(h.n_v_heads, 1.0),
966                beta_alpha: BetaAlpha::Split {
967                    beta: mat(h.n_v_heads, n_embd, rnd(h.n_v_heads * n_embd, 1.0)),
968                    alpha: mat(h.n_v_heads, n_embd, rnd(h.n_v_heads * n_embd, 1.0)),
969                },
970                norm: rnd(h.head_dim, 1.0),
971                out_proj: mat(n_embd, h.value_dim(), rnd(n_embd * h.value_dim(), 1.0)),
972            };
973            let qkv = rnd(conv_dim, 1.0);
974            let beta_in = rnd(h.n_v_heads, 4.0);
975            // Wide enough that some `alpha + dt_bias` clears 88, which
976            // is where the softplus threshold becomes OBSERVABLE: below
977            // it `log(1 + exp(z))` is already `z` in f32, so a draw that
978            // only reached 20 left the threshold untested and a kernel
979            // without it passing. Above 88 `exp` overflows and the
980            // naive form returns infinity where the host returns `z`.
981            let alpha_in = rnd(h.n_v_heads, 400.0);
982            let conv0 = rnd(h.state_floats().0, 1.0);
983            let eps = 1e-6f32;
984
985            let aligned = |v: &[f32]| {
986                let mut a = frink_core::recurrent_state::AlignedF32::zeros(v.len());
987                a.copy_from_slice(v);
988                a
989            };
990            let mut host_state = RecurrentState {
991                conv: aligned(&conv0),
992                ssm: frink_core::recurrent_state::AlignedF32::zeros(h.state_floats().1),
993            };
994            let (hq, hk, hv, hg, hbeta) =
995                m.conv_and_gates_for_rows(1, &qkv, &beta_in, &alpha_in, &mut host_state, eps);
996
997            let mut device_conv = conv0;
998            let (dq, dk, dv, dg, dbeta) = launch_gdn_head(
999                HeadShape {
1000                    n_k_heads: h.n_k_heads,
1001                    n_v_heads: h.n_v_heads,
1002                    head_dim: h.head_dim,
1003                    d_conv: h.d_conv,
1004                },
1005                &mut device_conv,
1006                &m.conv1d,
1007                &qkv,
1008                &beta_in,
1009                &alpha_in,
1010                &m.dt_bias,
1011                &m.a,
1012                eps,
1013            )
1014            .expect("the kernels launch");
1015
1016            let tol = 2e-5;
1017            for (what, a, b) in [
1018                ("q", &dq, &hq),
1019                ("k", &dk, &hk),
1020                ("v", &dv, &hv),
1021                ("g", &dg, &hg),
1022                ("beta", &dbeta, &hbeta),
1023                ("conv state", &device_conv, &host_state.conv[..].to_vec()),
1024            ] {
1025                assert_eq!(a.len(), b.len(), "{what} length");
1026                for (i, (x, y)) in a.iter().zip(b.iter()).enumerate() {
1027                    assert!(
1028                        (x - y).abs() <= tol * y.abs().max(1.0),
1029                        "{}x{} {what}[{i}]: device={x} host={y}",
1030                        h.n_v_heads,
1031                        h.head_dim
1032                    );
1033                }
1034            }
1035        }
1036    }
1037
1038    /// The device BRANCH is the host branch: same convolution, same
1039    /// gates, same recurrence, same gated norm, same projection, and
1040    /// the same two states left behind.
1041    ///
1042    /// The oracle is the host pieces composed in the order the row body
1043    /// composes them -- `conv_and_gates_for_rows`, `delta_step`, the
1044    /// gated norm, `out_proj` -- because that IS the definition, and
1045    /// `forward_rows` now takes the device path when one is available,
1046    /// so it cannot be its own reference.
1047    #[cfg(feature = "metal")]
1048    #[test]
1049    #[ignore = "needs a real Metal-capable GPU; run manually with --ignored on Apple Silicon"]
1050    fn the_device_branch_matches_the_host_branch() {
1051        use frink_core::gdn::delta_step;
1052
1053        for h in [
1054            GdnHparams {
1055                d_conv: 4,
1056                head_dim: 128,
1057                n_k_heads: 4,
1058                n_v_heads: 48,
1059                map: HeadMap::Tiled,
1060            },
1061            GdnHparams {
1062                d_conv: 2,
1063                head_dim: 8,
1064                n_k_heads: 2,
1065                n_v_heads: 4,
1066                map: HeadMap::Grouped,
1067            },
1068        ] {
1069            let n_embd = 16;
1070            let mut seed = 777_001u32;
1071            let mut rnd = |n: usize, scale: f32| -> Vec<f32> {
1072                (0..n)
1073                    .map(|_| {
1074                        seed = seed.wrapping_mul(1664525).wrapping_add(1013904223);
1075                        (((seed >> 9) as f32 / (1u32 << 23) as f32) - 0.5) * scale
1076                    })
1077                    .collect()
1078            };
1079            let mat = |rows: usize, cols: usize, v: Vec<f32>| {
1080                WeightMatrix::F32(Tensor::new(v, vec![rows, cols]))
1081            };
1082            let (key_dim, value_dim, conv_dim) = (h.key_dim(), h.value_dim(), h.conv_dim());
1083            let m = Gdn {
1084                h,
1085                qkv: mat(conv_dim, n_embd, rnd(conv_dim * n_embd, 1.0)),
1086                z_proj: mat(value_dim, n_embd, rnd(value_dim * n_embd, 1.0)),
1087                conv1d: rnd(h.d_conv * conv_dim, 1.0),
1088                dt_bias: rnd(h.n_v_heads, 1.0),
1089                a: rnd(h.n_v_heads, 1.0),
1090                beta_alpha: BetaAlpha::Split {
1091                    beta: mat(h.n_v_heads, n_embd, rnd(h.n_v_heads * n_embd, 1.0)),
1092                    alpha: mat(h.n_v_heads, n_embd, rnd(h.n_v_heads * n_embd, 1.0)),
1093                },
1094                norm: rnd(h.head_dim, 1.0),
1095                out_proj: mat(n_embd, value_dim, rnd(n_embd * value_dim, 1.0)),
1096            };
1097            let (conv_len, ssm_len) = h.state_floats();
1098            let conv0 = rnd(conv_len, 1.0);
1099            let ssm0 = rnd(ssm_len, 0.5);
1100            let normed = rnd(n_embd, 1.0);
1101            let eps = 1e-6f32;
1102            let state0 = || RecurrentState {
1103                conv: {
1104                    let mut a = frink_core::recurrent_state::AlignedF32::zeros(conv_len);
1105                    a.copy_from_slice(&conv0);
1106                    a
1107                },
1108                ssm: {
1109                    let mut a = frink_core::recurrent_state::AlignedF32::zeros(ssm_len);
1110                    a.copy_from_slice(&ssm0);
1111                    a
1112                },
1113            };
1114
1115            // The host branch, composed from the pieces the row body
1116            // composes.
1117            let mut host_state = state0();
1118            let qkv_all = m.qkv.apply(&normed);
1119            let z_all = m.z_proj.apply(&normed);
1120            let (beta_all, alpha_all) = match &m.beta_alpha {
1121                BetaAlpha::Split { beta, alpha } => (beta.apply(&normed), alpha.apply(&normed)),
1122                BetaAlpha::Fused { .. } => unreachable!("split above"),
1123            };
1124            let (q, k, v, g, beta) =
1125                m.conv_and_gates_for_rows(1, &qkv_all, &beta_all, &alpha_all, &mut host_state, eps);
1126            let mut o = vec![0.0f32; value_dim];
1127            delta_step(
1128                h.delta_dims(),
1129                &mut host_state.ssm,
1130                &q,
1131                &k,
1132                &v,
1133                &g,
1134                &beta,
1135                &mut o,
1136            );
1137            let mut ys = vec![0.0f32; value_dim];
1138            for hd in 0..h.n_v_heads {
1139                let normed_head =
1140                    rms_norm(&o[hd * h.head_dim..(hd + 1) * h.head_dim], &m.norm, eps);
1141                for i in 0..h.head_dim {
1142                    ys[hd * h.head_dim + i] =
1143                        normed_head[i] * super::silu(z_all[hd * h.head_dim + i]);
1144                }
1145            }
1146            let host_out = m.out_proj.apply(&ys);
1147
1148            // The device branch, through the path `forward_rows` takes.
1149            let mut device_state = state0();
1150            let device_out = m
1151                .device_branch_full(
1152                    &mut device_state,
1153                    eps,
1154                    None,
1155                    None,
1156                    Some((&qkv_all, &z_all, &beta_all, &alpha_all)),
1157                )
1158                .expect("this shape is one the kernels serve");
1159
1160            let tol = 2e-4;
1161            for (what, a, b) in [
1162                ("out", &device_out, &host_out),
1163                (
1164                    "conv state",
1165                    &device_state.conv[..].to_vec(),
1166                    &host_state.conv[..].to_vec(),
1167                ),
1168            ] {
1169                assert_eq!(a.len(), b.len(), "{what} length");
1170                for (i, (x, y)) in a.iter().zip(b.iter()).enumerate() {
1171                    assert!(
1172                        (x - y).abs() <= tol * y.abs().max(1.0),
1173                        "{}x{} {what}[{i}]: device={x} host={y}",
1174                        h.n_v_heads,
1175                        h.head_dim
1176                    );
1177                }
1178            }
1179            for (i, (x, y)) in device_state
1180                .ssm
1181                .iter()
1182                .zip(host_state.ssm.iter())
1183                .enumerate()
1184            {
1185                assert!(
1186                    (x - y).abs() <= tol * y.abs().max(1.0),
1187                    "{}x{} ssm state[{i}]: device={x} host={y}",
1188                    h.n_v_heads,
1189                    h.head_dim
1190                );
1191            }
1192            let _ = key_dim;
1193        }
1194    }
1195
1196    /// The fused WHOLE LAYER is the host layer: the input norm, the
1197    /// four projections, the branch, the residual add, the FFN norm,
1198    /// the FFN and the second residual add.
1199    ///
1200    /// This is Bonsai's production decode path, so the oracle is the
1201    /// host pieces composed in the order the decoder composes them --
1202    /// `forward_rows` for the branch, then `rms_norm`, the SwiGLU
1203    /// expert and two adds -- and NOT `fused_layer` itself.
1204    #[cfg(feature = "metal")]
1205    #[test]
1206    #[ignore = "needs a real Metal-capable GPU; run manually with --ignored on Apple Silicon"]
1207    fn the_fused_layer_matches_the_host_layer() {
1208        use crate::fused_layer::{LayerFfnParts, LayerHeadParts};
1209
1210        let h = GdnHparams {
1211            d_conv: 4,
1212            head_dim: 128,
1213            n_k_heads: 4,
1214            n_v_heads: 48,
1215            map: HeadMap::Tiled,
1216        };
1217        let (n_embd, ffn_dim) = (64usize, 96usize);
1218        let mut seed = 31_337u32;
1219        let mut rnd = |n: usize, scale: f32| -> Vec<f32> {
1220            (0..n)
1221                .map(|_| {
1222                    seed = seed.wrapping_mul(1664525).wrapping_add(1013904223);
1223                    (((seed >> 9) as f32 / (1u32 << 23) as f32) - 0.5) * scale
1224                })
1225                .collect()
1226        };
1227        let mat = |rows: usize, cols: usize, v: Vec<f32>| {
1228            WeightMatrix::F32(Tensor::new(v, vec![rows, cols]))
1229        };
1230        let (value_dim, conv_dim) = (h.value_dim(), h.conv_dim());
1231        let m = Gdn {
1232            h,
1233            qkv: mat(conv_dim, n_embd, rnd(conv_dim * n_embd, 1.0)),
1234            z_proj: mat(value_dim, n_embd, rnd(value_dim * n_embd, 1.0)),
1235            conv1d: rnd(h.d_conv * conv_dim, 1.0),
1236            dt_bias: rnd(h.n_v_heads, 1.0),
1237            a: rnd(h.n_v_heads, 1.0),
1238            beta_alpha: BetaAlpha::Split {
1239                beta: mat(h.n_v_heads, n_embd, rnd(h.n_v_heads * n_embd, 1.0)),
1240                alpha: mat(h.n_v_heads, n_embd, rnd(h.n_v_heads * n_embd, 1.0)),
1241            },
1242            norm: rnd(h.head_dim, 1.0),
1243            out_proj: mat(n_embd, value_dim, rnd(n_embd * value_dim, 1.0)),
1244        };
1245        // `qkv` and `z` share ONE fold and the two gate matrices carry
1246        // none, which is Bonsai's layout and the only one the kernel
1247        // serves. Without a real fold here the ORDER of the head's four
1248        // projections around the rotation is unobservable, and moving
1249        // the gate projections after it left this test green -- which
1250        // is why it carries one.
1251        let signs: std::sync::Arc<[f32]> = rnd(n_embd, 2.0)
1252            .iter()
1253            .map(|x| if *x < 0.0 { -1.0f32 } else { 1.0 })
1254            .collect::<Vec<f32>>()
1255            .into();
1256        let fold = std::sync::Arc::new(frink_core::weight_matrix::hadamard::HadamardFold {
1257            block: 16,
1258            signs: Some(signs),
1259            perm: None,
1260            site: frink_core::weight_matrix::hadamard::FoldSite::Input,
1261        });
1262        let mut m = m;
1263        m.qkv.fold_hadamard(fold.clone());
1264        m.z_proj.fold_hadamard(fold.clone());
1265        let m = m;
1266        let attn_norm = rnd(n_embd, 0.5).iter().map(|x| 1.0 + x).collect::<Vec<_>>();
1267        let ffn_norm = rnd(n_embd, 0.5).iter().map(|x| 1.0 + x).collect::<Vec<_>>();
1268        let (gate_v, up_v, down_v) = (
1269            rnd(ffn_dim * n_embd, 1.0),
1270            rnd(ffn_dim * n_embd, 1.0),
1271            rnd(n_embd * ffn_dim, 1.0),
1272        );
1273        let gate = mat(ffn_dim, n_embd, gate_v.clone());
1274        let up = mat(ffn_dim, n_embd, up_v.clone());
1275        let down = mat(n_embd, ffn_dim, down_v.clone());
1276        let (conv_len, ssm_len) = h.state_floats();
1277        let conv0 = rnd(conv_len, 1.0);
1278        let ssm0 = rnd(ssm_len, 0.5);
1279        let hidden = rnd(n_embd, 1.0);
1280        let eps = 1e-6f32;
1281        let state0 = || RecurrentState {
1282            conv: {
1283                let mut a = frink_core::recurrent_state::AlignedF32::zeros(conv_len);
1284                a.copy_from_slice(&conv0);
1285                a
1286            },
1287            ssm: {
1288                let mut a = frink_core::recurrent_state::AlignedF32::zeros(ssm_len);
1289                a.copy_from_slice(&ssm0);
1290                a
1291            },
1292        };
1293
1294        // The host layer, piece by piece as the decoder runs it.
1295        let mut host_state = state0();
1296        let normed = rms_norm(&hidden, &attn_norm, eps);
1297        let branch = m.forward_rows(&normed, 1, &mut host_state, eps);
1298        let mut host_out = hidden.clone();
1299        for (x, b) in host_out.iter_mut().zip(branch.iter()) {
1300            *x += b;
1301        }
1302        let normed2 = rms_norm(&host_out, &ffn_norm, eps);
1303        let ffn_out = frink_moe::run_expert(
1304            &normed2,
1305            &frink_moe::ExpertWeights {
1306                gate: mat(ffn_dim, n_embd, gate_v),
1307                up: mat(ffn_dim, n_embd, up_v),
1308                down: mat(n_embd, ffn_dim, down_v),
1309            },
1310            frink_moe::GluAct::Swiglu,
1311        );
1312        for (x, f) in host_out.iter_mut().zip(ffn_out.iter()) {
1313            *x += f;
1314        }
1315
1316        // The fused layer, through the path the decoder takes.
1317        let mut device_state = state0();
1318        let ffn = LayerFfnParts::from_parts(&ffn_norm, eps, &gate, &up, &down);
1319        let head = LayerHeadParts::for_block(
1320            &attn_norm,
1321            eps,
1322            &m.qkv,
1323            &m.z_proj,
1324            match &m.beta_alpha {
1325                BetaAlpha::Split { beta, .. } => beta,
1326                BetaAlpha::Fused { .. } => unreachable!("split above"),
1327            },
1328            match &m.beta_alpha {
1329                BetaAlpha::Split { alpha, .. } => alpha,
1330                BetaAlpha::Fused { .. } => unreachable!("split above"),
1331            },
1332        )
1333        .expect("every field is required");
1334        // Both arms: the head on the device, and the head on the host
1335        // with only the branch and the FFN fused.
1336        let with_head = m
1337            .device_branch_full(
1338                &mut device_state,
1339                eps,
1340                Some(&head),
1341                Some((&ffn, &hidden)),
1342                None,
1343            )
1344            .expect("this shape is one the kernels serve");
1345        let mut host_head_state = state0();
1346        let host_head = m
1347            .fused_layer(
1348                &attn_norm,
1349                &normed,
1350                &mut host_head_state,
1351                eps,
1352                &ffn,
1353                &hidden,
1354            )
1355            .expect("this shape is one the kernels serve");
1356
1357        let tol = 2e-4;
1358        for (what, got) in [
1359            ("head on device", &with_head),
1360            ("through fused_layer", &host_head),
1361        ] {
1362            for (i, (x, y)) in got.iter().zip(host_out.iter()).enumerate() {
1363                assert!(
1364                    (x - y).abs() <= tol * y.abs().max(1.0),
1365                    "{what} out[{i}]: device={x} host={y}"
1366                );
1367            }
1368        }
1369        for (i, (x, y)) in device_state
1370            .ssm
1371            .iter()
1372            .zip(host_state.ssm.iter())
1373            .enumerate()
1374        {
1375            assert!(
1376                (x - y).abs() <= tol * y.abs().max(1.0),
1377                "ssm state[{i}]: device={x} host={y}"
1378            );
1379        }
1380    }
1381
1382    /// Batched rows and one-at-a-time rows agree and leave the same
1383    /// state.
1384    #[test]
1385    fn rows_and_one_at_a_time_agree_and_leave_the_same_state() {
1386        let h = hp();
1387        let n_embd = 3;
1388        let mut seed = 5u32;
1389        let mut rnd = |n: usize| -> Vec<f32> {
1390            (0..n)
1391                .map(|_| {
1392                    seed = seed.wrapping_mul(1664525).wrapping_add(1013904223);
1393                    ((seed >> 9) as f32 / (1u32 << 23) as f32) - 0.5
1394                })
1395                .collect()
1396        };
1397        let mat = |rows: usize, cols: usize, v: Vec<f32>| {
1398            WeightMatrix::F32(Tensor::new(v, vec![rows, cols]))
1399        };
1400        let m = Gdn {
1401            h,
1402            qkv: mat(h.conv_dim(), n_embd, rnd(h.conv_dim() * n_embd)),
1403            z_proj: mat(h.value_dim(), n_embd, rnd(h.value_dim() * n_embd)),
1404            conv1d: rnd(h.d_conv * h.conv_dim()),
1405            dt_bias: rnd(h.n_v_heads),
1406            a: vec![-0.7, -1.2],
1407            beta_alpha: BetaAlpha::Split {
1408                beta: mat(h.n_v_heads, n_embd, rnd(h.n_v_heads * n_embd)),
1409                alpha: mat(h.n_v_heads, n_embd, rnd(h.n_v_heads * n_embd)),
1410            },
1411            norm: vec![1.1, 0.9],
1412            out_proj: mat(n_embd, h.value_dim(), rnd(n_embd * h.value_dim())),
1413        };
1414        let tokens: Vec<Vec<f32>> = (0..4).map(|_| rnd(n_embd)).collect();
1415        let flat: Vec<f32> = tokens.concat();
1416        let mut s_batch = m.zero_state();
1417        let batched = m.forward_rows(&flat, 4, &mut s_batch, 1e-5);
1418        let mut s_seq = m.zero_state();
1419        let mut seq = Vec::new();
1420        for t in &tokens {
1421            seq.extend(m.forward_rows(t, 1, &mut s_seq, 1e-5));
1422        }
1423        for (a, b) in batched.iter().zip(&seq) {
1424            assert!((a - b).abs() < 1e-6, "{a} vs {b}");
1425        }
1426        // Close, not identical: a batch takes the CHUNKED delta rule
1427        // (`frink_core::gdn_chunk`), which is the same recurrence
1428        // with the rank-one updates unrolled across the chunk, so the
1429        // float association differs from stepping row by row. The
1430        // chunked module pins the two against each other directly; what
1431        // this test is for is that the batched path has not lost a
1432        // FEATURE, which a tolerance still catches.
1433        assert_eq!(s_batch.conv, s_seq.conv, "the conv window is exact");
1434        for (a, b) in s_batch.ssm.iter().zip(s_seq.ssm.iter()) {
1435            assert!((a - b).abs() < 1e-6, "state: {a} vs {b}");
1436        }
1437        let again = m.forward_rows(&flat, 4, &mut s_batch, 1e-5);
1438        assert!(again
1439            .iter()
1440            .zip(&batched)
1441            .any(|(a, b)| (a - b).abs() > 1e-6));
1442    }
1443}